refactor: use pinned golangci-lint Docker image for linting
All checks were successful
check / check (push) Successful in 1m37s

Refactor Dockerfile to use a separate lint stage with a pinned
golangci-lint v2.11.3 Docker image instead of installing
golangci-lint via curl in the builder stage. This follows the
pattern used by sneak/pixa.

Changes:
- Dockerfile: separate lint stage using golangci/golangci-lint:v2.11.3
  (Debian-based, pinned by sha256) with COPY --from=lint dependency
- Bump Go from 1.24 to 1.26.1 (golang:1.26.1-bookworm, pinned)
- Bump golangci-lint from v1.64.8 to v2.11.3
- Migrate .golangci.yml from v1 to v2 format (same linters, format only)
- All Docker images pinned by sha256 digest
- Fix all lint issues from the v2 linter upgrade:
  - Add package comments to all packages
  - Add doc comments to all exported types, functions, and methods
  - Fix unchecked errors (errcheck)
  - Fix unused parameters (revive)
  - Fix gosec warnings (MaxBytesReader for form parsing)
  - Fix staticcheck suggestions (fmt.Fprintf instead of WriteString)
  - Rename DeliveryTask to Task to avoid stutter (delivery.Task)
  - Rename shadowed builtin 'max' parameter
- Update README.md version requirements
This commit is contained in:
clawbot
2026-03-17 05:46:03 -07:00
parent d771fe14df
commit 32a9170428
59 changed files with 7792 additions and 4282 deletions

View File

