package api import ( "encoding/json" "net/http" "net/http/httptest" "testing" "time" "codeberg.org/SquidSE/ClusterCanvas/service/internal/auth" "codeberg.org/SquidSE/ClusterCanvas/service/internal/settings" ) func TestMiddlewareReturnsSessionIdle(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) keyBytes := make([]byte, 32) for index := range keyBytes { keyBytes[index] = byte(index + 1) } store, err := settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions: %v", err) } if len(store.Sessions) != 1 { t.Fatalf("expected 1 session, got %d", len(store.Sessions)) } store.Sessions[0].LastSeenAt = time.Now().UTC().Add(-31 * time.Minute) if err := settings.SaveSessions(configDir, store, keyBytes); err != nil { t.Fatalf("SaveSessions: %v", err) } router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/status", nil), cookie) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Fatalf("expected status %d, got %d body=%s", http.StatusUnauthorized, recorder.Code, recorder.Body.String()) } var payload apiErrorResponse if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error != "session_idle" { t.Fatalf("expected session_idle, got %q", payload.Error) } cleared := false for _, setCookie := range recorder.Result().Cookies() { if setCookie.Name == auth.SessionCookieName && setCookie.MaxAge < 0 { cleared = true } } if !cleared { t.Fatal("expected session cookie to be cleared") } } func TestMiddlewareReturnsAuthenticationRequiredWithoutCookie(t *testing.T) { configDir := t.TempDir() _ = seedCompletedSetup(t, configDir) router := NewRouter(configDir) request := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Fatalf("expected status %d, got %d body=%s", http.StatusUnauthorized, recorder.Code, recorder.Body.String()) } var payload apiErrorResponse if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error != "authentication required" { t.Fatalf("expected authentication required, got %q", payload.Error) } } func TestMiddlewareIdleExemptDoesNotAdvanceLastSeenAt(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) keyBytes := make([]byte, 32) for index := range keyBytes { keyBytes[index] = byte(index + 1) } store, err := settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions: %v", err) } originalLastSeen := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Second) store.Sessions[0].LastSeenAt = originalLastSeen if err := settings.SaveSessions(configDir, store, keyBytes); err != nil { t.Fatalf("SaveSessions: %v", err) } router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/status", nil), cookie) request.Header.Set(sessionIdleExemptHeader, "1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, recorder.Code, recorder.Body.String()) } store, err = settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions after request: %v", err) } if len(store.Sessions) != 1 { t.Fatalf("expected 1 session, got %d", len(store.Sessions)) } if !store.Sessions[0].LastSeenAt.Equal(originalLastSeen) { t.Fatalf( "expected LastSeenAt %v unchanged, got %v", originalLastSeen, store.Sessions[0].LastSeenAt, ) } } func TestMiddlewareIdleExemptStillReturnsSessionIdle(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) keyBytes := make([]byte, 32) for index := range keyBytes { keyBytes[index] = byte(index + 1) } store, err := settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions: %v", err) } store.Sessions[0].LastSeenAt = time.Now().UTC().Add(-31 * time.Minute) if err := settings.SaveSessions(configDir, store, keyBytes); err != nil { t.Fatalf("SaveSessions: %v", err) } router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/status", nil), cookie) request.Header.Set(sessionIdleExemptHeader, "1") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusUnauthorized { t.Fatalf("expected status %d, got %d body=%s", http.StatusUnauthorized, recorder.Code, recorder.Body.String()) } var payload apiErrorResponse if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Error != "session_idle" { t.Fatalf("expected session_idle, got %q", payload.Error) } } func TestMiddlewareNormalRequestAdvancesLastSeenAt(t *testing.T) { configDir := t.TempDir() cookie := seedCompletedSetup(t, configDir) keyBytes := make([]byte, 32) for index := range keyBytes { keyBytes[index] = byte(index + 1) } store, err := settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions: %v", err) } originalLastSeen := time.Now().UTC().Add(-10 * time.Minute).Truncate(time.Second) store.Sessions[0].LastSeenAt = originalLastSeen if err := settings.SaveSessions(configDir, store, keyBytes); err != nil { t.Fatalf("SaveSessions: %v", err) } router := NewRouter(configDir) request := withSession(httptest.NewRequest(http.MethodGet, "/api/v1/status", nil), cookie) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("expected status %d, got %d body=%s", http.StatusOK, recorder.Code, recorder.Body.String()) } store, err = settings.LoadSessions(configDir, keyBytes) if err != nil { t.Fatalf("LoadSessions after request: %v", err) } if len(store.Sessions) != 1 { t.Fatalf("expected 1 session, got %d", len(store.Sessions)) } if !store.Sessions[0].LastSeenAt.After(originalLastSeen) { t.Fatalf( "expected LastSeenAt after %v, got %v", originalLastSeen, store.Sessions[0].LastSeenAt, ) } }