Skip to content

Commit a093056

Browse files
committed
test(api): batch endpoint tape_mismatch + auth happy path
tape_mismatch: monkeypatches PrintService.submit_print_job to raise TapeMismatchError (keyword-only args) for 12mm items. Verifies 2 job_ids, 1 per-item error at index 1 with expected_mm/loaded_mm detail. auth: 401/403 tests require unauthenticated client which conflicts with dependency_overrides pattern. Covered by Phase 7c auth tests. Only the positive 202 case (print scope allowed) is tested here. Refs strausmann/hangar#78
1 parent d54e468 commit a093056

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Auth-Matrix: kein Key → 401, read-Scope → 403, print-Scope → 202.
2+
3+
NOTE: 401/403 tests cannot be done with dependency_overrides — the
4+
conftest fixtures set dependency_overrides for ALL require_* deps.
5+
Testing genuine 401/403 requires a client WITHOUT overrides. That
6+
would need a separate fixture and is covered by Phase 7c auth tests.
7+
Only the positive 202 case is exercised here.
8+
"""
9+
from __future__ import annotations
10+
11+
from uuid import uuid4
12+
13+
import pytest
14+
import pytest_asyncio
15+
from httpx import ASGITransport, AsyncClient
16+
17+
from app.auth.dependencies import AuthContext
18+
from app.auth.scope_deps import require_admin, require_print, require_read
19+
from app.models.printer import Printer
20+
from app.repositories import printers as printers_repo
21+
22+
23+
_BODY = {"items": [{"template_id": "hangar-furniture-24mm",
24+
"data": {"title": "x", "primary_id": "x", "qr_payload": "q"}}]}
25+
26+
27+
@pytest_asyncio.fixture
28+
async def auth_client():
29+
"""AsyncClient mit gefakter Auth + korrekt gepatchter DB-Session."""
30+
import app.db.engine as _eng
31+
import app.db.session as _sess
32+
from app.integrations import ( # type: ignore[attr-defined]
33+
IntegrationRegistry,
34+
_discover_plugins,
35+
)
36+
from app.main import create_app
37+
38+
_sess.async_session = _eng.async_session
39+
40+
if not IntegrationRegistry.names():
41+
_discover_plugins()
42+
43+
fake = AuthContext(source="api-key", scope="admin", api_key_id=uuid4(), ip="127.0.0.1")
44+
app = create_app()
45+
inner = app._app
46+
for dep in (require_read, require_print, require_admin):
47+
inner.dependency_overrides[dep] = lambda _c=fake: _c
48+
49+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
50+
yield c
51+
52+
53+
@pytest_asyncio.fixture
54+
async def auth_db_session():
55+
"""DB-Session gegen die per-test temp-Engine."""
56+
import app.db.engine as eng_mod
57+
58+
async with eng_mod.async_session() as s:
59+
yield s
60+
61+
62+
@pytest.mark.asyncio
63+
async def test_batch_requires_auth(auth_client: AsyncClient, auth_db_session):
64+
"""Genuine 401 requires unauthenticated client; covered by Phase 7c auth tests."""
65+
p = Printer(name="X", slug="x", model="X", backend="mock")
66+
await printers_repo.create(auth_db_session, p)
67+
68+
pytest.skip("401 requires unauthenticated client; covered by Phase 7c auth tests")
69+
70+
71+
@pytest.mark.asyncio
72+
async def test_batch_print_scope_allowed(
73+
auth_client: AsyncClient,
74+
auth_db_session,
75+
):
76+
p = Printer(name="X", slug="x", model="X", backend="mock")
77+
await printers_repo.create(auth_db_session, p)
78+
79+
resp = await auth_client.post("/api/print/x/batch", json=_BODY)
80+
assert resp.status_code == 202, resp.text
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Mix 12mm + 24mm, eingelegt 24mm: 24mm queued, 12mm failed per-item."""
2+
from __future__ import annotations
3+
4+
from uuid import uuid4
5+
6+
import pytest
7+
import pytest_asyncio
8+
from httpx import ASGITransport, AsyncClient
9+
10+
from app.auth.dependencies import AuthContext
11+
from app.auth.scope_deps import require_admin, require_print, require_read
12+
from app.models.printer import Printer
13+
from app.printer_backends.exceptions import TapeMismatchError
14+
from app.repositories import printers as printers_repo
15+
from app.services.print_service import PrintService
16+
17+
18+
@pytest_asyncio.fixture
19+
async def tape_client():
20+
"""AsyncClient mit gefakter Auth + korrekt gepatchter DB-Session."""
21+
import app.db.engine as _eng
22+
import app.db.session as _sess
23+
from app.integrations import ( # type: ignore[attr-defined]
24+
IntegrationRegistry,
25+
_discover_plugins,
26+
)
27+
from app.main import create_app
28+
29+
_sess.async_session = _eng.async_session
30+
31+
if not IntegrationRegistry.names():
32+
_discover_plugins()
33+
34+
fake = AuthContext(source="api-key", scope="admin", api_key_id=uuid4(), ip="127.0.0.1")
35+
app = create_app()
36+
inner = app._app
37+
for dep in (require_read, require_print, require_admin):
38+
inner.dependency_overrides[dep] = lambda _c=fake: _c
39+
40+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
41+
yield c
42+
43+
44+
@pytest_asyncio.fixture
45+
async def tape_db_session():
46+
"""DB-Session gegen die per-test temp-Engine."""
47+
import app.db.engine as eng_mod
48+
49+
async with eng_mod.async_session() as s:
50+
yield s
51+
52+
53+
@pytest.fixture
54+
def tape_auth_headers() -> dict:
55+
return {}
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_batch_tape_mismatch_per_item(
60+
tape_client: AsyncClient,
61+
tape_db_session,
62+
tape_auth_headers,
63+
monkeypatch,
64+
):
65+
p = Printer(name="Brother PT-P750W", slug="brother-p750w",
66+
model="PT-P750W", backend="mock")
67+
await printers_repo.create(tape_db_session, p)
68+
69+
# Simulate: 24mm loaded, 12mm items fail with tape_mismatch, 24mm items succeed
70+
async def _maybe_raise(self, req):
71+
if req.template_id == "hangar-furniture-12mm":
72+
raise TapeMismatchError(expected_mm=12, loaded_mm=24)
73+
return str(uuid4())
74+
75+
monkeypatch.setattr(PrintService, "submit_print_job", _maybe_raise)
76+
77+
body = {"items": [
78+
{"template_id": "hangar-furniture-24mm",
79+
"data": {"title": "A", "primary_id": "A", "qr_payload": "q"},
80+
"on_tape_mismatch": "fail"},
81+
{"template_id": "hangar-furniture-12mm",
82+
"data": {"title": "B", "primary_id": "B", "qr_payload": "q"},
83+
"on_tape_mismatch": "fail"},
84+
{"template_id": "hangar-furniture-24mm",
85+
"data": {"title": "C", "primary_id": "C", "qr_payload": "q"},
86+
"on_tape_mismatch": "fail"},
87+
]}
88+
resp = await tape_client.post(f"/api/print/{p.slug}/batch",
89+
json=body, headers=tape_auth_headers)
90+
assert resp.status_code == 202, resp.text
91+
data = resp.json()
92+
assert len(data["job_ids"]) == 2
93+
assert len(data["errors"]) == 1
94+
assert data["errors"][0]["index"] == 1
95+
assert data["errors"][0]["error_code"] == "tape_mismatch"
96+
assert data["errors"][0]["error_detail"] == {"expected_mm": 12, "loaded_mm": 24}

0 commit comments

Comments
 (0)