detached c37592d

fix(security): wire SecurityHeaders middleware and add tests

commit
c37592d0aac73b427097c2698bc18911a3764c95
author
bnema · · 2026-01-18 02:24 UTC
parent
65d96d2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +71 -0
A internal/adapters/in/http/middleware/security_test.go +67 -0
@@ -0,0 +1,67 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestSecurityHeaders(t *testing.T) {
12+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
13+ w.WriteHeader(http.StatusOK)
14+ })
15+
16+ wrapped := SecurityHeaders(handler)
17+
18+ req := httptest.NewRequest(http.MethodGet, "/", nil)
19+ rec := httptest.NewRecorder()
20+ wrapped.ServeHTTP(rec, req)
21+
22+ assert.Equal(t, http.StatusOK, rec.Code)
23+
24+ // Verify all security headers are set
25+ assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options"))
26+ assert.Equal(t, "DENY", rec.Header().Get("X-Frame-Options"))
27+ assert.Equal(t, "1; mode=block", rec.Header().Get("X-XSS-Protection"))
28+ assert.Equal(t, "strict-origin-when-cross-origin", rec.Header().Get("Referrer-Policy"))
29+ assert.Equal(t, "geolocation=(), microphone=(), camera=()", rec.Header().Get("Permissions-Policy"))
30+}
31+
32+func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) {
33+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
34+ w.Header().Set("Content-Type", "application/json")
35+ w.Header().Set("X-Custom-Header", "custom-value")
36+ w.WriteHeader(http.StatusOK)
37+ })
38+
39+ wrapped := SecurityHeaders(handler)
40+
41+ req := httptest.NewRequest(http.MethodGet, "/", nil)
42+ rec := httptest.NewRecorder()
43+ wrapped.ServeHTTP(rec, req)
44+
45+ // Security headers should be set
46+ assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options"))
47+
48+ // Handler's headers should also be present
49+ assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
50+ assert.Equal(t, "custom-value", rec.Header().Get("X-Custom-Header"))
51+}
52+
53+func TestSecurityHeaders_CallsNextHandler(t *testing.T) {
54+ called := false
55+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56+ called = true
57+ w.WriteHeader(http.StatusOK)
58+ })
59+
60+ wrapped := SecurityHeaders(handler)
61+
62+ req := httptest.NewRequest(http.MethodGet, "/", nil)
63+ rec := httptest.NewRecorder()
64+ wrapped.ServeHTTP(rec, req)
65+
66+ assert.True(t, called, "next handler should have been called")
67+}
@@ -0,0 +1,67 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+ "net/http/httptest"
6+ "testing"
7+
8+ "github.com/stretchr/testify/assert"
9+)
10+
11+func TestSecurityHeaders(t *testing.T) {
12+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
13+ w.WriteHeader(http.StatusOK)
14+ })
15+
16+ wrapped := SecurityHeaders(handler)
17+
18+ req := httptest.NewRequest(http.MethodGet, "/", nil)
19+ rec := httptest.NewRecorder()
20+ wrapped.ServeHTTP(rec, req)
21+
22+ assert.Equal(t, http.StatusOK, rec.Code)
23+
24+ // Verify all security headers are set
25+ assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options"))
26+ assert.Equal(t, "DENY", rec.Header().Get("X-Frame-Options"))
27+ assert.Equal(t, "1; mode=block", rec.Header().Get("X-XSS-Protection"))
28+ assert.Equal(t, "strict-origin-when-cross-origin", rec.Header().Get("Referrer-Policy"))
29+ assert.Equal(t, "geolocation=(), microphone=(), camera=()", rec.Header().Get("Permissions-Policy"))
30+}
31+
32+func TestSecurityHeaders_PreservesExistingHeaders(t *testing.T) {
33+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
34+ w.Header().Set("Content-Type", "application/json")
35+ w.Header().Set("X-Custom-Header", "custom-value")
36+ w.WriteHeader(http.StatusOK)
37+ })
38+
39+ wrapped := SecurityHeaders(handler)
40+
41+ req := httptest.NewRequest(http.MethodGet, "/", nil)
42+ rec := httptest.NewRecorder()
43+ wrapped.ServeHTTP(rec, req)
44+
45+ // Security headers should be set
46+ assert.Equal(t, "nosniff", rec.Header().Get("X-Content-Type-Options"))
47+
48+ // Handler's headers should also be present
49+ assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
50+ assert.Equal(t, "custom-value", rec.Header().Get("X-Custom-Header"))
51+}
52+
53+func TestSecurityHeaders_CallsNextHandler(t *testing.T) {
54+ called := false
55+ handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56+ called = true
57+ w.WriteHeader(http.StatusOK)
58+ })
59+
60+ wrapped := SecurityHeaders(handler)
61+
62+ req := httptest.NewRequest(http.MethodGet, "/", nil)
63+ rec := httptest.NewRecorder()
64+ wrapped.ServeHTTP(rec, req)
65+
66+ assert.True(t, called, "next handler should have been called")
67+}
M internal/app/run.go +4 -0
@@ -872,6 +872,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
872872 registryMiddlewares := []func(http.Handler) http.Handler{
873873 middleware.PanicRecovery(log),
874874 middleware.RequestLogger(log),
875+ middleware.SecurityHeaders,
875876 }
876877
877878 // Create rate limiters using the factory
@@ -910,6 +911,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
910911 tokenWithMiddleware := middleware.Chain(
911912 middleware.PanicRecovery(log),
912913 middleware.RequestLogger(log),
914+ middleware.SecurityHeaders,
913915 rateLimitMiddleware,
914916 )(svc.tokenHandler)
915917 registryMux.Handle("/v2/token", tokenWithMiddleware)
@@ -921,6 +923,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
921923 adminMiddlewares := []func(http.Handler) http.Handler{
922924 middleware.PanicRecovery(log),
923925 middleware.RequestLogger(log),
926+ middleware.SecurityHeaders,
924927 }
925928 // Add admin auth middleware if auth is enabled
926929 if svc.authSvc != nil {
@@ -938,6 +941,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
938941 proxyMiddlewares := []func(http.Handler) http.Handler{
939942 middleware.PanicRecovery(log),
940943 middleware.RequestLogger(log),
944+ middleware.SecurityHeaders,
941945 middleware.CORS,
942946 }
943947
@@ -872,6 +872,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
872 registryMiddlewares := []func(http.Handler) http.Handler{872 registryMiddlewares := []func(http.Handler) http.Handler{
873 middleware.PanicRecovery(log),873 middleware.PanicRecovery(log),
874 middleware.RequestLogger(log),874 middleware.RequestLogger(log),
875+ middleware.SecurityHeaders,
875 }876 }
876 877
877 // Create rate limiters using the factory878 // Create rate limiters using the factory
@@ -910,6 +911,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
910 tokenWithMiddleware := middleware.Chain(911 tokenWithMiddleware := middleware.Chain(
911 middleware.PanicRecovery(log),912 middleware.PanicRecovery(log),
912 middleware.RequestLogger(log),913 middleware.RequestLogger(log),
914+ middleware.SecurityHeaders,
913 rateLimitMiddleware,915 rateLimitMiddleware,
914 )(svc.tokenHandler)916 )(svc.tokenHandler)
915 registryMux.Handle("/v2/token", tokenWithMiddleware)917 registryMux.Handle("/v2/token", tokenWithMiddleware)
@@ -921,6 +923,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
921 adminMiddlewares := []func(http.Handler) http.Handler{923 adminMiddlewares := []func(http.Handler) http.Handler{
922 middleware.PanicRecovery(log),924 middleware.PanicRecovery(log),
923 middleware.RequestLogger(log),925 middleware.RequestLogger(log),
926+ middleware.SecurityHeaders,
924 }927 }
925 // Add admin auth middleware if auth is enabled928 // Add admin auth middleware if auth is enabled
926 if svc.authSvc != nil {929 if svc.authSvc != nil {
@@ -938,6 +941,7 @@ func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Ha
938 proxyMiddlewares := []func(http.Handler) http.Handler{941 proxyMiddlewares := []func(http.Handler) http.Handler{
939 middleware.PanicRecovery(log),942 middleware.PanicRecovery(log),
940 middleware.RequestLogger(log),943 middleware.RequestLogger(log),
944+ middleware.SecurityHeaders,
941 middleware.CORS,945 middleware.CORS,
942 }946 }
943 947

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close