go-api/secure_test.go
Snider 65ac37d3e7
Some checks failed
Security Scan / security (push) Successful in 11s
Test / test (push) Failing after 42s
feat: modernise to Go 1.26 iterators and stdlib helpers
Add Endpoints and MiddlewareChain iterators on API server, bridge
ResponseFieldsSeq/HeadersSeq. Use strings.SplitSeq in SDK codegen,
slices.SortFunc in OpenAPI spec generation.

Co-Authored-By: Gemini <noreply@google.com>
Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-23 05:47:16 +00:00

185 lines
4.9 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api_test
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
api "forge.lthn.ai/core/go-api"
)
// ── WithSecure ──────────────────────────────────────────────────────────
func TestWithSecure_Good_SetsHSTSHeader(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
sts := w.Header().Get("Strict-Transport-Security")
if sts == "" {
t.Fatal("expected Strict-Transport-Security header to be set")
}
if !strings.Contains(sts, "max-age=31536000") {
t.Fatalf("expected max-age=31536000 in STS header, got %q", sts)
}
if !strings.Contains(strings.ToLower(sts), "includesubdomains") {
t.Fatalf("expected includeSubdomains in STS header, got %q", sts)
}
}
func TestWithSecure_Good_SetsFrameOptionsDeny(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
xfo := w.Header().Get("X-Frame-Options")
if xfo != "DENY" {
t.Fatalf("expected X-Frame-Options=%q, got %q", "DENY", xfo)
}
}
func TestWithSecure_Good_SetsContentTypeNosniff(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
cto := w.Header().Get("X-Content-Type-Options")
if cto != "nosniff" {
t.Fatalf("expected X-Content-Type-Options=%q, got %q", "nosniff", cto)
}
}
func TestWithSecure_Good_SetsReferrerPolicy(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
rp := w.Header().Get("Referrer-Policy")
if rp != "strict-origin-when-cross-origin" {
t.Fatalf("expected Referrer-Policy=%q, got %q", "strict-origin-when-cross-origin", rp)
}
}
func TestWithSecure_Good_AllHeadersPresent(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
e.Register(&stubGroup{})
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/stub/ping", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
// Verify all security headers are present on a regular route.
checks := map[string]string{
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "strict-origin-when-cross-origin",
}
for header, want := range checks {
got := w.Header().Get(header)
if got != want {
t.Errorf("header %s: expected %q, got %q", header, want, got)
}
}
sts := w.Header().Get("Strict-Transport-Security")
if sts == "" {
t.Error("expected Strict-Transport-Security header to be set")
}
}
func TestWithSecure_Good_CombinesWithOtherMiddleware(t *testing.T) {
gin.SetMode(gin.TestMode)
e, _ := api.New(
api.WithSecure(),
api.WithRequestID(),
)
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
// Both secure headers and request ID should be present.
if w.Header().Get("X-Frame-Options") != "DENY" {
t.Fatal("expected X-Frame-Options header from WithSecure")
}
if w.Header().Get("X-Request-ID") == "" {
t.Fatal("expected X-Request-ID header from WithRequestID")
}
}
func TestWithSecure_Bad_NoSSLRedirect(t *testing.T) {
// SSL redirect is not enabled — the middleware runs behind a TLS-terminating
// reverse proxy. Verify plain HTTP requests are not redirected.
gin.SetMode(gin.TestMode)
e, _ := api.New(api.WithSecure())
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
// Should get 200, not a 301/302 redirect.
if w.Code != http.StatusOK {
t.Fatalf("expected 200 (no SSL redirect), got %d", w.Code)
}
}
func TestWithSecure_Ugly_DoubleSecureDoesNotPanic(t *testing.T) {
// Applying WithSecure twice should not panic or cause issues.
gin.SetMode(gin.TestMode)
e, _ := api.New(
api.WithSecure(),
api.WithSecure(),
)
h := e.Handler()
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/health", nil)
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
// Headers should still be correctly set.
if w.Header().Get("X-Frame-Options") != "DENY" {
t.Fatal("expected X-Frame-Options=DENY after double WithSecure")
}
}