Main coves client
1import 'package:coves_flutter/models/community.dart';
2import 'package:coves_flutter/services/api_exceptions.dart';
3import 'package:coves_flutter/services/coves_api_service.dart';
4import 'package:dio/dio.dart';
5import 'package:flutter_test/flutter_test.dart';
6import 'package:http_mock_adapter/http_mock_adapter.dart';
7
8void main() {
9 TestWidgetsFlutterBinding.ensureInitialized();
10
11 group('CovesApiService - listCommunities', () {
12 late Dio dio;
13 late DioAdapter dioAdapter;
14 late CovesApiService apiService;
15
16 setUp(() {
17 dio = Dio(BaseOptions(baseUrl: 'https://api.test.coves.social'));
18 dioAdapter = DioAdapter(dio: dio);
19 apiService = CovesApiService(
20 dio: dio,
21 tokenGetter: () async => 'test-token',
22 );
23 });
24
25 tearDown(() {
26 apiService.dispose();
27 });
28
29 test('should successfully fetch communities', () async {
30 final mockResponse = {
31 'communities': [
32 {
33 'did': 'did:plc:community1',
34 'name': 'test-community-1',
35 'displayName': 'Test Community 1',
36 'subscriberCount': 100,
37 'memberCount': 50,
38 },
39 {
40 'did': 'did:plc:community2',
41 'name': 'test-community-2',
42 'displayName': 'Test Community 2',
43 'subscriberCount': 200,
44 'memberCount': 100,
45 },
46 ],
47 'cursor': 'next-cursor',
48 };
49
50 dioAdapter.onGet(
51 '/xrpc/social.coves.community.list',
52 (server) => server.reply(200, mockResponse),
53 queryParameters: {
54 'limit': 50,
55 'sort': 'popular',
56 },
57 );
58
59 final response = await apiService.listCommunities();
60
61 expect(response, isA<CommunitiesResponse>());
62 expect(response.communities.length, 2);
63 expect(response.cursor, 'next-cursor');
64 expect(response.communities[0].did, 'did:plc:community1');
65 expect(response.communities[0].name, 'test-community-1');
66 expect(response.communities[1].did, 'did:plc:community2');
67 });
68
69 test('should handle empty communities response', () async {
70 final mockResponse = {
71 'communities': [],
72 'cursor': null,
73 };
74
75 dioAdapter.onGet(
76 '/xrpc/social.coves.community.list',
77 (server) => server.reply(200, mockResponse),
78 queryParameters: {
79 'limit': 50,
80 'sort': 'popular',
81 },
82 );
83
84 final response = await apiService.listCommunities();
85
86 expect(response.communities, isEmpty);
87 expect(response.cursor, null);
88 });
89
90 test('should handle null communities array', () async {
91 final mockResponse = {
92 'communities': null,
93 'cursor': null,
94 };
95
96 dioAdapter.onGet(
97 '/xrpc/social.coves.community.list',
98 (server) => server.reply(200, mockResponse),
99 queryParameters: {
100 'limit': 50,
101 'sort': 'popular',
102 },
103 );
104
105 final response = await apiService.listCommunities();
106
107 expect(response.communities, isEmpty);
108 });
109
110 test('should fetch communities with custom limit', () async {
111 final mockResponse = {
112 'communities': [],
113 'cursor': null,
114 };
115
116 dioAdapter.onGet(
117 '/xrpc/social.coves.community.list',
118 (server) => server.reply(200, mockResponse),
119 queryParameters: {
120 'limit': 25,
121 'sort': 'popular',
122 },
123 );
124
125 final response = await apiService.listCommunities(limit: 25);
126
127 expect(response, isA<CommunitiesResponse>());
128 });
129
130 test('should fetch communities with cursor for pagination', () async {
131 const cursor = 'pagination-cursor-123';
132
133 final mockResponse = {
134 'communities': [
135 {
136 'did': 'did:plc:community3',
137 'name': 'paginated-community',
138 },
139 ],
140 'cursor': 'next-cursor-456',
141 };
142
143 dioAdapter.onGet(
144 '/xrpc/social.coves.community.list',
145 (server) => server.reply(200, mockResponse),
146 queryParameters: {
147 'limit': 50,
148 'sort': 'popular',
149 'cursor': cursor,
150 },
151 );
152
153 final response = await apiService.listCommunities(cursor: cursor);
154
155 expect(response.communities.length, 1);
156 expect(response.cursor, 'next-cursor-456');
157 });
158
159 test('should fetch communities with custom sort', () async {
160 final mockResponse = {
161 'communities': [],
162 'cursor': null,
163 };
164
165 dioAdapter.onGet(
166 '/xrpc/social.coves.community.list',
167 (server) => server.reply(200, mockResponse),
168 queryParameters: {
169 'limit': 50,
170 'sort': 'new',
171 },
172 );
173
174 final response = await apiService.listCommunities(sort: 'new');
175
176 expect(response, isA<CommunitiesResponse>());
177 });
178
179 test('should handle 401 unauthorized error', () async {
180 dioAdapter.onGet(
181 '/xrpc/social.coves.community.list',
182 (server) => server.reply(401, {
183 'error': 'Unauthorized',
184 'message': 'Invalid token',
185 }),
186 queryParameters: {
187 'limit': 50,
188 'sort': 'popular',
189 },
190 );
191
192 expect(
193 () => apiService.listCommunities(),
194 throwsA(isA<AuthenticationException>()),
195 );
196 });
197
198 test('should handle 500 server error', () async {
199 dioAdapter.onGet(
200 '/xrpc/social.coves.community.list',
201 (server) => server.reply(500, {
202 'error': 'InternalServerError',
203 'message': 'Database error',
204 }),
205 queryParameters: {
206 'limit': 50,
207 'sort': 'popular',
208 },
209 );
210
211 expect(
212 () => apiService.listCommunities(),
213 throwsA(isA<ServerException>()),
214 );
215 });
216
217 test('should handle network timeout', () async {
218 dioAdapter.onGet(
219 '/xrpc/social.coves.community.list',
220 (server) => server.throws(
221 408,
222 DioException.connectionTimeout(
223 timeout: const Duration(seconds: 30),
224 requestOptions: RequestOptions(),
225 ),
226 ),
227 queryParameters: {
228 'limit': 50,
229 'sort': 'popular',
230 },
231 );
232
233 expect(
234 () => apiService.listCommunities(),
235 throwsA(isA<NetworkException>()),
236 );
237 });
238 });
239
240 group('CovesApiService - createPost', () {
241 late Dio dio;
242 late DioAdapter dioAdapter;
243 late CovesApiService apiService;
244
245 setUp(() {
246 dio = Dio(BaseOptions(baseUrl: 'https://api.test.coves.social'));
247 dioAdapter = DioAdapter(dio: dio);
248 apiService = CovesApiService(
249 dio: dio,
250 tokenGetter: () async => 'test-token',
251 );
252 });
253
254 tearDown(() {
255 apiService.dispose();
256 });
257
258 test('should successfully create a post with all fields', () async {
259 final mockResponse = {
260 'uri': 'at://did:plc:user/social.coves.community.post/123',
261 'cid': 'bafyreicid123',
262 };
263
264 dioAdapter.onPost(
265 '/xrpc/social.coves.community.post.create',
266 (server) => server.reply(200, mockResponse),
267 data: {
268 'community': 'did:plc:community1',
269 'title': 'Test Post Title',
270 'content': 'Test post content',
271 'embed': {
272 'uri': 'https://example.com/article',
273 'title': 'Article Title',
274 },
275 'langs': ['en'],
276 'labels': {
277 'values': [
278 {'val': 'nsfw'},
279 ],
280 },
281 },
282 );
283
284 final response = await apiService.createPost(
285 community: 'did:plc:community1',
286 title: 'Test Post Title',
287 content: 'Test post content',
288 embed: const ExternalEmbedInput(
289 uri: 'https://example.com/article',
290 title: 'Article Title',
291 ),
292 langs: ['en'],
293 labels: const SelfLabels(values: [SelfLabel(val: 'nsfw')]),
294 );
295
296 expect(response, isA<CreatePostResponse>());
297 expect(response.uri, 'at://did:plc:user/social.coves.community.post/123');
298 expect(response.cid, 'bafyreicid123');
299 });
300
301 test('should successfully create a minimal post', () async {
302 final mockResponse = {
303 'uri': 'at://did:plc:user/social.coves.community.post/456',
304 'cid': 'bafyreicid456',
305 };
306
307 dioAdapter.onPost(
308 '/xrpc/social.coves.community.post.create',
309 (server) => server.reply(200, mockResponse),
310 data: {
311 'community': 'did:plc:community1',
312 'title': 'Just a title',
313 },
314 );
315
316 final response = await apiService.createPost(
317 community: 'did:plc:community1',
318 title: 'Just a title',
319 );
320
321 expect(response, isA<CreatePostResponse>());
322 expect(response.uri, 'at://did:plc:user/social.coves.community.post/456');
323 });
324
325 test('should successfully create a link post', () async {
326 final mockResponse = {
327 'uri': 'at://did:plc:user/social.coves.community.post/789',
328 'cid': 'bafyreicid789',
329 };
330
331 dioAdapter.onPost(
332 '/xrpc/social.coves.community.post.create',
333 (server) => server.reply(200, mockResponse),
334 data: {
335 'community': 'did:plc:community1',
336 'embed': {
337 'uri': 'https://example.com/article',
338 },
339 },
340 );
341
342 final response = await apiService.createPost(
343 community: 'did:plc:community1',
344 embed: const ExternalEmbedInput(uri: 'https://example.com/article'),
345 );
346
347 expect(response, isA<CreatePostResponse>());
348 });
349
350 test('should handle 401 unauthorized error', () async {
351 dioAdapter.onPost(
352 '/xrpc/social.coves.community.post.create',
353 (server) => server.reply(401, {
354 'error': 'Unauthorized',
355 'message': 'Authentication required',
356 }),
357 data: {
358 'community': 'did:plc:community1',
359 'title': 'Test',
360 },
361 );
362
363 expect(
364 () => apiService.createPost(
365 community: 'did:plc:community1',
366 title: 'Test',
367 ),
368 throwsA(isA<AuthenticationException>()),
369 );
370 });
371
372 test('should handle 404 community not found', () async {
373 dioAdapter.onPost(
374 '/xrpc/social.coves.community.post.create',
375 (server) => server.reply(404, {
376 'error': 'NotFound',
377 'message': 'Community not found',
378 }),
379 data: {
380 'community': 'did:plc:nonexistent',
381 'title': 'Test',
382 },
383 );
384
385 expect(
386 () => apiService.createPost(
387 community: 'did:plc:nonexistent',
388 title: 'Test',
389 ),
390 throwsA(isA<NotFoundException>()),
391 );
392 });
393
394 test('should handle 400 validation error', () async {
395 dioAdapter.onPost(
396 '/xrpc/social.coves.community.post.create',
397 (server) => server.reply(400, {
398 'error': 'ValidationError',
399 'message': 'Title exceeds maximum length',
400 }),
401 data: {
402 'community': 'did:plc:community1',
403 'title': 'a' * 1000, // Very long title
404 },
405 );
406
407 expect(
408 () => apiService.createPost(
409 community: 'did:plc:community1',
410 title: 'a' * 1000,
411 ),
412 throwsA(isA<ApiException>()),
413 );
414 });
415
416 test('should handle 500 server error', () async {
417 dioAdapter.onPost(
418 '/xrpc/social.coves.community.post.create',
419 (server) => server.reply(500, {
420 'error': 'InternalServerError',
421 'message': 'Database error',
422 }),
423 data: {
424 'community': 'did:plc:community1',
425 'title': 'Test',
426 },
427 );
428
429 expect(
430 () => apiService.createPost(
431 community: 'did:plc:community1',
432 title: 'Test',
433 ),
434 throwsA(isA<ServerException>()),
435 );
436 });
437
438 test('should handle network timeout', () async {
439 dioAdapter.onPost(
440 '/xrpc/social.coves.community.post.create',
441 (server) => server.throws(
442 408,
443 DioException.connectionTimeout(
444 timeout: const Duration(seconds: 30),
445 requestOptions: RequestOptions(),
446 ),
447 ),
448 data: {
449 'community': 'did:plc:community1',
450 'title': 'Test',
451 },
452 );
453
454 expect(
455 () => apiService.createPost(
456 community: 'did:plc:community1',
457 title: 'Test',
458 ),
459 throwsA(isA<NetworkException>()),
460 );
461 });
462 });
463}