Spaces:
Build error
Build error
File size: 7,313 Bytes
d483b58 4e19890 8f5abd4 4e19890 8f5abd4 4e19890 8f5abd4 4e19890 8f5abd4 b0b88cc 8f5abd4 4e19890 9226a4f 4e19890 9226a4f 4e19890 8f5abd4 4e19890 9226a4f 4e19890 8f5abd4 4e19890 8f5abd4 9226a4f 4e19890 8f5abd4 4e19890 9226a4f 4e19890 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | """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
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 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"
|