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