"""Coverage for animetix.api.developer view module. Covers the B2B developer RAG endpoint, API-key metadata/generation and the (now free/manual) Pro-tier subscription mock endpoint. External collaborators are mocked: - the constructor-injected agentic RAG via a real-container provider override - usage logging via ``developer.DjangoUsageAdapter`` ORM access uses the real (sqlite) test DB through ``@pytest.mark.django_db``. """ from contextlib import contextmanager from unittest.mock import MagicMock, patch import pytest from animetix.api import developer as developer_mod from animetix.containers import get_container from django.contrib.auth.models import User from django.urls import reverse from rest_framework.test import APIClient real_container = get_container() @contextmanager def _override_agentic_rag(agent): real_container.agentic.agentic_rag.override(agent) try: yield finally: real_container.agentic.agentic_rag.reset_last_overriding() def _astream(events): """Async-generator factory mirroring aplan_and_solve_stream's signature.""" async def _gen(query, media_type, user_id=None): for e in events: yield e return _gen @pytest.fixture def api_client(): return APIClient() @pytest.fixture def user(db): # Profile is auto-created via post_save signal. return User.objects.create_user(username="devuser", password="password") @pytest.fixture def auth_client(api_client, user): api_client.force_authenticate(user=user) return api_client # --------------------------------------------------------------------------- # # DeveloperRAGView # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_developer_rag_missing_query(auth_client): """No query -> 400.""" response = auth_client.post(reverse("developer_rag"), {}, format="json") assert response.status_code == 400 assert response.json()["error"] == "No query provided" @pytest.mark.django_db def test_developer_rag_success_result_event(auth_client): """Stream emits a 'result' event -> answer extracted from it.""" agent = MagicMock() agent.aplan_and_solve_stream.side_effect = _astream( [ {"type": "status", "content": "thinking"}, {"type": "result", "content": "Luffy is the captain."}, ] ) with ( _override_agentic_rag(agent), patch.object(developer_mod, "DjangoUsageAdapter") as usage_cls, ): response = auth_client.post( reverse("developer_rag"), {"query": "Who is Luffy?", "media_type": "Manga"}, format="json", ) assert response.status_code == 200 body = response.json() assert body["status"] == "success" assert body["answer"] == "Luffy is the captain." assert body["media_type"] == "Manga" usage_cls.return_value.log_usage.assert_called_once() @pytest.mark.django_db def test_developer_rag_fallback_to_last_non_status_event(auth_client): """No explicit 'result' event -> falls back to last non-error/status event.""" agent = MagicMock() agent.aplan_and_solve_stream.side_effect = _astream( [ {"type": "status", "content": "thinking"}, {"type": "token", "content": "partial answer"}, ] ) with ( _override_agentic_rag(agent), patch.object(developer_mod, "DjangoUsageAdapter"), ): response = auth_client.post( reverse("developer_rag"), {"query": "tell me"}, format="json", ) assert response.status_code == 200 body = response.json() assert body["answer"] == "partial answer" assert body["media_type"] == "Anime" # default @pytest.mark.django_db def test_developer_rag_exception_returns_500(auth_client): """Agent raising -> 500 with error message.""" agent = MagicMock() agent.aplan_and_solve_stream.side_effect = RuntimeError("boom") with _override_agentic_rag(agent): response = auth_client.post( reverse("developer_rag"), {"query": "x"}, format="json", ) assert response.status_code == 500 assert response.json()["error"] == "Internal server error" @pytest.mark.django_db def test_developer_rag_uses_async_stream(auth_client): """The view consumes the single async stream — the agent mock exposes ONLY aplan_and_solve_stream; touching the retired sync stream would raise.""" agent = MagicMock(spec=["aplan_and_solve_stream"]) async def _agen(query, media_type, user_id=None): yield {"type": "status", "content": "thinking"} yield {"type": "result", "content": "Async answer."} agent.aplan_and_solve_stream.side_effect = _agen with ( _override_agentic_rag(agent), patch.object(developer_mod, "DjangoUsageAdapter"), ): response = auth_client.post( reverse("developer_rag"), {"query": "Who is Luffy?"}, format="json", ) assert response.status_code == 200 assert response.json()["answer"] == "Async answer." # --------------------------------------------------------------------------- # # DeveloperApiKeyView # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_api_key_get_metadata_no_key(auth_client, user): response = auth_client.get(reverse("developer_api_key")) assert response.status_code == 200 body = response.json() assert body["tier"] == "free" assert body["has_api_key"] is False assert body["api_key_prefix"] is None @pytest.mark.django_db def test_api_key_get_metadata_with_key(auth_client, user): user.profile.set_api_key("ax_pro_1_secret") user.profile.save() response = auth_client.get(reverse("developer_api_key")) body = response.json() assert body["has_api_key"] is True assert body["api_key_prefix"] == "ax_pro_" @pytest.mark.django_db def test_api_key_post_forbidden_for_non_pro(auth_client, user): """Free-tier users cannot generate keys -> 403.""" response = auth_client.post(reverse("developer_api_key")) assert response.status_code == 403 @pytest.mark.django_db def test_api_key_post_generates_key_for_pro(auth_client, user): user.profile.tier = "pro" user.profile.save() response = auth_client.post(reverse("developer_api_key")) assert response.status_code == 201 body = response.json() assert body["api_key"].startswith(f"ax_pro_{user.profile.id}_") user.profile.refresh_from_db() assert user.profile.api_key_hash # hashed key persisted # --------------------------------------------------------------------------- # # DeveloperSubscriptionMockView (Pro tier is now free/manual — no Stripe) # --------------------------------------------------------------------------- # @pytest.mark.django_db def test_subscribe_mock_sets_pro_tier(auth_client, user): response = auth_client.post(reverse("developer_subscribe_mock")) assert response.status_code == 200 body = response.json() assert body["status"] == "subscribed" assert body["tier"] == "pro" # Pro tier is now free/manual — no Stripe IDs are minted or returned. assert "stripe_customer_id" not in body user.profile.refresh_from_db() assert user.profile.tier == "pro"