Series URL repair: owner-typed, gated by the poller's own fetch gate (#151)
This commit is contained in:
@@ -60,10 +60,11 @@ type Bookmark struct {
|
||||
// exists once no matter how many bookmarks point at it (ADR-0003).
|
||||
//
|
||||
// Title, SeriesURL and Cover are written once, at creation: a PUT naming an
|
||||
// existing Series has them ignored, and only the backend's own Poll may change
|
||||
// them. Kind and the latest-chapter fields are last-write-wins like the
|
||||
// bookmark's own fields. Never serialized: the wire format is the flat
|
||||
// Bookmark (ADR-0004).
|
||||
// existing Series has them ignored, and only the backend's own Poll may
|
||||
// change them. The one exception is SeriesURL, which the owner's
|
||||
// SetSeriesURL may repair (issue #151). Kind and the latest-chapter fields
|
||||
// are last-write-wins like the bookmark's own fields. Never serialized: the
|
||||
// wire format is the flat Bookmark (ADR-0004).
|
||||
type Series struct {
|
||||
Site string
|
||||
SeriesID string
|
||||
@@ -1394,6 +1395,21 @@ func (s *Store) SetLatestChapter(site, seriesID, label string, num float64) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSeriesURL stores the owner's repair for a Series' source address
|
||||
// (issue #151): the one write that lifts the write-once rule documented on
|
||||
// Series.SeriesURL. It is a store, not a verification — the caller has
|
||||
// already passed the poller's fetch gate. The handler 404s on an unknown row
|
||||
// before calling; the write itself is a plain single-column UPDATE like
|
||||
// MarkLatestChecked.
|
||||
func (s *Store) SetSeriesURL(site, seriesID, seriesURL string) error {
|
||||
if _, err := s.db.Exec(
|
||||
`UPDATE series SET series_url = $3 WHERE site = $1 AND series_id = $2`,
|
||||
site, seriesID, seriesURL); err != nil {
|
||||
return fmt.Errorf("set series url %s:%s: %w", site, seriesID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CorrectLatestChapter makes the Latest Chapter the owner's: one UPDATE
|
||||
// carrying the number, the derived label and the correction stamp. The label
|
||||
// shape is the poller's and the userscript's ("Chapter " + the number as
|
||||
|
||||
@@ -2144,3 +2144,38 @@ func TestGetCoverResolvesLegacyURLDerivedAddress(t *testing.T) {
|
||||
t.Fatalf("legacy cover = (%q, %q), want the planted bytes", body, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSeriesURL is the one write that lifts the write-once rule of
|
||||
// Series.SeriesURL (issue #151): a client PUT naming an existing Series still
|
||||
// has its new URL dropped, yet the owner's repair lands where the Upsert
|
||||
// would have ignored it.
|
||||
func TestSetSeriesURLWritesWhereUpsertIgnores(t *testing.T) {
|
||||
store := newTestStore(t)
|
||||
base := Bookmark{
|
||||
Key: "asura:solo", Site: "asura", SeriesID: "solo",
|
||||
Title: "Solo Leveling", SeriesURL: "https://asurascans.com/comics/solo",
|
||||
UpdatedAt: 1000,
|
||||
}
|
||||
if _, err := store.Upsert(store.OwnerID(), base); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
// A client PUT naming the existing Series is refused: the row is shared,
|
||||
// so the stored URL stands.
|
||||
base.SeriesURL = "https://evil.example/solo"
|
||||
if got, err := store.Upsert(store.OwnerID(), base); err != nil {
|
||||
t.Fatalf("Upsert: %v", err)
|
||||
} else if got.SeriesURL != "https://asurascans.com/comics/solo" {
|
||||
t.Fatalf("Upsert stored %q, want the original URL untouched", got.SeriesURL)
|
||||
}
|
||||
|
||||
// The owner's repair writes where the Upsert would have ignored it.
|
||||
repair := "https://asurascans.com/comics/solo-renumbered"
|
||||
if err := store.SetSeriesURL("asura", "solo", repair); err != nil {
|
||||
t.Fatalf("SetSeriesURL: %v", err)
|
||||
}
|
||||
sr := readSeries(t, store, "asura", "solo")
|
||||
if sr.SeriesURL != repair {
|
||||
t.Fatalf("stored URL = %q, want %q", sr.SeriesURL, repair)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user