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, " 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, "report.pdf • Scratchbox") { 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 }