newdrive/internal/transport/httpapi/server_test.go
Elijah 715423ab8e
Some checks failed
CI / Backend (push) Failing after 1s
CI / Frontend (push) Successful in 13s
CI / Contracts and repository policy (push) Failing after 3s
CI / Container (push) Has been skipped
Implement owner setup and browser authentication sessions
2026-07-16 19:45:40 -07:00

192 lines
6.1 KiB
Go

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())
}
}