Files
scratchbox/internal/http/handlers_integration_security_test.go
s1d3sw1ped ad4355df17
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
Heavy security and file splitting
2026-06-01 20:15:28 -05:00

102 lines
3.2 KiB
Go

package httpapi
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestReadRoutesAreRateLimitedWhenEnabled(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: true,
})
createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString("hello scratch"))
createReq.Header.Set("Content-Type", "text/plain; charset=utf-8")
createReq.RemoteAddr = "127.0.0.2:9111"
createRec := httptest.NewRecorder()
handler.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusCreated {
t.Fatalf("create status = %d, want %d", createRec.Code, http.StatusCreated)
}
var payload map[string]any
if err := json.Unmarshal(createRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode create payload error = %v", err)
}
id, _ := payload["id"].(string)
if id == "" {
t.Fatalf("create payload missing id")
}
firstRawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
firstRawReq.RemoteAddr = "127.0.0.1:9111"
firstRawRec := httptest.NewRecorder()
handler.ServeHTTP(firstRawRec, firstRawReq)
if firstRawRec.Code != http.StatusOK {
t.Fatalf("first raw status = %d, want %d", firstRawRec.Code, http.StatusOK)
}
secondRawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
secondRawReq.RemoteAddr = "127.0.0.1:9111"
secondRawRec := httptest.NewRecorder()
handler.ServeHTTP(secondRawRec, secondRawReq)
if secondRawRec.Code != http.StatusTooManyRequests {
t.Fatalf("second raw status = %d, want %d", secondRawRec.Code, http.StatusTooManyRequests)
}
}
// TestSecurityHeadersOnAllResponses verifies the new always-on security headers middleware
// (wired early, applies to UI/API/assets/plain/other responses per audit).
func TestSecurityHeadersOnAllResponses(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
cases := []struct {
path string
method string
}{
{"/", "GET"},
{"/u", "GET"},
{"/api/config", "GET"},
{"/this-is-404", "GET"},
{"/robots.txt", "GET"},
{"/favicon.svg", "GET"},
}
for _, c := range cases {
req := httptest.NewRequest(c.method, c.path, nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" {
t.Errorf("path=%s X-Content-Type-Options=%q want nosniff", c.path, got)
}
if got := rec.Header().Get("Referrer-Policy"); got != "strict-origin-when-cross-origin" {
t.Errorf("path=%s Referrer-Policy=%q want strict-origin-when-cross-origin", c.path, got)
}
if got := rec.Header().Get("X-Frame-Options"); got != "DENY" {
t.Errorf("path=%s X-Frame-Options=%q want DENY", c.path, got)
}
if got := rec.Header().Get("Strict-Transport-Security"); !strings.Contains(got, "max-age=31536000") {
t.Errorf("path=%s HSTS=%q want max-age", c.path, got)
}
csp := rec.Header().Get("Content-Security-Policy")
if !strings.Contains(csp, "default-src 'self'") || !strings.Contains(csp, "frame-ancestors 'none'") {
t.Errorf("path=%s CSP=%q missing expected directives", c.path, csp)
}
}
}