Update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 2m51s

Bump the golangci-lint Docker image pin in Dockerfile and the
release-archive sha256 pins in script/bootstrap from 2.11.3 to
2.12.2, and replace .golangci.yml with the canonical config. The
canonical config moves lll/funlen/cyclop/dupl settings from the
top-level linters-settings key (ignored by the v2 schema) to
linters.settings, so those thresholds now actually apply.

Fix all findings the newly applied thresholds surfaced:

- lll: wrap or shorten seven over-length lines (struct tag
  comments, test logger construction, a func signature, and a
  nosec comment)
- goconst: use http.MethodPost/http.MethodPut and new shared
  constants for repeated test strings; add tmplKeyError and
  tmplKeyWebhook constants for template data keys in handlers
- dupl: merge buildHTTPTargetConfig and buildSlackTargetConfig
  into a parameterized buildURLTargetConfig; drop the duplicate
  iWebhookDB test helper in favor of testWebhookDB; extract
  shared helpers in middleware and session tests
This commit is contained in:
2026-08-07 16:51:42 +00:00
parent 81413c56e9
commit 641a8ebd66
21 changed files with 214 additions and 233 deletions

View File

@@ -1,5 +1,9 @@
version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run:
timeout: 5m
modules-download-mode: readonly
@@ -14,19 +18,17 @@ linters:
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
linters-settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0

View File

