| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- """Integration tests for the Orca Cloud device-pairing routes.
- Covers the /device/start -> /device/poll pairing loop, its terminal outcomes,
- token persistence, and status/logout — all in auth-disabled mode (the global
- Settings-table fallback), with the service's network calls patched out.
- """
- from __future__ import annotations
- from datetime import datetime, timedelta, timezone
- from unittest.mock import patch
- import pytest
- from httpx import AsyncClient
- from backend.app.services import orca_cloud as orca_service
- from backend.app.services.orca_cloud import DevicePoll, OrcaCloudService
- AUTH_DISABLED = "backend.app.core.auth.is_auth_enabled"
- @pytest.fixture(autouse=True)
- def _dummy_shared_client():
- """Register a throwaway shared HTTP client so per-request
- OrcaCloudService() instances don't spin up (and leak) a real one — the
- network methods are patched anyway."""
- from unittest.mock import MagicMock
- orca_service.set_shared_http_client(MagicMock())
- yield
- orca_service.set_shared_http_client(None)
- _DEVICE_CODE_RESPONSE = {
- "device_code": "DEV-SECRET-1",
- "user_code": "ABCD-EF12",
- "verification_uri": "https://cloud.orcaslicer.com/app/settings",
- "verification_uri_complete": "https://cloud.orcaslicer.com/app/settings?user_code=ABCD-EF12",
- "expires_in": 600,
- "interval": 5,
- }
- async def _start(async_client: AsyncClient):
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch.object(OrcaCloudService, "request_device_code", return_value=dict(_DEVICE_CODE_RESPONSE)),
- ):
- return await async_client.post("/api/v1/orca-cloud/device/start")
- class TestDeviceStart:
- @pytest.mark.asyncio
- async def test_start_returns_user_code_and_hides_device_code(self, async_client: AsyncClient):
- resp = await _start(async_client)
- assert resp.status_code == 200
- body = resp.json()
- assert body["user_code"] == "ABCD-EF12"
- assert body["interval"] == 5
- assert body["verification_uri_complete"].endswith("user_code=ABCD-EF12")
- # The device_code is a secret and must NOT be echoed to the client.
- assert "device_code" not in body
- class TestDevicePoll:
- @pytest.mark.asyncio
- async def test_poll_without_pending_is_400(self, async_client: AsyncClient):
- with patch(AUTH_DISABLED, return_value=False):
- resp = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert resp.status_code == 400
- @pytest.mark.asyncio
- async def test_poll_pending_reports_in_progress(self, async_client: AsyncClient):
- await _start(async_client)
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch.object(OrcaCloudService, "poll_token", return_value=(DevicePoll.PENDING, None)),
- ):
- resp = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert resp.status_code == 200
- body = resp.json()
- assert body["status"] == DevicePoll.PENDING
- assert body["connected"] is False
- @pytest.mark.asyncio
- async def test_poll_complete_persists_tokens_and_connects(self, async_client: AsyncClient):
- await _start(async_client)
- async def fake_complete(self, device_code):
- assert device_code == "DEV-SECRET-1" # the stored secret is used
- self.access_token = "oc_ext_new"
- self.refresh_token = "oc_ext_rt_new"
- self.token_expiry = datetime.now(timezone.utc) + timedelta(seconds=86400)
- return DevicePoll.COMPLETE, {"access_token": "oc_ext_new"}
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch.object(OrcaCloudService, "poll_token", new=fake_complete),
- patch.object(OrcaCloudService, "introspect", return_value={"user_id": "user-123"}),
- ):
- resp = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert resp.status_code == 200
- body = resp.json()
- assert body["status"] == DevicePoll.COMPLETE
- assert body["connected"] is True
- assert body["user_id"] == "user-123"
- # Status now reflects the connection, and the pending state is
- # cleared (a fresh poll finds nothing pending -> 400).
- status = await async_client.get("/api/v1/orca-cloud/status")
- assert status.json()["connected"] is True
- again = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert again.status_code == 400
- @pytest.mark.asyncio
- async def test_poll_denied_clears_pending(self, async_client: AsyncClient):
- await _start(async_client)
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch.object(OrcaCloudService, "poll_token", return_value=(DevicePoll.DENIED, None)),
- ):
- resp = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert resp.json()["status"] == DevicePoll.DENIED
- # Pending cleared -> next poll has nothing to poll.
- with patch(AUTH_DISABLED, return_value=False):
- again = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert again.status_code == 400
- @pytest.mark.asyncio
- async def test_poll_expires_by_ttl_without_network(self, async_client: AsyncClient):
- """A pending code older than DEVICE_CODE_TTL is reported expired
- without even calling the token endpoint. Shrinking the TTL to a
- negative window makes any just-created pending state 'stale'."""
- await _start(async_client)
- # poll_token must NOT be called; if it were, this would blow up.
- def _boom(*a, **k):
- raise AssertionError("poll_token should not be called for an expired code")
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch("backend.app.api.routes.orca_cloud.DEVICE_CODE_TTL", timedelta(seconds=-1)),
- patch.object(OrcaCloudService, "poll_token", new=_boom),
- ):
- resp = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert resp.status_code == 200
- assert resp.json()["status"] == DevicePoll.EXPIRED
- # And the expired pending state is cleared.
- with patch(AUTH_DISABLED, return_value=False):
- again = await async_client.post("/api/v1/orca-cloud/device/poll")
- assert again.status_code == 400
- class TestLogout:
- @pytest.mark.asyncio
- async def test_logout_clears_connection(self, async_client: AsyncClient):
- await _start(async_client)
- async def fake_complete(self, device_code):
- self.access_token = "oc_ext_new"
- self.refresh_token = "oc_ext_rt_new"
- self.token_expiry = datetime.now(timezone.utc) + timedelta(seconds=86400)
- return DevicePoll.COMPLETE, {"access_token": "oc_ext_new"}
- with (
- patch(AUTH_DISABLED, return_value=False),
- patch.object(OrcaCloudService, "poll_token", new=fake_complete),
- patch.object(OrcaCloudService, "introspect", return_value={"user_id": "u"}),
- ):
- await async_client.post("/api/v1/orca-cloud/device/poll")
- with patch(AUTH_DISABLED, return_value=False):
- out = await async_client.post("/api/v1/orca-cloud/logout")
- assert out.status_code == 200
- status = await async_client.get("/api/v1/orca-cloud/status")
- assert status.json()["connected"] is False
|