"""Additional coverage for animetix.api.multiverse view module. Complements tests/api/test_multiverse_api.py and test_multiverse_export.py by exercising the uncovered branches: empty results, default/invalid query params, incoming relations, and the description/cosmology/character/relation fallbacks. The multiverse views resolve their Neo4j manager through the DI container via ``Provide[Container.persistence.graph_persistence_port]``; we override that provider with an ``Object`` provider wrapping a MagicMock (same pattern as the existing multiverse tests). """ from unittest.mock import MagicMock import pytest from animetix.containers import container from dependency_injector import providers from django.contrib.auth.models import User from django.urls import reverse from rest_framework.test import APIClient @pytest.fixture def api_client(): return APIClient() @pytest.fixture def authenticated_user(api_client, db): user = User.objects.create_user(username="mvuser", password="password") api_client.force_authenticate(user=user) return user def _override(neo4j_mock): return container.persistence.graph_persistence_port.override( providers.Object(neo4j_mock) ) # --------------------------------------------------------------------------- # # MultiverseGalleryView # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_gallery_empty_results(api_client, authenticated_user): """No synthetic media -> empty nodes/links (success, empty graph).""" neo4j = MagicMock() neo4j.execute_query.return_value = [] with _override(neo4j): response = api_client.get(reverse("api_multiverse_gallery")) assert response.status_code == 200 body = response.json() assert body["nodes"] == [] assert body["links"] == [] @pytest.mark.django_db def test_gallery_skips_records_missing_media_or_genre(api_client, authenticated_user): """Records lacking media or genre are skipped (continue branch).""" genre = MagicMock() genre.get.side_effect = lambda k, d=None: {"name": "Seinen"}.get(k, d) neo4j = MagicMock() neo4j.execute_query.return_value = [ {"m": None, "g": genre, "characters": []}, # missing media -> skipped {"m": MagicMock(), "g": None, "characters": []}, # missing genre -> skipped ] with _override(neo4j): response = api_client.get(reverse("api_multiverse_gallery")) assert response.status_code == 200 body = response.json() assert body["nodes"] == [] assert body["links"] == [] # --------------------------------------------------------------------------- # # MultiverseCatalogView # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_catalog_defaults_no_params(api_client, authenticated_user): """No query params -> defaults applied, empty data set still succeeds.""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ [{"total": 0}], # count [], # data [], # genres ] with _override(neo4j): response = api_client.get(reverse("api_multiverse_catalog")) assert response.status_code == 200 body = response.json() assert body["results"] == [] assert body["pagination"]["page"] == 1 assert body["pagination"]["page_size"] == 12 assert body["pagination"]["total"] == 0 assert body["pagination"]["total_pages"] == 1 assert body["pagination"]["has_next"] is False assert body["pagination"]["has_previous"] is False assert body["filters"]["sort"] == "newest" assert body["available_genres"] == [] @pytest.mark.django_db def test_catalog_invalid_page_params_fallback(api_client, authenticated_user): """Non-integer page/page_size fall back to defaults (except branches).""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ [{"total": 0}], [], [], ] with _override(neo4j): response = api_client.get( reverse("api_multiverse_catalog"), {"page": "abc", "page_size": "xyz", "sort": "unknown_sort"}, ) assert response.status_code == 200 body = response.json() # invalid page/page_size -> defaults 1 / 12 assert body["pagination"]["page"] == 1 assert body["pagination"]["page_size"] == 12 # unknown sort still echoed in filters (mapped internally to default order) assert body["filters"]["sort"] == "unknown_sort" @pytest.mark.django_db def test_catalog_page_size_clamped_and_pagination_flags(api_client, authenticated_user): """page_size clamped to max 48; has_next/has_previous computed.""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ [{"total": 100}], # count -> multiple pages [ { "media": {"title": "Fallback Title Universe"}, # name missing -> title "genre": {}, # genre name missing -> "Unknown" "characters": [], "char_count": 0, } ], [{"name": "Isekai", "count": 5}], ] with _override(neo4j): response = api_client.get( reverse("api_multiverse_catalog"), {"page": "2", "page_size": "999", "sort": "characters", "search": "x"}, ) assert response.status_code == 200 body = response.json() assert body["pagination"]["page"] == 2 assert body["pagination"]["page_size"] == 48 # clamped assert body["pagination"]["has_previous"] is True assert body["pagination"]["has_next"] is True # name/title + genre fallbacks result = body["results"][0] assert result["name"] == "Fallback Title Universe" assert result["genre"] == "Unknown" assert result["character_count"] == 0 @pytest.mark.django_db def test_catalog_handles_empty_count_result(api_client, authenticated_user): """count_result empty list -> total defaults to 0 (ternary false branch).""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ [], # count returns empty -> total = 0 [], # data None, # genres None -> available_genres = [] ] with _override(neo4j): response = api_client.get(reverse("api_multiverse_catalog")) assert response.status_code == 200 body = response.json() assert body["pagination"]["total"] == 0 assert body["available_genres"] == [] # --------------------------------------------------------------------------- # # MultiverseExportPDFView # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_export_pdf_fallbacks_no_chars_no_relations(api_client, authenticated_user): """Missing description/cosmology, no characters, no relations -> fallback text. Exercises the 'else' branches that print the placeholder messages and the incoming-relation handling is covered in a separate test below. """ neo4j = MagicMock() neo4j.execute_query.side_effect = [ # universe query: media without description/cosmology, no genre [{"media": {"name": "Bare Universe"}, "genre": None}], # characters query: empty [], # relations query: empty [], ] with _override(neo4j): response = api_client.get( reverse("api_multiverse_export_pdf", args=["Bare Universe"]) ) assert response.status_code == 200 assert response["Content-Type"] == "application/pdf" pdf = b"".join(response.streaming_content) assert pdf.startswith(b"%PDF") @pytest.mark.django_db def test_export_pdf_incoming_relation_and_title_fallback( api_client, authenticated_user ): """Incoming relation (is_outgoing False) + media uses 'title' fallback.""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ # universe: 'title' instead of 'name', has genre [{"media": {"title": "Titled Universe"}, "genre": {"name": "Mecha"}}], # characters with one entry [{"character": {"name": "Pilot", "role": "Hero", "power_level": 42}}], # incoming relation: is_outgoing False, no labels -> "Entity" [ { "rel_type": "INSPIRED_BY", "labels": [], "properties": {"title": "Some Concept"}, "is_outgoing": False, }, # relation without properties -> filtered out {"rel_type": "NOOP", "labels": ["X"], "properties": None}, ], ] with _override(neo4j): response = api_client.get( reverse("api_multiverse_export_pdf", args=["Titled Universe"]) ) assert response.status_code == 200 pdf = b"".join(response.streaming_content) assert pdf.startswith(b"%PDF") @pytest.mark.django_db def test_export_pdf_sanitizes_empty_filename(api_client, authenticated_user): """A universe name with only special chars -> filename falls back to default.""" neo4j = MagicMock() neo4j.execute_query.side_effect = [ [{"media": {"name": "***"}, "genre": {"name": "G"}}], [], [], ] with _override(neo4j): response = api_client.get(reverse("api_multiverse_export_pdf", args=["***"])) assert response.status_code == 200 # safe_filename empty -> "multiverse_lore" assert "wiki_multiverse_lore.pdf" in response["Content-Disposition"]