a digital entity named phi that roams bsky phi.zzstoatzz.io
2
fork

Configure Feed

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

at main 200 lines 6.9 kB view raw
1"""Tests for the graze.social REST client.""" 2 3from unittest.mock import AsyncMock, MagicMock, patch 4 5import httpx 6import pytest 7 8from bot.core.graze_client import BASE_URL, GrazeClient 9 10 11@pytest.fixture 12def graze(): 13 return GrazeClient(handle="test.bsky.social", password="test-pass") 14 15 16def _login_response(): 17 """Fake successful login response.""" 18 resp = httpx.Response( 19 200, 20 json={"user": {"id": 42}}, 21 request=httpx.Request("POST", f"{BASE_URL}/app/login"), 22 ) 23 return resp 24 25 26def _ok_response(json=None): 27 resp = httpx.Response( 28 200, 29 json=json or {}, 30 request=httpx.Request("GET", BASE_URL), 31 ) 32 return resp 33 34 35class TestLogin: 36 async def test_login_caches_session(self, graze): 37 with patch("bot.core.graze_client.httpx.AsyncClient") as mock_cls: 38 mock_client = AsyncMock() 39 mock_client.post.return_value = _login_response() 40 mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client) 41 mock_cls.return_value.__aexit__ = AsyncMock(return_value=False) 42 43 await graze._login() 44 assert graze._user_id == 42 45 assert graze._cookies is not None 46 47 async def test_ensure_session_skips_if_cached(self, graze): 48 graze._cookies = httpx.Cookies() 49 graze._user_id = 42 50 # should not attempt login 51 with patch.object(graze, "_login") as mock_login: 52 await graze._ensure_session() 53 mock_login.assert_not_called() 54 55 async def test_ensure_session_logs_in_if_no_cookies(self, graze): 56 with patch.object(graze, "_login") as mock_login: 57 await graze._ensure_session() 58 mock_login.assert_called_once() 59 60 61class TestCreateFeed: 62 async def test_full_create_flow(self, graze): 63 """Test the 5-step create flow: putRecord → migrate → complete → publish → set-publicity.""" 64 graze._cookies = httpx.Cookies() 65 graze._user_id = 42 66 67 # mock bot_client for PDS putRecord 68 mock_bot = MagicMock() 69 mock_bot.authenticate = AsyncMock() 70 mock_bot.client.me.did = "did:plc:testdid" 71 mock_bot.client.com.atproto.repo.put_record = MagicMock() 72 73 call_log = [] 74 75 async def fake_request(method, path, **kwargs): 76 call_log.append((method, path)) 77 if path == "/app/migrate_algo": 78 return _ok_response(json={"id": 99}) 79 return _ok_response() 80 81 with ( 82 patch("bot.core.graze_client.bot_client", mock_bot), 83 patch.object(graze, "_request", side_effect=fake_request), 84 ): 85 result = await graze.create_feed( 86 rkey="jazz-feed", 87 display_name="Jazz Music", 88 description="posts about jazz", 89 filter_manifest={ 90 "filter": {"and": [{"regex_any": ["text", ["jazz", "bebop"]]}]} 91 }, 92 ) 93 94 assert result["uri"] == "at://did:plc:testdid/app.bsky.feed.generator/jazz-feed" 95 assert result["algo_id"] == 99 96 97 # verify PDS record was created 98 mock_bot.client.com.atproto.repo.put_record.assert_called_once() 99 put_data = mock_bot.client.com.atproto.repo.put_record.call_args 100 record = put_data.kwargs["data"]["record"] 101 assert record["displayName"] == "Jazz Music" 102 assert record["did"] == "did:web:api.graze.social" 103 104 # verify all 5 graze API calls in order 105 assert call_log == [ 106 ("POST", "/app/migrate_algo"), 107 ("POST", "/app/complete_migration"), 108 ("GET", "/app/publish_algo/99"), 109 ("GET", "/app/api/v1/algorithm-management/set-publicity/99/true"), 110 ("POST", "/app/api/v1/algorithm-management/backfill/99"), 111 ] 112 113 async def test_create_feed_propagates_errors(self, graze): 114 graze._cookies = httpx.Cookies() 115 graze._user_id = 42 116 117 mock_bot = MagicMock() 118 mock_bot.authenticate = AsyncMock() 119 mock_bot.client.me.did = "did:plc:testdid" 120 mock_bot.client.com.atproto.repo.put_record = MagicMock() 121 122 async def fail_migrate(method, path, **kwargs): 123 raise httpx.HTTPStatusError( 124 "bad request", 125 request=httpx.Request("POST", f"{BASE_URL}/app/migrate_algo"), 126 response=httpx.Response(400), 127 ) 128 129 with ( 130 patch("bot.core.graze_client.bot_client", mock_bot), 131 patch.object(graze, "_request", side_effect=fail_migrate), 132 ): 133 with pytest.raises(httpx.HTTPStatusError): 134 await graze.create_feed("test", "Test", "test", {"filter": {}}) 135 136 137class TestListFeeds: 138 async def test_list_feeds(self, graze): 139 feeds_data = [ 140 {"id": 1, "display_name": "Jazz", "feed_uri": "at://did/gen/jazz"}, 141 {"id": 2, "display_name": "Blues", "feed_uri": "at://did/gen/blues"}, 142 ] 143 144 async def fake_request(method, path, **kwargs): 145 return _ok_response(json=feeds_data) 146 147 with patch.object(graze, "_request", side_effect=fake_request): 148 result = await graze.list_feeds() 149 150 assert len(result) == 2 151 assert result[0]["display_name"] == "Jazz" 152 153 154class TestDeleteFeed: 155 async def test_delete_feed(self, graze): 156 graze._user_id = 42 157 158 async def fake_request(method, path, **kwargs): 159 assert method == "POST" 160 assert path == "/app/delete_algo" 161 assert kwargs["json"] == {"id": 99, "user_id": 42} 162 return _ok_response() 163 164 with patch.object(graze, "_request", side_effect=fake_request): 165 await graze.delete_feed(99) 166 167 168class TestReloginOn401: 169 async def test_request_retries_on_401(self, graze): 170 graze._cookies = httpx.Cookies() 171 graze._user_id = 42 172 173 call_count = 0 174 175 async def mock_request(method, path, **kwargs): 176 nonlocal call_count 177 call_count += 1 178 if call_count == 1: 179 return httpx.Response( 180 401, 181 request=httpx.Request("GET", f"{BASE_URL}{path}"), 182 ) 183 return httpx.Response( 184 200, 185 json={"ok": True}, 186 request=httpx.Request("GET", f"{BASE_URL}{path}"), 187 ) 188 189 with ( 190 patch("bot.core.graze_client.httpx.AsyncClient") as mock_cls, 191 patch.object(graze, "_login", new_callable=AsyncMock) as mock_login, 192 ): 193 mock_client = AsyncMock() 194 mock_client.request = mock_request 195 mock_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client) 196 mock_cls.return_value.__aexit__ = AsyncMock(return_value=False) 197 198 r = await graze._request("GET", "/app/my_feeds") 199 assert r.status_code == 200 200 mock_login.assert_called_once()