From 7c6580234b5dca40846b60d402576baffdf519a1 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 05:17:58 +0000 Subject: [PATCH] Test a logout after a restart and one that cannot be saved (part of #256) The new server test logs in and out on pixa built as pixad builds it, starts pixa again on the same state directory, and checks that a cookie copied before the logout is refused, which fails if pixad keeps logouts in memory only. The new handler test closes the database so the logout cannot be saved, and checks for 500, no session cookie set, and the session still valid. Model: opus-5-5 --- .../handlers/auth_session_internal_test.go | 55 +++++++++ internal/server/logout_internal_test.go | 112 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 internal/server/logout_internal_test.go diff --git a/internal/handlers/auth_session_internal_test.go b/internal/handlers/auth_session_internal_test.go index bf95d9d..c0451c4 100644 --- a/internal/handlers/auth_session_internal_test.go +++ b/internal/handlers/auth_session_internal_test.go @@ -222,6 +222,61 @@ func TestHandleLogout_EndsCopiedSession(t *testing.T) { } } +// TestHandleLogout_SaveFails_Answers500 verifies that when the logout cannot +// be saved in the database, GET /logout answers 500, leaves the session +// cookie in place and ends no session. +func TestHandleLogout_SaveFails_Answers500(t *testing.T) { + t.Parallel() + + h, srv := newCSRFTestRouter(t) + + db := setupTestDB(t) + + sessMgr, err := session.NewManagerWithDatabase( + t.Context(), testSigningKey, db) + if err != nil { + t.Fatalf("session.NewManagerWithDatabase() error = %v", err) + } + + h.sessMgr = sessMgr + cookie := newSessionCookie(t, h) + + // Saving the logout fails on a closed database. + err = db.Close() + if err != nil { + t.Fatalf("closing the database: %v", err) + } + + req := httptest.NewRequestWithContext( + t.Context(), http.MethodGet, "/logout", nil) + req.AddCookie(cookie) + + rec := httptest.NewRecorder() + h.HandleLogout().ServeHTTP(rec, req) + + t.Logf("GET /logout: status %d, Set-Cookie %q", + rec.Code, rec.Header().Get("Set-Cookie")) + + if rec.Code != http.StatusInternalServerError { + t.Errorf("GET /logout status = %d, want %d", + rec.Code, http.StatusInternalServerError) + } + + if c := findSessionCookie(rec); c != nil { + t.Errorf("a logout that failed set the session cookie: %s", c) + } + + req = httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) + req.AddCookie(cookie) + + rec = httptest.NewRecorder() + srv.ServeHTTP(rec, req) + + if !strings.Contains(rec.Body.String(), generatorForm) { + t.Error("a logout that failed ended the session") + } +} + // TestGeneratePost_NoSession_RedirectsToLogin verifies that POST /generate // with a valid CSRF token but no login session answers 303 to / and makes no // URL. diff --git a/internal/server/logout_internal_test.go b/internal/server/logout_internal_test.go new file mode 100644 index 0000000..360112b --- /dev/null +++ b/internal/server/logout_internal_test.go @@ -0,0 +1,112 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "go.uber.org/fx" + "go.uber.org/fx/fxtest" + + "sneak.berlin/go/pixa/internal/config" + "sneak.berlin/go/pixa/internal/database" + "sneak.berlin/go/pixa/internal/globals" + "sneak.berlin/go/pixa/internal/handlers" + "sneak.berlin/go/pixa/internal/healthcheck" + "sneak.berlin/go/pixa/internal/logger" + "sneak.berlin/go/pixa/internal/middleware" + "sneak.berlin/go/pixa/internal/session" +) + +// startPixa starts the components pixad's fx app builds, from cfg, and +// returns the server with its routes, as newTestServer does. +func startPixa(t *testing.T, cfg *config.Config) *Server { + t.Helper() + + var ( + h *handlers.Handlers + mw *middleware.Middleware + ) + + app := fxtest.New(t, + fx.Supply(cfg), + fx.Provide( + globals.New, + logger.New, + database.New, + healthcheck.New, + handlers.New, + middleware.New, + ), + fx.Populate(&h, &mw), + ) + app.RequireStart() + t.Cleanup(app.RequireStop) + + s := &Server{config: cfg, mw: mw, h: h} + s.SetupRoutes() + + return s +} + +// opensGeneratorPage reports whether GET / with cookie shows s's generator +// page, which only a valid login session opens. +func opensGeneratorPage(t *testing.T, s *Server, cookie *http.Cookie) bool { + t.Helper() + + req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) + req.AddCookie(cookie) + + rec := httptest.NewRecorder() + s.ServeHTTP(rec, req) + + return rec.Code == http.StatusOK && + strings.Contains(rec.Body.String(), `action="/generate"`) +} + +// TestLogoutOutlastsRestart verifies that a copy of the session cookie taken +// before /logout is still refused by a pixa started again on the same state +// directory, as pixad is after a restart. +func TestLogoutOutlastsRestart(t *testing.T) { + t.Parallel() + + s := newTestServer(t) + + login := postLogin(t, s, firstClient, "", testSigningKey) + + var copied *http.Cookie + + for _, c := range login.Result().Cookies() { + if c.Name == session.CookieName { + copied = c + } + } + + if copied == nil || !opensGeneratorPage(t, s, copied) { + t.Fatal("logging in gave no session cookie that opens the generator page") + } + + req := httptest.NewRequestWithContext( + t.Context(), http.MethodGet, "/logout", nil) + req.AddCookie(copied) + + logout := httptest.NewRecorder() + s.ServeHTTP(logout, req) + + if logout.Code != http.StatusSeeOther { + t.Fatalf("GET /logout status = %d, want %d", + logout.Code, http.StatusSeeOther) + } + + restarted := startPixa(t, s.config) + + opens := opensGeneratorPage(t, restarted, copied) + t.Logf("after the restart, the copied cookie opens the generator page: %v", + opens) + + if opens { + t.Error("after a restart, a cookie copied before /logout " + + "still opens the generator page") + } +}