Validate watchlist add JSON body shape
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.29.4 - 2026-05-16
|
||||||
|
|
||||||
|
- Fixed `/api/watchlist/add` malformed-body handling so non-object JSON payloads now return `400 Bad Request` instead of surfacing as an internal server error.
|
||||||
|
- Expanded regression coverage for invalid watchlist add payload shapes.
|
||||||
|
|
||||||
## 0.29.3 - 2026-05-16
|
## 0.29.3 - 2026-05-16
|
||||||
|
|
||||||
- Fixed duplicate watchlist add responses so already-tracked shows now return `200 OK` with `created: false` instead of the misleading `201 Created`.
|
- Fixed duplicate watchlist add responses so already-tracked shows now return `200 OK` with `created: false` instead of the misleading `201 Created`.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A local web UI for a system-wide `ani-cli` install.
|
A local web UI for a system-wide `ani-cli` install.
|
||||||
|
|
||||||
Current version: `0.29.3`
|
Current version: `0.29.4`
|
||||||
|
|
||||||
## Project Layout
|
## Project Layout
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ The app currently includes:
|
|||||||
- A paged SQLite-backed download queue with retry, cancel, remove, retry-failed, and clear-finished actions.
|
- A paged SQLite-backed download queue with retry, cancel, remove, retry-failed, and clear-finished actions.
|
||||||
- A paged SQLite-backed watchlist with summary cards, per-show refresh, bulk refresh, source links, and thumbnail tools.
|
- A paged SQLite-backed watchlist with summary cards, per-show refresh, bulk refresh, source links, and thumbnail tools.
|
||||||
- Duplicate watchlist adds now return `200 OK` with `created: false`, keeping the API status aligned with the no-op result.
|
- Duplicate watchlist adds now return `200 OK` with `created: false`, keeping the API status aligned with the no-op result.
|
||||||
|
- Malformed watchlist add requests now also return `400 Bad Request` when the JSON body is not an object.
|
||||||
- Watchlist category tabs for `Watching`, `Planned`, `Finished`, and `Dropped`, with per-anime category editing.
|
- Watchlist category tabs for `Watching`, `Planned`, `Finished`, and `Dropped`, with per-anime category editing.
|
||||||
- AnimeSchedule-backed watchlist completion tracking that compares live sub/dub availability with the expected total episode count when known.
|
- AnimeSchedule-backed watchlist completion tracking that compares live sub/dub availability with the expected total episode count when known.
|
||||||
- Automatic watchlist sync after successful downloads, including downloaded-vs-manual finished markers.
|
- Automatic watchlist sync after successful downloads, including downloaded-vs-manual finished markers.
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ from uuid import uuid4
|
|||||||
PROJECT_ROOT = Path(__file__).resolve().parent
|
PROJECT_ROOT = Path(__file__).resolve().parent
|
||||||
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
ANI_CLI = os.environ.get("ANI_CLI_BIN") or shutil.which("ani-cli") or "ani-cli"
|
||||||
APP_NAME = "ani-cli-web"
|
APP_NAME = "ani-cli-web"
|
||||||
VERSION = "0.29.3"
|
VERSION = "0.29.4"
|
||||||
ALLANIME_BASE = "allanime.day"
|
ALLANIME_BASE = "allanime.day"
|
||||||
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
ALLANIME_API = f"https://api.{ALLANIME_BASE}"
|
||||||
ALLANIME_REFERER = "https://allmanga.to"
|
ALLANIME_REFERER = "https://allmanga.to"
|
||||||
|
|||||||
+1
-1
@@ -242,7 +242,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.json(actions[action](job_id))
|
self.json(actions[action](job_id))
|
||||||
elif parsed.path in {"/api/watchlist", "/api/watchlist/add"}:
|
elif parsed.path in {"/api/watchlist", "/api/watchlist/add"}:
|
||||||
Handler._runtime(self)
|
Handler._runtime(self)
|
||||||
result = Handler._context(self).add_to_watchlist(self.body_json())
|
result = Handler._context(self).add_to_watchlist(Handler.require_json_object(self, self.body_json()))
|
||||||
status = HTTPStatus.CREATED if result.get("created") else HTTPStatus.OK
|
status = HTTPStatus.CREATED if result.get("created") else HTTPStatus.OK
|
||||||
self.json(result, status)
|
self.json(result, status)
|
||||||
elif parsed.path == "/api/watchlist/ensure-thumbnails":
|
elif parsed.path == "/api/watchlist/ensure-thumbnails":
|
||||||
|
|||||||
@@ -1125,6 +1125,13 @@ class HandlerRouteTests(unittest.TestCase):
|
|||||||
self.assertEqual(handler.json_payload, result)
|
self.assertEqual(handler.json_payload, result)
|
||||||
self.assertEqual(handler.json_status, HTTPStatus.OK)
|
self.assertEqual(handler.json_status, HTTPStatus.OK)
|
||||||
|
|
||||||
|
def test_watchlist_add_rejects_non_object_json_body(self):
|
||||||
|
body = b"[]"
|
||||||
|
handler = DummyHandler("/api/watchlist/add", body=body, content_length=len(body))
|
||||||
|
APP.Handler.do_POST(handler)
|
||||||
|
self.assertEqual(handler.error_status, HTTPStatus.BAD_REQUEST)
|
||||||
|
self.assertIn("json object", handler.error_message.lower())
|
||||||
|
|
||||||
def test_ensure_thumbnails_rejects_non_array_show_ids(self):
|
def test_ensure_thumbnails_rejects_non_array_show_ids(self):
|
||||||
body = b'{"show_ids":"show-1"}'
|
body = b'{"show_ids":"show-1"}'
|
||||||
handler = DummyHandler("/api/watchlist/ensure-thumbnails", body=body, content_length=len(body))
|
handler = DummyHandler("/api/watchlist/ensure-thumbnails", body=body, content_length=len(body))
|
||||||
|
|||||||
Reference in New Issue
Block a user