Main coves client
1import 'package:coves_flutter/models/comment.dart';
2import 'package:flutter_test/flutter_test.dart';
3
4void main() {
5 group('CommentsResponse', () {
6 test('should parse valid JSON with comments', () {
7 final json = {
8 'post': {'uri': 'at://test/post/123'},
9 'cursor': 'next-cursor',
10 'comments': [
11 {
12 'comment': {
13 'uri': 'at://did:plc:test/comment/1',
14 'cid': 'cid1',
15 'content': 'Test comment',
16 'createdAt': '2025-01-01T12:00:00Z',
17 'indexedAt': '2025-01-01T12:00:00Z',
18 'author': {
19 'did': 'did:plc:author',
20 'handle': 'test.user',
21 },
22 'post': {
23 'uri': 'at://did:plc:test/post/123',
24 'cid': 'post-cid',
25 },
26 'stats': {
27 'upvotes': 10,
28 'downvotes': 2,
29 'score': 8,
30 },
31 },
32 'hasMore': false,
33 },
34 ],
35 };
36
37 final response = CommentsResponse.fromJson(json);
38
39 expect(response.comments.length, 1);
40 expect(response.cursor, 'next-cursor');
41 expect(response.comments[0].comment.uri, 'at://did:plc:test/comment/1');
42 expect(response.comments[0].comment.content, 'Test comment');
43 });
44
45 test('should handle null comments array', () {
46 final json = {
47 'post': {'uri': 'at://test/post/123'},
48 'cursor': null,
49 'comments': null,
50 };
51
52 final response = CommentsResponse.fromJson(json);
53
54 expect(response.comments, isEmpty);
55 expect(response.cursor, null);
56 });
57
58 test('should handle empty comments array', () {
59 final json = {
60 'post': {'uri': 'at://test/post/123'},
61 'cursor': null,
62 'comments': [],
63 };
64
65 final response = CommentsResponse.fromJson(json);
66
67 expect(response.comments, isEmpty);
68 expect(response.cursor, null);
69 });
70
71 test('should parse without cursor', () {
72 final json = {
73 'post': {'uri': 'at://test/post/123'},
74 'comments': [
75 {
76 'comment': {
77 'uri': 'at://did:plc:test/comment/1',
78 'cid': 'cid1',
79 'content': 'Test',
80 'createdAt': '2025-01-01T12:00:00Z',
81 'indexedAt': '2025-01-01T12:00:00Z',
82 'author': {
83 'did': 'did:plc:author',
84 'handle': 'test.user',
85 },
86 'post': {
87 'uri': 'at://did:plc:test/post/123',
88 'cid': 'post-cid',
89 },
90 'stats': {
91 'upvotes': 0,
92 'downvotes': 0,
93 'score': 0,
94 },
95 },
96 'hasMore': false,
97 },
98 ],
99 };
100
101 final response = CommentsResponse.fromJson(json);
102
103 expect(response.cursor, null);
104 expect(response.comments.length, 1);
105 });
106 });
107
108 group('ThreadViewComment', () {
109 test('should parse valid JSON', () {
110 final json = {
111 'comment': {
112 'uri': 'at://did:plc:test/comment/1',
113 'cid': 'cid1',
114 'content': 'Test comment',
115 'createdAt': '2025-01-01T12:00:00Z',
116 'indexedAt': '2025-01-01T12:00:00Z',
117 'author': {
118 'did': 'did:plc:author',
119 'handle': 'test.user',
120 },
121 'post': {
122 'uri': 'at://did:plc:test/post/123',
123 'cid': 'post-cid',
124 },
125 'stats': {
126 'upvotes': 10,
127 'downvotes': 2,
128 'score': 8,
129 },
130 },
131 'hasMore': true,
132 };
133
134 final thread = ThreadViewComment.fromJson(json);
135
136 expect(thread.comment.uri, 'at://did:plc:test/comment/1');
137 expect(thread.hasMore, true);
138 expect(thread.replies, null);
139 });
140
141 test('should parse with nested replies', () {
142 final json = {
143 'comment': {
144 'uri': 'at://did:plc:test/comment/1',
145 'cid': 'cid1',
146 'content': 'Parent comment',
147 'createdAt': '2025-01-01T12:00:00Z',
148 'indexedAt': '2025-01-01T12:00:00Z',
149 'author': {
150 'did': 'did:plc:author',
151 'handle': 'test.user',
152 },
153 'post': {
154 'uri': 'at://did:plc:test/post/123',
155 'cid': 'post-cid',
156 },
157 'stats': {
158 'upvotes': 5,
159 'downvotes': 1,
160 'score': 4,
161 },
162 },
163 'replies': [
164 {
165 'comment': {
166 'uri': 'at://did:plc:test/comment/2',
167 'cid': 'cid2',
168 'content': 'Reply comment',
169 'createdAt': '2025-01-01T13:00:00Z',
170 'indexedAt': '2025-01-01T13:00:00Z',
171 'author': {
172 'did': 'did:plc:author2',
173 'handle': 'test.user2',
174 },
175 'post': {
176 'uri': 'at://did:plc:test/post/123',
177 'cid': 'post-cid',
178 },
179 'parent': {
180 'uri': 'at://did:plc:test/comment/1',
181 'cid': 'cid1',
182 },
183 'stats': {
184 'upvotes': 3,
185 'downvotes': 0,
186 'score': 3,
187 },
188 },
189 'hasMore': false,
190 },
191 ],
192 'hasMore': false,
193 };
194
195 final thread = ThreadViewComment.fromJson(json);
196
197 expect(thread.comment.uri, 'at://did:plc:test/comment/1');
198 expect(thread.replies, isNotNull);
199 expect(thread.replies!.length, 1);
200 expect(thread.replies![0].comment.uri, 'at://did:plc:test/comment/2');
201 expect(thread.replies![0].comment.content, 'Reply comment');
202 });
203
204 test('should default hasMore to false when missing', () {
205 final json = {
206 'comment': {
207 'uri': 'at://did:plc:test/comment/1',
208 'cid': 'cid1',
209 'content': 'Test',
210 'createdAt': '2025-01-01T12:00:00Z',
211 'indexedAt': '2025-01-01T12:00:00Z',
212 'author': {
213 'did': 'did:plc:author',
214 'handle': 'test.user',
215 },
216 'post': {
217 'uri': 'at://did:plc:test/post/123',
218 'cid': 'post-cid',
219 },
220 'stats': {
221 'upvotes': 0,
222 'downvotes': 0,
223 'score': 0,
224 },
225 },
226 };
227
228 final thread = ThreadViewComment.fromJson(json);
229
230 expect(thread.hasMore, false);
231 });
232 });
233
234 group('CommentView', () {
235 test('should parse complete JSON', () {
236 final json = {
237 'uri': 'at://did:plc:test/comment/1',
238 'cid': 'cid1',
239 'content': 'Test comment content',
240 'contentFacets': [
241 {
242 'type': 'mention',
243 'index': {'start': 0, 'end': 10},
244 'features': [
245 {
246 'type': 'app.bsky.richtext.facet#mention',
247 'did': 'did:plc:mentioned',
248 },
249 ],
250 },
251 ],
252 'createdAt': '2025-01-01T12:00:00Z',
253 'indexedAt': '2025-01-01T12:05:00Z',
254 'author': {
255 'did': 'did:plc:author',
256 'handle': 'test.user',
257 'displayName': 'Test User',
258 },
259 'post': {
260 'uri': 'at://did:plc:test/post/123',
261 'cid': 'post-cid',
262 },
263 'parent': {
264 'uri': 'at://did:plc:test/comment/parent',
265 'cid': 'parent-cid',
266 },
267 'stats': {
268 'upvotes': 10,
269 'downvotes': 2,
270 'score': 8,
271 },
272 'viewer': {
273 'vote': 'upvote',
274 },
275 'embed': {
276 'type': 'social.coves.embed.external',
277 'data': {},
278 },
279 };
280
281 final comment = CommentView.fromJson(json);
282
283 expect(comment.uri, 'at://did:plc:test/comment/1');
284 expect(comment.cid, 'cid1');
285 expect(comment.content, 'Test comment content');
286 expect(comment.contentFacets, isNotNull);
287 expect(comment.contentFacets!.length, 1);
288 expect(comment.createdAt, DateTime.parse('2025-01-01T12:00:00Z'));
289 expect(comment.indexedAt, DateTime.parse('2025-01-01T12:05:00Z'));
290 expect(comment.author.did, 'did:plc:author');
291 expect(comment.post.uri, 'at://did:plc:test/post/123');
292 expect(comment.parent, isNotNull);
293 expect(comment.parent!.uri, 'at://did:plc:test/comment/parent');
294 expect(comment.stats.score, 8);
295 expect(comment.viewer, isNotNull);
296 expect(comment.viewer!.vote, 'upvote');
297 expect(comment.embed, isNotNull);
298 });
299
300 test('should parse minimal JSON with required fields only', () {
301 final json = {
302 'uri': 'at://did:plc:test/comment/1',
303 'cid': 'cid1',
304 'content': 'Test',
305 'createdAt': '2025-01-01T12:00:00Z',
306 'indexedAt': '2025-01-01T12:00:00Z',
307 'author': {
308 'did': 'did:plc:author',
309 'handle': 'test.user',
310 },
311 'post': {
312 'uri': 'at://did:plc:test/post/123',
313 'cid': 'post-cid',
314 },
315 'stats': {
316 'upvotes': 0,
317 'downvotes': 0,
318 'score': 0,
319 },
320 };
321
322 final comment = CommentView.fromJson(json);
323
324 expect(comment.uri, 'at://did:plc:test/comment/1');
325 expect(comment.content, 'Test');
326 expect(comment.contentFacets, null);
327 expect(comment.parent, null);
328 expect(comment.viewer, null);
329 expect(comment.embed, null);
330 });
331
332 test('should handle null optional fields', () {
333 final json = {
334 'uri': 'at://did:plc:test/comment/1',
335 'cid': 'cid1',
336 'content': 'Test',
337 'contentFacets': null,
338 'createdAt': '2025-01-01T12:00:00Z',
339 'indexedAt': '2025-01-01T12:00:00Z',
340 'author': {
341 'did': 'did:plc:author',
342 'handle': 'test.user',
343 },
344 'post': {
345 'uri': 'at://did:plc:test/post/123',
346 'cid': 'post-cid',
347 },
348 'parent': null,
349 'stats': {
350 'upvotes': 0,
351 'downvotes': 0,
352 'score': 0,
353 },
354 'viewer': null,
355 'embed': null,
356 };
357
358 final comment = CommentView.fromJson(json);
359
360 expect(comment.contentFacets, null);
361 expect(comment.parent, null);
362 expect(comment.viewer, null);
363 expect(comment.embed, null);
364 });
365
366 test('should parse dates correctly', () {
367 final json = {
368 'uri': 'at://did:plc:test/comment/1',
369 'cid': 'cid1',
370 'content': 'Test',
371 'createdAt': '2025-01-15T14:30:45.123Z',
372 'indexedAt': '2025-01-15T14:30:50.456Z',
373 'author': {
374 'did': 'did:plc:author',
375 'handle': 'test.user',
376 },
377 'post': {
378 'uri': 'at://did:plc:test/post/123',
379 'cid': 'post-cid',
380 },
381 'stats': {
382 'upvotes': 0,
383 'downvotes': 0,
384 'score': 0,
385 },
386 };
387
388 final comment = CommentView.fromJson(json);
389
390 expect(comment.createdAt.year, 2025);
391 expect(comment.createdAt.month, 1);
392 expect(comment.createdAt.day, 15);
393 expect(comment.createdAt.hour, 14);
394 expect(comment.createdAt.minute, 30);
395 expect(comment.indexedAt, isA<DateTime>());
396 });
397 });
398
399 group('CommentRef', () {
400 test('should parse valid JSON', () {
401 final json = {
402 'uri': 'at://did:plc:test/comment/1',
403 'cid': 'cid1',
404 };
405
406 final ref = CommentRef.fromJson(json);
407
408 expect(ref.uri, 'at://did:plc:test/comment/1');
409 expect(ref.cid, 'cid1');
410 });
411 });
412
413 group('CommentStats', () {
414 test('should parse valid JSON with all fields', () {
415 final json = {
416 'upvotes': 15,
417 'downvotes': 3,
418 'score': 12,
419 };
420
421 final stats = CommentStats.fromJson(json);
422
423 expect(stats.upvotes, 15);
424 expect(stats.downvotes, 3);
425 expect(stats.score, 12);
426 });
427
428 test('should default to zero for missing fields', () {
429 final json = <String, dynamic>{};
430
431 final stats = CommentStats.fromJson(json);
432
433 expect(stats.upvotes, 0);
434 expect(stats.downvotes, 0);
435 expect(stats.score, 0);
436 });
437
438 test('should handle null values with defaults', () {
439 final json = {
440 'upvotes': null,
441 'downvotes': null,
442 'score': null,
443 };
444
445 final stats = CommentStats.fromJson(json);
446
447 expect(stats.upvotes, 0);
448 expect(stats.downvotes, 0);
449 expect(stats.score, 0);
450 });
451
452 test('should parse mixed null and valid values', () {
453 final json = {
454 'upvotes': 10,
455 'downvotes': null,
456 'score': 8,
457 };
458
459 final stats = CommentStats.fromJson(json);
460
461 expect(stats.upvotes, 10);
462 expect(stats.downvotes, 0);
463 expect(stats.score, 8);
464 });
465 });
466
467 group('CommentViewerState', () {
468 test('should parse with vote', () {
469 final json = {
470 'vote': 'upvote',
471 };
472
473 final viewer = CommentViewerState.fromJson(json);
474
475 expect(viewer.vote, 'upvote');
476 });
477
478 test('should parse with downvote', () {
479 final json = {
480 'vote': 'downvote',
481 };
482
483 final viewer = CommentViewerState.fromJson(json);
484
485 expect(viewer.vote, 'downvote');
486 });
487
488 test('should parse with null vote', () {
489 final json = {
490 'vote': null,
491 };
492
493 final viewer = CommentViewerState.fromJson(json);
494
495 expect(viewer.vote, null);
496 });
497
498 test('should handle missing vote field', () {
499 final json = <String, dynamic>{};
500
501 final viewer = CommentViewerState.fromJson(json);
502
503 expect(viewer.vote, null);
504 });
505 });
506
507 group('Edge cases', () {
508 test('should handle deeply nested comment threads', () {
509 final json = {
510 'comment': {
511 'uri': 'at://did:plc:test/comment/1',
512 'cid': 'cid1',
513 'content': 'Level 1',
514 'createdAt': '2025-01-01T12:00:00Z',
515 'indexedAt': '2025-01-01T12:00:00Z',
516 'author': {
517 'did': 'did:plc:author',
518 'handle': 'test.user',
519 },
520 'post': {
521 'uri': 'at://did:plc:test/post/123',
522 'cid': 'post-cid',
523 },
524 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
525 },
526 'replies': [
527 {
528 'comment': {
529 'uri': 'at://did:plc:test/comment/2',
530 'cid': 'cid2',
531 'content': 'Level 2',
532 'createdAt': '2025-01-01T12:00:00Z',
533 'indexedAt': '2025-01-01T12:00:00Z',
534 'author': {
535 'did': 'did:plc:author',
536 'handle': 'test.user',
537 },
538 'post': {
539 'uri': 'at://did:plc:test/post/123',
540 'cid': 'post-cid',
541 },
542 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
543 },
544 'replies': [
545 {
546 'comment': {
547 'uri': 'at://did:plc:test/comment/3',
548 'cid': 'cid3',
549 'content': 'Level 3',
550 'createdAt': '2025-01-01T12:00:00Z',
551 'indexedAt': '2025-01-01T12:00:00Z',
552 'author': {
553 'did': 'did:plc:author',
554 'handle': 'test.user',
555 },
556 'post': {
557 'uri': 'at://did:plc:test/post/123',
558 'cid': 'post-cid',
559 },
560 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
561 },
562 'hasMore': false,
563 },
564 ],
565 'hasMore': false,
566 },
567 ],
568 'hasMore': false,
569 };
570
571 final thread = ThreadViewComment.fromJson(json);
572
573 expect(thread.comment.content, 'Level 1');
574 expect(thread.replies![0].comment.content, 'Level 2');
575 expect(thread.replies![0].replies![0].comment.content, 'Level 3');
576 });
577
578 test('should handle empty content string', () {
579 final json = {
580 'uri': 'at://did:plc:test/comment/1',
581 'cid': 'cid1',
582 'content': '',
583 'createdAt': '2025-01-01T12:00:00Z',
584 'indexedAt': '2025-01-01T12:00:00Z',
585 'author': {
586 'did': 'did:plc:author',
587 'handle': 'test.user',
588 },
589 'post': {
590 'uri': 'at://did:plc:test/post/123',
591 'cid': 'post-cid',
592 },
593 'stats': {
594 'upvotes': 0,
595 'downvotes': 0,
596 'score': 0,
597 },
598 };
599
600 final comment = CommentView.fromJson(json);
601
602 expect(comment.content, '');
603 });
604
605 test('should handle very long content', () {
606 final longContent = 'a' * 10000;
607 final json = {
608 'uri': 'at://did:plc:test/comment/1',
609 'cid': 'cid1',
610 'content': longContent,
611 'createdAt': '2025-01-01T12:00:00Z',
612 'indexedAt': '2025-01-01T12:00:00Z',
613 'author': {
614 'did': 'did:plc:author',
615 'handle': 'test.user',
616 },
617 'post': {
618 'uri': 'at://did:plc:test/post/123',
619 'cid': 'post-cid',
620 },
621 'stats': {
622 'upvotes': 0,
623 'downvotes': 0,
624 'score': 0,
625 },
626 };
627
628 final comment = CommentView.fromJson(json);
629
630 expect(comment.content.length, 10000);
631 });
632
633 test('should handle negative vote counts', () {
634 final json = {
635 'upvotes': 5,
636 'downvotes': 20,
637 'score': -15,
638 };
639
640 final stats = CommentStats.fromJson(json);
641
642 expect(stats.upvotes, 5);
643 expect(stats.downvotes, 20);
644 expect(stats.score, -15);
645 });
646 });
647}