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") + } +}