next -> main (1.0.0 milestone) (#105)
check / check (push) Failing after 0s

Accumulating milestone branch. One squashed commit per closed issue; `next` is kept green and mergeable to `main` at any time without notice.

Landed so far:

- `chore: update golangci-lint to v2.12.2 with canonical config` (#54) — canonical v2-schema `.golangci.yml`, pins bumped in `Dockerfile` and `script/bootstrap`, tree at `0 issues.`. Three behaviour deltas are recorded in that PR's body: `Cache.StoreVariant` takes a context, `MetadataStorage.Store` no longer leaks temp files on failure, and the `signing_key` too-short error text gained a `value too short:` prefix.

Sequencing for the milestone is tracked in #103.

Reviewed-on: #105
Co-authored-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #105.
This commit is contained in:
2026-09-21 09:31:54 +02:00
committed by sneak
parent 63fbc98e63
commit 04b5db6fbf
61 changed files with 3550 additions and 2472 deletions
@@ -0,0 +1,95 @@
package middleware
import (
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"sneak.berlin/go/pixa/internal/config"
)
func TestSecurityHeaders(t *testing.T) {
t.Parallel()
// Create middleware instance
cfg := &config.Config{}
mw := &Middleware{
log: slog.Default(),
config: cfg,
}
// Create a test handler
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Wrap with security headers middleware
handler := mw.SecurityHeaders()(testHandler)
// Make a test request
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// Check security headers
tests := []struct {
header string
want string
}{
{"X-Content-Type-Options", "nosniff"},
{"X-Frame-Options", "DENY"},
{"Referrer-Policy", "strict-origin-when-cross-origin"},
{"X-XSS-Protection", "0"},
}
for _, tt := range tests {
t.Run(tt.header, func(t *testing.T) {
t.Parallel()
got := rec.Header().Get(tt.header)
if got != tt.want {
t.Errorf("%s = %q, want %q", tt.header, got, tt.want)
}
})
}
}
func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) {
t.Parallel()
cfg := &config.Config{}
mw := &Middleware{
log: slog.Default(),
config: cfg,
}
// Handler that sets its own headers
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Custom-Header", "custom-value")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
})
handler := mw.SecurityHeaders()(testHandler)
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// Security headers should be present
if rec.Header().Get("X-Content-Type-Options") != "nosniff" {
t.Error("X-Content-Type-Options not set")
}
// Custom headers should still be there
if rec.Header().Get("X-Custom-Header") != "custom-value" {
t.Error("Custom header was overwritten")
}
if rec.Header().Get("Content-Type") != "application/json" {
t.Error("Content-Type was overwritten")
}
}