ad4355df17
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
250 lines
6.6 KiB
Go
250 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|