test_obico_api.py 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. """Integration tests for Obico API endpoints (#172 follow-up).
  2. Verifies the /obico/cached-frame/{nonce} endpoint used by Obico's ML API to fetch
  3. pre-captured JPEG frames. This endpoint lets the detection loop sidestep Obico's
  4. hardcoded 5s read timeout by pre-populating a cache before issuing the ML call.
  5. """
  6. import pytest
  7. from httpx import AsyncClient
  8. from backend.app.services.obico_detection import _frame_cache, stash_frame
  9. FAKE_JPEG = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9"
  10. @pytest.fixture(autouse=True)
  11. def clear_cache():
  12. _frame_cache.clear()
  13. yield
  14. _frame_cache.clear()
  15. class TestObicoCachedFrame:
  16. @pytest.mark.asyncio
  17. @pytest.mark.integration
  18. async def test_valid_nonce_returns_jpeg(self, async_client: AsyncClient):
  19. """A stashed nonce returns the stored JPEG bytes with image/jpeg."""
  20. nonce = await stash_frame(FAKE_JPEG)
  21. response = await async_client.get(f"/api/v1/obico/cached-frame/{nonce}")
  22. assert response.status_code == 200
  23. assert response.headers["content-type"] == "image/jpeg"
  24. assert response.content == FAKE_JPEG
  25. @pytest.mark.asyncio
  26. @pytest.mark.integration
  27. async def test_unknown_nonce_is_404(self, async_client: AsyncClient):
  28. """An unguessable URL must not leak that the endpoint exists — return 404."""
  29. response = await async_client.get("/api/v1/obico/cached-frame/definitely-not-a-real-nonce")
  30. assert response.status_code == 404
  31. @pytest.mark.asyncio
  32. @pytest.mark.integration
  33. async def test_nonce_is_single_use(self, async_client: AsyncClient):
  34. """A second fetch with the same nonce returns 404 — prevents replay."""
  35. nonce = await stash_frame(FAKE_JPEG)
  36. first = await async_client.get(f"/api/v1/obico/cached-frame/{nonce}")
  37. assert first.status_code == 200
  38. second = await async_client.get(f"/api/v1/obico/cached-frame/{nonce}")
  39. assert second.status_code == 404
  40. @pytest.mark.asyncio
  41. @pytest.mark.integration
  42. async def test_endpoint_is_public(self, async_client: AsyncClient):
  43. """Obico's ML API can't send auth headers, so the nonce IS the credential.
  44. The path must be in PUBLIC_API_PATTERNS (no auth wall)."""
  45. nonce = await stash_frame(FAKE_JPEG)
  46. # Intentionally omit any auth headers even if the fixture would normally inject them
  47. response = await async_client.get(
  48. f"/api/v1/obico/cached-frame/{nonce}",
  49. headers={}, # no Authorization header
  50. )
  51. assert response.status_code == 200
  52. @pytest.mark.asyncio
  53. @pytest.mark.integration
  54. async def test_response_is_not_cached(self, async_client: AsyncClient):
  55. """Browsers/proxies must not hold onto the image after Obico consumes it."""
  56. nonce = await stash_frame(FAKE_JPEG)
  57. response = await async_client.get(f"/api/v1/obico/cached-frame/{nonce}")
  58. assert response.status_code == 200
  59. assert "no-store" in response.headers.get("cache-control", "")