audio streaming app plyr.fm
38
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 205 lines 6.9 kB view raw
1"""tests for developer token api endpoints.""" 2 3from collections.abc import Generator 4from unittest.mock import AsyncMock, patch 5 6import pytest 7from fastapi import FastAPI 8from httpx import ASGITransport, AsyncClient 9from sqlalchemy.ext.asyncio import AsyncSession 10 11from backend._internal import Session, create_session, require_auth 12from backend.main import app 13 14 15class MockSession(Session): 16 """mock session for auth bypass in tests.""" 17 18 def __init__(self, did: str = "did:test:user123"): 19 self.did = did 20 self.handle = "testuser.bsky.social" 21 self.session_id = "test_session_id" 22 self.access_token = "test_token" 23 self.refresh_token = "test_refresh" 24 self.oauth_session = { 25 "did": did, 26 "handle": "testuser.bsky.social", 27 "pds_url": "https://test.pds", 28 "authserver_iss": "https://auth.test", 29 "scope": "atproto transition:generic", 30 "access_token": "test_token", 31 "refresh_token": "test_refresh", 32 "dpop_private_key_pem": "fake_key", 33 "dpop_authserver_nonce": "", 34 "dpop_pds_nonce": "", 35 } 36 37 38@pytest.fixture 39def test_app(db_session: AsyncSession) -> Generator[FastAPI, None, None]: 40 """create test app with mocked auth.""" 41 42 async def mock_require_auth() -> Session: 43 return MockSession() 44 45 app.dependency_overrides[require_auth] = mock_require_auth 46 47 yield app 48 49 app.dependency_overrides.clear() 50 51 52async def test_start_developer_token_flow(test_app: FastAPI, db_session: AsyncSession): 53 """test starting the developer token OAuth flow.""" 54 with patch( 55 "backend.api.auth.start_oauth_flow", new_callable=AsyncMock 56 ) as mock_oauth: 57 mock_oauth.return_value = ( 58 "https://auth.example.com/authorize?...", 59 "test_state", 60 ) 61 62 async with AsyncClient( 63 transport=ASGITransport(app=test_app), base_url="http://test" 64 ) as client: 65 response = await client.post( 66 "/auth/developer-token/start", 67 json={"name": "my-token", "expires_in_days": 30}, 68 ) 69 70 assert response.status_code == 200 71 data = response.json() 72 assert "auth_url" in data 73 assert data["auth_url"].startswith("https://auth.example.com") 74 mock_oauth.assert_called_once_with("testuser.bsky.social") 75 76 77async def test_start_developer_token_default_expiration( 78 test_app: FastAPI, db_session: AsyncSession 79): 80 """test starting dev token flow with default expiration.""" 81 with patch( 82 "backend.api.auth.start_oauth_flow", new_callable=AsyncMock 83 ) as mock_oauth: 84 mock_oauth.return_value = ("https://auth.example.com/authorize", "test_state") 85 86 async with AsyncClient( 87 transport=ASGITransport(app=test_app), base_url="http://test" 88 ) as client: 89 response = await client.post( 90 "/auth/developer-token/start", 91 json={}, 92 ) 93 94 assert response.status_code == 200 95 # verify pending dev token was saved (would fail if expiration wasn't set) 96 97 98async def test_start_developer_token_exceeds_max( 99 test_app: FastAPI, db_session: AsyncSession 100): 101 """test that expiration cannot exceed max allowed.""" 102 async with AsyncClient( 103 transport=ASGITransport(app=test_app), base_url="http://test" 104 ) as client: 105 response = await client.post( 106 "/auth/developer-token/start", 107 json={"expires_in_days": 999}, # exceeds default max of 365 108 ) 109 110 assert response.status_code == 400 111 assert "cannot exceed" in response.json()["detail"] 112 113 114async def test_start_developer_token_requires_auth(db_session: AsyncSession): 115 """test that developer token start requires authentication.""" 116 async with AsyncClient( 117 transport=ASGITransport(app=app), base_url="http://test" 118 ) as client: 119 response = await client.post( 120 "/auth/developer-token/start", 121 json={}, 122 ) 123 124 assert response.status_code == 401 125 126 127async def test_list_developer_tokens(test_app: FastAPI, db_session: AsyncSession): 128 """test listing developer tokens.""" 129 mock_session = MockSession() 130 131 # create a dev token directly in the database 132 await create_session( 133 did=mock_session.did, 134 handle=mock_session.handle, 135 oauth_session=mock_session.oauth_session, 136 expires_in_days=30, 137 is_developer_token=True, 138 token_name="list-test-token", 139 ) 140 141 async with AsyncClient( 142 transport=ASGITransport(app=test_app), base_url="http://test" 143 ) as client: 144 response = await client.get("/auth/developer-tokens") 145 146 assert response.status_code == 200 147 data = response.json() 148 assert "tokens" in data 149 assert len(data["tokens"]) >= 1 150 151 # find our token 152 token = next((t for t in data["tokens"] if t["name"] == "list-test-token"), None) 153 assert token is not None 154 assert "session_id" in token 155 assert "created_at" in token 156 157 158async def test_revoke_developer_token(test_app: FastAPI, db_session: AsyncSession): 159 """test revoking a developer token.""" 160 mock_session = MockSession() 161 162 # create a dev token directly 163 await create_session( 164 did=mock_session.did, 165 handle=mock_session.handle, 166 oauth_session=mock_session.oauth_session, 167 expires_in_days=30, 168 is_developer_token=True, 169 token_name="revoke-test-token", 170 ) 171 172 async with AsyncClient( 173 transport=ASGITransport(app=test_app), base_url="http://test" 174 ) as client: 175 # list tokens to get session_id prefix 176 list_response = await client.get("/auth/developer-tokens") 177 assert list_response.status_code == 200 178 tokens = list_response.json()["tokens"] 179 token = next((t for t in tokens if t["name"] == "revoke-test-token"), None) 180 assert token is not None 181 182 # revoke the token 183 revoke_response = await client.delete( 184 f"/auth/developer-tokens/{token['session_id']}" 185 ) 186 assert revoke_response.status_code == 200 187 assert revoke_response.json()["message"] == "token revoked successfully" 188 189 # verify it's gone 190 final_list = await client.get("/auth/developer-tokens") 191 remaining = [ 192 t for t in final_list.json()["tokens"] if t["name"] == "revoke-test-token" 193 ] 194 assert len(remaining) == 0 195 196 197async def test_revoke_nonexistent_token(test_app: FastAPI, db_session: AsyncSession): 198 """test revoking a token that doesn't exist.""" 199 async with AsyncClient( 200 transport=ASGITransport(app=test_app), base_url="http://test" 201 ) as client: 202 response = await client.delete("/auth/developer-tokens/nonexist") 203 204 assert response.status_code == 404 205 assert response.json()["detail"] == "token not found"