139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|