Latest Chapter correction: one numeric input, owner-gated (#149)

This commit is contained in:
2026-08-22 09:04:24 +07:00
parent 889f0f3f38
commit c9b1f2a334
10 changed files with 423 additions and 19 deletions
+133
View File
@@ -3238,3 +3238,136 @@ func TestSeriesPollCapsBody(t *testing.T) {
t.Errorf("an oversized body still stamped the request:\n%s", body)
}
}
// The correction route validates at the boundary: a non-numeric, zero,
// negative or non-finite chapter answers 400 and never reaches the store, and
// a finite number greater than zero stores the number, the derived label and
// the stamp. The answer is the freshly rendered meta fragment, so the figures
// describe the state after the press (#149).
func TestCorrectLatestChapterRoute(t *testing.T) {
st, dsn := newTestStoreURL(t)
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open %s: %v", dsn, err)
}
defer db.Close()
seedSeriesRow(t, st, db, seriesRowSeed{
key: "asura:solo", url: "u", checkedAt: 9000, bookmarks: 1, latestNum: floatPtr(3),
})
router := newRouter(st, testConfig())
cookie := sessionCookie(t, st)
for _, body := range []string{
"chapter=abc", "chapter=", "chapter=0", "chapter=-1", "chapter=NaN", "chapter=Inf",
} {
req := httptest.NewRequest(http.MethodPost, "/admin/series/asura:solo/latest", strings.NewReader(body))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Errorf("POST latest with body %q: status = %d, want 400", body, rr.Code)
}
}
// Nothing reached the store: the seeded number stands, unstamped.
var num float64
var stamp int64
if err := db.QueryRow(`
SELECT latest_chapter_num, latest_corrected_at
FROM series WHERE site = 'asura' AND series_id = 'solo'`).
Scan(&num, &stamp); err != nil {
t.Fatalf("read back: %v", err)
}
if num != 3 || stamp != 0 {
t.Fatalf("after 400s the row is num %v, stamp %d; want 3, 0", num, stamp)
}
// A good press stores the number, the derived label and the stamp, and
// answers with the meta fragment describing the state after the press.
req := httptest.NewRequest(http.MethodPost, "/admin/series/asura:solo/latest", strings.NewReader("chapter=12.5"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("POST latest status = %d, want 200 (body %s)", rr.Code, rr.Body.String())
}
body := rr.Body.String()
if !strings.Contains(body, `id="detail-meta"`) {
t.Errorf("correction answer is not the meta fragment:\n%s", body)
}
if !strings.Contains(body, `<span class="mark">corrected `) {
t.Errorf("correction answer lacks the fresh corrected marker:\n%s", body)
}
var label string
if err := db.QueryRow(`
SELECT latest_chapter, latest_chapter_num, latest_corrected_at
FROM series WHERE site = 'asura' AND series_id = 'solo'`).
Scan(&label, &num, &stamp); err != nil {
t.Fatalf("read back: %v", err)
}
if label != "Chapter 12.5" || num != 12.5 {
t.Errorf("stored = %q, %v; want the derived label and 12.5", label, num)
}
if stamp == 0 {
t.Error("stamp = 0, want the correction stamp written")
}
}
// The detail page offers the one-input correction with the plain copy, and
// the corrected marker rides the meta line while the stamp is set — then
// disappears the moment a machine writes the number (#149).
func TestAdminSeriesDetailCorrectionMarker(t *testing.T) {
st, dsn := newTestStoreURL(t)
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open %s: %v", dsn, err)
}
defer db.Close()
seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:solo", url: "u", checkedAt: 9000, bookmarks: 1})
router := newRouter(st, testConfig())
cookie := sessionCookie(t, st)
body := seriesDetailPage(t, router, st, "asura:solo")
for _, want := range []string{
`name="chapter"`,
`hx-post="/admin/series/asura:solo/latest"`,
"The next successful Poll overwrites this value.",
} {
if !strings.Contains(body, want) {
t.Errorf("detail page lacks %q:\n%s", want, body)
}
}
if strings.Contains(body, "corrected ") {
t.Errorf("uncorrected detail already carries the marker:\n%s", body)
}
// The press lands the marker on the meta line.
req := httptest.NewRequest(http.MethodPost, "/admin/series/asura:solo/latest", strings.NewReader("chapter=7"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("POST latest status = %d, want 200", rr.Code)
}
body = seriesDetailPage(t, router, st, "asura:solo")
if !strings.Contains(body, `<span class="mark">corrected `) {
t.Errorf("detail page lacks the corrected marker after the press:\n%s", body)
}
if !strings.Contains(body, "ch 7") {
t.Errorf("detail page does not show the corrected number:\n%s", body)
}
// A machine write (the poller's setter) kills the marker.
if err := st.SetLatestChapter("asura", "solo", "Chapter 8", 8); err != nil {
t.Fatalf("SetLatestChapter: %v", err)
}
body = seriesDetailPage(t, router, st, "asura:solo")
if strings.Contains(body, "corrected ") {
t.Errorf("marker survives a machine write:\n%s", body)
}
if !strings.Contains(body, "ch 8") {
t.Errorf("detail page does not show the machine-written number:\n%s", body)
}
}