import pytest from fake_http import FakeResponse, FakeSession from teamsy.auth import TeamsAuthError from teamsy.device_code import ( DeviceCodeDeclined, DeviceCodeExpired, DeviceCodeInfo, poll_device_code, start_device_code, ) @pytest.fixture(autouse=True) def no_real_sleep(monkeypatch): async def fake_sleep(_delay): pass monkeypatch.setattr("teamsy.device_code.asyncio.sleep", fake_sleep) monkeypatch.setattr("teamsy.executor.asyncio.sleep", fake_sleep) @pytest.mark.asyncio async def test_start_device_code_parses_response(): session = FakeSession( [ FakeResponse( 200, { "device_code": "dc", "user_code": "ABCD1234", "verification_uri": "https://microsoft.com/devicelogin", "interval": 5, "expires_in": 900, }, ) ] ) info = await start_device_code(session) assert info.user_code == "ABCD1234" assert info.verification_uri == "https://microsoft.com/devicelogin" assert info.interval == 5 @pytest.mark.asyncio async def test_start_device_code_falls_back_to_verification_url_key(): # The legacy v1 device-code endpoint uses "verification_url", not # "verification_uri". session = FakeSession( [ FakeResponse( 200, { "device_code": "dc", "user_code": "ABCD1234", "verification_url": "https://microsoft.com/devicelogin", }, ) ] ) info = await start_device_code(session) assert info.verification_uri == "https://microsoft.com/devicelogin" @pytest.mark.asyncio async def test_start_device_code_raises_on_missing_fields(): session = FakeSession([FakeResponse(200, {"device_code": "dc"})]) with pytest.raises(TeamsAuthError): await start_device_code(session) def _info(**overrides) -> DeviceCodeInfo: defaults = { "device_code": "dc", "user_code": "ABCD1234", "verification_uri": "https://microsoft.com/devicelogin", "interval": 5, "expires_in": 900, } defaults.update(overrides) return DeviceCodeInfo(**defaults) @pytest.mark.asyncio async def test_poll_device_code_succeeds_after_pending(): session = FakeSession( [ FakeResponse(200, {"error": "authorization_pending"}), FakeResponse( 200, { "access_token": "access-1", "refresh_token": "refresh-1", "expires_in": 3600, }, ), ] ) refresh_token = await poll_device_code(session, _info()) assert refresh_token == "refresh-1" @pytest.mark.asyncio async def test_poll_device_code_raises_missing_refresh_token(): session = FakeSession([FakeResponse(200, {"access_token": "access-1"})]) with pytest.raises(TeamsAuthError): await poll_device_code(session, _info()) @pytest.mark.asyncio async def test_poll_device_code_declined(): session = FakeSession([FakeResponse(200, {"error": "authorization_declined"})]) with pytest.raises(DeviceCodeDeclined): await poll_device_code(session, _info()) @pytest.mark.asyncio async def test_poll_device_code_server_side_expiry(): session = FakeSession([FakeResponse(200, {"error": "expired_token"})]) with pytest.raises(DeviceCodeExpired): await poll_device_code(session, _info()) @pytest.mark.asyncio async def test_poll_device_code_local_deadline_expiry(): # expires_in=0 means the deadline (time.monotonic() + expires_in) is # already in the past by the time the first sleep would return. session = FakeSession([]) # no request should even be made with pytest.raises(DeviceCodeExpired): await poll_device_code(session, _info(expires_in=0)) @pytest.mark.asyncio async def test_poll_device_code_unknown_error_raises_generic(): session = FakeSession([FakeResponse(200, {"error": "something_else"})]) with pytest.raises(TeamsAuthError) as exc_info: await poll_device_code(session, _info()) assert not isinstance(exc_info.value, (DeviceCodeDeclined, DeviceCodeExpired))