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
102 lines
3.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|