package terminal import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "nadir/internal/auth" "github.com/coder/websocket" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humago" ) func TestTerminalConnectUnauthorized(t *testing.T) { sessions, err := auth.NewSessionStore("file::memory:?cache=shared") if err != nil { t.Fatalf("session db: %v", err) } mux := http.NewServeMux() api := humago.New(mux, huma.DefaultConfig("Test", "1.0.0")) mod := New(sessions) mod.Register(api) srv := httptest.NewServer(mux) defer srv.Close() wsURL := strings.Replace(srv.URL, "http://", "ws://", 1) + "/api/terminal" // Try without cookie ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() _, resp, err := websocket.Dial(ctx, wsURL, nil) if err == nil { t.Fatal("expected error, got success") } if resp != nil && resp.StatusCode != http.StatusUnauthorized { t.Errorf("expected 401, got %d", resp.StatusCode) } } func TestTerminalConnectPlainGET(t *testing.T) { // An authenticated but non-WebSocket GET (e.g. the docs "Try it" button) must // get a clean 426 Upgrade Required, not a raw websocket protocol error. sessions, err := auth.NewSessionStore("file::memory:?cache=shared") if err != nil { t.Fatalf("session db: %v", err) } token, err := sessions.Create("root") if err != nil { t.Fatalf("create session: %v", err) } mux := http.NewServeMux() api := humago.New(mux, huma.DefaultConfig("Test", "1.0.0")) New(sessions).Register(api) srv := httptest.NewServer(mux) defer srv.Close() req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/terminal", nil) req.Header.Set("Cookie", "nadir_session_id="+token) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUpgradeRequired { t.Errorf("expected 426, got %d", resp.StatusCode) } } func TestTerminalConnectAuthorized(t *testing.T) { // Root or specific system configs might cause PTY or su to fail if run in constrained environments. // But we expect the websocket upgrade to succeed and then maybe close with an error if PTY fails. sessions, err := auth.NewSessionStore("file::memory:?cache=shared") if err != nil { t.Fatalf("session db: %v", err) } token, err := sessions.Create("root") if err != nil { t.Fatalf("create session: %v", err) } mux := http.NewServeMux() api := humago.New(mux, huma.DefaultConfig("Test", "1.0.0")) mod := New(sessions) mod.Register(api) srv := httptest.NewServer(mux) defer srv.Close() wsURL := strings.Replace(srv.URL, "http://", "ws://", 1) + "/api/terminal" ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() opts := &websocket.DialOptions{ HTTPHeader: http.Header{}, } opts.HTTPHeader.Set("Cookie", "nadir_session_id="+token) conn, resp, err := websocket.Dial(ctx, wsURL, opts) if err != nil { // Depending on the test environment, "su" or "pty" might fail, but // the websocket upgrade itself should succeed before it drops. // If it fails to upgrade, that's a real error. t.Fatalf("dial failed: %v (status %d)", err, resp.StatusCode) } defer conn.CloseNow() // The connection was upgraded successfully. // If PTY allocation failed or su failed, the server might close the connection immediately. // We just verify the upgrade succeeded. if resp.StatusCode != http.StatusSwitchingProtocols { t.Errorf("expected 101, got %d", resp.StatusCode) } }