package handlers_test import ( "encoding/json" "fmt" "strings" "testing" ) func TestBookmarkCRUDAndOwnership(t *testing.T) { st, sc, h, booksDir := setupAPI(t) tok := adminToken(t, h) lib, root := newLibrary(t, st, h, tok, booksDir, "bm") writeCBZ(t, root+"/x.cbz", 5) scanNow(t, sc, lib) bs := []map[string]any{} json.Unmarshal(do(h, "GET", "/api/books?q=x", tok, nil).Body.Bytes(), &bs) bid := itoa(bs[0]["id"]) w := do(h, "POST", "/api/books/"+bid+"/bookmarks", tok, map[string]any{"locator": map[string]int{"page": 3}, "percent": 0.6, "note": "伏笔"}) if w.Code != 201 { t.Fatalf("create %d %s", w.Code, w.Body) } var bm map[string]any json.Unmarshal(w.Body.Bytes(), &bm) bmID := itoa(bm["id"]) if bm["note"] != "伏笔" { t.Fatalf("echo: %s", w.Body) } // 第二本书 + 第二条书签:列表按 percent 升序且不跨书泄漏 writeCBZ(t, fmt.Sprintf("%s/y.cbz", root), 5) scanNow(t, sc, lib) bs = nil json.Unmarshal(do(h, "GET", "/api/books?q=y", tok, nil).Body.Bytes(), &bs) yid := itoa(bs[0]["id"]) do(h, "POST", "/api/books/"+bid+"/bookmarks", tok, map[string]any{"locator": map[string]int{"page": 1}, "percent": 0.2}) arr := []map[string]any{} json.Unmarshal(do(h, "GET", "/api/books/"+bid+"/bookmarks", tok, nil).Body.Bytes(), &arr) if len(arr) != 2 || arr[0]["percent"].(float64) > arr[1]["percent"].(float64) { t.Fatalf("list order/scope: %v", arr) } if w := do(h, "GET", "/api/books/"+yid+"/bookmarks", tok, nil); strings.TrimSpace(w.Body.String()) != "[]" { t.Fatalf("other book leak: %s", w.Body) } if w := do(h, "PATCH", "/api/bookmarks/"+bmID, tok, map[string]string{"note": "改了"}); w.Code != 200 { t.Fatalf("patch %d %s", w.Code, w.Body) } // 校验:note 超长 / percent 越界 / locator 缺失 if w := do(h, "POST", "/api/books/"+bid+"/bookmarks", tok, map[string]any{"locator": map[string]int{"page": 1}, "percent": 0.5, "note": strings.Repeat("字", 501)}); w.Code != 400 { t.Fatalf("long note want 400 got %d", w.Code) } if w := do(h, "POST", "/api/books/"+bid+"/bookmarks", tok, map[string]any{"locator": map[string]int{"page": 1}, "percent": 1.5}); w.Code != 400 { t.Fatalf("percent want 400 got %d", w.Code) } if w := do(h, "POST", "/api/books/"+bid+"/bookmarks", tok, map[string]any{"percent": 0.5}); w.Code != 400 { t.Fatalf("locator required got %d", w.Code) } // 所有权:member carl 看不见/改不了/删不了 alice 的书签 do(h, "POST", "/api/users", tok, map[string]string{"username": "carl", "password": testPW, "role": "member"}) lr := map[string]string{} json.Unmarshal(do(h, "POST", "/api/auth/login", "", map[string]string{"username": "carl", "password": testPW}).Body.Bytes(), &lr) bt := lr["token"] if w := do(h, "GET", "/api/books/"+bid+"/bookmarks", bt, nil); strings.TrimSpace(w.Body.String()) != "[]" { t.Fatalf("cross-user leak: %s", w.Body) } if w := do(h, "PATCH", "/api/bookmarks/"+bmID, bt, map[string]string{"note": "抢"}); w.Code != 404 { t.Fatalf("cross-user patch want 404 got %d", w.Code) } if w := do(h, "DELETE", "/api/bookmarks/"+bmID, bt, nil); w.Code != 404 { t.Fatalf("cross-user delete want 404 got %d", w.Code) } if w := do(h, "DELETE", "/api/bookmarks/"+bmID, tok, nil); w.Code != 204 { t.Fatalf("delete %d", w.Code) } if w := do(h, "DELETE", "/api/bookmarks/"+bmID, tok, nil); w.Code != 404 { t.Fatalf("re-delete want 404 got %d", w.Code) } }