1+ from collections .abc import AsyncGenerator
2+
3+ import httpx
4+ import pytest
5+ import respx
6+
7+ from bubble_data_api_client import configure
18from bubble_data_api_client .client import raw_client
9+ from bubble_data_api_client .pool import close_clients
10+
11+
12+ @pytest .fixture
13+ async def clean_client_pool () -> AsyncGenerator [None ]:
14+ """Ensure client pool is clean before and after each test."""
15+ await close_clients ()
16+ yield
17+ await close_clients ()
18+
19+
20+ @pytest .fixture
21+ def configured_client (clean_client_pool : None ) -> None :
22+ """Configure the client for testing."""
23+ configure (
24+ data_api_root_url = "https://test.example.com" ,
25+ api_key = "test-key" ,
26+ retry = None ,
27+ )
228
329
430async def test_raw_client_init () -> None :
@@ -14,3 +40,159 @@ async def test_raw_client_init() -> None:
1440 # test creating with async context manager
1541 async with raw_client .RawClient () as client_instance :
1642 assert isinstance (client_instance , raw_client .RawClient )
43+
44+
45+ @respx .mock
46+ async def test_replace (configured_client : None ) -> None :
47+ """Test that replace uses PUT to fully replace a thing."""
48+ route = respx .put ("https://test.example.com/customer/123x456" ).mock (return_value = httpx .Response (204 ))
49+
50+ async with raw_client .RawClient () as client :
51+ response = await client .replace (
52+ typename = "customer" ,
53+ uid = "123x456" ,
54+ data = {"name" : "New Name" , "email" : "new@example.com" },
55+ )
56+
57+ assert response .status_code == 204
58+ assert route .call_count == 1
59+
60+
61+ @respx .mock
62+ async def test_bulk_create (configured_client : None ) -> None :
63+ """Test that bulk_create posts newline-delimited JSON."""
64+ route = respx .post ("https://test.example.com/customer/bulk" ).mock (
65+ return_value = httpx .Response (200 , json = {"status" : "success" , "count" : 2 })
66+ )
67+
68+ async with raw_client .RawClient () as client :
69+ response = await client .bulk_create (
70+ typename = "customer" ,
71+ data = [{"name" : "Alice" }, {"name" : "Bob" }],
72+ )
73+
74+ assert response .status_code == 200
75+ assert route .call_count == 1
76+ # verify it sent newline-delimited JSON
77+ request_content = route .calls [0 ].request .content .decode ()
78+ assert request_content == '{"name": "Alice"}\n {"name": "Bob"}'
79+
80+
81+ @respx .mock
82+ async def test_find_with_parameters (configured_client : None ) -> None :
83+ """Test that find passes optional parameters correctly."""
84+ route = respx .get ("https://test.example.com/customer" ).mock (
85+ return_value = httpx .Response (200 , json = {"response" : {"results" : [], "count" : 0 , "remaining" : 0 }})
86+ )
87+
88+ async with raw_client .RawClient () as client :
89+ await client .find (
90+ typename = "customer" ,
91+ cursor = 10 ,
92+ limit = 50 ,
93+ sort_field = "name" ,
94+ descending = True ,
95+ exclude_remaining = True ,
96+ )
97+
98+ assert route .call_count == 1
99+ request = route .calls [0 ].request
100+ assert "cursor=10" in str (request .url )
101+ assert "limit=50" in str (request .url )
102+ assert "sort_field=name" in str (request .url )
103+ assert "descending=true" in str (request .url )
104+ assert "exclude_remaining=true" in str (request .url )
105+
106+
107+ @respx .mock
108+ async def test_find_with_additional_sort_fields (configured_client : None ) -> None :
109+ """Test that find passes additional_sort_fields correctly."""
110+ route = respx .get ("https://test.example.com/customer" ).mock (
111+ return_value = httpx .Response (200 , json = {"response" : {"results" : [], "count" : 0 , "remaining" : 0 }})
112+ )
113+
114+ async with raw_client .RawClient () as client :
115+ await client .find (
116+ typename = "customer" ,
117+ additional_sort_fields = [{"sort_field" : "age" , "descending" : False }],
118+ )
119+
120+ assert route .call_count == 1
121+ request = route .calls [0 ].request
122+ assert "additional_sort_fields" in str (request .url )
123+
124+
125+ @respx .mock
126+ async def test_count (configured_client : None ) -> None :
127+ """Test that count returns total from count + remaining."""
128+ respx .get ("https://test.example.com/customer" ).mock (
129+ return_value = httpx .Response (200 , json = {"response" : {"results" : [], "count" : 5 , "remaining" : 95 }})
130+ )
131+
132+ async with raw_client .RawClient () as client :
133+ total = await client .count (typename = "customer" )
134+
135+ assert total == 100
136+
137+
138+ @respx .mock
139+ async def test_exists_by_uid_found (configured_client : None ) -> None :
140+ """Test exists returns True when record found by uid."""
141+ respx .get ("https://test.example.com/customer/123x456" ).mock (
142+ return_value = httpx .Response (200 , json = {"response" : {"_id" : "123x456" }})
143+ )
144+
145+ async with raw_client .RawClient () as client :
146+ result = await client .exists (typename = "customer" , uid = "123x456" )
147+
148+ assert result is True
149+
150+
151+ @respx .mock
152+ async def test_exists_by_uid_not_found (configured_client : None ) -> None :
153+ """Test exists returns False when record not found by uid."""
154+ respx .get ("https://test.example.com/customer/123x456" ).mock (
155+ return_value = httpx .Response (404 , json = {"status" : "NOT_FOUND" })
156+ )
157+
158+ async with raw_client .RawClient () as client :
159+ result = await client .exists (typename = "customer" , uid = "123x456" )
160+
161+ assert result is False
162+
163+
164+ @respx .mock
165+ async def test_exists_by_uid_error_reraises (configured_client : None ) -> None :
166+ """Test exists re-raises non-404 HTTP errors."""
167+ respx .get ("https://test.example.com/customer/123x456" ).mock (
168+ return_value = httpx .Response (500 , json = {"error" : "server error" })
169+ )
170+
171+ async with raw_client .RawClient () as client :
172+ with pytest .raises (httpx .HTTPStatusError ) as exc_info :
173+ await client .exists (typename = "customer" , uid = "123x456" )
174+
175+ assert exc_info .value .response .status_code == 500
176+
177+
178+ @respx .mock
179+ async def test_exists_by_constraints (configured_client : None ) -> None :
180+ """Test exists with constraints uses find."""
181+ respx .get ("https://test.example.com/customer" ).mock (
182+ return_value = httpx .Response (200 , json = {"response" : {"results" : [{"_id" : "1x1" }], "count" : 1 , "remaining" : 0 }})
183+ )
184+
185+ async with raw_client .RawClient () as client :
186+ result = await client .exists (
187+ typename = "customer" ,
188+ constraints = [{"key" : "email" , "constraint_type" : "equals" , "value" : "test@example.com" }],
189+ )
190+
191+ assert result is True
192+
193+
194+ async def test_exists_uid_and_constraints_raises (configured_client : None ) -> None :
195+ """Test exists raises when both uid and constraints provided."""
196+ async with raw_client .RawClient () as client :
197+ with pytest .raises (ValueError , match = "Cannot specify both" ):
198+ await client .exists (typename = "customer" , uid = "123x456" , constraints = [{"key" : "x" }])
0 commit comments