Ctrl K
ComunidadGuías…

internal/middleware/security_headers_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package middleware

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestSecurityHeadersNoHashes(t *testing.T) {
	// When no hashes are provided (UI not built), script-src falls back
	// to 'unsafe-inline'.
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	handler := SecurityHeaders(nil)(inner)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	tests := []struct {
		header   string
		expected string
	}{
		{"X-Content-Type-Options", "nosniff"},
		{"X-Frame-Options", "DENY"},
		{"Referrer-Policy", "strict-origin-when-cross-origin"},
		{"Permissions-Policy", "interest-cohort=()"},
		{"Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'"},
	}

	for _, tt := range tests {
		got := rec.Header().Get(tt.header)
		if got != tt.expected {
			t.Errorf("%s = %q, want %q", tt.header, got, tt.expected)
		}
	}
}

func TestSecurityHeadersWithHashes(t *testing.T) {
	// When hashes are provided, script-src uses hashes instead of
	// 'unsafe-inline'.
	hashes := []string{"'sha256-abc123'", "'sha256-def456'"}
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	})

	handler := SecurityHeaders(hashes)(inner)

	req := httptest.NewRequest(http.MethodGet, "/", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	csp := rec.Header().Get("Content-Security-Policy")

	// script-src must NOT contain 'unsafe-inline'
	for _, part := range strings.Split(csp, ";") {
		trimmed := strings.TrimSpace(part)
		if strings.HasPrefix(trimmed, "script-src") {
			if strings.Contains(trimmed, "'unsafe-inline'") {
				t.Errorf("script-src contains 'unsafe-inline' when hashes provided: %s", trimmed)
			}
		}
	}
	for _, h := range hashes {
		if !strings.Contains(csp, h) {
			t.Errorf("CSP missing hash %s: %s", h, csp)
		}
	}
	if !strings.Contains(csp, "frame-ancestors 'none'") {
		t.Errorf("CSP missing frame-ancestors 'none': %s", csp)
	}
}

func TestSecurityHeadersPortalAllowsFraming(t *testing.T) {
	handler := SecurityHeaders(nil)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest(http.MethodGet, "/portal", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if got := rec.Header().Get("X-Frame-Options"); got != "" {
		t.Errorf("X-Frame-Options on /portal = %q, want unset", got)
	}
	if got := rec.Header().Get("Content-Security-Policy"); !strings.Contains(got, "frame-ancestors *") {
		t.Errorf("CSP on /portal = %q, want frame-ancestors *", got)
	}
}

func TestSecurityHeadersPassthrough(t *testing.T) {
	// Verify the middleware calls the next handler and doesn't block.
	called := false
	inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		called = true
		w.Header().Set("X-Custom", "test")
		w.WriteHeader(http.StatusCreated)
	})

	handler := SecurityHeaders(nil)(inner)

	req := httptest.NewRequest(http.MethodPost, "/api/test", nil)
	rec := httptest.NewRecorder()
	handler.ServeHTTP(rec, req)

	if !called {
		t.Error("inner handler was not called")
	}
	if rec.Code != http.StatusCreated {
		t.Errorf("status = %d, want %d", rec.Code, http.StatusCreated)
	}
	if rec.Header().Get("X-Custom") != "test" {
		t.Error("inner handler's custom header was lost")
	}
	// Security headers should still be present.
	if rec.Header().Get("X-Content-Type-Options") != "nosniff" {
		t.Error("security header missing on POST response")
	}
}

func TestBuildCSP(t *testing.T) {
	t.Run("no hashes falls back to unsafe-inline", func(t *testing.T) {
		csp := buildCSP(nil)
		if !strings.Contains(csp, "'unsafe-inline'") {
			t.Errorf("expected 'unsafe-inline' in script-src without hashes: %s", csp)
		}
	})

	t.Run("with hashes omits unsafe-inline", func(t *testing.T) {
		csp := buildCSP([]string{"'sha256-test123'"})
		for _, part := range strings.Split(csp, ";") {
			trimmed := strings.TrimSpace(part)
			if strings.HasPrefix(trimmed, "script-src") {
				if strings.Contains(trimmed, "'unsafe-inline'") {
					t.Errorf("script-src contains 'unsafe-inline' with hashes: %s", trimmed)
				}
				if !strings.Contains(trimmed, "'sha256-test123'") {
					t.Errorf("script-src missing hash: %s", trimmed)
				}
			}
			// style-src should still have 'unsafe-inline'
			if strings.HasPrefix(trimmed, "style-src") {
				if !strings.Contains(trimmed, "'unsafe-inline'") {
					t.Errorf("style-src should keep 'unsafe-inline': %s", trimmed)
				}
			}
		}
	})
}