Spaces:
Build error
Build error
Download tests/api/test_manga_progress_api.py from MissawB/animetix-web: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/spaces/MissawB/animetix-web/resolve/main/tests/api/test_manga_progress_api.py
- Command line
-
hf download hf://spaces/MissawB/animetix-web/tests/api/test_manga_progress_api.py
-
curl -L -o test_manga_progress_api.py https://huggingface.co/spaces/MissawB/animetix-web/resolve/main/tests/api/test_manga_progress_api.py
14.4 kB
| import pytest | |
| from animetix.models import MangaChapter, MangaPage, MangaReadingProgress, MediaItem | |
| from django.contrib.auth.models import User | |
| from django.urls import reverse | |
| from rest_framework.test import APIClient | |
| def reader(db): | |
| client = APIClient() | |
| user = User.objects.create_user(username="progress_reader", password="pw") | |
| client.force_authenticate(user=user) | |
| manga = MediaItem.objects.create( | |
| external_id="suwayomi:1:809", media_type="Manga", title="OPM" | |
| ) | |
| chapter = MangaChapter.objects.create(manga=manga, number=164.2, external_id="7") | |
| for i in range(1, 4): | |
| MangaPage.objects.create( | |
| chapter=chapter, number=i, image_url=f"http://h/{i}.jpg" | |
| ) | |
| return client, user, manga, chapter | |
| def test_progress_requires_authentication(api_client): | |
| url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| assert api_client.get(url).status_code == 403 | |
| def test_progress_returns_204_for_a_manga_absent_from_the_catalog(reader): | |
| client, *_ = reader | |
| url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:unknown"}) | |
| assert client.get(url).status_code == 204 | |
| def test_put_then_get_progress_roundtrip(reader): | |
| client, user, _manga, chapter = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": 1, "is_read": False}, format="json") | |
| assert res.status_code == 200 | |
| assert res.data["last_page_read"] == 1 | |
| get_url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| payload = client.get(get_url).data | |
| assert payload["chapters"][0] == { | |
| "number": 164.2, | |
| "is_read": False, | |
| "is_bookmarked": False, | |
| "last_page_read": 1, | |
| "page_count": 3, | |
| } | |
| assert payload["resume"] == {"chapter_number": 164.2, "last_page_read": 1} | |
| assert MangaReadingProgress.objects.filter(user=user, chapter=chapter).count() == 1 | |
| def test_decimal_chapter_numbers_are_preserved(reader): | |
| client, _user, _manga, _chapter = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": 2, "is_read": False}, format="json") | |
| assert res.data["number"] == 164.2 | |
| def test_put_on_unknown_chapter_returns_404(reader): | |
| client, *_ = reader | |
| url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "999"}, | |
| ) | |
| assert ( | |
| client.put( | |
| url, {"last_page_read": 0, "is_read": False}, format="json" | |
| ).status_code | |
| == 404 | |
| ) | |
| def test_mark_read_in_bulk(reader): | |
| client, _user, manga, _chapter = reader | |
| MangaChapter.objects.create(manga=manga, number=165.0, external_id="8") | |
| url = reverse("api_manga_progress_mark_read", kwargs={"media_id": "suwayomi:1:809"}) | |
| res = client.post( | |
| url, {"chapter_numbers": [164.2, 165.0], "is_read": True}, format="json" | |
| ) | |
| assert res.status_code == 200 | |
| assert res.data["updated"] == 2 | |
| def test_chapter_completed_pushes_to_trackers_once_on_transition(reader, mocker): | |
| # Mocked where it is *used* (animetix.api.core.manga), not where it is | |
| # defined: the view imports it at module level, so patching there is what | |
| # actually intercepts the call. | |
| push_mock = mocker.patch( | |
| "animetix.api.core.manga.progress.push_manga_progress_to_trackers" | |
| ) | |
| client, *_ = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| first = client.put(put_url, {"last_page_read": 2, "is_read": True}, format="json") | |
| assert first.status_code == 200 | |
| assert push_mock.call_count == 1 | |
| # Same chapter, already read: chapter_completed is only True on the | |
| # unread->read transition, so a repeat PUT must NOT push again. | |
| second = client.put(put_url, {"last_page_read": 2, "is_read": True}, format="json") | |
| assert second.status_code == 200 | |
| assert push_mock.call_count == 1 | |
| def test_tracker_push_failure_does_not_break_the_response(reader, mocker): | |
| mocker.patch( | |
| "animetix.api.core.manga.progress.push_manga_progress_to_trackers", | |
| side_effect=RuntimeError("tracker down"), | |
| ) | |
| client, user, _manga, chapter = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": 2, "is_read": True}, format="json") | |
| assert res.status_code == 200 | |
| assert ( | |
| MangaReadingProgress.objects.get(user=user, chapter=chapter).last_page_read == 2 | |
| ) | |
| def test_progress_is_isolated_between_users(reader): | |
| client, _user, _manga, _chapter = reader | |
| other_client = APIClient() | |
| other_user = User.objects.create_user(username="other_reader", password="pw") | |
| other_client.force_authenticate(user=other_user) | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| client.put(put_url, {"last_page_read": 3, "is_read": False}, format="json") | |
| get_url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| other_payload = other_client.get(get_url).data | |
| # The second user has never read anything: no last_page_read leaked from | |
| # the first user's write. | |
| assert other_payload["chapters"][0]["last_page_read"] == 0 | |
| assert other_payload["resume"] is None | |
| # The second user's own write must not clobber the first user's progress. | |
| other_client.put(put_url, {"last_page_read": 1, "is_read": False}, format="json") | |
| first_payload = client.get(get_url).data | |
| assert first_payload["chapters"][0]["last_page_read"] == 3 | |
| def test_put_with_negative_last_page_read_returns_400(reader): | |
| client, *_ = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": -1, "is_read": False}, format="json") | |
| assert res.status_code == 400 | |
| def test_put_with_non_numeric_chapter_number_returns_400(reader): | |
| client, *_ = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "not-a-number"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": 0, "is_read": False}, format="json") | |
| assert res.status_code == 400 | |
| def test_mark_read_with_non_numeric_chapter_number_returns_400(reader): | |
| client, *_ = reader | |
| url = reverse("api_manga_progress_mark_read", kwargs={"media_id": "suwayomi:1:809"}) | |
| res = client.post( | |
| url, | |
| {"chapter_numbers": [164.2, "not-a-number"], "is_read": True}, | |
| format="json", | |
| ) | |
| assert res.status_code == 400 | |
| def test_string_false_is_not_read_as_true(reader): | |
| """`bool("false")` vaut True : un corps form-encoded inversait la demande.""" | |
| client, _user, _manga, _chapter = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| res = client.put(put_url, {"last_page_read": 1, "is_read": "false"}, format="json") | |
| assert res.status_code == 200 | |
| assert res.data["is_read"] is False | |
| res = client.put(put_url, {"last_page_read": 2, "is_read": "true"}, format="json") | |
| assert res.data["is_read"] is True | |
| def test_progress_payload_carries_the_bookmark_flag(reader): | |
| client, user, _manga, chapter = reader | |
| MangaReadingProgress.objects.create(user=user, chapter=chapter, is_bookmarked=True) | |
| url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| rows = client.get(url).json()["chapters"] | |
| assert [row["is_bookmarked"] for row in rows] == [True] | |
| def test_a_chapter_without_progress_is_not_bookmarked(reader): | |
| """Le drapeau doit valoir `False` — pas être absent — pour un chapitre jamais | |
| touché : le client filtre dessus sans avoir à distinguer les deux cas.""" | |
| client, *_ = reader | |
| url = reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| rows = client.get(url).json()["chapters"] | |
| assert rows[0]["is_bookmarked"] is False | |
| def test_mark_read_string_false_unmarks(reader): | |
| """Le seul chemin de remise à zéro ne doit pas se faire retourner par "false".""" | |
| client, _user, _manga, _chapter = reader | |
| put_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| client.put(put_url, {"last_page_read": 2, "is_read": True}, format="json") | |
| mark_url = reverse( | |
| "api_manga_progress_mark_read", kwargs={"media_id": "suwayomi:1:809"} | |
| ) | |
| res = client.post( | |
| mark_url, {"chapter_numbers": [164.2], "is_read": "false"}, format="json" | |
| ) | |
| assert res.status_code == 200 | |
| payload = client.get( | |
| reverse("api_manga_progress", kwargs={"media_id": "suwayomi:1:809"}) | |
| ).data | |
| assert payload["chapters"][0]["is_read"] is False | |
| assert payload["chapters"][0]["last_page_read"] == 0 | |
| def _bookmark_url(number: str = "164.2") -> str: | |
| return reverse( | |
| "api_manga_chapter_bookmark", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": number}, | |
| ) | |
| def test_bookmark_toggles_on_and_off(reader): | |
| client, user, _manga, chapter = reader | |
| assert ( | |
| client.put(_bookmark_url(), {"is_bookmarked": True}, format="json").status_code | |
| == 200 | |
| ) | |
| assert ( | |
| MangaReadingProgress.objects.get(user=user, chapter=chapter).is_bookmarked | |
| is True | |
| ) | |
| assert ( | |
| client.put(_bookmark_url(), {"is_bookmarked": False}, format="json").status_code | |
| == 200 | |
| ) | |
| assert ( | |
| MangaReadingProgress.objects.get(user=user, chapter=chapter).is_bookmarked | |
| is False | |
| ) | |
| def test_bookmarking_a_never_opened_chapter_creates_its_progress_row(reader): | |
| """Marquer un chapitre qu'on n'a jamais ouvert est légitime : la ligne de | |
| progression est créée, avec son curseur à zéro.""" | |
| client, user, _manga, chapter = reader | |
| client.put(_bookmark_url(), {"is_bookmarked": True}, format="json") | |
| row = MangaReadingProgress.objects.get(user=user, chapter=chapter) | |
| assert row.is_bookmarked is True | |
| assert row.last_page_read == 0 | |
| assert row.is_read is False | |
| # Les deux tests suivants sont le cœur de la séparation des deux routes. Ils | |
| # gardent une régression FUTURE, pas un bug actuel : `update_or_create` préserve | |
| # aujourd'hui les champs qu'il ne nomme pas. Ils rougiraient si quelqu'un | |
| # ajoutait `is_bookmarked` aux `defaults` du `PUT` de progression, ou | |
| # `last_page_read` à ceux de la bascule. | |
| def test_turning_a_page_does_not_erase_a_bookmark(reader): | |
| client, user, _manga, chapter = reader | |
| client.put(_bookmark_url(), {"is_bookmarked": True}, format="json") | |
| progress_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| client.put(progress_url, {"last_page_read": 2, "is_read": False}, format="json") | |
| row = MangaReadingProgress.objects.get(user=user, chapter=chapter) | |
| assert row.is_bookmarked is True | |
| assert row.last_page_read == 2 | |
| def test_bookmarking_does_not_reset_the_page_cursor(reader): | |
| client, user, _manga, chapter = reader | |
| progress_url = reverse( | |
| "api_manga_chapter_progress", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| client.put(progress_url, {"last_page_read": 2, "is_read": True}, format="json") | |
| client.put(_bookmark_url(), {"is_bookmarked": True}, format="json") | |
| row = MangaReadingProgress.objects.get(user=user, chapter=chapter) | |
| assert row.last_page_read == 2 | |
| assert row.is_read is True | |
| def test_marking_a_chapter_read_does_not_erase_its_bookmark(reader): | |
| """Le troisième écrivain de la ligne, gardé lui aussi. | |
| `POST progress/mark-read/` touche la même ligne que la progression et le | |
| signet, et il est monté dans le composant qui porte l'étoile : ajouter | |
| `is_bookmarked` à son `bulk_update` (ou le forcer à `False`) effacerait le | |
| signet sans qu'aucun autre test ne bronche. | |
| """ | |
| client, user, _manga, chapter = reader | |
| client.put(_bookmark_url(), {"is_bookmarked": True}, format="json") | |
| mark_read_url = reverse( | |
| "api_manga_progress_mark_read", kwargs={"media_id": "suwayomi:1:809"} | |
| ) | |
| client.post( | |
| mark_read_url, | |
| {"chapter_numbers": [164.2], "is_read": True}, | |
| format="json", | |
| ) | |
| row = MangaReadingProgress.objects.get(user=user, chapter=chapter) | |
| assert row.is_read is True | |
| assert row.is_bookmarked is True | |
| # Et la remise à zéro non plus : elle repart de la page 0, pas du signet. | |
| client.post( | |
| mark_read_url, | |
| {"chapter_numbers": [164.2], "is_read": False}, | |
| format="json", | |
| ) | |
| row.refresh_from_db() | |
| assert row.is_read is False | |
| assert row.is_bookmarked is True | |
| def test_bookmark_rejects_an_unknown_chapter(reader): | |
| client, *_ = reader | |
| res = client.put(_bookmark_url("999"), {"is_bookmarked": True}, format="json") | |
| assert res.status_code == 404 | |
| # Même corps que la route sœur de progression, pas un 404 muet. | |
| assert res.data == {"error": "Chapter not found"} | |
| def test_bookmark_requires_authentication(api_client): | |
| url = reverse( | |
| "api_manga_chapter_bookmark", | |
| kwargs={"media_id": "suwayomi:1:809", "chapter_number": "164.2"}, | |
| ) | |
| assert ( | |
| api_client.put(url, {"is_bookmarked": True}, format="json").status_code == 403 | |
| ) | |