package session import ( "log/slog" "net/http" "net/http/httptest" "os" "testing" "github.com/gorilla/sessions" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/config" ) // testSession creates a Session with a real cookie store for testing. func testSession(t *testing.T) *Session { t.Helper() key := make([]byte, 32) for i := range key { key[i] = byte(i + 42) } store := sessions.NewCookieStore(key) store.Options = &sessions.Options{ Path: "/", MaxAge: 86400 * 7, HttpOnly: true, Secure: false, SameSite: http.SameSiteLaxMode, } cfg := &config.Config{ Environment: config.EnvironmentDev, } log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})) return NewForTest(store, cfg, log) } // --- Get and Save Tests --- func TestGet_NewSession(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) require.NotNil(t, sess) assert.True(t, sess.IsNew, "session should be new when no cookie is present") } func TestGet_ExistingSession(t *testing.T) { t.Parallel() s := testSession(t) // Create and save a session req1 := httptest.NewRequest(http.MethodGet, "/", nil) w1 := httptest.NewRecorder() sess1, err := s.Get(req1) require.NoError(t, err) sess1.Values["test_key"] = "test_value" require.NoError(t, s.Save(req1, w1, sess1)) // Extract cookies cookies := w1.Result().Cookies() require.NotEmpty(t, cookies) // Make a new request with the session cookie req2 := httptest.NewRequest(http.MethodGet, "/", nil) for _, c := range cookies { req2.AddCookie(c) } sess2, err := s.Get(req2) require.NoError(t, err) assert.False(t, sess2.IsNew, "session should not be new when cookie is present") assert.Equal(t, "test_value", sess2.Values["test_key"]) } func TestSave_SetsCookie(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() sess, err := s.Get(req) require.NoError(t, err) sess.Values["key"] = "value" err = s.Save(req, w, sess) require.NoError(t, err) cookies := w.Result().Cookies() require.NotEmpty(t, cookies, "Save should set a cookie") // Verify the cookie has the expected name var found bool for _, c := range cookies { if c.Name == SessionName { found = true assert.True(t, c.HttpOnly, "session cookie should be HTTP-only") break } } assert.True(t, found, "should find a cookie named %s", SessionName) } // --- SetUser and User Retrieval Tests --- func TestSetUser_SetsAllFields(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-abc-123", "alice") assert.Equal(t, "user-abc-123", sess.Values[UserIDKey]) assert.Equal(t, "alice", sess.Values[UsernameKey]) assert.Equal(t, true, sess.Values[AuthenticatedKey]) } func TestGetUserID(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) // Before setting user userID, ok := s.GetUserID(sess) assert.False(t, ok, "should return false when no user ID is set") assert.Empty(t, userID) // After setting user s.SetUser(sess, "user-xyz", "bob") userID, ok = s.GetUserID(sess) assert.True(t, ok) assert.Equal(t, "user-xyz", userID) } func TestGetUsername(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) // Before setting user username, ok := s.GetUsername(sess) assert.False(t, ok, "should return false when no username is set") assert.Empty(t, username) // After setting user s.SetUser(sess, "user-xyz", "bob") username, ok = s.GetUsername(sess) assert.True(t, ok) assert.Equal(t, "bob", username) } // --- IsAuthenticated Tests --- func TestIsAuthenticated_NoSession(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) assert.False(t, s.IsAuthenticated(sess), "new session should not be authenticated") } func TestIsAuthenticated_AfterSetUser(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-123", "alice") assert.True(t, s.IsAuthenticated(sess)) } func TestIsAuthenticated_AfterClearUser(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-123", "alice") require.True(t, s.IsAuthenticated(sess)) s.ClearUser(sess) assert.False(t, s.IsAuthenticated(sess), "should not be authenticated after ClearUser") } func TestIsAuthenticated_WrongType(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) // Set authenticated to a non-bool value sess.Values[AuthenticatedKey] = "yes" assert.False(t, s.IsAuthenticated(sess), "should return false for non-bool authenticated value") } // --- ClearUser Tests --- func TestClearUser_RemovesAllKeys(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-123", "alice") s.ClearUser(sess) _, hasUserID := sess.Values[UserIDKey] assert.False(t, hasUserID, "UserIDKey should be removed") _, hasUsername := sess.Values[UsernameKey] assert.False(t, hasUsername, "UsernameKey should be removed") _, hasAuth := sess.Values[AuthenticatedKey] assert.False(t, hasAuth, "AuthenticatedKey should be removed") } // --- Destroy Tests --- func TestDestroy_InvalidatesSession(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-123", "alice") s.Destroy(sess) // After Destroy: MaxAge should be -1 (delete cookie) and user data cleared assert.Equal(t, -1, sess.Options.MaxAge, "Destroy should set MaxAge to -1") assert.False(t, s.IsAuthenticated(sess), "should not be authenticated after Destroy") _, hasUserID := sess.Values[UserIDKey] assert.False(t, hasUserID, "Destroy should clear user ID") } // --- Session Persistence Round-Trip --- func TestSessionPersistence_RoundTrip(t *testing.T) { t.Parallel() s := testSession(t) // Step 1: Create session, set user, save req1 := httptest.NewRequest(http.MethodGet, "/", nil) w1 := httptest.NewRecorder() sess1, err := s.Get(req1) require.NoError(t, err) s.SetUser(sess1, "user-round-trip", "charlie") require.NoError(t, s.Save(req1, w1, sess1)) cookies := w1.Result().Cookies() require.NotEmpty(t, cookies) // Step 2: New request with cookies — session data should persist req2 := httptest.NewRequest(http.MethodGet, "/profile", nil) for _, c := range cookies { req2.AddCookie(c) } sess2, err := s.Get(req2) require.NoError(t, err) assert.True(t, s.IsAuthenticated(sess2), "session should be authenticated after round-trip") userID, ok := s.GetUserID(sess2) assert.True(t, ok) assert.Equal(t, "user-round-trip", userID) username, ok := s.GetUsername(sess2) assert.True(t, ok) assert.Equal(t, "charlie", username) } // --- Constants Tests --- func TestSessionConstants(t *testing.T) { t.Parallel() assert.Equal(t, "webhooker_session", SessionName) assert.Equal(t, "user_id", UserIDKey) assert.Equal(t, "username", UsernameKey) assert.Equal(t, "authenticated", AuthenticatedKey) } // --- Edge Cases --- func TestSetUser_OverwritesPreviousUser(t *testing.T) { t.Parallel() s := testSession(t) req := httptest.NewRequest(http.MethodGet, "/", nil) sess, err := s.Get(req) require.NoError(t, err) s.SetUser(sess, "user-1", "alice") assert.True(t, s.IsAuthenticated(sess)) // Overwrite with a different user s.SetUser(sess, "user-2", "bob") userID, ok := s.GetUserID(sess) assert.True(t, ok) assert.Equal(t, "user-2", userID) username, ok := s.GetUsername(sess) assert.True(t, ok) assert.Equal(t, "bob", username) } func TestDestroy_ThenSave_DeletesCookie(t *testing.T) { t.Parallel() s := testSession(t) // Create a session req1 := httptest.NewRequest(http.MethodGet, "/", nil) w1 := httptest.NewRecorder() sess, err := s.Get(req1) require.NoError(t, err) s.SetUser(sess, "user-123", "alice") require.NoError(t, s.Save(req1, w1, sess)) cookies := w1.Result().Cookies() require.NotEmpty(t, cookies) // Destroy and save req2 := httptest.NewRequest(http.MethodGet, "/logout", nil) for _, c := range cookies { req2.AddCookie(c) } w2 := httptest.NewRecorder() sess2, err := s.Get(req2) require.NoError(t, err) s.Destroy(sess2) require.NoError(t, s.Save(req2, w2, sess2)) // The cookie should have MaxAge = -1 (browser should delete it) responseCookies := w2.Result().Cookies() var sessionCookie *http.Cookie for _, c := range responseCookies { if c.Name == SessionName { sessionCookie = c break } } require.NotNil(t, sessionCookie, "should have a session cookie in response") assert.True(t, sessionCookie.MaxAge < 0, "destroyed session cookie should have negative MaxAge") }