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) } } }