175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
package api
|
|
|
|
import (
|
|
"palette/internal/store"
|
|
|
|
"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()
|
|
store.TimeNow = func() time.Time { return base }
|
|
t.Cleanup(func() { store.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
|
|
store.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
|
|
store.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) {
|
|
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)
|
|
}
|
|
}
|