animetix-web / tests /api /test_developer_coverage.py
MissawB's picture
Upload folder using huggingface_hub (part 5)
9226a4f verified
Raw
History Blame Contribute Delete
7.42 kB
"""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"