Archived and finished buckets, userscript nav chips #4
@@ -79,6 +79,7 @@ func (h *webHandler) register(mux *http.ServeMux) {
|
||||
|
||||
mux.HandleFunc("GET /ui/list", h.requireSession(h.uiList))
|
||||
mux.HandleFunc("POST /ui/bookmarks/{key}/favorite", h.requireSession(h.uiFavorite))
|
||||
mux.HandleFunc("POST /ui/bookmarks/{key}/status", h.requireSession(h.uiStatus))
|
||||
mux.HandleFunc("POST /ui/bookmarks/{key}/chapter", h.requireSession(h.uiChapter))
|
||||
mux.HandleFunc("DELETE /ui/bookmarks/{key}", h.requireSession(h.uiDelete))
|
||||
}
|
||||
@@ -283,6 +284,32 @@ func (h *webHandler) uiFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
h.saveAndRenderCard(w, b)
|
||||
}
|
||||
|
||||
// uiStatus moves a bookmark between lifecycle buckets. This is the only place
|
||||
// a series can be marked finished — the JSON API refuses that value, so the
|
||||
// userscript cannot set it even by accident.
|
||||
//
|
||||
// last_chapter_num is untouched, so Upsert keeps the stored updated_at and the
|
||||
// list does not reorder.
|
||||
func (h *webHandler) uiStatus(w http.ResponseWriter, r *http.Request) {
|
||||
b, ok := h.loadForMutation(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch s := r.PostFormValue("status"); s {
|
||||
case statusReading, statusArchived, statusFinished:
|
||||
b.Status = s
|
||||
default:
|
||||
http.Error(w, "invalid status", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
h.saveAndRenderCard(w, b)
|
||||
}
|
||||
|
||||
// uiChapter forces the read chapter to a value the user typed.
|
||||
//
|
||||
// Writing the number also clears last_chapter_url: that URL points at the
|
||||
|
||||
@@ -599,3 +599,79 @@ func TestRecentStripExcludesArchivedAndFinished(t *testing.T) {
|
||||
t.Fatal("recent strip dropped the reading series")
|
||||
}
|
||||
}
|
||||
|
||||
func postStatus(t *testing.T, srv http.Handler, cfg Config, key, status string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
form := url.Values{"status": {status}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/ui/bookmarks/"+key+"/status",
|
||||
strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(sessionCookie(t, cfg))
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
return rr
|
||||
}
|
||||
|
||||
func TestUIStatusSetsBucket(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
for _, want := range []string{statusArchived, statusFinished, statusReading} {
|
||||
if rr := postStatus(t, srv, cfg, "asura:reading", want); rr.Code != http.StatusOK {
|
||||
t.Fatalf("set %s: status = %d, body %s", want, rr.Code, rr.Body.String())
|
||||
}
|
||||
b, ok, err := store.Get("asura:reading")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("Get: ok=%v err=%v", ok, err)
|
||||
}
|
||||
if b.Status != want {
|
||||
t.Fatalf("stored status = %q, want %q", b.Status, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIStatusRejectsUnknownValue(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
if rr := postStatus(t, srv, cfg, "asura:reading", "dropped"); rr.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want 400", rr.Code)
|
||||
}
|
||||
b, _, _ := store.Get("asura:reading")
|
||||
if b.Status != statusReading {
|
||||
t.Fatalf("stored status = %q, want it untouched", b.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIStatusRequiresSession(t *testing.T) {
|
||||
srv, store := newWebTestServer(t, webConfig())
|
||||
seedStatusRows(t, store)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/ui/bookmarks/asura:reading/status",
|
||||
strings.NewReader("status=archived"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status = %d, want 401", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIStatusDoesNotReorderList(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seedStatusRows(t, store)
|
||||
|
||||
before, _, _ := store.Get("asura:reading")
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
if rr := postStatus(t, srv, cfg, "asura:reading", statusArchived); rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d", rr.Code)
|
||||
}
|
||||
after, _, _ := store.Get("asura:reading")
|
||||
if after.UpdatedAt != before.UpdatedAt {
|
||||
t.Fatalf("UpdatedAt moved %d -> %d", before.UpdatedAt, after.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user