Spaces:
Running
Running
| """Coverage-focused tests for animetix.api.core view module. | |
| Pattern: build a MagicMock container, patch ``animetix.api.core.get_container`` | |
| to return it, drive the views via ``RequestFactory`` + ``force_authenticate`` | |
| with ``permission_classes`` neutralised (matching the existing test_cognition | |
| style). DB-backed views use the real ORM under ``@pytest.mark.django_db``. | |
| These complement (do not replace) tests/api/test_explore.py. | |
| """ | |
| import base64 | |
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| from animetix.api.core import ( | |
| ConfigView, | |
| CurrentUserView, | |
| CustomConfigDataView, | |
| FavoriteMangaListView, | |
| FavoriteMangaToggleView, | |
| LoginView, | |
| LogoutView, | |
| MangaChapterDetailView, | |
| MangaChapterListView, | |
| MangaChapterSyncView, | |
| MediaDetailView, | |
| MediaSearchView, | |
| RegisterView, | |
| SuwayomiExtensionsActionView, | |
| SuwayomiExtensionsListView, | |
| SuwayomiImportView, | |
| SuwayomiSearchView, | |
| SuwayomiSourcesView, | |
| TrackerConnectionLinkView, | |
| TrackerConnectionListView, | |
| TrackerConnectionUnlinkView, | |
| TransparencyDataView, | |
| image_proxy_view, | |
| suwayomi_image_proxy, | |
| ) | |
| from django.contrib.auth.models import User | |
| from django.test import RequestFactory | |
| from rest_framework.test import force_authenticate | |
| GET_CONTAINER = "animetix.api.core.get_container" | |
| def factory(): | |
| return RequestFactory() | |
| def _drive(view_cls, request, user=None, **view_kwargs): | |
| """Neutralise permissions, authenticate, and dispatch a view.""" | |
| with patch.object(view_cls, "permission_classes", []): | |
| view = view_cls.as_view() | |
| if user is not None: | |
| force_authenticate(request, user=user) | |
| return view(request, **view_kwargs) | |
| # --------------------------------------------------------------------------- # | |
| # image_proxy_view (function-based) | |
| # --------------------------------------------------------------------------- # | |
| def test_image_proxy_missing_params(factory): | |
| request = factory.get("/img/") | |
| response = image_proxy_view(request) | |
| assert response.status_code == 400 | |
| def test_image_proxy_bad_base64(factory): | |
| request = factory.get("/img/", {"url": "@@@not-base64@@@", "sig": "x"}) | |
| response = image_proxy_view(request) | |
| assert response.status_code == 400 | |
| def test_image_proxy_invalid_signature(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "deadbeef"}) | |
| with patch("animetix.api.core.verify_proxy_signature", return_value=False): | |
| response = image_proxy_view(request) | |
| assert response.status_code == 403 | |
| def test_image_proxy_cache_hit(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "good"}) | |
| with ( | |
| patch("animetix.api.core.verify_proxy_signature", return_value=True), | |
| patch("animetix.api.core.cache") as mock_cache, | |
| ): | |
| mock_cache.get.return_value = { | |
| "content": b"BYTES", | |
| "content_type": "image/png", | |
| } | |
| response = image_proxy_view(request) | |
| assert response.status_code == 200 | |
| assert response["Content-Type"] == "image/png" | |
| def test_image_proxy_success_fetch(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "good"}) | |
| upstream = MagicMock( | |
| status_code=200, content=b"IMG", headers={"Content-Type": "image/png"} | |
| ) | |
| with ( | |
| patch("animetix.api.core.verify_proxy_signature", return_value=True), | |
| patch("animetix.api.core.cache") as mock_cache, | |
| patch("animetix.api.core.safe_http_request", return_value=upstream), | |
| patch("animetix.api.core.validate_file_size", return_value=True), | |
| patch("animetix.api.core.validate_file_mime_type", return_value=True), | |
| ): | |
| mock_cache.get.return_value = None | |
| response = image_proxy_view(request) | |
| assert response.status_code == 200 | |
| assert response.content == b"IMG" | |
| def test_image_proxy_too_large(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "good"}) | |
| upstream = MagicMock(status_code=200, content=b"IMG", headers={}) | |
| with ( | |
| patch("animetix.api.core.verify_proxy_signature", return_value=True), | |
| patch("animetix.api.core.cache") as mock_cache, | |
| patch("animetix.api.core.safe_http_request", return_value=upstream), | |
| patch("animetix.api.core.validate_file_size", return_value=False), | |
| ): | |
| mock_cache.get.return_value = None | |
| response = image_proxy_view(request) | |
| assert response.status_code == 413 | |
| def test_image_proxy_wrong_mime(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "good"}) | |
| upstream = MagicMock(status_code=200, content=b"IMG", headers={}) | |
| with ( | |
| patch("animetix.api.core.verify_proxy_signature", return_value=True), | |
| patch("animetix.api.core.cache") as mock_cache, | |
| patch("animetix.api.core.safe_http_request", return_value=upstream), | |
| patch("animetix.api.core.validate_file_size", return_value=True), | |
| patch("animetix.api.core.validate_file_mime_type", return_value=False), | |
| ): | |
| mock_cache.get.return_value = None | |
| response = image_proxy_view(request) | |
| assert response.status_code == 403 | |
| def test_image_proxy_unsafe_request(factory): | |
| encoded = base64.b64encode(b"http://example.com/a.png").decode() | |
| request = factory.get("/img/", {"url": encoded, "sig": "good"}) | |
| with ( | |
| patch("animetix.api.core.verify_proxy_signature", return_value=True), | |
| patch("animetix.api.core.cache") as mock_cache, | |
| patch("animetix.api.core.safe_http_request", side_effect=ValueError("blocked")), | |
| ): | |
| mock_cache.get.return_value = None | |
| response = image_proxy_view(request) | |
| assert response.status_code == 403 | |
| # --------------------------------------------------------------------------- # | |
| # MediaSearchView | |
| # --------------------------------------------------------------------------- # | |
| def test_media_search_empty_returns_empty_list(factory): | |
| request = factory.get("/search/") | |
| response = _drive(MediaSearchView, request) | |
| assert response.status_code == 200 | |
| assert response.data == [] | |
| def test_media_search_success(factory): | |
| request = factory.get("/search/", {"q": "naruto", "media_type": "Anime"}) | |
| container = MagicMock() | |
| # The view calls the provider: container.core.catalog_service().search_items(...) | |
| container.core.catalog_service.return_value.search_items.return_value = [ | |
| {"id": "1", "title": "Naruto"} | |
| ] | |
| with patch(GET_CONTAINER, return_value=container): | |
| view = MediaSearchView() | |
| view.guardrail_service = MagicMock() | |
| view.guardrail_service.validate_input.return_value = {"is_safe": True} | |
| view.usage_port = MagicMock() | |
| # call the bound get directly to control injected services | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| drf_request = Request(request, parsers=[JSONParser()]) | |
| response = view.get(drf_request) | |
| assert response.status_code == 200 | |
| assert response.data[0]["id"] == "1" | |
| def test_media_item_serializer_exposes_type_and_image_url(): | |
| """The search UI reads item.type and item.image_url; the serializer must | |
| emit both (without them: no thumbnails, empty type tabs, and detail links | |
| resolving to /media/undefined/{id}/).""" | |
| from animetix.serializers import MediaItemSerializer | |
| raw = { | |
| "id": "5", | |
| "title": "Naruto", | |
| "media_type": "Anime", | |
| "image": "http://cdn/naruto.jpg", | |
| } | |
| data = MediaItemSerializer(raw).data | |
| assert data["type"] == "Anime" | |
| assert data["image_url"] == "http://cdn/naruto.jpg" | |
| # Backward compat: legacy `image` key stays populated. | |
| assert data["image"] == "http://cdn/naruto.jpg" | |
| def test_media_search_unsafe_query(factory): | |
| request = factory.get("/search/", {"q": "evil"}) | |
| view = MediaSearchView() | |
| view.guardrail_service = MagicMock() | |
| view.guardrail_service.validate_input.return_value = { | |
| "is_safe": False, | |
| "reason": "blocked", | |
| } | |
| view.usage_port = MagicMock() | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| drf_request = Request(request, parsers=[JSONParser()]) | |
| response = view.get(drf_request) | |
| assert response.status_code == 400 | |
| assert response.data["error"] == "blocked" | |
| def test_media_search_post_unauthenticated(factory): | |
| request = factory.post("/search/") | |
| response = _drive(MediaSearchView, request) | |
| # AllowAny + manual is_authenticated check -> 401 | |
| assert response.status_code == 401 | |
| # --------------------------------------------------------------------------- # | |
| # GameSessionView covered indirectly; Config / CurrentUser / CustomConfig | |
| # --------------------------------------------------------------------------- # | |
| def test_config_view_anonymous(factory): | |
| request = factory.get("/config/") | |
| response = _drive(ConfigView, request) | |
| assert response.status_code == 200 | |
| assert response.data["user"]["is_authenticated"] is False | |
| assert response.data["theme"] == "auto" | |
| def test_config_view_authenticated(factory): | |
| user = User.objects.create_user(username="cfg", password="pw") | |
| request = factory.get("/config/") | |
| # NOTE: production ConfigView reads ``request.user.profile.rank`` which the | |
| # Profile model does not define; patch it so the auth branch is exercised. | |
| with patch("animetix.models.Profile.rank", "Bronze", create=True): | |
| response = _drive(ConfigView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["user"]["username"] == "cfg" | |
| def test_current_user_unauthenticated(factory): | |
| request = factory.get("/auth/me/") | |
| response = _drive(CurrentUserView, request) | |
| assert response.status_code == 401 | |
| def test_current_user_authenticated(factory): | |
| user = User.objects.create_user(username="me", password="pw") | |
| request = factory.get("/auth/me/") | |
| response = _drive(CurrentUserView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["user"]["username"] == "me" | |
| def test_custom_config_guest(factory): | |
| request = factory.get("/custom-config/") | |
| response = _drive(CustomConfigDataView, request) | |
| assert response.status_code == 200 | |
| assert response.data["status"] == "guest" | |
| def test_custom_config_authenticated(factory): | |
| user = User.objects.create_user(username="cc", password="pw") | |
| request = factory.get("/custom-config/") | |
| response = _drive(CustomConfigDataView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["status"] == "stub" | |
| # --------------------------------------------------------------------------- # | |
| # Auth: Login / Logout / Register | |
| # --------------------------------------------------------------------------- # | |
| def test_login_success(factory): | |
| User.objects.create_user(username="lo", password="secret123") | |
| request = factory.post("/auth/login/", {"username": "lo", "password": "secret123"}) | |
| with patch("animetix.api.core.login"): | |
| response = _drive(LoginView, request) | |
| assert response.status_code == 200 | |
| assert response.data["success"] is True | |
| def test_login_invalid(factory): | |
| request = factory.post("/auth/login/", {"username": "x", "password": "bad"}) | |
| response = _drive(LoginView, request) | |
| assert response.status_code == 401 | |
| assert response.data["success"] is False | |
| def test_logout(factory): | |
| user = User.objects.create_user(username="out", password="pw") | |
| request = factory.post("/auth/logout/") | |
| with patch("animetix.api.core.logout"): | |
| response = _drive(LogoutView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["success"] is True | |
| def test_register_missing_fields(factory): | |
| request = factory.post("/auth/register/", {"username": "only"}) | |
| response = _drive(RegisterView, request) | |
| assert response.status_code == 400 | |
| assert response.data["error"] == "Missing fields" | |
| def test_register_duplicate(factory): | |
| User.objects.create_user(username="dup", password="pw", email="d@x.com") | |
| request = factory.post( | |
| "/auth/register/", | |
| {"username": "dup", "password": "pw", "email": "d@x.com"}, | |
| ) | |
| response = _drive(RegisterView, request) | |
| assert response.status_code == 400 | |
| assert "exists" in response.data["error"] | |
| def test_register_success(factory): | |
| request = factory.post( | |
| "/auth/register/", | |
| {"username": "newbie", "password": "pw12345", "email": "n@x.com"}, | |
| ) | |
| with patch("animetix.api.core.login"): | |
| response = _drive(RegisterView, request) | |
| assert response.status_code == 200 | |
| assert response.data["success"] is True | |
| assert User.objects.filter(username="newbie").exists() | |
| # --------------------------------------------------------------------------- # | |
| # MediaDetailView | |
| # --------------------------------------------------------------------------- # | |
| def test_media_detail_from_sql(factory): | |
| from animetix.models import MediaItem | |
| MediaItem.objects.create(external_id="sql-1", media_type="Anime", title="From SQL") | |
| request = factory.get("/media/Anime/sql-1/") | |
| response = _drive(MediaDetailView, request, media_type="Anime", item_id="sql-1") | |
| assert response.status_code == 200 | |
| assert response.data["id"] == "sql-1" | |
| def test_media_detail_catalog_fallback(factory): | |
| request = factory.get("/media/Anime/cat-9/") | |
| container = MagicMock() | |
| container.core.catalog_service.return_value.load_data.return_value = { | |
| "db": [ | |
| { | |
| "id": "cat-9", | |
| "title": "Catalog Hit", | |
| "graph_nodes": { | |
| "studios": ["MAPPA"], | |
| "author": "Someone", | |
| "related_items": [], | |
| }, | |
| } | |
| ] | |
| } | |
| with patch(GET_CONTAINER, return_value=container): | |
| response = _drive(MediaDetailView, request, media_type="Anime", item_id="cat-9") | |
| assert response.status_code == 200 | |
| assert response.data["studios"] == ["MAPPA"] | |
| def test_media_detail_not_found(factory): | |
| request = factory.get("/media/Anime/missing/") | |
| container = MagicMock() | |
| container.core.catalog_service.load_data.return_value = {"db": []} | |
| with patch(GET_CONTAINER, return_value=container): | |
| response = _drive( | |
| MediaDetailView, request, media_type="Anime", item_id="missing" | |
| ) | |
| assert response.status_code == 404 | |
| # --------------------------------------------------------------------------- # | |
| # TransparencyDataView (ORM aggregation) | |
| # --------------------------------------------------------------------------- # | |
| def test_transparency_data(factory): | |
| request = factory.get("/transparency/") | |
| response = _drive(TransparencyDataView, request) | |
| assert response.status_code == 200 | |
| assert response.data["status"] == "synchronized" | |
| assert "global_metrics" in response.data | |
| # --------------------------------------------------------------------------- # | |
| # Manga chapters (service-backed; serializer patched to avoid deep ORM) | |
| # --------------------------------------------------------------------------- # | |
| def test_manga_chapter_list(factory): | |
| request = factory.get("/media/Manga/m1/chapters/") | |
| container = MagicMock() | |
| container.core.manga_service.return_value.get_chapters.return_value = [] | |
| with ( | |
| patch(GET_CONTAINER, return_value=container), | |
| patch("animetix.api.core.MangaChapterSerializer") as mock_ser, | |
| ): | |
| mock_ser.return_value.data = [{"number": 1.0}] | |
| response = _drive(MangaChapterListView, request, media_id="m1") | |
| assert response.status_code == 200 | |
| assert response.data == [{"number": 1.0}] | |
| def test_manga_chapter_detail_found(factory): | |
| request = factory.get("/media/Manga/m1/chapters/1/") | |
| container = MagicMock() | |
| container.core.manga_service.return_value.get_chapter_details.return_value = ( | |
| object() | |
| ) | |
| with ( | |
| patch(GET_CONTAINER, return_value=container), | |
| patch("animetix.api.core.MangaChapterSerializer") as mock_ser, | |
| ): | |
| mock_ser.return_value.data = {"number": 1.0} | |
| response = _drive( | |
| MangaChapterDetailView, request, media_id="m1", chapter_number="1" | |
| ) | |
| assert response.status_code == 200 | |
| def test_manga_chapter_detail_not_found(factory): | |
| request = factory.get("/media/Manga/m1/chapters/99/") | |
| container = MagicMock() | |
| container.core.manga_service.return_value.get_chapter_details.return_value = None | |
| with patch(GET_CONTAINER, return_value=container): | |
| response = _drive( | |
| MangaChapterDetailView, request, media_id="m1", chapter_number="99" | |
| ) | |
| assert response.status_code == 404 | |
| # --------------------------------------------------------------------------- # | |
| # suwayomi_image_proxy (function-based) | |
| # --------------------------------------------------------------------------- # | |
| def test_suwayomi_image_missing_url(factory): | |
| request = factory.get("/suwayomi-image/") | |
| response = suwayomi_image_proxy(request) | |
| assert response.status_code == 400 | |
| def test_suwayomi_image_bad_base64(factory): | |
| request = factory.get("/suwayomi-image/", {"page_url": "@@bad@@"}) | |
| response = suwayomi_image_proxy(request) | |
| assert response.status_code == 400 | |
| def test_suwayomi_image_forbidden_external(factory): | |
| encoded = base64.b64encode(b"http://evil.example/x.png").decode() | |
| request = factory.get("/suwayomi-image/", {"page_url": encoded}) | |
| with patch("core.config.settings") as mock_settings: | |
| mock_settings.SUWAYOMI_URL = "http://suwayomi.local" | |
| mock_settings.SUWAYOMI_PASSWORD = "" | |
| response = suwayomi_image_proxy(request) | |
| assert response.status_code == 403 | |
| def test_suwayomi_image_success(factory): | |
| encoded = base64.b64encode(b"relative/page.png").decode() | |
| request = factory.get("/suwayomi-image/", {"page_url": encoded}) | |
| upstream = MagicMock( | |
| status_code=200, content=b"PNG", headers={"Content-Type": "image/png"} | |
| ) | |
| with ( | |
| patch("core.config.settings") as mock_settings, | |
| patch("animetix.api.core.safe_http_request", return_value=upstream), | |
| ): | |
| mock_settings.SUWAYOMI_URL = "http://suwayomi.local" | |
| mock_settings.SUWAYOMI_PASSWORD = "secret" | |
| response = suwayomi_image_proxy(request) | |
| assert response.status_code == 200 | |
| assert response.content == b"PNG" | |
| # --------------------------------------------------------------------------- # | |
| # Suwayomi adapter-backed views | |
| # --------------------------------------------------------------------------- # | |
| def test_suwayomi_sources_not_configured(factory): | |
| request = factory.get("/explore/suwayomi/sources/") | |
| with patch.object(SuwayomiSourcesView, "permission_classes", []): | |
| view = SuwayomiSourcesView() | |
| view.suwayomi_adapter = None | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 500 | |
| def test_suwayomi_sources_success(factory): | |
| request = factory.get("/explore/suwayomi/sources/") | |
| view = SuwayomiSourcesView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.get_sources.return_value = [{"id": "1"}] | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 200 | |
| assert response.data == [{"id": "1"}] | |
| def test_suwayomi_search_missing_source(factory): | |
| request = factory.get("/explore/suwayomi/search/") | |
| view = SuwayomiSearchView() | |
| view.suwayomi_adapter = MagicMock() | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 400 | |
| def test_suwayomi_search_success(factory): | |
| request = factory.get("/explore/suwayomi/search/", {"source_id": "s1", "q": "x"}) | |
| view = SuwayomiSearchView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.search_manga.return_value = [{"id": "m"}] | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 200 | |
| assert response.data == [{"id": "m"}] | |
| def test_suwayomi_extensions_list_success(factory): | |
| request = factory.get("/explore/suwayomi/extensions/") | |
| view = SuwayomiExtensionsListView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.get_extensions.return_value = [{"pkg": "x"}] | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 200 | |
| def test_suwayomi_extensions_list_error(factory): | |
| request = factory.get("/explore/suwayomi/extensions/") | |
| view = SuwayomiExtensionsListView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.get_extensions.side_effect = RuntimeError("boom") | |
| from rest_framework.request import Request | |
| response = view.get(Request(request)) | |
| assert response.status_code == 500 | |
| def test_suwayomi_extensions_action_invalid_params(factory): | |
| request = factory.post("/extensions/action/", {}, content_type="application/json") | |
| view = SuwayomiExtensionsActionView() | |
| view.suwayomi_adapter = MagicMock() | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 400 | |
| def test_suwayomi_extensions_action_invalid_action(factory): | |
| import json | |
| request = factory.post( | |
| "/extensions/action/", | |
| json.dumps({"ids": ["1"], "action": "explode"}), | |
| content_type="application/json", | |
| ) | |
| view = SuwayomiExtensionsActionView() | |
| view.suwayomi_adapter = MagicMock() | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 400 | |
| def test_suwayomi_extensions_action_success(factory): | |
| import json | |
| request = factory.post( | |
| "/extensions/action/", | |
| json.dumps({"ids": ["1"], "action": "install"}), | |
| content_type="application/json", | |
| ) | |
| view = SuwayomiExtensionsActionView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.update_extensions.return_value = {"ok": True} | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 200 | |
| # --------------------------------------------------------------------------- # | |
| # SuwayomiImportView | |
| # --------------------------------------------------------------------------- # | |
| def test_suwayomi_import_missing_params(factory): | |
| import json | |
| request = factory.post("/import/", json.dumps({}), content_type="application/json") | |
| view = SuwayomiImportView() | |
| view.suwayomi_adapter = MagicMock() | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 400 | |
| def test_suwayomi_import_details_missing(factory): | |
| import json | |
| request = factory.post( | |
| "/import/", | |
| json.dumps({"source_id": "s", "suwayomi_manga_id": "m"}), | |
| content_type="application/json", | |
| ) | |
| view = SuwayomiImportView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.get_manga_details.return_value = None | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 404 | |
| def test_suwayomi_import_success(factory): | |
| import json | |
| request = factory.post( | |
| "/import/", | |
| json.dumps({"source_id": "s", "suwayomi_manga_id": "m"}), | |
| content_type="application/json", | |
| ) | |
| view = SuwayomiImportView() | |
| view.suwayomi_adapter = MagicMock() | |
| view.suwayomi_adapter.get_manga_details.return_value = { | |
| "title": "Imported", | |
| "description": "d", | |
| "thumbnailUrl": "http://thumb/x.png", | |
| } | |
| container = MagicMock() | |
| from rest_framework.parsers import JSONParser | |
| from rest_framework.request import Request | |
| with patch(GET_CONTAINER, return_value=container): | |
| response = view.post(Request(request, parsers=[JSONParser()])) | |
| assert response.status_code == 200 | |
| assert response.data["success"] is True | |
| assert response.data["media_item"]["title"] == "Imported" | |
| # --------------------------------------------------------------------------- # | |
| # Tracker connections | |
| # --------------------------------------------------------------------------- # | |
| def test_tracker_list(factory): | |
| user = User.objects.create_user(username="tl", password="pw") | |
| request = factory.get("/profile/trackers/") | |
| response = _drive(TrackerConnectionListView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data == [] | |
| def test_tracker_link_missing_fields(factory): | |
| import json | |
| user = User.objects.create_user(username="tk", password="pw") | |
| request = factory.post( | |
| "/profile/trackers/link/", | |
| json.dumps({"tracker": "anilist"}), | |
| content_type="application/json", | |
| ) | |
| response = _drive(TrackerConnectionLinkView, request, user=user) | |
| assert response.status_code == 400 | |
| def test_tracker_link_invalid_type(factory): | |
| import json | |
| user = User.objects.create_user(username="tk2", password="pw") | |
| request = factory.post( | |
| "/profile/trackers/link/", | |
| json.dumps({"tracker": "bogus", "username": "u", "token": "t"}), | |
| content_type="application/json", | |
| ) | |
| response = _drive(TrackerConnectionLinkView, request, user=user) | |
| assert response.status_code == 400 | |
| def test_tracker_link_success(factory): | |
| import json | |
| user = User.objects.create_user(username="tk3", password="pw") | |
| request = factory.post( | |
| "/profile/trackers/link/", | |
| json.dumps({"tracker": "anilist", "username": "u", "token": "t"}), | |
| content_type="application/json", | |
| ) | |
| response = _drive(TrackerConnectionLinkView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["success"] is True | |
| def test_tracker_unlink_missing(factory): | |
| import json | |
| user = User.objects.create_user(username="tu", password="pw") | |
| request = factory.post( | |
| "/profile/trackers/unlink/", | |
| json.dumps({}), | |
| content_type="application/json", | |
| ) | |
| response = _drive(TrackerConnectionUnlinkView, request, user=user) | |
| assert response.status_code == 400 | |
| def test_tracker_unlink_success(factory): | |
| import json | |
| from animetix.models import TrackerConnection | |
| user = User.objects.create_user(username="tu2", password="pw") | |
| TrackerConnection.objects.create(user=user, tracker="anilist", token="t") | |
| request = factory.post( | |
| "/profile/trackers/unlink/", | |
| json.dumps({"tracker": "anilist"}), | |
| content_type="application/json", | |
| ) | |
| response = _drive(TrackerConnectionUnlinkView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data["deleted"] is True | |
| # --------------------------------------------------------------------------- # | |
| # Favorites | |
| # --------------------------------------------------------------------------- # | |
| def test_favorite_list(factory): | |
| user = User.objects.create_user(username="fl", password="pw") | |
| request = factory.get("/media/favorites/") | |
| response = _drive(FavoriteMangaListView, request, user=user) | |
| assert response.status_code == 200 | |
| assert response.data == [] | |
| def test_favorite_toggle_get(factory): | |
| user = User.objects.create_user(username="ft", password="pw") | |
| request = factory.get("/media/Manga/x/favorite/") | |
| response = _drive(FavoriteMangaToggleView, request, user=user, media_id="x") | |
| assert response.status_code == 200 | |
| assert response.data["is_favorite"] is False | |
| def test_favorite_toggle_add_and_remove(factory): | |
| from animetix.models import MediaItem | |
| user = User.objects.create_user(username="ft2", password="pw") | |
| MediaItem.objects.create(external_id="man-1", media_type="Manga", title="M") | |
| # First toggle -> favorite added | |
| request = factory.post( | |
| "/media/Manga/man-1/favorite/", {}, content_type="application/json" | |
| ) | |
| response = _drive(FavoriteMangaToggleView, request, user=user, media_id="man-1") | |
| assert response.status_code == 200 | |
| assert response.data["is_favorite"] is True | |
| # Second toggle -> removed | |
| request2 = factory.post( | |
| "/media/Manga/man-1/favorite/", {}, content_type="application/json" | |
| ) | |
| response2 = _drive(FavoriteMangaToggleView, request2, user=user, media_id="man-1") | |
| assert response2.data["is_favorite"] is False | |
| def test_favorite_toggle_missing_manga_no_import_params(factory): | |
| user = User.objects.create_user(username="ft3", password="pw") | |
| request = factory.post( | |
| "/media/Manga/none/favorite/", {}, content_type="application/json" | |
| ) | |
| response = _drive(FavoriteMangaToggleView, request, user=user, media_id="none") | |
| assert response.status_code == 400 | |
| # --------------------------------------------------------------------------- # | |
| # MangaChapterSyncView | |
| # --------------------------------------------------------------------------- # | |
| def test_chapter_sync_manga_not_found(factory): | |
| user = User.objects.create_user(username="cs", password="pw") | |
| request = factory.post("/sync/", {}, content_type="application/json") | |
| response = _drive( | |
| MangaChapterSyncView, request, user=user, media_id="nope", chapter_number="1" | |
| ) | |
| assert response.status_code == 404 | |
| def test_chapter_sync_no_trackers(factory): | |
| from animetix.models import MediaItem | |
| user = User.objects.create_user(username="cs2", password="pw") | |
| MediaItem.objects.create(external_id="man-2", media_type="Manga", title="M2") | |
| request = factory.post("/sync/", {}, content_type="application/json") | |
| response = _drive( | |
| MangaChapterSyncView, request, user=user, media_id="man-2", chapter_number="3" | |
| ) | |
| assert response.status_code == 200 | |
| assert "No trackers" in response.data["message"] | |
| def test_chapter_sync_anilist_simulated(factory): | |
| from animetix.models import MediaItem, TrackerConnection | |
| user = User.objects.create_user(username="cs3", password="pw") | |
| MediaItem.objects.create( | |
| external_id="123", media_type="Manga", title="Numeric", metadata={} | |
| ) | |
| TrackerConnection.objects.create( | |
| user=user, tracker="anilist", token="mock-token", username="u" | |
| ) | |
| request = factory.post("/sync/", {}, content_type="application/json") | |
| response = _drive( | |
| MangaChapterSyncView, request, user=user, media_id="123", chapter_number="5" | |
| ) | |
| assert response.status_code == 200 | |
| assert response.data["results"]["anilist"]["simulated"] is True | |
| def test_chapter_sync_mal_simulated(factory): | |
| from animetix.models import MediaItem, TrackerConnection | |
| user = User.objects.create_user(username="cs4", password="pw") | |
| MediaItem.objects.create( | |
| external_id="man-3", | |
| media_type="Manga", | |
| title="MalManga", | |
| metadata={"idMal": 42}, | |
| ) | |
| TrackerConnection.objects.create( | |
| user=user, tracker="myanimelist", token="test-token", username="u" | |
| ) | |
| request = factory.post("/sync/", {}, content_type="application/json") | |
| response = _drive( | |
| MangaChapterSyncView, request, user=user, media_id="man-3", chapter_number="7" | |
| ) | |
| assert response.status_code == 200 | |
| assert response.data["results"]["myanimelist"]["simulated"] is True | |