Files

139 lines
3.7 KiB
Go
Raw Permalink Normal View History

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
)
func listTrash(t *testing.T, s *Server) []trashItem {
t.Helper()
rec := httptest.NewRecorder()
s.handleListTrash(rec, httptest.NewRequest(http.MethodGet, "/api/trash", nil))
if rec.Code != http.StatusOK {
t.Fatalf("list trash: code=%d", rec.Code)
}
var items []trashItem
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
t.Fatalf("decode trash: %v", err)
}
return items
}
func TestTrashListAndRestore(t *testing.T) {
s, content, _ := newTestServer(t)
// Delete foo, then it should show up in the trash.
rec := httptest.NewRecorder()
s.handleDelete(rec, form(url.Values{"slug": {"foo"}}))
if rec.Code != http.StatusOK {
t.Fatalf("delete: %d", rec.Code)
}
items := listTrash(t, s)
if len(items) != 1 {
t.Fatalf("trash has %d items, want 1", len(items))
}
it := items[0]
if it.Slug != "foo" || !it.HasMeta || it.Image == "" {
t.Errorf("trash item wrong: %+v", it)
}
// Restore it.
rec = httptest.NewRecorder()
s.handleRestore(rec, form(url.Values{"id": {it.ID}}))
if rec.Code != http.StatusOK {
t.Fatalf("restore: code=%d body=%s", rec.Code, rec.Body)
}
for _, p := range []string{"foo.png", "foo.toml"} {
if _, err := os.Stat(filepath.Join(content, p)); err != nil {
t.Errorf("restored %s missing: %v", p, err)
}
}
if items := listTrash(t, s); len(items) != 0 {
t.Errorf("trash not empty after restore: %+v", items)
}
// Caption survived the round trip.
if readMeta(t, content, "foo").Caption != "Foo" {
t.Error("restored sidecar lost its caption")
}
}
func TestRestoreRefusesLiveCollision(t *testing.T) {
s, content, _ := newTestServer(t)
rec := httptest.NewRecorder()
s.handleDelete(rec, form(url.Values{"slug": {"foo"}}))
if rec.Code != http.StatusOK {
t.Fatalf("delete: %d", rec.Code)
}
id := listTrash(t, s)[0].ID
// A new live post now occupies the slug.
writePNG(t, filepath.Join(content, "foo.png"))
rec = httptest.NewRecorder()
s.handleRestore(rec, form(url.Values{"id": {id}}))
if rec.Code != http.StatusConflict {
t.Errorf("restore over live post: code=%d, want 409", rec.Code)
}
// The trashed copy is left intact for a manual decision.
if len(listTrash(t, s)) != 1 {
t.Error("trashed item should remain after refused restore")
}
}
func TestEmptyTrash(t *testing.T) {
s, _, _ := newTestServer(t)
// Trash both posts.
rec := httptest.NewRecorder()
s.handleDelete(rec, form(url.Values{"slugs": {"foo,bar"}}))
if rec.Code != http.StatusOK {
t.Fatalf("delete: %d", rec.Code)
}
items := listTrash(t, s)
if len(items) != 2 {
t.Fatalf("trash has %d, want 2", len(items))
}
// Empty one by id.
rec = httptest.NewRecorder()
s.handleEmptyTrash(rec, form(url.Values{"id": {items[0].ID}}))
if rec.Code != http.StatusOK {
t.Fatalf("empty one: %d", rec.Code)
}
if len(listTrash(t, s)) != 1 {
t.Error("expected 1 item left after deleting one")
}
// Empty all.
rec = httptest.NewRecorder()
s.handleEmptyTrash(rec, form(url.Values{"all": {"1"}}))
if rec.Code != http.StatusOK {
t.Fatalf("empty all: %d", rec.Code)
}
if len(listTrash(t, s)) != 0 {
t.Error("trash not empty after all")
}
}
func TestSplitTrashIDRejectsJunk(t *testing.T) {
cases := map[string]bool{
"foo.20260102-150405": true,
"foo.bar.20260102-150405": false, // slug "foo.bar" has a dot → rejected
"../x.20260102-150405": false, // traversal
"foo.2026": false, // malformed stamp
"foo": false, // no stamp
"": false,
}
for id, want := range cases {
if _, _, ok := splitTrashID(id); ok != want {
t.Errorf("splitTrashID(%q) = %v, want %v", id, ok, want)
}
}
}