Pulse/internal/cloudcp/security_test.go

138 lines
3.7 KiB
Go

package cloudcp
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/cpsec"
)
func TestCPSecurityHeaders_SetsCSPWithNonce(t *testing.T) {
var capturedNonce string
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedNonce = cpsec.NonceFromContext(r.Context())
w.WriteHeader(http.StatusOK)
})
handler := CPSecurityHeaders(inner)
req := httptest.NewRequest(http.MethodGet, "/signup", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if capturedNonce == "" {
t.Fatal("expected nonce in context, got empty")
}
csp := rec.Header().Get("Content-Security-Policy")
if csp == "" {
t.Fatal("expected Content-Security-Policy header")
}
if !strings.Contains(csp, "'nonce-"+capturedNonce+"'") {
t.Fatalf("CSP does not contain nonce: %q", csp)
}
if strings.Contains(csp, "'unsafe-inline'") {
t.Fatalf("CSP should not contain unsafe-inline when nonce is present: %q", csp)
}
if strings.Contains(csp, "'unsafe-eval'") {
t.Fatalf("CSP should not contain unsafe-eval: %q", csp)
}
}
func TestCPSecurityHeaders_CSPDirectives(t *testing.T) {
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := CPSecurityHeaders(inner)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
csp := rec.Header().Get("Content-Security-Policy")
requiredDirectives := []string{
"default-src 'self'",
"script-src 'self' 'nonce-",
"style-src 'self' 'nonce-",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self' data:",
"form-action 'self' https:",
"frame-ancestors 'none'",
}
for _, d := range requiredDirectives {
if !strings.Contains(csp, d) {
t.Errorf("CSP missing directive %q in: %q", d, csp)
}
}
}
func TestCPSecurityHeaders_SetsOtherHeaders(t *testing.T) {
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := CPSecurityHeaders(inner)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
expected := map[string]string{
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "0",
"Referrer-Policy": "strict-origin-when-cross-origin",
}
for k, want := range expected {
if got := rec.Header().Get(k); got != want {
t.Errorf("header %s = %q, want %q", k, got, want)
}
}
pp := rec.Header().Get("Permissions-Policy")
if pp == "" {
t.Error("expected Permissions-Policy header")
}
}
func TestCPSecurityHeaders_UniqueNoncePerRequest(t *testing.T) {
var nonces []string
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonces = append(nonces, cpsec.NonceFromContext(r.Context()))
w.WriteHeader(http.StatusOK)
})
handler := CPSecurityHeaders(inner)
for i := 0; i < 5; i++ {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
}
seen := make(map[string]bool)
for _, n := range nonces {
if n == "" {
t.Fatal("got empty nonce")
}
if seen[n] {
t.Fatalf("duplicate nonce: %q", n)
}
seen[n] = true
}
}
func TestCPCSPNonce_ContextRoundTrip(t *testing.T) {
// Verify the cpsec package round-trips correctly.
ctx := cpsec.WithNonce(t.Context(), "test-nonce-123")
if got := cpsec.NonceFromContext(ctx); got != "test-nonce-123" {
t.Fatalf("NonceFromContext = %q, want %q", got, "test-nonce-123")
}
}
func TestCPCSPNonce_EmptyContext(t *testing.T) {
if got := cpsec.NonceFromContext(t.Context()); got != "" {
t.Fatalf("NonceFromContext on bare context = %q, want empty", got)
}
}