package httpapi import ( "context" "errors" "io" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "drive.local/drivev2/internal/application" "drive.local/drivev2/internal/config" ) type systemServiceStub struct { readinessErr error setupStatus application.SetupStatus setupErr error } func (s systemServiceStub) Readiness(context.Context) error { return s.readinessErr } func (s systemServiceStub) SetupStatus(context.Context) (application.SetupStatus, error) { return s.setupStatus, s.setupErr } type authenticationServiceStub struct { setupResult application.SetupOwnerResult setupErr error loginResult application.AuthenticatedSession loginErr error session application.BrowserSession sessionErr error logoutErr error } func (s authenticationServiceStub) SetupOwner(context.Context, application.SetupOwnerInput) (application.SetupOwnerResult, error) { return s.setupResult, s.setupErr } func (s authenticationServiceStub) Login(context.Context, application.LoginInput) (application.AuthenticatedSession, error) { return s.loginResult, s.loginErr } func (s authenticationServiceStub) CurrentSession(context.Context, string) (application.BrowserSession, error) { return s.session, s.sessionErr } func (s authenticationServiceStub) Logout(context.Context, string, string, string) error { return s.logoutErr } func TestLiveHealth(t *testing.T) { t.Parallel() server := NewServer( config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()}, slog.New(slog.NewTextHandler(io.Discard, nil)), systemServiceStub{}, authenticationServiceStub{}, http.NotFoundHandler(), ) request := httptest.NewRequest(http.MethodGet, "/health/live", nil) recorder := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) } if !strings.Contains(recorder.Body.String(), `"status":"live"`) { t.Fatalf("unexpected body: %s", recorder.Body.String()) } } func TestReadinessReportsDependencyFailure(t *testing.T) { t.Parallel() server := NewServer( config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()}, slog.New(slog.NewTextHandler(io.Discard, nil)), systemServiceStub{readinessErr: errors.New("database unavailable")}, authenticationServiceStub{}, http.NotFoundHandler(), ) request := httptest.NewRequest(http.MethodGet, "/health/ready", nil) recorder := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusServiceUnavailable) } if contentType := recorder.Header().Get("Content-Type"); contentType != "application/problem+json" { t.Fatalf("Content-Type = %q, want application/problem+json", contentType) } if !strings.Contains(recorder.Body.String(), `"code":"dependency_unavailable"`) { t.Fatalf("unexpected body: %s", recorder.Body.String()) } } func TestSetupStatusUsesApplicationService(t *testing.T) { t.Parallel() server := NewServer( config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()}, slog.New(slog.NewTextHandler(io.Discard, nil)), systemServiceStub{setupStatus: application.SetupStatus{Initialized: true, Phase: "phase-1", Version: "test"}}, authenticationServiceStub{}, http.NotFoundHandler(), ) request := httptest.NewRequest(http.MethodGet, "/api/v1/setup/status", nil) recorder := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK) } if !strings.Contains(recorder.Body.String(), `"initialized":true`) { t.Fatalf("unexpected body: %s", recorder.Body.String()) } } func TestOwnerSetupSetsHardenedAuthenticationCookies(t *testing.T) { t.Parallel() expiresAt := time.Now().Add(time.Hour).UTC() authenticated := application.AuthenticatedSession{ Session: application.BrowserSession{ID: "session", OwnerID: "owner", ExpiresAt: expiresAt}, SessionToken: "session-secret", CSRFToken: "csrf-secret", } server := NewServer( config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()}, slog.New(slog.NewTextHandler(io.Discard, nil)), systemServiceStub{}, authenticationServiceStub{setupResult: application.SetupOwnerResult{ OwnerID: "owner", RootNodeID: "root", Username: "owner", DisplayName: "Owner", RecoveryCodes: []string{"AAAAA-BBBBB-CCCCC-DDDDD"}, Authentication: authenticated, }}, http.NotFoundHandler(), ) request := httptest.NewRequest(http.MethodPost, "/api/v1/setup", strings.NewReader(`{"username":"owner","displayName":"Owner","password":"correct horse battery staple"}`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated { t.Fatalf("status = %d, want %d: %s", recorder.Code, http.StatusCreated, recorder.Body.String()) } cookies := recorder.Result().Cookies() if len(cookies) != 2 { t.Fatalf("cookie count = %d, want 2", len(cookies)) } for _, cookie := range cookies { if !cookie.Secure || cookie.SameSite != http.SameSiteStrictMode || cookie.Path != "/" { t.Fatalf("cookie is not hardened: %#v", cookie) } } if !cookies[0].HttpOnly || cookies[1].HttpOnly { t.Fatalf("unexpected HttpOnly policy: %#v", cookies) } } func TestLogoutRejectsInvalidCSRF(t *testing.T) { t.Parallel() server := NewServer( config.Config{HTTPAddress: ":0", WebRoot: t.TempDir()}, slog.New(slog.NewTextHandler(io.Discard, nil)), systemServiceStub{}, authenticationServiceStub{logoutErr: application.ErrCSRFValidation}, http.NotFoundHandler(), ) request := httptest.NewRequest(http.MethodDelete, "/api/v1/sessions/current", nil) recorder := httptest.NewRecorder() server.httpServer.Handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d", recorder.Code, http.StatusForbidden) } if !strings.Contains(recorder.Body.String(), `"code":"csrf_validation_failed"`) { t.Fatalf("unexpected body: %s", recorder.Body.String()) } }