@@ -1,6 +1,7 @@
package session
package session_test
import (
"context"
"log/slog"
"net/http"
"net/http/httptest"
@@ -11,15 +12,22 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/webhooker/internal/config"
"sneak.berlin/go/webhooker/internal/session"
)
// testSession creates a Session with a real cookie store for testing.
func testSession(t *testing.T) *Session {
const testKeySize = 32
// testSession creates a Session with a real cookie store for
// testing.
func testSession(t *testing.T) *session.Session {
t.Helper()
key := make([]byte, 32)
key := make([]byte, testKeySize)
for i := range key {
key[i] = byte(i + 42)
}
store := sessions.NewCookieStore(key)
store.Options = &sessions.Options{
Path: "/",
@@ -32,34 +40,47 @@ func testSession(t *testing.T) *Session {
cfg := &config.Config{
Environment: config.EnvironmentDev,
}
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
return NewForTest(store, cfg, log, key)
log := slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{Level: slog.LevelDebug},
))
return session.NewForTest(store, cfg, log, key)
}
// --- Get and Save Tests ---
func TestGet_NewSession(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), 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")
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)
req1 := httptest.NewRequestWithContext(
context.Background(), 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))
@@ -68,26 +89,34 @@ func TestGet_ExistingSession(t *testing.T) {
require.NotEmpty(t, cookies)
// Make a new request with the session cookie
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
req2 := httptest.NewRequestWithContext(
context.Background(), 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.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)
req := httptest.NewRequestWithContext(
context.Background(), 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)
@@ -98,48 +127,73 @@ func TestSave_SetsCookie(t *testing.T) {
// Verify the cookie has the expected name
var found bool
for _, c := range cookies {
if c.Name == SessionName {
if c.Name == session.SessionName {
found = true
assert.True(t, c.HttpOnly, "session cookie should be HTTP-only")
assert.True(
t, c.HttpOnly,
"session cookie should be HTTP-only",
)
break
}
}
assert.True(t, found, "should find a cookie named %s", SessionName)
assert.True(
t, found,
"should find a cookie named %s", session.SessionName,
)
}
// --- SetUser and User Retrieval Tests ---
func TestSetUser_SetsAllFields(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), 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])
assert.Equal(
t, "user-abc-123", sess.Values[session.UserIDKey],
)
assert.Equal(
t, "alice", sess.Values[session.UsernameKey],
)
assert.Equal(
t, true, sess.Values[session.AuthenticatedKey],
)
}
func TestGetUserID(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), 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.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)
@@ -147,19 +201,25 @@ func TestGetUserID(t *testing.T) {
func TestGetUsername(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), 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.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)
@@ -169,20 +229,29 @@ func TestGetUsername(t *testing.T) {
func TestIsAuthenticated_NoSession(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
sess, err := s.Get(req)
require.NoError(t, err)
assert.False(t, s.IsAuthenticated(sess), "new session should not be authenticated")
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)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
sess, err := s.Get(req)
require.NoError(t, err)
@@ -192,9 +261,12 @@ func TestIsAuthenticated_AfterSetUser(t *testing.T) {
func TestIsAuthenticated_AfterClearUser(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
sess, err := s.Get(req)
require.NoError(t, err)
@@ -202,52 +274,71 @@ func TestIsAuthenticated_AfterClearUser(t *testing.T) {
require.True(t, s.IsAuthenticated(sess))
s.ClearUser(sess)
assert.False(t, s.IsAuthenticated(sess), "should not be authenticated after ClearUser")
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)
req := httptest.NewRequestWithContext(
context.Background(), 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")
sess.Values[session.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)
req := httptest.NewRequestWithContext(
context.Background(), 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]
_, hasUserID := sess.Values[session.UserIDKey]
assert.False(t, hasUserID, "UserIDKey should be removed")
_, hasUsername := sess.Values[UsernameKey]
_, hasUsername := sess.Values[session.UsernameKey]
assert.False(t, hasUsername, "UsernameKey should be removed")
_, hasAuth := sess.Values[AuthenticatedKey]
assert.False(t, hasAuth, "AuthenticatedKey should be removed")
_, hasAuth := sess.Values[session.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)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
sess, err := s.Get(req)
require.NoError(t, err)
@@ -255,11 +346,18 @@ func TestDestroy_InvalidatesSession(t *testing.T) {
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")
// 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]
_, hasUserID := sess.Values[session.UserIDKey]
assert.False(t, hasUserID, "Destroy should clear user ID")
}
@@ -267,10 +365,12 @@ func TestDestroy_InvalidatesSession(t *testing.T) {
func TestSessionPersistence_RoundTrip(t *testing.T) {
t.Parallel()
s := testSession(t)
// Step 1: Create session, set user, save
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
req1 := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
w1 := httptest.NewRecorder()
sess1, err := s.Get(req1)
@@ -281,8 +381,13 @@ func TestSessionPersistence_RoundTrip(t *testing.T) {
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)
// Step 2: New request with cookies -- session data should
// persist
req2 := httptest.NewRequestWithContext(
context.Background(),
http.MethodGet, "/profile", nil,
)
for _, c := range cookies {
req2.AddCookie(c)
}
@@ -290,7 +395,10 @@ func TestSessionPersistence_RoundTrip(t *testing.T) {
sess2, err := s.Get(req2)
require.NoError(t, err)
assert.True(t, s.IsAuthenticated(sess2), "session should be authenticated after round-trip")
assert.True(
t, s.IsAuthenticated(sess2),
"session should be authenticated after round-trip",
)
userID, ok := s.GetUserID(sess2)
assert.True(t, ok)
@@ -305,19 +413,23 @@ func TestSessionPersistence_RoundTrip(t *testing.T) {
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)
assert.Equal(t, "webhooker_session", session.SessionName)
assert.Equal(t, "user_id", session.UserIDKey)
assert.Equal(t, "username", session.UsernameKey)
assert.Equal(t, "authenticated", session.AuthenticatedKey)
}
// --- Edge Cases ---
func TestSetUser_OverwritesPreviousUser(t *testing.T) {
t.Parallel()
s := testSession(t)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
sess, err := s.Get(req)
require.NoError(t, err)
@@ -338,10 +450,12 @@ func TestSetUser_OverwritesPreviousUser(t *testing.T) {
func TestDestroy_ThenSave_DeletesCookie(t *testing.T) {
t.Parallel()
s := testSession(t)
// Create a session
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
req1 := httptest.NewRequestWithContext(
context.Background(), http.MethodGet, "/", nil)
w1 := httptest.NewRecorder()
sess, err := s.Get(req1)
@@ -353,10 +467,15 @@ func TestDestroy_ThenSave_DeletesCookie(t *testing.T) {
require.NotEmpty(t, cookies)
// Destroy and save
req2 := httptest.NewRequest(http.MethodGet, "/logout", nil)
req2 := httptest.NewRequestWithContext(
context.Background(),
http.MethodGet, "/logout", nil,
)
for _, c := range cookies {
req2.AddCookie(c)
}
w2 := httptest.NewRecorder()
sess2, err := s.Get(req2)
@@ -364,15 +483,25 @@ func TestDestroy_ThenSave_DeletesCookie(t *testing.T) {
s.Destroy(sess2)
require.NoError(t, s.Save(req2, w2, sess2))
// The cookie should have MaxAge = -1 (browser should delete it)
// The cookie should have MaxAge = -1 (browser should delete)
responseCookies := w2.Result().Cookies()
var sessionCookie *http.Cookie
for _, c := range responseCookies {
if c.Name == SessionName {
if c.Name == session.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")
require.NotNil(
t, sessionCookie,
"should have a session cookie in response",
)
assert.Negative(
t, sessionCookie.MaxAge,
"destroyed session cookie should have negative MaxAge",
)
}