package main import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) type anyHandler interface { ServeHTTP(http.ResponseWriter, *http.Request) } // createBurnReads creates a burn-after-N-reads paste and returns its id. func createBurnReads(t *testing.T, h anyHandler, reads int) string { t.Helper() body, _ := json.Marshal(map[string]any{"content": "limited", "burn_after_read": true, "burn_after_reads": reads}) req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(string(body))) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 201 { t.Fatalf("create burn_after_reads=%d: %d %s", reads, rec.Code, rec.Body.String()) } var created struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &created) if created.ID == "" { t.Fatal("no id in create response") } return created.ID } func getWithCookie(t *testing.T, h anyHandler, id, viewer string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest("GET", "/api/pastes/"+id, nil) if viewer != "" { req.AddCookie(&http.Cookie{Name: "vwr", Value: viewer}) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func TestBurnAfterNReadsDistinctViewers(t *testing.T) { s := testServer(t) h := s.routes() id := createBurnReads(t, h, 2) // viewer A: ok (read 1) if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("read 1 (viewer A): %d %s", rec.Code, rec.Body.String()) } // viewer B: ok (read 2) if rec := getWithCookie(t, h, id, "bbb"); rec.Code != 200 { t.Fatalf("read 2 (viewer B): %d %s", rec.Code, rec.Body.String()) } // viewer C: burned -> 404 if rec := getWithCookie(t, h, id, "ccc"); rec.Code != 404 { t.Fatalf("read 3 expected 404, got %d", rec.Code) } } func TestBurnReadsSameViewerWithinWindowNoDecrement(t *testing.T) { s := testServer(t) h := s.routes() id := createBurnReads(t, h, 2) // same viewer reads twice within the window: second is deduped if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("read 1: %d", rec.Code) } if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("deduped re-read expected 200, got %d", rec.Code) } // another viewer still gets read 2 (budget not consumed by re-reads) if rec := getWithCookie(t, h, id, "bbb"); rec.Code != 200 { t.Fatalf("read 2: %d", rec.Code) } } func TestBurnReadsWindowExpiryRecounts(t *testing.T) { s := testServer(t) h := s.routes() id := createBurnReads(t, h, 2) base := time.Now() timeNow = func() time.Time { return base } t.Cleanup(func() { timeNow = time.Now }) if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("read 1: %d", rec.Code) } // 10 minutes later: still within window, deduped timeNow = func() time.Time { return base.Add(10 * time.Minute) } if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("re-read within window: %d", rec.Code) } // 20 minutes after first read: window expired, counts as read 2 timeNow = func() time.Time { return base.Add(20 * time.Minute) } if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 200 { t.Fatalf("re-read after window expected 200, got %d", rec.Code) } // budget exhausted -> 404 even for the same viewer if rec := getWithCookie(t, h, id, "aaa"); rec.Code != 404 { t.Fatalf("after budget expected 404, got %d", rec.Code) } } func TestBurnReadsDefaultOne(t *testing.T) { s := testServer(t) h := s.routes() // burn_after_read without burn_after_reads defaults to 1 read req := httptest.NewRequest("POST", "/api/pastes", strings.NewReader(`{"content":"one","burn_after_read":true}`)) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) var created struct { ID string `json:"id"` } json.Unmarshal(rec.Body.Bytes(), &created) if rec := getWithCookie(t, h, created.ID, "aaa"); rec.Code != 200 { t.Fatalf("read 1: %d", rec.Code) } if rec := getWithCookie(t, h, created.ID, "bbb"); rec.Code != 404 { t.Fatalf("read 2 expected 404, got %d", rec.Code) } } func TestBurnReadsPageViewCounts(t *testing.T) { if webUIInstance == nil { ui, err := NewWebUI() if err != nil { t.Fatal(err) } webUIInstance = ui } s := testServer(t) h := s.routes() id := createBurnReads(t, h, 2) // HTML page view counts as a read too (documented decision) req := httptest.NewRequest("GET", "/"+id, nil) req.AddCookie(&http.Cookie{Name: "vwr", Value: "aaa"}) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("page view 1: %d", rec.Code) } // re-view within window: deduped req = httptest.NewRequest("GET", "/"+id, nil) req.AddCookie(&http.Cookie{Name: "vwr", Value: "aaa"}) rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("page re-view: %d", rec.Code) } // distinct viewer: read 2, page renders with reads remaining req = httptest.NewRequest("GET", "/"+id, nil) req.AddCookie(&http.Cookie{Name: "vwr", Value: "bbb"}) rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("page view 2: %d", rec.Code) } if !strings.Contains(rec.Body.String(), "Reads left") { t.Fatal("stats pill missing 'Reads left'") } // third viewer: burned req = httptest.NewRequest("GET", "/"+id, nil) req.AddCookie(&http.Cookie{Name: "vwr", Value: "ccc"}) rec = httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != 404 { t.Fatalf("page view 3 expected 404, got %d", rec.Code) } }