Archived and finished buckets, userscript nav chips #4

Merged
sulthan merged 10 commits from feat/status-buckets into main 2026-07-27 16:58:06 +07:00
2 changed files with 62 additions and 0 deletions
Showing only changes of commit f783b72a81 - Show all commits
+15
View File
@@ -60,6 +60,21 @@ func (h *bookmarkHandler) put(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
// An empty status is "no opinion" and Upsert keeps the stored bucket.
// Finishing a series is a web-UI decision, so the JSON API refuses it
// rather than trusting every client to leave it alone.
switch b.Status {
case "", statusReading, statusArchived:
case statusFinished:
http.Error(w, "status "+statusFinished+" can only be set from the web UI",
http.StatusBadRequest)
return
default:
http.Error(w, "invalid status", http.StatusBadRequest)
return
}
// Candidate timestamp, not a decision: Upsert keeps the stored one unless // Candidate timestamp, not a decision: Upsert keeps the stored one unless
// reading progress actually moved. Any client value is ignored. // reading progress actually moved. Any client value is ignored.
b.UpdatedAt = time.Now().UnixMilli() b.UpdatedAt = time.Now().UnixMilli()
+47
View File
@@ -1,6 +1,11 @@
package main package main
import ( import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -113,3 +118,45 @@ func TestLoadLatestPollClampsAndFallsBack(t *testing.T) {
}) })
} }
} }
func TestPutStatusValidation(t *testing.T) {
cases := []struct {
name string
status string
want int
}{
{"empty is no opinion", "", http.StatusOK},
{"reading", "reading", http.StatusOK},
{"archived", "archived", http.StatusOK},
{"finished is web-only", "finished", http.StatusBadRequest},
{"garbage", "dropped", http.StatusBadRequest},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := newTestServer(t)
body := fmt.Sprintf(`{"title":"Solo","status":%q}`, tc.status)
req := auth(httptest.NewRequest(http.MethodPut, "/bookmarks/asura:solo",
strings.NewReader(body)))
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != tc.want {
t.Fatalf("status = %d, want %d (body %s)", rr.Code, tc.want, rr.Body.String())
}
if tc.want != http.StatusOK {
return
}
var got Bookmark
if err := json.Unmarshal(rr.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
want := tc.status
if want == "" {
want = "reading"
}
if got.Status != want {
t.Fatalf("stored status = %q, want %q", got.Status, want)
}
})
}
}