fix: address code review on #27

- The empty state is about an empty library, not a brand-new Reader:
  listView.Fresh becomes EmptyLibrary and moves behind the tab-specific
  branches, so "No favourites yet" is no longer shadowed for a Reader whose
  library happens to be empty. The action key stays put — hiding it was
  never asked for.
- The owner is not a revocable Reader: their row offers no button and
  POST /readers/{owner}/revoke is a 404, so the one row where the control
  would sign out the tapping browser cannot be reached by a hand-rolled
  POST either.
- Modify isolation is asserted in both directions, and the owner's own
  sign-in through the OAuth callback is pinned to the seeded row.
- CUTOVER.md and REDEPLOY.md still grepped API_TOKEN out of .env for their
  smoke tests, which the last commit deleted; both now take the acting
  Reader's derived credential.
- Roster type follows the machine-fact spec (500 10-11px mono, tracked),
  and PRODUCT.md names the Readers panel instead of claiming there is no
  owner surface at all.
This commit is contained in:
2026-08-08 20:14:21 +07:00
parent b0bf6fe770
commit f4f6c9c9e9
10 changed files with 115 additions and 40 deletions
+21 -2
View File
@@ -119,8 +119,27 @@ func TestPerReaderIsolation(t *testing.T) {
if err := json.Unmarshal(rr.Body.Bytes(), &owners); err != nil {
t.Fatalf("decode: %v", err)
}
if len(owners) != 1 || owners[0].Title != "Solo Leveling" {
t.Fatalf("owner list = %+v, want their own row", owners)
if len(owners) != 1 || owners[0].Title != "Solo Leveling" || owners[0].LastChapterNum != 0 {
t.Fatalf("owner list = %+v, want their own row at their own progress", owners)
}
// The mirror: the owner's write does not move the other Reader's progress
// either. Without it, isolation is only asserted in one direction.
req = credRequest(http.MethodPut, "/bookmarks/"+ownerKey, ownerCredential())
req.Header.Set("Content-Type", "application/json")
rr = httptest.NewRecorder()
srv.ServeHTTP(rr, withBody(req, `{"key":"asura:solo","site":"asura","series_id":"solo","title":"Solo Leveling","last_chapter_num":9}`))
if rr.Code != http.StatusOK {
t.Fatalf("owner put: status = %d, want 200", rr.Code)
}
rr = httptest.NewRecorder()
srv.ServeHTTP(rr, credRequest(http.MethodGet, "/bookmarks", readerCredential("other-reader")))
var theirs3 []store.Bookmark
if err := json.Unmarshal(rr.Body.Bytes(), &theirs3); err != nil {
t.Fatalf("decode: %v", err)
}
if len(theirs3) != 1 || theirs3[0].LastChapterNum != 3 {
t.Fatalf("other reader list = %+v, want progress 3 after the owner's write", theirs3)
}
// DELETE is scoped to its caller too, asserted in both directions: each