test: add tests for delivery, middleware, and session packages
All checks were successful
check / check (push) Successful in 4s
All checks were successful
check / check (push) Successful in 4s
Add comprehensive test coverage for three previously-untested packages: delivery (37% → 75%): - processNewTask with inline and large (DB-fetched) bodies - processRetryTask success, skip non-retrying, large body fetch - Worker lifecycle start/stop, retry channel processing - processDelivery unknown target type handling - recoverPendingDeliveries, recoverWebhookDeliveries, recoverInFlight - HTTP delivery with custom headers, timeout, invalid config - Notify batching middleware (0% → 70%): - Logging middleware status code capture and pass-through - LoggingResponseWriter delegation - CORS dev mode (allow-all) and prod mode (no-op) - RequireAuth redirect for unauthenticated, pass-through for authenticated - MetricsAuth basic auth validation - ipFromHostPort helper session (0% → 52%): - Get/Save round-trip with real cookie store - SetUser, GetUserID, GetUsername, IsAuthenticated - ClearUser removes all keys - Destroy invalidates session (MaxAge -1) - Session persistence across requests - Edge cases: overwrite user, wrong type, constants Test helpers added: - database.NewTestDatabase / NewTestWebhookDBManager for cross-package testing - session.NewForTest for middleware tests without fx lifecycle closes #28
This commit is contained in:
378
internal/session/session_test.go
Normal file
378
internal/session/session_test.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user