package session_test import ( "database/sql" "errors" "net/http" "net/http/httptest" "testing" "sneak.berlin/go/pixa/internal/database" "sneak.berlin/go/pixa/internal/session" ) const logoutTestSigningKey = "test-signing-key-12345" // newTestDatabase opens an empty in-memory database with pixa's schema. It // has one connection, as pixad's has: each connection to ":memory:" would // open a database of its own. func newTestDatabase(t *testing.T) *sql.DB { t.Helper() db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatalf("sql.Open() error = %v", err) } db.SetMaxOpenConns(1) t.Cleanup(func() { _ = db.Close() }) err = database.ApplyMigrations(t.Context(), db, nil) if err != nil { t.Fatalf("ApplyMigrations() error = %v", err) } return db } // createSessionCookie logs in with mgr and returns the session cookie it // sets. func createSessionCookie(t *testing.T, mgr *session.Manager) *http.Cookie { t.Helper() rec := httptest.NewRecorder() err := mgr.CreateSession(rec) if err != nil { t.Fatalf("CreateSession() error = %v", err) } for _, c := range rec.Result().Cookies() { if c.Name == session.CookieName { return c } } t.Fatalf("CreateSession() did not set a %q cookie", session.CookieName) return nil } // requestWithCookie returns a GET request carrying cookie, or no cookie when // cookie is nil. func requestWithCookie(t *testing.T, cookie *http.Cookie) *http.Request { t.Helper() req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil) if cookie != nil { req.AddCookie(cookie) } return req } // TestLogout_EndsEverySessionMadeBeforeIt verifies that after a logout a // copy of the session cookie taken before it is refused and a login after it // works, and that both still hold for a new Manager on the same database, as // after a restart. func TestLogout_EndsEverySessionMadeBeforeIt(t *testing.T) { t.Parallel() db := newTestDatabase(t) mgr, err := session.NewManagerWithDatabase( t.Context(), logoutTestSigningKey, db) if err != nil { t.Fatalf("NewManagerWithDatabase() error = %v", err) } copied := createSessionCookie(t, mgr) err = mgr.Logout(httptest.NewRecorder(), requestWithCookie(t, copied)) if err != nil { t.Fatalf("Logout() error = %v", err) } _, err = mgr.ValidateSession(requestWithCookie(t, copied)) t.Logf("cookie copied before the logout: ValidateSession() error = %v", err) if !errors.Is(err, session.ErrInvalidSession) { t.Errorf("cookie copied before the logout: error = %v, want %v", err, session.ErrInvalidSession) } loginAfter := createSessionCookie(t, mgr) _, err = mgr.ValidateSession(requestWithCookie(t, loginAfter)) if err != nil { t.Errorf("login after the logout: ValidateSession() error = %v", err) } restarted, err := session.NewManagerWithDatabase( t.Context(), logoutTestSigningKey, db) if err != nil { t.Fatalf("NewManagerWithDatabase() after restart error = %v", err) } if restarted.IsAuthenticated(requestWithCookie(t, copied)) { t.Error("after a restart, a cookie copied before the logout is accepted") } if !restarted.IsAuthenticated(requestWithCookie(t, loginAfter)) { t.Error("after a restart, a login made after the logout is refused") } } // TestLogout_WithoutSession_EndsNoSession verifies that a logout from a // request without a session cookie ends no session, so someone who has not // logged in cannot end the sessions of those who have. func TestLogout_WithoutSession_EndsNoSession(t *testing.T) { t.Parallel() mgr, err := session.NewManagerWithDatabase( t.Context(), logoutTestSigningKey, newTestDatabase(t)) if err != nil { t.Fatalf("NewManagerWithDatabase() error = %v", err) } cookie := createSessionCookie(t, mgr) err = mgr.Logout(httptest.NewRecorder(), requestWithCookie(t, nil)) if err != nil { t.Fatalf("Logout() error = %v", err) } if !mgr.IsAuthenticated(requestWithCookie(t, cookie)) { t.Error("a logout without a session cookie ended a session") } }