test: add tests for delivery, middleware, and session packages #32

Merged
sneak merged 1 commits from fix/issue-28-test-coverage into main 2026-03-04 12:07:24 +01:00
5 changed files with 1919 additions and 0 deletions
Showing only changes of commit 09dc38faa6 - Show all commits

View File

@@ -0,0 +1,28 @@
package database
import (
"log/slog"
"os"
"gorm.io/gorm"
)
// NewTestDatabase creates a Database wrapper around a pre-opened *gorm.DB.
// Intended for use in tests that need a *database.Database without the
// full fx lifecycle. The caller is responsible for closing the underlying
// sql.DB connection.
func NewTestDatabase(db *gorm.DB) *Database {
return &Database{
db: db,
log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})),
}
}
// NewTestWebhookDBManager creates a WebhookDBManager backed by the given
// data directory. Intended for use in tests without the fx lifecycle.
func NewTestWebhookDBManager(dataDir string) *WebhookDBManager {
return &WebhookDBManager{
dataDir: dataDir,
log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})),
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,471 @@
package middleware
import (
"encoding/base64"
"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"
"sneak.berlin/go/webhooker/internal/session"
)
// testMiddleware creates a Middleware with minimal dependencies for testing.
// It uses a real session.Session backed by an in-memory cookie store.
func testMiddleware(t *testing.T, env string) (*Middleware, *session.Session) {
t.Helper()
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg := &config.Config{
Environment: env,
}
// Create a real session manager with a known key
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
}
sessManager := newTestSession(t, store, cfg, log)
m := &Middleware{
log: log,
params: &MiddlewareParams{
Config: cfg,
},
session: sessManager,
}
return m, sessManager
}
// newTestSession creates a session.Session with a pre-configured cookie store
// for testing. This avoids needing the fx lifecycle and database.
func newTestSession(t *testing.T, store *sessions.CookieStore, cfg *config.Config, log *slog.Logger) *session.Session {
t.Helper()
return session.NewForTest(store, cfg, log)
}
// --- Logging Middleware Tests ---
func TestLogging_SetsStatusCode(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
handler := m.Logging()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
if _, err := w.Write([]byte("created")); err != nil {
return
}
}))
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "created", w.Body.String())
}
func TestLogging_DefaultStatusOK(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
handler := m.Logging()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := w.Write([]byte("ok")); err != nil {
return
}
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// When no explicit WriteHeader is called, default is 200
assert.Equal(t, http.StatusOK, w.Code)
}
func TestLogging_PassesThroughToNext(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
var called bool
handler := m.Logging()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodPost, "/api/webhook", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.True(t, called, "logging middleware should call the next handler")
}
// --- LoggingResponseWriter Tests ---
func TestLoggingResponseWriter_CapturesStatusCode(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
lrw := NewLoggingResponseWriter(w)
// Default should be 200
assert.Equal(t, http.StatusOK, lrw.statusCode)
// WriteHeader should capture the status code
lrw.WriteHeader(http.StatusNotFound)
assert.Equal(t, http.StatusNotFound, lrw.statusCode)
// Underlying writer should also get the status code
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestLoggingResponseWriter_WriteDelegatesToUnderlying(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
lrw := NewLoggingResponseWriter(w)
n, err := lrw.Write([]byte("hello world"))
require.NoError(t, err)
assert.Equal(t, 11, n)
assert.Equal(t, "hello world", w.Body.String())
}
// --- CORS Middleware Tests ---
func TestCORS_DevMode_AllowsAnyOrigin(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
handler := m.CORS()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Preflight request
req := httptest.NewRequest(http.MethodOptions, "/api/test", nil)
req.Header.Set("Origin", "http://localhost:3000")
req.Header.Set("Access-Control-Request-Method", "POST")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// In dev mode, CORS should allow any origin
assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin"))
}
func TestCORS_ProdMode_NoOp(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentProd)
var called bool
handler := m.CORS()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/api/test", nil)
req.Header.Set("Origin", "http://evil.com")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.True(t, called, "prod CORS middleware should pass through to handler")
// In prod, no CORS headers should be set (no-op middleware)
assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin"),
"prod mode should not set CORS headers")
}
// --- RequireAuth Middleware Tests ---
func TestRequireAuth_NoSession_RedirectsToLogin(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
var called bool
handler := m.RequireAuth()(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
called = true
}))
req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.False(t, called, "handler should not be called for unauthenticated request")
assert.Equal(t, http.StatusSeeOther, w.Code)
assert.Equal(t, "/pages/login", w.Header().Get("Location"))
}
func TestRequireAuth_AuthenticatedSession_PassesThrough(t *testing.T) {
t.Parallel()
m, sessManager := testMiddleware(t, config.EnvironmentDev)
var called bool
handler := m.RequireAuth()(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
called = true
}))
// Create an authenticated session by making a request, setting session data,
// and saving the session cookie
setupReq := httptest.NewRequest(http.MethodGet, "/setup", nil)
setupW := httptest.NewRecorder()
sess, err := sessManager.Get(setupReq)
require.NoError(t, err)
sessManager.SetUser(sess, "user-123", "testuser")
require.NoError(t, sessManager.Save(setupReq, setupW, sess))
// Extract the cookie from the setup response
cookies := setupW.Result().Cookies()
require.NotEmpty(t, cookies, "session cookie should be set")
// Make the actual request with the session cookie
req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
for _, c := range cookies {
req.AddCookie(c)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.True(t, called, "handler should be called for authenticated request")
}
func TestRequireAuth_UnauthenticatedSession_RedirectsToLogin(t *testing.T) {
t.Parallel()
m, sessManager := testMiddleware(t, config.EnvironmentDev)
var called bool
handler := m.RequireAuth()(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
called = true
}))
// Create a session but don't authenticate it
setupReq := httptest.NewRequest(http.MethodGet, "/setup", nil)
setupW := httptest.NewRecorder()
sess, err := sessManager.Get(setupReq)
require.NoError(t, err)
// Don't call SetUser — session exists but is not authenticated
require.NoError(t, sessManager.Save(setupReq, setupW, sess))
cookies := setupW.Result().Cookies()
require.NotEmpty(t, cookies)
req := httptest.NewRequest(http.MethodGet, "/dashboard", nil)
for _, c := range cookies {
req.AddCookie(c)
}
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.False(t, called, "handler should not be called for unauthenticated session")
assert.Equal(t, http.StatusSeeOther, w.Code)
assert.Equal(t, "/pages/login", w.Header().Get("Location"))
}
// --- Helper Tests ---
func TestIpFromHostPort(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{"ipv4 with port", "192.168.1.1:8080", "192.168.1.1"},
{"ipv6 with port", "[::1]:8080", "::1"},
{"invalid format", "not-a-host-port", ""},
{"empty string", "", ""},
{"localhost", "127.0.0.1:80", "127.0.0.1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := ipFromHostPort(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// --- MetricsAuth Tests ---
func TestMetricsAuth_ValidCredentials(t *testing.T) {
t.Parallel()
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg := &config.Config{
Environment: config.EnvironmentDev,
MetricsUsername: "admin",
MetricsPassword: "secret",
}
key := make([]byte, 32)
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{Path: "/", MaxAge: 86400}
sessManager := session.NewForTest(store, cfg, log)
m := &Middleware{
log: log,
params: &MiddlewareParams{
Config: cfg,
},
session: sessManager,
}
var called bool
handler := m.MetricsAuth()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
req.SetBasicAuth("admin", "secret")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.True(t, called, "handler should be called with valid basic auth")
assert.Equal(t, http.StatusOK, w.Code)
}
func TestMetricsAuth_InvalidCredentials(t *testing.T) {
t.Parallel()
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg := &config.Config{
Environment: config.EnvironmentDev,
MetricsUsername: "admin",
MetricsPassword: "secret",
}
key := make([]byte, 32)
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{Path: "/", MaxAge: 86400}
sessManager := session.NewForTest(store, cfg, log)
m := &Middleware{
log: log,
params: &MiddlewareParams{
Config: cfg,
},
session: sessManager,
}
var called bool
handler := m.MetricsAuth()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
req.SetBasicAuth("admin", "wrong-password")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.False(t, called, "handler should not be called with invalid basic auth")
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestMetricsAuth_NoCredentials(t *testing.T) {
t.Parallel()
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg := &config.Config{
Environment: config.EnvironmentDev,
MetricsUsername: "admin",
MetricsPassword: "secret",
}
key := make([]byte, 32)
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{Path: "/", MaxAge: 86400}
sessManager := session.NewForTest(store, cfg, log)
m := &Middleware{
log: log,
params: &MiddlewareParams{
Config: cfg,
},
session: sessManager,
}
var called bool
handler := m.MetricsAuth()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
called = true
}))
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
// No basic auth header
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.False(t, called, "handler should not be called without credentials")
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// --- CORS Dev Mode Detailed Tests ---
func TestCORS_DevMode_AllowsMethods(t *testing.T) {
t.Parallel()
m, _ := testMiddleware(t, config.EnvironmentDev)
handler := m.CORS()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Preflight for POST
req := httptest.NewRequest(http.MethodOptions, "/api/webhooks", nil)
req.Header.Set("Origin", "http://localhost:5173")
req.Header.Set("Access-Control-Request-Method", "POST")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
allowMethods := w.Header().Get("Access-Control-Allow-Methods")
assert.Contains(t, allowMethods, "POST")
}
// --- Base64 key validation for completeness ---
func TestSessionKeyFormat(t *testing.T) {
t.Parallel()
// Verify that the session initialization correctly validates key format.
// A proper 32-byte key encoded as base64 should work.
key := make([]byte, 32)
for i := range key {
key[i] = byte(i + 1)
}
encoded := base64.StdEncoding.EncodeToString(key)
decoded, err := base64.StdEncoding.DecodeString(encoded)
require.NoError(t, err)
assert.Len(t, decoded, 32)
}

View File

@@ -0,0 +1,378 @@
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")
}

View File

@@ -0,0 +1,19 @@
package session
import (
"log/slog"
"github.com/gorilla/sessions"
"sneak.berlin/go/webhooker/internal/config"
)
// NewForTest creates a Session with a pre-configured cookie store for use
// in tests. This bypasses the fx lifecycle and database dependency, allowing
// middleware and handler tests to use real session functionality.
func NewForTest(store *sessions.CookieStore, cfg *config.Config, log *slog.Logger) *Session {
return &Session{
store: store,
config: cfg,
log: log,
}
}