@@ -1,8 +1,8 @@
# Lint stage
# golangci/golangci-lint:v2.11.3 (Debian-based), 2026-03-17
# golangci/golangci-lint:v2.12.2 (Debian-based), 2026-08-07
# Using Debian-based image because mattn/go-sqlite3 (CGO) does not
# compile on Alpine musl (off64_t is a glibc type).
FROM golangci/golangci-lint:v2.11.3@sha256:e838e8ab68aaefe83e2408691510867ade9329c0e0b895a3fb35eb93d1c2a4ba AS lint
FROM golangci/golangci-lint:v2.12.2@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
RUN apt-get update && apt-get install -y --no-install-recommends make && rm -rf /var/lib/apt/lists/*

View File

@@ -28,6 +28,11 @@ databases currently grow without bound.
# Completed Steps
- 2026-08-07 Update golangci-lint to v2.12.2 (Docker image digest in
`Dockerfile`, release-archive sha256 pins in `script/bootstrap`),
adopt the canonical `.golangci.yml` (v2 `linters.settings` layout so
`lll`/`funlen`/`cyclop`/`dupl` thresholds actually apply), and fix
all newly surfaced lint findings
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
Makefile shims, README Entrypoints section
- 2026-03-25 pin golangci-lint Docker image for linting (#55)

View File

@@ -11,6 +11,15 @@ import (
"sneak.berlin/go/webhooker/internal/logger"
)
const (
// testAppname is the Globals.Appname used in tests.
testAppname = "webhooker-test"
// testVersion is the Globals.Version used in tests.
testVersion = "test"
// testContentType is the event content type used in tests.
testContentType = "application/json"
)
func setupTestDB(
t *testing.T,
) (*database.Database, *fxtest.Lifecycle) {
@@ -19,8 +28,8 @@ func setupTestDB(
lc := fxtest.NewLifecycle(t)
g := &globals.Globals{
Appname: "webhooker-test",
Version: "test",
Appname: testAppname,
Version: testVersion,
}
l, err := logger.New(

View File

@@ -4,10 +4,13 @@ package database
type Entrypoint struct {
BaseModel
WebhookID string `gorm:"type:uuid;not null" json:"webhookId"`
Path string `gorm:"uniqueIndex;not null" json:"path"` // URL path for this entrypoint
WebhookID string `gorm:"type:uuid;not null" json:"webhookId"`
// Path is the URL path for this entrypoint.
Path string `gorm:"uniqueIndex;not null" json:"path"`
Description string `json:"description"`
Active bool `gorm:"default:true" json:"active"`
Active bool `gorm:"default:true" json:"active"`
// Relations
Webhook Webhook `json:"webhook,omitzero"`

View File

@@ -23,7 +23,8 @@ type Target struct {
// Configuration fields (JSON stored based on type)
Config string `gorm:"type:text" json:"config"` // JSON configuration
// For HTTP targets (max_retries=0 means fire-and-forget, >0 enables retries with backoff)
// For HTTP targets (max_retries=0 means fire-and-forget,
// >0 enables retries with backoff)
MaxRetries int `json:"maxRetries,omitempty"`
MaxQueueSize int `json:"maxQueueSize,omitempty"`

View File

@@ -4,10 +4,12 @@ package database
type Webhook struct {
BaseModel
UserID string `gorm:"type:uuid;not null" json:"userId"`
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
RetentionDays int `gorm:"default:30" json:"retentionDays"` // Days to retain events
UserID string `gorm:"type:uuid;not null" json:"userId"`
Name string `gorm:"not null" json:"name"`
Description string `json:"description"`
// RetentionDays is the number of days to retain events.
RetentionDays int `gorm:"default:30" json:"retentionDays"`
// Relations
User User `json:"user,omitzero"`

View File

@@ -2,6 +2,7 @@ package database_test
import (
"context"
"net/http"
"testing"
"time"
@@ -30,8 +31,8 @@ func setupRetentionTest(t *testing.T) *retentionTestEnv {
lc := fxtest.NewLifecycle(t)
g := &globals.Globals{
Appname: "webhooker-test",
Version: "test",
Appname: testAppname,
Version: testVersion,
}
l, err := logger.New(lc, logger.LoggerParams{Globals: g})
@@ -117,9 +118,9 @@ func seedEventChain(
event := &database.Event{
WebhookID: webhookID,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Body: `{"seed": true}`,
ContentType: "application/json",
ContentType: testContentType,
}
event.CreatedAt = createdAt
require.NoError(t, db.Create(event).Error)

View File

@@ -13,8 +13,11 @@ import (
// 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})),
db: db,
log: slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{Level: slog.LevelDebug},
)),
}
}
@@ -23,6 +26,9 @@ func NewTestDatabase(db *gorm.DB) *Database {
func NewTestWebhookDBManager(dataDir string) *WebhookDBManager {
return &WebhookDBManager{
dataDir: dataDir,
log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})),
log: slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{Level: slog.LevelDebug},
)),
}
}

View File

@@ -2,6 +2,7 @@ package database_test
import (
"context"
"net/http"
"os"
"path/filepath"
"testing"
@@ -25,8 +26,8 @@ func setupTestWebhookDBManager(
lc := fxtest.NewLifecycle(t)
g := &globals.Globals{
Appname: "webhooker-test",
Version: "test",
Appname: testAppname,
Version: testVersion,
}
l, err := logger.New(
@@ -83,10 +84,10 @@ func TestWebhookDBManager_CreateAndGetDB(t *testing.T) {
event := &database.Event{
WebhookID: webhookID,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Headers: `{"Content-Type":["application/json"]}`,
Body: `{"test": true}`,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(event).Error)
assert.NotEmpty(t, event.ID)
@@ -99,7 +100,7 @@ func TestWebhookDBManager_CreateAndGetDB(t *testing.T) {
db.First(&readEvent, "id = ?", event.ID).Error,
)
assert.Equal(t, webhookID, readEvent.WebhookID)
assert.Equal(t, "POST", readEvent.Method)
assert.Equal(t, http.MethodPost, readEvent.Method)
assert.Equal(t, `{"test": true}`, readEvent.Body)
}
@@ -123,9 +124,9 @@ func TestWebhookDBManager_DeleteDB(t *testing.T) {
event := &database.Event{
WebhookID: webhookID,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Body: `{"test": true}`,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(event).Error)
@@ -196,10 +197,10 @@ func seedDeliveryWorkflow(
event := &database.Event{
WebhookID: webhookID,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Headers: `{"Content-Type":["application/json"]}`,
Body: `{"payload": "test"}`,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(event).Error)
@@ -231,7 +232,7 @@ func verifyPendingDeliveries(
)
require.Len(t, pending, 1)
assert.Equal(t, event.ID, pending[0].EventID)
assert.Equal(t, "POST", pending[0].Event.Method)
assert.Equal(t, http.MethodPost, pending[0].Event.Method)
}
func completeDelivery(
@@ -303,16 +304,16 @@ func TestWebhookDBManager_MultipleWebhooks(t *testing.T) {
event1 := &database.Event{
WebhookID: webhook1,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Body: `{"webhook": 1}`,
ContentType: "application/json",
ContentType: testContentType,
}
event2 := &database.Event{
WebhookID: webhook2,
EntrypointID: uuid.New().String(),
Method: "PUT",
Method: http.MethodPut,
Body: `{"webhook": 2}`,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db1.Create(event1).Error)

View File

@@ -126,36 +126,6 @@ func iHTTPConfig(url string) string {
return string(data)
}
func iWebhookDB(t *testing.T) *gorm.DB {
t.Helper()
dbPath := filepath.Join(
t.TempDir(), "events-test.db",
)
dsn := fmt.Sprintf(
"file:%s?cache=shared&mode=rwc", dbPath,
)
sqlDB, err := sql.Open("sqlite", dsn)
require.NoError(t, err)
t.Cleanup(func() { _ = sqlDB.Close() })
db, err := gorm.Open(
sqlite.Dialector{Conn: sqlDB}, &gorm.Config{},
)
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(
&database.Event{},
&database.Delivery{},
&database.DeliveryResult{},
))
return db
}
func iEngine(
t *testing.T, workers int,
) *delivery.Engine {
@@ -182,10 +152,10 @@ func iSeedEvent(
event := database.Event{
WebhookID: webhookID,
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Headers: `{}`,
Body: body,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(&event).Error)
@@ -935,7 +905,7 @@ func TestDeliverHTTP_CustomTargetHeaders(t *testing.T) {
func TestDeliverHTTP_TargetTimeout(t *testing.T) {
t.Parallel()
db := iWebhookDB(t)
db := testWebhookDB(t)
e := iEngine(t, 1)
ts := httptest.NewServer(
@@ -987,10 +957,10 @@ func iSeedEventAndDelivery(
event := database.Event{
WebhookID: uuid.New().String(),
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Headers: `{"Content-Type":["application/json"]}`,
Body: body,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(&event).Error)
@@ -1067,7 +1037,7 @@ func iAssertResultFailed(
func TestDeliverHTTP_InvalidConfig(t *testing.T) {
t.Parallel()
db := iWebhookDB(t)
db := testWebhookDB(t)
e := iEngine(t, 1)
event, del := iSeedEventAndDelivery(

View File

@@ -27,6 +27,9 @@ import (
"sneak.berlin/go/webhooker/internal/delivery"
)
// testContentType is the event content type used in tests.
const testContentType = "application/json"
func testWebhookDB(t *testing.T) *gorm.DB {
t.Helper()
@@ -94,10 +97,10 @@ func seedEvent(
event := database.Event{
WebhookID: uuid.New().String(),
EntrypointID: uuid.New().String(),
Method: "POST",
Method: http.MethodPost,
Headers: `{"Content-Type":["application/json"]}`,
Body: body,
ContentType: "application/json",
ContentType: testContentType,
}
require.NoError(t, db.Create(&event).Error)
@@ -1117,10 +1120,10 @@ func TestDoHTTPRequest_ForwardsHeaders(t *testing.T) {
}
event := &database.Event{
Method: "POST",
Method: http.MethodPost,
Headers: `{"X-Custom":["value1"],"Content-Type":["application/json"]}`,
Body: `{"test":true}`,
ContentType: "application/json",
ContentType: testContentType,
}
statusCode, _, _, err := e.ExportDoHTTPRequest(
@@ -1142,7 +1145,7 @@ func TestDoHTTPRequest_ForwardsHeaders(t *testing.T) {
)
assert.Equal(t,
"application/json",
testContentType,
receivedHeaders.Get("Content-Type"),
)
@@ -1289,8 +1292,8 @@ func TestFormatSlackMessage_JSONBody(t *testing.T) {
t.Parallel()
event := &database.Event{
Method: "POST",
ContentType: "application/json",
Method: http.MethodPost,
ContentType: testContentType,
Body: `{"action":"push",` +
`"repo":"test/repo",` +
`"ref":"refs/heads/main"}`,
@@ -1315,7 +1318,7 @@ func TestFormatSlackMessage_NonJSONBody(t *testing.T) {
t.Parallel()
event := &database.Event{
Method: "POST",
Method: http.MethodPost,
ContentType: "text/plain",
Body: "hello world plain text",
}
@@ -1338,8 +1341,8 @@ func TestFormatSlackMessage_EmptyBody(t *testing.T) {
t.Parallel()
event := &database.Event{
Method: "POST",
ContentType: "application/json",
Method: http.MethodPost,
ContentType: testContentType,
Body: "",
}
event.CreatedAt = time.Date(
@@ -1367,8 +1370,8 @@ func TestFormatSlackMessage_LargeJSONTruncated(
require.NoError(t, err)
event := &database.Event{
Method: "POST",
ContentType: "application/json",
Method: http.MethodPost,
ContentType: testContentType,
Body: string(largeJSON),
}
event.CreatedAt = time.Date(
@@ -1697,7 +1700,7 @@ func assertLogLineComplete(
"log line must contain the webhook id",
)
assert.Contains(t, out, "application/json",
assert.Contains(t, out, testContentType,
"log line must contain the content type",
)
}

View File

@@ -495,5 +495,5 @@ func applyRequestHeaders(
func executeHTTPRequest(
client *http.Client, req *http.Request,
) (*http.Response, error) {
return client.Do(req) //#nosec G704 -- URL validated by parseHTTPConfig/parseSlackConfig and SSRF-safe transport
return client.Do(req) //#nosec G704 -- validated URL, SSRF-safe transport
}

View File

@@ -19,7 +19,7 @@ func (h *Handlers) HandleLoginPage() http.HandlerFunc {
// Render login page
data := map[string]any{
"Error": "",
tmplKeyError: "",
}
h.renderTemplate(w, r, "login.html", data)
@@ -86,7 +86,7 @@ func (h *Handlers) renderLoginError(
status int,
) {
data := map[string]any{
"Error": msg,
tmplKeyError: msg,
}
w.WriteHeader(status)

View File

@@ -13,12 +13,16 @@ func (s *Handlers) RenderTemplateForTest(
s.renderTemplate(w, r, pageTemplate, data)
}
// BuildSlackTargetConfigForTest exposes buildSlackTargetConfig
// for use in the handlers_test package.
// BuildSlackTargetConfigForTest exposes buildURLTargetConfig
// with the Slack target parameters for use in the
// handlers_test package.
func (s *Handlers) BuildSlackTargetConfigForTest(
w http.ResponseWriter,
r *http.Request,
targetURL string,
) (string, error) {
return s.buildSlackTargetConfig(w, r, targetURL)
return s.buildURLTargetConfig(
w, r, targetURL, "webhookUrl",
"Webhook URL is required for Slack targets",
)
}

View File

@@ -30,6 +30,11 @@ const (
defaultRetentionDays = 30
// paginationPerPage is the number of items per page.
paginationPerPage = 25
// tmplKeyError is the template data key for an error message.
tmplKeyError = "Error"
// tmplKeyWebhook is the template data key for a webhook.
tmplKeyWebhook = "Webhook"
)
// errInvalidPassword is returned when a password does not match.

View File

@@ -106,7 +106,7 @@ func (h *Handlers) buildWebhookListItems(
func (h *Handlers) HandleSourceCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := map[string]any{
"Error": "",
tmplKeyError: "",
}
h.renderTemplate(w, r, "sources_new.html", data)
@@ -145,7 +145,7 @@ func (h *Handlers) HandleSourceCreateSubmit() http.HandlerFunc {
if name == "" {
data := map[string]any{
"Error": "Name is required",
tmplKeyError: "Name is required",
}
w.WriteHeader(http.StatusBadRequest)
@@ -315,11 +315,11 @@ func (h *Handlers) renderSourceDetail(
}
data := map[string]any{
"Webhook": webhook,
"Entrypoints": entrypoints,
"Targets": targets,
"Events": events,
"BaseURL": scheme + "://" + host,
tmplKeyWebhook: webhook,
"Entrypoints": entrypoints,
"Targets": targets,
"Events": events,
"BaseURL": scheme + "://" + host,
}
h.renderTemplate(w, r, "source_detail.html", data)
@@ -351,8 +351,8 @@ func (h *Handlers) HandleSourceEdit() http.HandlerFunc {
}
data := map[string]any{
"Webhook": webhook,
"Error": "",
tmplKeyWebhook: webhook,
tmplKeyError: "",
}
h.renderTemplate(w, r, "source_edit.html", data)
@@ -415,8 +415,8 @@ func (h *Handlers) applyWebhookEdit(
name := r.FormValue("name")
if name == "" {
data := map[string]any{
"Webhook": *webhook,
"Error": "Name is required",
tmplKeyWebhook: *webhook,
tmplKeyError: "Name is required",
}
w.WriteHeader(http.StatusBadRequest)
@@ -589,15 +589,15 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
}
data := map[string]any{
"Webhook": webhook,
"Events": evts,
"Page": page,
"TotalPages": totalPages,
"TotalEvents": total,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
tmplKeyWebhook: webhook,
"Events": evts,
"Page": page,
"TotalPages": totalPages,
"TotalEvents": total,
"HasPrev": page > 1,
"HasNext": page < totalPages,
"PrevPage": page - 1,
"NextPage": page + 1,
}
h.renderTemplate(w, r, "source_logs.html", data)
@@ -900,9 +900,15 @@ func (h *Handlers) buildTargetConfig(
) (string, error) {
switch targetType {
case database.TargetTypeHTTP:
return h.buildHTTPTargetConfig(w, r, targetURL)
return h.buildURLTargetConfig(
w, r, targetURL, "url",
"URL is required for HTTP targets",
)
case database.TargetTypeSlack:
return h.buildSlackTargetConfig(w, r, targetURL)
return h.buildURLTargetConfig(
w, r, targetURL, "webhookUrl",
"Webhook URL is required for Slack targets",
)
case database.TargetTypeDatabase, database.TargetTypeLog:
return "", nil
default:
@@ -915,16 +921,18 @@ func (h *Handlers) buildTargetConfig(
}
}
// buildHTTPTargetConfig builds config JSON for an HTTP target.
func (h *Handlers) buildHTTPTargetConfig(
// buildURLTargetConfig builds config JSON for a target whose
// configuration is a single SSRF-validated URL stored under
// configKey. missingMsg is the error shown when no URL is given.
func (h *Handlers) buildURLTargetConfig(
w http.ResponseWriter,
r *http.Request,
targetURL string,
targetURL, configKey, missingMsg string,
) (string, error) {
if targetURL == "" {
http.Error(
w,
"URL is required for HTTP targets",
missingMsg,
http.StatusBadRequest,
)
@@ -949,56 +957,7 @@ func (h *Handlers) buildHTTPTargetConfig(
return "", err
}
cfg := map[string]any{"url": targetURL}
configBytes, err := json.Marshal(cfg)
if err != nil {
http.Error(
w, "Internal server error",
http.StatusInternalServerError,
)
return "", err
}
return string(configBytes), nil
}
// buildSlackTargetConfig builds config JSON for a Slack target.
func (h *Handlers) buildSlackTargetConfig(
w http.ResponseWriter,
r *http.Request,
targetURL string,
) (string, error) {
if targetURL == "" {
http.Error(
w,
"Webhook URL is required for Slack targets",
http.StatusBadRequest,
)
return "", errMissingURL
}
err := delivery.ValidateTargetURL(
r.Context(), targetURL,
)
if err != nil {
h.log.Warn(
"target URL blocked by SSRF protection",
"url", targetURL,
"error", err,
)
http.Error(
w,
"Invalid target URL: "+err.Error(),
http.StatusBadRequest,
)
return "", err
}
cfg := map[string]any{"webhookUrl": targetURL}
cfg := map[string]any{configKey: targetURL}
configBytes, err := json.Marshal(cfg)
if err != nil {

View File

@@ -484,8 +484,13 @@ func metricsAuthMiddleware(
return middleware.NewForTest(log, cfg, sessManager)
}
func TestMetricsAuth_ValidCredentials(t *testing.T) {
t.Parallel()
// runMetricsAuthRequest sends a GET /metrics request with the
// given basic-auth password through MetricsAuth and reports
// whether the wrapped handler ran plus the recorded response.
func runMetricsAuthRequest(
t *testing.T, password string,
) (bool, *httptest.ResponseRecorder) {
t.Helper()
m := metricsAuthMiddleware(t)
@@ -503,12 +508,20 @@ func TestMetricsAuth_ValidCredentials(t *testing.T) {
context.Background(),
http.MethodGet, "/metrics", nil,
)
req.SetBasicAuth("admin", "secret")
req.SetBasicAuth("admin", password)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
return called, w
}
func TestMetricsAuth_ValidCredentials(t *testing.T) {
t.Parallel()
called, w := runMetricsAuthRequest(t, "secret")
assert.True(
t, called,
"handler should be called with valid basic auth",
@@ -519,27 +532,7 @@ func TestMetricsAuth_ValidCredentials(t *testing.T) {
func TestMetricsAuth_InvalidCredentials(t *testing.T) {
t.Parallel()
m := metricsAuthMiddleware(t)
var called bool
handler := m.MetricsAuth()(http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
called = true
w.WriteHeader(http.StatusOK)
},
))
req := httptest.NewRequestWithContext(
context.Background(),
http.MethodGet, "/metrics", nil,
)
req.SetBasicAuth("admin", "wrong-password")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
called, w := runMetricsAuthRequest(t, "wrong-password")
assert.False(
t, called,

View File

@@ -173,8 +173,18 @@ func TestSetUser_SetsAllFields(t *testing.T) {
)
}
func TestGetUserID(t *testing.T) {
t.Parallel()
// testSessionGetter exercises a session string getter before and
// after SetUser: it must report false with an empty value on a
// fresh session, then true with the expected value once
// SetUser(sess, "user-xyz", "bob") has run.
func testSessionGetter(
t *testing.T,
get func(
*session.Session, *sessions.Session,
) (string, bool),
expected string,
) {
t.Helper()
s := testSession(t)
@@ -185,44 +195,46 @@ func TestGetUserID(t *testing.T) {
require.NoError(t, err)
// Before setting user
userID, ok := s.GetUserID(sess)
val, ok := get(s, sess)
assert.False(
t, ok, "should return false when no user ID is set",
t, ok, "should return false before SetUser",
)
assert.Empty(t, userID)
assert.Empty(t, val)
// After setting user
s.SetUser(sess, "user-xyz", "bob")
userID, ok = s.GetUserID(sess)
val, ok = get(s, sess)
assert.True(t, ok)
assert.Equal(t, "user-xyz", userID)
assert.Equal(t, expected, val)
}
func TestGetUserID(t *testing.T) {
t.Parallel()
testSessionGetter(
t,
func(
s *session.Session, sess *sessions.Session,
) (string, bool) {
return s.GetUserID(sess)
},
"user-xyz",
)
}
func TestGetUsername(t *testing.T) {
t.Parallel()
s := testSession(t)
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",
testSessionGetter(
t,
func(
s *session.Session, sess *sessions.Session,
) (string, bool) {
return s.GetUsername(sess)
},
"bob",
)
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 ---

View File

@@ -12,7 +12,12 @@ import (
// middleware and handler tests to use real session functionality. The key
// parameter is the raw 32-byte authentication key used for session encryption
// and CSRF cookie signing.
func NewForTest(store *sessions.CookieStore, cfg *config.Config, log *slog.Logger, key []byte) *Session {
func NewForTest(
store *sessions.CookieStore,
cfg *config.Config,
log *slog.Logger,
key []byte,
) *Session {
return &Session{
store: store,
key: key,

View File

@@ -10,11 +10,11 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-07. Never "latest"; exact versions only.
GOLANGCI_LINT_VERSION="2.11.3"
# sha256 of golangci-lint-2.11.3-linux-<arch>.tar.gz release archives
GOLANGCI_LINT_SHA256_AMD64="87bb8cddbcc825d5778b64e8a91b46c0526b247f4e2f2904dea74ec7450475d1"
GOLANGCI_LINT_SHA256_ARM64="ee3d95f301359e7d578e6d99c8ad5aeadbabc5a13009a30b2b0df11c8058afe9"
# Pinned versions, 2026-08-07. Never "latest"; exact versions only.
GOLANGCI_LINT_VERSION="2.12.2"
# sha256 of golangci-lint-2.12.2-linux-<arch>.tar.gz release archives
GOLANGCI_LINT_SHA256_AMD64="8df580d2670fed8fa984aac0507099af8df275e665215f5c7a2ae3943893a553"
GOLANGCI_LINT_SHA256_ARM64="44cd40a8c76c86755375adfeea52cfd3533cb43d7bd647771e0ae065e166df3a"
PKGMGR=""
SUDO=""