fix(backend): valid hx-target selector and reject NaN/Infinity chapter input
hx-target="#card-<key>" is an invalid CSS selector for any key containing a colon (every real bookmark key is "<site>:<series_id>"), so htmx threw before swapping and the favourite/delete/chapter-override controls were dead in the browser. Switch to the attribute-selector form [id='card-<key>'], which querySelectorAll accepts regardless of the id's characters. Also close a validation gap in uiChapter: strconv.ParseFloat accepts "NaN"/"Infinity"/"-Inf" with err == nil, and every comparison against NaN is false, so num < 0 let both through to last_chapter_num and permanently broke HasNewChapter. Reject non-finite values explicitly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-1
@@ -281,6 +281,42 @@ func TestFavoriteTogglesWithoutReordering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCardHxTargetIsValidSelectorForColonKey asserts the rendered card's
|
||||
// hx-target attributes use the fixed-string attribute-selector form
|
||||
// ([id='card-<key>']) rather than a bare CSS id-selector (#card-<key>).
|
||||
//
|
||||
// A key like "asura:solo" makes "#card-asura:solo" an invalid CSS selector:
|
||||
// the browser parses ":solo" as an unrecognised pseudo-class and htmx's
|
||||
// querySelectorAll throws SyntaxError, so the button never resolves its
|
||||
// swap target. httptest never executes htmx, so this only checks the
|
||||
// rendered attribute's shape — it is not proof the browser accepts the
|
||||
// selector, just a regression guard against reintroducing the bare-id form.
|
||||
func TestCardHxTargetIsValidSelectorForColonKey(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
seed(t, store, Bookmark{
|
||||
Key: "asura:solo", Site: "asura", SeriesID: "solo",
|
||||
Title: "Solo Leveling", LastChapter: "45", LastChapterNum: 45,
|
||||
UpdatedAt: 1_000_000,
|
||||
})
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, uiRequest(t, cfg, http.MethodGet, "/ui/list", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rr.Code)
|
||||
}
|
||||
body := rr.Body.String()
|
||||
|
||||
want := `hx-target="[id='card-asura:solo']"`
|
||||
if strings.Count(body, want) != 3 {
|
||||
t.Fatalf("body has %d occurrences of %s, want 3 (favorite button, delete button, chapter form)",
|
||||
strings.Count(body, want), want)
|
||||
}
|
||||
if strings.Contains(body, `hx-target="#card-asura:solo"`) {
|
||||
t.Fatal("body still uses the bare id CSS selector, which is invalid for a key containing ':'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChapterOverrideMovesUpdatedAt(t *testing.T) {
|
||||
cfg := webConfig()
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
@@ -324,7 +360,7 @@ func TestChapterOverrideRejectsBadInput(t *testing.T) {
|
||||
Title: "Solo Leveling", LastChapterNum: 45, UpdatedAt: 1_000_000,
|
||||
})
|
||||
|
||||
for _, bad := range []string{"", "abc", "-3"} {
|
||||
for _, bad := range []string{"", "abc", "-3", "NaN", "Infinity", "-Inf"} {
|
||||
t.Run("input "+bad, func(t *testing.T) {
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, uiRequest(t, cfg, http.MethodPost,
|
||||
|
||||
Reference in New Issue
Block a user