Main coves client
1import 'package:coves_flutter/models/comment.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 - getComments', () {
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 comments', () async {
30 const postUri = 'at://did:plc:test/social.coves.post.record/123';
31
32 final mockResponse = {
33 'post': {'uri': postUri},
34 'cursor': 'next-cursor',
35 'comments': [
36 {
37 'comment': {
38 'uri': 'at://did:plc:test/comment/1',
39 'cid': 'cid1',
40 'content': 'Test comment 1',
41 'createdAt': '2025-01-01T12:00:00Z',
42 'indexedAt': '2025-01-01T12:00:00Z',
43 'author': {
44 'did': 'did:plc:author1',
45 'handle': 'user1.test',
46 'displayName': 'User One',
47 },
48 'post': {'uri': postUri, 'cid': 'post-cid'},
49 'stats': {'upvotes': 10, 'downvotes': 2, 'score': 8},
50 },
51 'hasMore': false,
52 },
53 {
54 'comment': {
55 'uri': 'at://did:plc:test/comment/2',
56 'cid': 'cid2',
57 'content': 'Test comment 2',
58 'createdAt': '2025-01-01T13:00:00Z',
59 'indexedAt': '2025-01-01T13:00:00Z',
60 'author': {'did': 'did:plc:author2', 'handle': 'user2.test'},
61 'post': {'uri': postUri, 'cid': 'post-cid'},
62 'stats': {'upvotes': 5, 'downvotes': 1, 'score': 4},
63 },
64 'hasMore': false,
65 },
66 ],
67 };
68
69 dioAdapter.onGet(
70 '/xrpc/social.coves.community.comment.getComments',
71 (server) => server.reply(200, mockResponse),
72 queryParameters: {
73 'post': postUri,
74 'sort': 'hot',
75 'depth': 10,
76 'limit': 50,
77 },
78 );
79
80 final response = await apiService.getComments(postUri: postUri);
81
82 expect(response, isA<CommentsResponse>());
83 expect(response.comments.length, 2);
84 expect(response.cursor, 'next-cursor');
85 expect(response.comments[0].comment.uri, 'at://did:plc:test/comment/1');
86 expect(response.comments[0].comment.content, 'Test comment 1');
87 expect(response.comments[1].comment.uri, 'at://did:plc:test/comment/2');
88 });
89
90 test('should handle empty comments response', () async {
91 const postUri = 'at://did:plc:test/social.coves.post.record/123';
92
93 final mockResponse = {
94 'post': {'uri': postUri},
95 'cursor': null,
96 'comments': [],
97 };
98
99 dioAdapter.onGet(
100 '/xrpc/social.coves.community.comment.getComments',
101 (server) => server.reply(200, mockResponse),
102 queryParameters: {
103 'post': postUri,
104 'sort': 'hot',
105 'depth': 10,
106 'limit': 50,
107 },
108 );
109
110 final response = await apiService.getComments(postUri: postUri);
111
112 expect(response.comments, isEmpty);
113 expect(response.cursor, null);
114 });
115
116 test('should handle null comments array', () async {
117 const postUri = 'at://did:plc:test/social.coves.post.record/123';
118
119 final mockResponse = {
120 'post': {'uri': postUri},
121 'cursor': null,
122 'comments': null,
123 };
124
125 dioAdapter.onGet(
126 '/xrpc/social.coves.community.comment.getComments',
127 (server) => server.reply(200, mockResponse),
128 queryParameters: {
129 'post': postUri,
130 'sort': 'hot',
131 'depth': 10,
132 'limit': 50,
133 },
134 );
135
136 final response = await apiService.getComments(postUri: postUri);
137
138 expect(response.comments, isEmpty);
139 });
140
141 test('should fetch comments with custom sort option', () async {
142 const postUri = 'at://did:plc:test/social.coves.post.record/123';
143
144 final mockResponse = {
145 'post': {'uri': postUri},
146 'cursor': null,
147 'comments': [
148 {
149 'comment': {
150 'uri': 'at://did:plc:test/comment/1',
151 'cid': 'cid1',
152 'content': 'Newest comment',
153 'createdAt': '2025-01-01T15:00:00Z',
154 'indexedAt': '2025-01-01T15:00:00Z',
155 'author': {'did': 'did:plc:author', 'handle': 'user.test'},
156 'post': {'uri': postUri, 'cid': 'post-cid'},
157 'stats': {'upvotes': 1, 'downvotes': 0, 'score': 1},
158 },
159 'hasMore': false,
160 },
161 ],
162 };
163
164 dioAdapter.onGet(
165 '/xrpc/social.coves.community.comment.getComments',
166 (server) => server.reply(200, mockResponse),
167 queryParameters: {
168 'post': postUri,
169 'sort': 'new',
170 'depth': 10,
171 'limit': 50,
172 },
173 );
174
175 final response = await apiService.getComments(
176 postUri: postUri,
177 sort: 'new',
178 );
179
180 expect(response.comments.length, 1);
181 expect(response.comments[0].comment.content, 'Newest comment');
182 });
183
184 test('should fetch comments with timeframe', () async {
185 const postUri = 'at://did:plc:test/social.coves.post.record/123';
186
187 final mockResponse = {
188 'post': {'uri': postUri},
189 'cursor': null,
190 'comments': [],
191 };
192
193 dioAdapter.onGet(
194 '/xrpc/social.coves.community.comment.getComments',
195 (server) => server.reply(200, mockResponse),
196 queryParameters: {
197 'post': postUri,
198 'sort': 'top',
199 'timeframe': 'week',
200 'depth': 10,
201 'limit': 50,
202 },
203 );
204
205 final response = await apiService.getComments(
206 postUri: postUri,
207 sort: 'top',
208 timeframe: 'week',
209 );
210
211 expect(response, isA<CommentsResponse>());
212 });
213
214 test('should fetch comments with cursor for pagination', () async {
215 const postUri = 'at://did:plc:test/social.coves.post.record/123';
216 const cursor = 'pagination-cursor-123';
217
218 final mockResponse = {
219 'post': {'uri': postUri},
220 'cursor': 'next-cursor-456',
221 'comments': [
222 {
223 'comment': {
224 'uri': 'at://did:plc:test/comment/10',
225 'cid': 'cid10',
226 'content': 'Paginated comment',
227 'createdAt': '2025-01-01T12:00:00Z',
228 'indexedAt': '2025-01-01T12:00:00Z',
229 'author': {'did': 'did:plc:author', 'handle': 'user.test'},
230 'post': {'uri': postUri, 'cid': 'post-cid'},
231 'stats': {'upvotes': 5, 'downvotes': 0, 'score': 5},
232 },
233 'hasMore': false,
234 },
235 ],
236 };
237
238 dioAdapter.onGet(
239 '/xrpc/social.coves.community.comment.getComments',
240 (server) => server.reply(200, mockResponse),
241 queryParameters: {
242 'post': postUri,
243 'sort': 'hot',
244 'depth': 10,
245 'limit': 50,
246 'cursor': cursor,
247 },
248 );
249
250 final response = await apiService.getComments(
251 postUri: postUri,
252 cursor: cursor,
253 );
254
255 expect(response.comments.length, 1);
256 expect(response.cursor, 'next-cursor-456');
257 });
258
259 test('should fetch comments with custom depth and limit', () async {
260 const postUri = 'at://did:plc:test/social.coves.post.record/123';
261
262 final mockResponse = {
263 'post': {'uri': postUri},
264 'cursor': null,
265 'comments': [],
266 };
267
268 dioAdapter.onGet(
269 '/xrpc/social.coves.community.comment.getComments',
270 (server) => server.reply(200, mockResponse),
271 queryParameters: {
272 'post': postUri,
273 'sort': 'hot',
274 'depth': 5,
275 'limit': 20,
276 },
277 );
278
279 final response = await apiService.getComments(
280 postUri: postUri,
281 depth: 5,
282 limit: 20,
283 );
284
285 expect(response, isA<CommentsResponse>());
286 });
287
288 test('should handle 404 error', () async {
289 const postUri = 'at://did:plc:test/social.coves.post.record/nonexistent';
290
291 dioAdapter.onGet(
292 '/xrpc/social.coves.community.comment.getComments',
293 (server) => server.reply(404, {
294 'error': 'NotFoundError',
295 'message': 'Post not found',
296 }),
297 queryParameters: {
298 'post': postUri,
299 'sort': 'hot',
300 'depth': 10,
301 'limit': 50,
302 },
303 );
304
305 expect(
306 () => apiService.getComments(postUri: postUri),
307 throwsA(isA<Exception>()),
308 );
309 });
310
311 test('should handle 500 internal server error', () async {
312 const postUri = 'at://did:plc:test/social.coves.post.record/123';
313
314 dioAdapter.onGet(
315 '/xrpc/social.coves.community.comment.getComments',
316 (server) => server.reply(500, {
317 'error': 'InternalServerError',
318 'message': 'Database connection failed',
319 }),
320 queryParameters: {
321 'post': postUri,
322 'sort': 'hot',
323 'depth': 10,
324 'limit': 50,
325 },
326 );
327
328 expect(
329 () => apiService.getComments(postUri: postUri),
330 throwsA(isA<Exception>()),
331 );
332 });
333
334 test('should handle network timeout', () async {
335 const postUri = 'at://did:plc:test/social.coves.post.record/123';
336
337 dioAdapter.onGet(
338 '/xrpc/social.coves.community.comment.getComments',
339 (server) => server.throws(
340 408,
341 DioException.connectionTimeout(
342 timeout: const Duration(seconds: 30),
343 requestOptions: RequestOptions(),
344 ),
345 ),
346 queryParameters: {
347 'post': postUri,
348 'sort': 'hot',
349 'depth': 10,
350 'limit': 50,
351 },
352 );
353
354 expect(
355 () => apiService.getComments(postUri: postUri),
356 throwsA(isA<NetworkException>()),
357 );
358 });
359
360 test('should handle network connection error', () async {
361 const postUri = 'at://did:plc:test/social.coves.post.record/123';
362
363 dioAdapter.onGet(
364 '/xrpc/social.coves.community.comment.getComments',
365 (server) => server.throws(
366 503,
367 DioException.connectionError(
368 reason: 'Connection refused',
369 requestOptions: RequestOptions(),
370 ),
371 ),
372 queryParameters: {
373 'post': postUri,
374 'sort': 'hot',
375 'depth': 10,
376 'limit': 50,
377 },
378 );
379
380 expect(
381 () => apiService.getComments(postUri: postUri),
382 throwsA(isA<NetworkException>()),
383 );
384 });
385
386 test('should handle invalid JSON response', () async {
387 const postUri = 'at://did:plc:test/social.coves.post.record/123';
388
389 dioAdapter.onGet(
390 '/xrpc/social.coves.community.comment.getComments',
391 (server) => server.reply(200, 'invalid json string'),
392 queryParameters: {
393 'post': postUri,
394 'sort': 'hot',
395 'depth': 10,
396 'limit': 50,
397 },
398 );
399
400 expect(
401 () => apiService.getComments(postUri: postUri),
402 throwsA(isA<ApiException>()),
403 );
404 });
405
406 test('should handle malformed JSON with missing required fields', () async {
407 const postUri = 'at://did:plc:test/social.coves.post.record/123';
408
409 final mockResponse = {
410 'post': {'uri': postUri},
411 'comments': [
412 {
413 'comment': {
414 'uri': 'at://did:plc:test/comment/1',
415 // Missing required 'cid' field
416 'content': 'Test',
417 'createdAt': '2025-01-01T12:00:00Z',
418 'indexedAt': '2025-01-01T12:00:00Z',
419 'author': {'did': 'did:plc:author', 'handle': 'user.test'},
420 'post': {'uri': postUri, 'cid': 'post-cid'},
421 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
422 },
423 'hasMore': false,
424 },
425 ],
426 };
427
428 dioAdapter.onGet(
429 '/xrpc/social.coves.community.comment.getComments',
430 (server) => server.reply(200, mockResponse),
431 queryParameters: {
432 'post': postUri,
433 'sort': 'hot',
434 'depth': 10,
435 'limit': 50,
436 },
437 );
438
439 expect(
440 () => apiService.getComments(postUri: postUri),
441 throwsA(isA<ApiException>()),
442 );
443 });
444
445 test('should handle comments with nested replies', () async {
446 const postUri = 'at://did:plc:test/social.coves.post.record/123';
447
448 final mockResponse = {
449 'post': {'uri': postUri},
450 'cursor': null,
451 'comments': [
452 {
453 'comment': {
454 'uri': 'at://did:plc:test/comment/1',
455 'cid': 'cid1',
456 'content': 'Parent comment',
457 'createdAt': '2025-01-01T12:00:00Z',
458 'indexedAt': '2025-01-01T12:00:00Z',
459 'author': {'did': 'did:plc:author1', 'handle': 'user1.test'},
460 'post': {'uri': postUri, 'cid': 'post-cid'},
461 'stats': {'upvotes': 10, 'downvotes': 2, 'score': 8},
462 },
463 'replies': [
464 {
465 'comment': {
466 'uri': 'at://did:plc:test/comment/2',
467 'cid': 'cid2',
468 'content': 'Reply comment',
469 'createdAt': '2025-01-01T13:00:00Z',
470 'indexedAt': '2025-01-01T13:00:00Z',
471 'author': {'did': 'did:plc:author2', 'handle': 'user2.test'},
472 'post': {'uri': postUri, 'cid': 'post-cid'},
473 'parent': {
474 'uri': 'at://did:plc:test/comment/1',
475 'cid': 'cid1',
476 },
477 'stats': {'upvotes': 5, 'downvotes': 0, 'score': 5},
478 },
479 'hasMore': false,
480 },
481 ],
482 'hasMore': false,
483 },
484 ],
485 };
486
487 dioAdapter.onGet(
488 '/xrpc/social.coves.community.comment.getComments',
489 (server) => server.reply(200, mockResponse),
490 queryParameters: {
491 'post': postUri,
492 'sort': 'hot',
493 'depth': 10,
494 'limit': 50,
495 },
496 );
497
498 final response = await apiService.getComments(postUri: postUri);
499
500 expect(response.comments.length, 1);
501 expect(response.comments[0].comment.content, 'Parent comment');
502 expect(response.comments[0].replies, isNotNull);
503 expect(response.comments[0].replies!.length, 1);
504 expect(response.comments[0].replies![0].comment.content, 'Reply comment');
505 });
506
507 test('should handle comments with viewer state', () async {
508 const postUri = 'at://did:plc:test/social.coves.post.record/123';
509
510 final mockResponse = {
511 'post': {'uri': postUri},
512 'cursor': null,
513 'comments': [
514 {
515 'comment': {
516 'uri': 'at://did:plc:test/comment/1',
517 'cid': 'cid1',
518 'content': 'Voted comment',
519 'createdAt': '2025-01-01T12:00:00Z',
520 'indexedAt': '2025-01-01T12:00:00Z',
521 'author': {'did': 'did:plc:author', 'handle': 'user.test'},
522 'post': {'uri': postUri, 'cid': 'post-cid'},
523 'stats': {'upvotes': 10, 'downvotes': 0, 'score': 10},
524 'viewer': {'vote': 'upvote'},
525 },
526 'hasMore': false,
527 },
528 ],
529 };
530
531 dioAdapter.onGet(
532 '/xrpc/social.coves.community.comment.getComments',
533 (server) => server.reply(200, mockResponse),
534 queryParameters: {
535 'post': postUri,
536 'sort': 'hot',
537 'depth': 10,
538 'limit': 50,
539 },
540 );
541
542 final response = await apiService.getComments(postUri: postUri);
543
544 expect(response.comments.length, 1);
545 expect(response.comments[0].comment.viewer, isNotNull);
546 expect(response.comments[0].comment.viewer!.vote, 'upvote');
547 });
548 });
549}