90f1cf8bdf
Format / gofmt (push) Successful in 5s
CI / Build (push) Successful in 12s
CI / Go Tests (push) Successful in 22s
Release Artifacts / Validate release tag (push) Successful in 1s
Release Artifacts / Build and release executables (push) Successful in 33s
Release Artifacts / Build and release Docker image (push) Successful in 3m10s
Co-authored-by: Cursor <cursoragent@cursor.com>
422 lines
14 KiB
Go
422 lines
14 KiB
Go
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
|
|
}
|
|
|
|
// Scratches larger than limits.raw_cache_max_size must stream from storage
|
|
// (no full in-memory buffer) so multi-GiB downloads stay viable.
|
|
func TestRawStreamsWhenLargerThanCache(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := bytes.Repeat([]byte("Z"), 8*1024) // 8 KiB body
|
|
handler := newTestHandler(t, testServerOptions{
|
|
maxUploadBytes: int64(len(payload) * 2),
|
|
defaultTTL: time.Hour,
|
|
rateLimitEnable: false,
|
|
rawCacheBytes: 1024, // smaller than payload => stream path
|
|
})
|
|
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewReader(payload))
|
|
createReq.Header.Set("Content-Type", "application/octet-stream")
|
|
createReq.RemoteAddr = "127.0.0.1:7100"
|
|
createRec := httptest.NewRecorder()
|
|
handler.ServeHTTP(createRec, createReq)
|
|
if createRec.Code != http.StatusCreated {
|
|
t.Fatalf("create status = %d, want 201 body=%q", createRec.Code, createRec.Body.String())
|
|
}
|
|
|
|
var created map[string]any
|
|
if err := json.Unmarshal(createRec.Body.Bytes(), &created); err != nil {
|
|
t.Fatalf("decode create payload: %v", err)
|
|
}
|
|
id, _ := created["id"].(string)
|
|
if id == "" {
|
|
t.Fatal("create payload missing id")
|
|
}
|
|
|
|
rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil)
|
|
rawReq.RemoteAddr = "127.0.0.1:7101"
|
|
rawRec := httptest.NewRecorder()
|
|
handler.ServeHTTP(rawRec, rawReq)
|
|
if rawRec.Code != http.StatusOK {
|
|
t.Fatalf("raw status = %d, want 200", rawRec.Code)
|
|
}
|
|
if got := rawRec.Header().Get("Accept-Ranges"); got != "none" {
|
|
t.Fatalf("Accept-Ranges = %q, want none (stream path)", got)
|
|
}
|
|
if got := rawRec.Body.Bytes(); !bytes.Equal(got, payload) {
|
|
t.Fatalf("raw body len=%d, want %d (content mismatch or truncated)", len(got), len(payload))
|
|
}
|
|
}
|