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 r'$type': 'social.coves.embed.external',
273 'external': {
274 'uri': 'https://example.com/article',
275 'title': 'Article Title',
276 },
277 },
278 'langs': ['en'],
279 'labels': {
280 'values': [
281 {'val': 'nsfw'},
282 ],
283 },
284 },
285 );
286
287 final response = await apiService.createPost(
288 community: 'did:plc:community1',
289 title: 'Test Post Title',
290 content: 'Test post content',
291 embed: ExternalEmbedInput(
292 uri: 'https://example.com/article',
293 title: 'Article Title',
294 ),
295 langs: ['en'],
296 labels: const SelfLabels(values: [SelfLabel(val: 'nsfw')]),
297 );
298
299 expect(response, isA<CreatePostResponse>());
300 expect(response.uri, 'at://did:plc:user/social.coves.community.post/123');
301 expect(response.cid, 'bafyreicid123');
302 });
303
304 test('should successfully create a minimal post', () async {
305 final mockResponse = {
306 'uri': 'at://did:plc:user/social.coves.community.post/456',
307 'cid': 'bafyreicid456',
308 };
309
310 dioAdapter.onPost(
311 '/xrpc/social.coves.community.post.create',
312 (server) => server.reply(200, mockResponse),
313 data: {
314 'community': 'did:plc:community1',
315 'title': 'Just a title',
316 },
317 );
318
319 final response = await apiService.createPost(
320 community: 'did:plc:community1',
321 title: 'Just a title',
322 );
323
324 expect(response, isA<CreatePostResponse>());
325 expect(response.uri, 'at://did:plc:user/social.coves.community.post/456');
326 });
327
328 test('should successfully create a link post', () async {
329 final mockResponse = {
330 'uri': 'at://did:plc:user/social.coves.community.post/789',
331 'cid': 'bafyreicid789',
332 };
333
334 dioAdapter.onPost(
335 '/xrpc/social.coves.community.post.create',
336 (server) => server.reply(200, mockResponse),
337 data: {
338 'community': 'did:plc:community1',
339 'embed': {
340 'uri': 'https://example.com/article',
341 },
342 },
343 );
344
345 final response = await apiService.createPost(
346 community: 'did:plc:community1',
347 embed: ExternalEmbedInput(uri: 'https://example.com/article'),
348 );
349
350 expect(response, isA<CreatePostResponse>());
351 });
352
353 test('should handle 401 unauthorized error', () async {
354 dioAdapter.onPost(
355 '/xrpc/social.coves.community.post.create',
356 (server) => server.reply(401, {
357 'error': 'Unauthorized',
358 'message': 'Authentication required',
359 }),
360 data: {
361 'community': 'did:plc:community1',
362 'title': 'Test',
363 },
364 );
365
366 expect(
367 () => apiService.createPost(
368 community: 'did:plc:community1',
369 title: 'Test',
370 ),
371 throwsA(isA<AuthenticationException>()),
372 );
373 });
374
375 test('should handle 404 community not found', () async {
376 dioAdapter.onPost(
377 '/xrpc/social.coves.community.post.create',
378 (server) => server.reply(404, {
379 'error': 'NotFound',
380 'message': 'Community not found',
381 }),
382 data: {
383 'community': 'did:plc:nonexistent',
384 'title': 'Test',
385 },
386 );
387
388 expect(
389 () => apiService.createPost(
390 community: 'did:plc:nonexistent',
391 title: 'Test',
392 ),
393 throwsA(isA<NotFoundException>()),
394 );
395 });
396
397 test('should handle 400 validation error', () async {
398 dioAdapter.onPost(
399 '/xrpc/social.coves.community.post.create',
400 (server) => server.reply(400, {
401 'error': 'ValidationError',
402 'message': 'Title exceeds maximum length',
403 }),
404 data: {
405 'community': 'did:plc:community1',
406 'title': 'a' * 1000, // Very long title
407 },
408 );
409
410 expect(
411 () => apiService.createPost(
412 community: 'did:plc:community1',
413 title: 'a' * 1000,
414 ),
415 throwsA(isA<ApiException>()),
416 );
417 });
418
419 test('should handle 500 server error', () async {
420 dioAdapter.onPost(
421 '/xrpc/social.coves.community.post.create',
422 (server) => server.reply(500, {
423 'error': 'InternalServerError',
424 'message': 'Database error',
425 }),
426 data: {
427 'community': 'did:plc:community1',
428 'title': 'Test',
429 },
430 );
431
432 expect(
433 () => apiService.createPost(
434 community: 'did:plc:community1',
435 title: 'Test',
436 ),
437 throwsA(isA<ServerException>()),
438 );
439 });
440
441 test('should handle network timeout', () async {
442 dioAdapter.onPost(
443 '/xrpc/social.coves.community.post.create',
444 (server) => server.throws(
445 408,
446 DioException.connectionTimeout(
447 timeout: const Duration(seconds: 30),
448 requestOptions: RequestOptions(),
449 ),
450 ),
451 data: {
452 'community': 'did:plc:community1',
453 'title': 'Test',
454 },
455 );
456
457 expect(
458 () => apiService.createPost(
459 community: 'did:plc:community1',
460 title: 'Test',
461 ),
462 throwsA(isA<NetworkException>()),
463 );
464 });
465 });
466}