192 lines
6.1 KiB
Go
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())
|
|
}
|
|
}
|