package httpapi import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" ) func TestConcurrentUploadsRemainConsistentAndReadable(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024 * 1024, defaultTTL: time.Hour, rateLimitEnable: false, }) const uploads = 80 type createdScratch struct { id string body string } resultsCh := make(chan createdScratch, uploads) errCh := make(chan error, uploads) var wg sync.WaitGroup for i := 0; i < uploads; i++ { wg.Add(1) go func(i int) { defer wg.Done() body := fmt.Sprintf("parallel-upload-%03d-%s", i, strings.Repeat("x", i%17)) createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString(body)) createReq.Header.Set("Content-Type", "text/plain; charset=utf-8") createReq.RemoteAddr = fmt.Sprintf("127.0.0.1:%d", 8200+i) createRec := httptest.NewRecorder() handler.ServeHTTP(createRec, createReq) if createRec.Code != http.StatusCreated { errCh <- fmt.Errorf("create status = %d, want %d", createRec.Code, http.StatusCreated) return } var payload map[string]any if err := json.Unmarshal(createRec.Body.Bytes(), &payload); err != nil { errCh <- fmt.Errorf("decode create payload error: %w", err) return } id, _ := payload["id"].(string) if id == "" { errCh <- fmt.Errorf("create payload missing id") return } metaReq := httptest.NewRequest(http.MethodGet, "/api/scratch/"+id, nil) metaRec := httptest.NewRecorder() handler.ServeHTTP(metaRec, metaReq) if metaRec.Code != http.StatusOK { errCh <- fmt.Errorf("metadata status = %d, want %d", metaRec.Code, http.StatusOK) return } rawReq := httptest.NewRequest(http.MethodGet, "/api/raw/"+id, nil) rawRec := httptest.NewRecorder() handler.ServeHTTP(rawRec, rawReq) if rawRec.Code != http.StatusOK { errCh <- fmt.Errorf("raw status = %d, want %d", rawRec.Code, http.StatusOK) return } if got := rawRec.Body.String(); got != body { errCh <- fmt.Errorf("raw body mismatch = %q, want %q", got, body) return } resultsCh <- createdScratch{id: id, body: body} }(i) } wg.Wait() close(resultsCh) close(errCh) for err := range errCh { t.Error(err) } seen := make(map[string]struct{}, uploads) for result := range resultsCh { if _, exists := seen[result.id]; exists { t.Errorf("duplicate scratch id generated: %s", result.id) continue } seen[result.id] = struct{}{} } if len(seen) != uploads { t.Fatalf("unique IDs = %d, want %d", len(seen), uploads) } } func TestParallelClientsCanReadSameScratchAcrossAllReadRoutes(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: time.Hour, rateLimitEnable: false, }) const body = "parallel readers" createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString(body)) createReq.Header.Set("Content-Type", "text/plain; charset=utf-8") createReq.RemoteAddr = "127.0.0.1:6010" 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") } paths := []string{ "/api/raw/" + id, "/s/" + id, "/api/scratch/" + id, } const requests = 120 errCh := make(chan error, requests) var wg sync.WaitGroup for i := 0; i < requests; i++ { wg.Add(1) go func(i int) { defer wg.Done() path := paths[i%len(paths)] req := httptest.NewRequest(http.MethodGet, path, nil) req.RemoteAddr = fmt.Sprintf("127.0.0.1:%d", 6100+i) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { errCh <- fmt.Errorf("path %s status = %d, want %d", path, rec.Code, http.StatusOK) return } switch { case strings.HasPrefix(path, "/api/raw/"): if got := rec.Body.String(); got != body { errCh <- fmt.Errorf("raw body = %q, want %q", got, body) } case strings.HasPrefix(path, "/s/"): if !strings.Contains(rec.Body.String(), `id="app"`) { errCh <- fmt.Errorf("view body missing SPA marker") } case strings.HasPrefix(path, "/api/scratch/"): var gotPayload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &gotPayload); err != nil { errCh <- fmt.Errorf("metadata decode error: %w", err) return } if gotID, _ := gotPayload["id"].(string); gotID != id { errCh <- fmt.Errorf("metadata id = %q, want %q", gotID, id) } } }(i) } wg.Wait() close(errCh) for err := range errCh { t.Error(err) } } func TestParallelClientsExpiredScratchReadsReturnGone(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: 80 * time.Millisecond, rateLimitEnable: false, }) createReq := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString("expires soon")) createReq.Header.Set("Content-Type", "text/plain; charset=utf-8") createReq.RemoteAddr = "127.0.0.1:7010" 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") } time.Sleep(130 * time.Millisecond) paths := []string{ "/api/raw/" + id, "/s/" + id, "/api/scratch/" + id, } const requests = 90 errCh := make(chan error, requests) var wg sync.WaitGroup for i := 0; i < requests; i++ { wg.Add(1) go func(i int) { defer wg.Done() path := paths[i%len(paths)] req := httptest.NewRequest(http.MethodGet, path, nil) req.RemoteAddr = fmt.Sprintf("127.0.0.1:%d", 7100+i) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusGone { errCh <- fmt.Errorf("expired path %s status = %d, want %d", path, rec.Code, http.StatusGone) return } if strings.HasPrefix(path, "/s/") && !strings.Contains(rec.Body.String(), `id="app"`) { errCh <- fmt.Errorf("expired view body missing SPA marker") } }(i) } wg.Wait() close(errCh) for err := range errCh { t.Error(err) } }