Heavy security and file splitting
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

This commit is contained in:
2026-06-01 20:15:28 -05:00
parent bdbe1a9416
commit ad4355df17
27 changed files with 1540 additions and 1166 deletions
@@ -0,0 +1,375 @@
package httpapi
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestCreateReadThenExpireScratch(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: 120 * time.Millisecond,
rateLimitEnable: false,
})
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.1:9999"
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")
}
metaReq := httptest.NewRequest(http.MethodGet, "/api/scratch/"+id, nil)
metaReq.RemoteAddr = "127.0.0.1:9999"
metaRec := httptest.NewRecorder()
handler.ServeHTTP(metaRec, metaReq)
if metaRec.Code != http.StatusOK {
t.Fatalf("read metadata status = %d, want %d", metaRec.Code, http.StatusOK)
}
rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
rawReq.RemoteAddr = "127.0.0.1:9999"
rawRec := httptest.NewRecorder()
handler.ServeHTTP(rawRec, rawReq)
if rawRec.Code != http.StatusOK {
t.Fatalf("read raw status = %d, want %d", rawRec.Code, http.StatusOK)
}
if got := rawRec.Body.String(); got != "hello scratch" {
t.Fatalf("raw body = %q, want %q", got, "hello scratch")
}
time.Sleep(180 * time.Millisecond)
expiredReq := httptest.NewRequest(http.MethodGet, "/api/scratch/"+id, nil)
expiredReq.RemoteAddr = "127.0.0.1:9999"
expiredRec := httptest.NewRecorder()
handler.ServeHTTP(expiredRec, expiredReq)
if expiredRec.Code != http.StatusGone {
t.Fatalf("expired status = %d, want %d", expiredRec.Code, http.StatusGone)
}
}
func TestUploadedScratchViewAndRawBeforeAndAfterExpiry(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: 120 * time.Millisecond,
rateLimitEnable: false,
})
createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString("hello from upload flow"))
createReq.Header.Set("Content-Type", "text/plain; charset=utf-8")
createReq.RemoteAddr = "127.0.0.1:5090"
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")
}
viewReq := httptest.NewRequest(http.MethodGet, "/s/"+id, nil)
viewReq.RemoteAddr = "127.0.0.1:5091"
viewRec := httptest.NewRecorder()
handler.ServeHTTP(viewRec, viewReq)
if viewRec.Code != http.StatusOK {
t.Fatalf("view-before-expiry status = %d, want %d", viewRec.Code, http.StatusOK)
}
if !strings.Contains(viewRec.Body.String(), `id="app"`) {
t.Fatalf("expected SPA shell body for /s/{id}, got %q", viewRec.Body.String())
}
rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
rawReq.RemoteAddr = "127.0.0.1:5092"
rawRec := httptest.NewRecorder()
handler.ServeHTTP(rawRec, rawReq)
if rawRec.Code != http.StatusOK {
t.Fatalf("raw-before-expiry status = %d, want %d", rawRec.Code, http.StatusOK)
}
if got := rawRec.Body.String(); got != "hello from upload flow" {
t.Fatalf("raw-before-expiry body = %q, want %q", got, "hello from upload flow")
}
time.Sleep(180 * time.Millisecond)
viewExpiredReq := httptest.NewRequest(http.MethodGet, "/s/"+id, nil)
viewExpiredReq.RemoteAddr = "127.0.0.1:5093"
viewExpiredRec := httptest.NewRecorder()
handler.ServeHTTP(viewExpiredRec, viewExpiredReq)
if viewExpiredRec.Code != http.StatusGone {
t.Fatalf("view-after-expiry status = %d, want %d", viewExpiredRec.Code, http.StatusGone)
}
if !strings.Contains(viewExpiredRec.Body.String(), `id="app"`) {
t.Fatalf("expected SPA shell body for expired /s/{id}, got %q", viewExpiredRec.Body.String())
}
rawExpiredReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
rawExpiredReq.RemoteAddr = "127.0.0.1:5094"
rawExpiredRec := httptest.NewRecorder()
handler.ServeHTTP(rawExpiredRec, rawExpiredReq)
if rawExpiredRec.Code != http.StatusGone {
t.Fatalf("raw-after-expiry status = %d, want %d", rawExpiredRec.Code, http.StatusGone)
}
if rawExpiredRec.Body.Len() != 0 {
t.Fatalf("expected empty raw body after expiry, got %q", rawExpiredRec.Body.String())
}
}
func TestGetScratchNotFoundReturnsGone(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
viewReq := httptest.NewRequest(http.MethodGet, "/s/does-not-exist", nil)
viewReq.RemoteAddr = "127.0.0.1:5095"
viewRec := httptest.NewRecorder()
handler.ServeHTTP(viewRec, viewReq)
if viewRec.Code != http.StatusGone {
t.Fatalf("view status = %d, want %d", viewRec.Code, http.StatusGone)
}
rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/does-not-exist", nil)
rawRec := httptest.NewRecorder()
handler.ServeHTTP(rawRec, rawReq)
if rawRec.Code != http.StatusGone {
t.Fatalf("raw status = %d, want %d", rawRec.Code, http.StatusGone)
}
if rawRec.Body.Len() != 0 {
t.Fatalf("expected empty response body, got %q", rawRec.Body.String())
}
}
func TestViewAndRawUnknownIDsReturnGone(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
viewReq := httptest.NewRequest(http.MethodGet, "/s/never-uploaded-id", nil)
viewReq.RemoteAddr = "127.0.0.1:5095"
viewRec := httptest.NewRecorder()
handler.ServeHTTP(viewRec, viewReq)
if viewRec.Code != http.StatusGone {
t.Fatalf("view-never-uploaded status = %d, want %d", viewRec.Code, http.StatusGone)
}
rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/never-uploaded-id", nil)
rawReq.RemoteAddr = "127.0.0.1:5096"
rawRec := httptest.NewRecorder()
handler.ServeHTTP(rawRec, rawReq)
if rawRec.Code != http.StatusGone {
t.Fatalf("raw-never-uploaded status = %d, want %d", rawRec.Code, http.StatusGone)
}
if rawRec.Body.Len() != 0 {
t.Fatalf("expected empty raw body for never-uploaded id, got %q", rawRec.Body.String())
}
metaReq := httptest.NewRequest(http.MethodGet, "/api/scratch/never-uploaded-id", nil)
metaReq.RemoteAddr = "127.0.0.1:5097"
metaRec := httptest.NewRecorder()
handler.ServeHTTP(metaRec, metaReq)
if metaRec.Code != http.StatusGone {
t.Fatalf("metadata-never-uploaded status = %d, want %d", metaRec.Code, http.StatusGone)
}
}
func TestCreateScratchRejectsMalformedMultipart(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
req := httptest.NewRequest(http.MethodPost, "/api/scratch", strings.NewReader("not-multipart"))
req.Header.Set("Content-Type", "multipart/form-data; boundary=missing")
req.RemoteAddr = "127.0.0.1:5057"
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
}
}
func TestUnknownPathReturnsNotFoundPage(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
req := httptest.NewRequest(http.MethodGet, "/this-path-does-not-exist", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
if !strings.Contains(rec.Body.String(), `id="app"`) {
t.Fatalf("unexpected not found body: %q", rec.Body.String())
}
}
// TestWellKnownPathsReturnPlainNotSPA verifies low-pri audit fix: robots/sitemap return small
// plain text (no full SPA html shell +404).
func TestWellKnownPathsReturnPlainNotSPA(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
// robots: 200 plain with disallow
req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("/robots status=%d want 200", rec.Code)
}
ct := rec.Header().Get("Content-Type")
if !strings.Contains(ct, "text/plain") {
t.Fatalf("/robots ct=%q want text/plain", ct)
}
body := rec.Body.String()
if !strings.Contains(body, "User-agent: *") || strings.Contains(body, "id=\"app\"") || strings.Contains(body, "<!doctype") {
t.Fatalf("/robots body unexpected (should be small plain disallow, no SPA): %q", body)
}
// sitemap: 404 plain (no html)
req2 := httptest.NewRequest(http.MethodGet, "/sitemap.xml", nil)
rec2 := httptest.NewRecorder()
handler.ServeHTTP(rec2, req2)
if rec2.Code != http.StatusNotFound {
t.Fatalf("/sitemap status=%d want 404", rec2.Code)
}
ct2 := rec2.Header().Get("Content-Type")
if !strings.Contains(ct2, "text/plain") {
t.Fatalf("/sitemap ct=%q want text/plain", ct2)
}
body2 := rec2.Body.String()
if strings.Contains(body2, "id=\"app\"") || strings.Contains(body2, "<!doctype") || len(body2) > 100 {
t.Fatalf("/sitemap body unexpected (small plain 404, no SPA): %q", body2)
}
// normal unknown path still gets SPA 404 shell
req3 := httptest.NewRequest(http.MethodGet, "/not-a-wellknown", nil)
rec3 := httptest.NewRecorder()
handler.ServeHTTP(rec3, req3)
if rec3.Code != http.StatusNotFound {
t.Fatalf("generic 404 status=%d want 404", rec3.Code)
}
if !strings.Contains(rec3.Body.String(), `id="app"`) {
t.Fatalf("generic 404 should still be SPA shell")
}
}
func TestFaviconServed(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
req := httptest.NewRequest(http.MethodGet, "/favicon.svg", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("favicon status = %d, want 200", rec.Code)
}
ct := rec.Header().Get("Content-Type")
if !strings.Contains(ct, "image/svg+xml") {
t.Fatalf("favicon ct=%q want image/svg+xml", ct)
}
}
func TestScratchPageServerMetaInjection(t *testing.T) {
t.Parallel()
handler := newTestHandler(t, testServerOptions{
maxUploadBytes: 1024,
defaultTTL: time.Hour,
rateLimitEnable: false,
})
// create one with filename
var body bytes.Buffer
writer := multipart.NewWriter(&body)
file, _ := writer.CreateFormFile("file", "report.pdf")
file.Write([]byte("pdf-bytes"))
writer.Close()
createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", &body)
createReq.Header.Set("Content-Type", writer.FormDataContentType())
createReq.RemoteAddr = "127.0.0.1:7777"
createRec := httptest.NewRecorder()
handler.ServeHTTP(createRec, createReq)
if createRec.Code != http.StatusCreated {
t.Fatalf("create status = %d", createRec.Code)
}
var payload map[string]any
json.Unmarshal(createRec.Body.Bytes(), &payload)
id := payload["id"].(string)
// view page should have injected title/meta
viewReq := httptest.NewRequest(http.MethodGet, "/s/"+id, nil)
viewRec := httptest.NewRecorder()
handler.ServeHTTP(viewRec, viewReq)
bodyStr := viewRec.Body.String()
if !strings.Contains(bodyStr, "<title>report.pdf • Scratchbox</title>") {
t.Fatalf("expected injected title with filename, got head: %s", bodyStr[:min(800, len(bodyStr))])
}
if !strings.Contains(bodyStr, `property="og:title"`) {
t.Fatalf("expected og:title meta")
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}