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': {'did': 'did:plc:author', 'handle': 'test.user'},
19 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
20 'stats': {'upvotes': 10, 'downvotes': 2, 'score': 8},
21 },
22 'hasMore': false,
23 },
24 ],
25 };
26
27 final response = CommentsResponse.fromJson(json);
28
29 expect(response.comments.length, 1);
30 expect(response.cursor, 'next-cursor');
31 expect(response.comments[0].comment.uri, 'at://did:plc:test/comment/1');
32 expect(response.comments[0].comment.content, 'Test comment');
33 });
34
35 test('should handle null comments array', () {
36 final json = {
37 'post': {'uri': 'at://test/post/123'},
38 'cursor': null,
39 'comments': null,
40 };
41
42 final response = CommentsResponse.fromJson(json);
43
44 expect(response.comments, isEmpty);
45 expect(response.cursor, null);
46 });
47
48 test('should handle empty comments array', () {
49 final json = {
50 'post': {'uri': 'at://test/post/123'},
51 'cursor': null,
52 'comments': [],
53 };
54
55 final response = CommentsResponse.fromJson(json);
56
57 expect(response.comments, isEmpty);
58 expect(response.cursor, null);
59 });
60
61 test('should parse without cursor', () {
62 final json = {
63 'post': {'uri': 'at://test/post/123'},
64 'comments': [
65 {
66 'comment': {
67 'uri': 'at://did:plc:test/comment/1',
68 'cid': 'cid1',
69 'content': 'Test',
70 'createdAt': '2025-01-01T12:00:00Z',
71 'indexedAt': '2025-01-01T12:00:00Z',
72 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
73 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
74 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
75 },
76 'hasMore': false,
77 },
78 ],
79 };
80
81 final response = CommentsResponse.fromJson(json);
82
83 expect(response.cursor, null);
84 expect(response.comments.length, 1);
85 });
86 });
87
88 group('ThreadViewComment', () {
89 test('should parse valid JSON', () {
90 final json = {
91 'comment': {
92 'uri': 'at://did:plc:test/comment/1',
93 'cid': 'cid1',
94 'content': 'Test comment',
95 'createdAt': '2025-01-01T12:00:00Z',
96 'indexedAt': '2025-01-01T12:00:00Z',
97 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
98 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
99 'stats': {'upvotes': 10, 'downvotes': 2, 'score': 8},
100 },
101 'hasMore': true,
102 };
103
104 final thread = ThreadViewComment.fromJson(json);
105
106 expect(thread.comment.uri, 'at://did:plc:test/comment/1');
107 expect(thread.hasMore, true);
108 expect(thread.replies, null);
109 });
110
111 test('should parse with nested replies', () {
112 final json = {
113 'comment': {
114 'uri': 'at://did:plc:test/comment/1',
115 'cid': 'cid1',
116 'content': 'Parent comment',
117 'createdAt': '2025-01-01T12:00:00Z',
118 'indexedAt': '2025-01-01T12:00:00Z',
119 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
120 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
121 'stats': {'upvotes': 5, 'downvotes': 1, 'score': 4},
122 },
123 'replies': [
124 {
125 'comment': {
126 'uri': 'at://did:plc:test/comment/2',
127 'cid': 'cid2',
128 'content': 'Reply comment',
129 'createdAt': '2025-01-01T13:00:00Z',
130 'indexedAt': '2025-01-01T13:00:00Z',
131 'author': {'did': 'did:plc:author2', 'handle': 'test.user2'},
132 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
133 'parent': {'uri': 'at://did:plc:test/comment/1', 'cid': 'cid1'},
134 'stats': {'upvotes': 3, 'downvotes': 0, 'score': 3},
135 },
136 'hasMore': false,
137 },
138 ],
139 'hasMore': false,
140 };
141
142 final thread = ThreadViewComment.fromJson(json);
143
144 expect(thread.comment.uri, 'at://did:plc:test/comment/1');
145 expect(thread.replies, isNotNull);
146 expect(thread.replies!.length, 1);
147 expect(thread.replies![0].comment.uri, 'at://did:plc:test/comment/2');
148 expect(thread.replies![0].comment.content, 'Reply comment');
149 });
150
151 test('should default hasMore to false when missing', () {
152 final json = {
153 'comment': {
154 'uri': 'at://did:plc:test/comment/1',
155 'cid': 'cid1',
156 'content': 'Test',
157 'createdAt': '2025-01-01T12:00:00Z',
158 'indexedAt': '2025-01-01T12:00:00Z',
159 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
160 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
161 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
162 },
163 };
164
165 final thread = ThreadViewComment.fromJson(json);
166
167 expect(thread.hasMore, false);
168 });
169 });
170
171 group('CommentView', () {
172 test('should parse complete JSON', () {
173 final json = {
174 'uri': 'at://did:plc:test/comment/1',
175 'cid': 'cid1',
176 'content': 'Test comment content',
177 'contentFacets': [
178 {
179 'type': 'mention',
180 'index': {'start': 0, 'end': 10},
181 'features': [
182 {
183 'type': 'app.bsky.richtext.facet#mention',
184 'did': 'did:plc:mentioned',
185 },
186 ],
187 },
188 ],
189 'createdAt': '2025-01-01T12:00:00Z',
190 'indexedAt': '2025-01-01T12:05:00Z',
191 'author': {
192 'did': 'did:plc:author',
193 'handle': 'test.user',
194 'displayName': 'Test User',
195 },
196 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
197 'parent': {
198 'uri': 'at://did:plc:test/comment/parent',
199 'cid': 'parent-cid',
200 },
201 'stats': {'upvotes': 10, 'downvotes': 2, 'score': 8},
202 'viewer': {'vote': 'upvote'},
203 'embed': {'type': 'social.coves.embed.external', 'data': {}},
204 };
205
206 final comment = CommentView.fromJson(json);
207
208 expect(comment.uri, 'at://did:plc:test/comment/1');
209 expect(comment.cid, 'cid1');
210 expect(comment.content, 'Test comment content');
211 expect(comment.contentFacets, isNotNull);
212 expect(comment.contentFacets!.length, 1);
213 expect(comment.createdAt, DateTime.parse('2025-01-01T12:00:00Z'));
214 expect(comment.indexedAt, DateTime.parse('2025-01-01T12:05:00Z'));
215 expect(comment.author.did, 'did:plc:author');
216 expect(comment.post.uri, 'at://did:plc:test/post/123');
217 expect(comment.parent, isNotNull);
218 expect(comment.parent!.uri, 'at://did:plc:test/comment/parent');
219 expect(comment.stats.score, 8);
220 expect(comment.viewer, isNotNull);
221 expect(comment.viewer!.vote, 'upvote');
222 expect(comment.embed, isNotNull);
223 });
224
225 test('should parse minimal JSON with required fields only', () {
226 final json = {
227 'uri': 'at://did:plc:test/comment/1',
228 'cid': 'cid1',
229 'content': 'Test',
230 'createdAt': '2025-01-01T12:00:00Z',
231 'indexedAt': '2025-01-01T12:00:00Z',
232 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
233 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
234 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
235 };
236
237 final comment = CommentView.fromJson(json);
238
239 expect(comment.uri, 'at://did:plc:test/comment/1');
240 expect(comment.content, 'Test');
241 expect(comment.contentFacets, null);
242 expect(comment.parent, null);
243 expect(comment.viewer, null);
244 expect(comment.embed, null);
245 });
246
247 test('should handle null optional fields', () {
248 final json = {
249 'uri': 'at://did:plc:test/comment/1',
250 'cid': 'cid1',
251 'content': 'Test',
252 'contentFacets': null,
253 'createdAt': '2025-01-01T12:00:00Z',
254 'indexedAt': '2025-01-01T12:00:00Z',
255 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
256 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
257 'parent': null,
258 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
259 'viewer': null,
260 'embed': null,
261 };
262
263 final comment = CommentView.fromJson(json);
264
265 expect(comment.contentFacets, null);
266 expect(comment.parent, null);
267 expect(comment.viewer, null);
268 expect(comment.embed, null);
269 });
270
271 test('should parse dates correctly', () {
272 final json = {
273 'uri': 'at://did:plc:test/comment/1',
274 'cid': 'cid1',
275 'content': 'Test',
276 'createdAt': '2025-01-15T14:30:45.123Z',
277 'indexedAt': '2025-01-15T14:30:50.456Z',
278 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
279 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
280 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
281 };
282
283 final comment = CommentView.fromJson(json);
284
285 expect(comment.createdAt.year, 2025);
286 expect(comment.createdAt.month, 1);
287 expect(comment.createdAt.day, 15);
288 expect(comment.createdAt.hour, 14);
289 expect(comment.createdAt.minute, 30);
290 expect(comment.indexedAt, isA<DateTime>());
291 });
292 });
293
294 group('CommentRef', () {
295 test('should parse valid JSON', () {
296 final json = {'uri': 'at://did:plc:test/comment/1', 'cid': 'cid1'};
297
298 final ref = CommentRef.fromJson(json);
299
300 expect(ref.uri, 'at://did:plc:test/comment/1');
301 expect(ref.cid, 'cid1');
302 });
303 });
304
305 group('CommentStats', () {
306 test('should parse valid JSON with all fields', () {
307 final json = {'upvotes': 15, 'downvotes': 3, 'score': 12};
308
309 final stats = CommentStats.fromJson(json);
310
311 expect(stats.upvotes, 15);
312 expect(stats.downvotes, 3);
313 expect(stats.score, 12);
314 });
315
316 test('should default to zero for missing fields', () {
317 final json = <String, dynamic>{};
318
319 final stats = CommentStats.fromJson(json);
320
321 expect(stats.upvotes, 0);
322 expect(stats.downvotes, 0);
323 expect(stats.score, 0);
324 });
325
326 test('should handle null values with defaults', () {
327 final json = {'upvotes': null, 'downvotes': null, 'score': null};
328
329 final stats = CommentStats.fromJson(json);
330
331 expect(stats.upvotes, 0);
332 expect(stats.downvotes, 0);
333 expect(stats.score, 0);
334 });
335
336 test('should parse mixed null and valid values', () {
337 final json = {'upvotes': 10, 'downvotes': null, 'score': 8};
338
339 final stats = CommentStats.fromJson(json);
340
341 expect(stats.upvotes, 10);
342 expect(stats.downvotes, 0);
343 expect(stats.score, 8);
344 });
345 });
346
347 group('CommentViewerState', () {
348 test('should parse with vote', () {
349 final json = {'vote': 'upvote'};
350
351 final viewer = CommentViewerState.fromJson(json);
352
353 expect(viewer.vote, 'upvote');
354 });
355
356 test('should parse with downvote', () {
357 final json = {'vote': 'downvote'};
358
359 final viewer = CommentViewerState.fromJson(json);
360
361 expect(viewer.vote, 'downvote');
362 });
363
364 test('should parse with null vote', () {
365 final json = {'vote': null};
366
367 final viewer = CommentViewerState.fromJson(json);
368
369 expect(viewer.vote, null);
370 });
371
372 test('should handle missing vote field', () {
373 final json = <String, dynamic>{};
374
375 final viewer = CommentViewerState.fromJson(json);
376
377 expect(viewer.vote, null);
378 });
379 });
380
381 group('Edge cases', () {
382 test('should handle deeply nested comment threads', () {
383 final json = {
384 'comment': {
385 'uri': 'at://did:plc:test/comment/1',
386 'cid': 'cid1',
387 'content': 'Level 1',
388 'createdAt': '2025-01-01T12:00:00Z',
389 'indexedAt': '2025-01-01T12:00:00Z',
390 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
391 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
392 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
393 },
394 'replies': [
395 {
396 'comment': {
397 'uri': 'at://did:plc:test/comment/2',
398 'cid': 'cid2',
399 'content': 'Level 2',
400 'createdAt': '2025-01-01T12:00:00Z',
401 'indexedAt': '2025-01-01T12:00:00Z',
402 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
403 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
404 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
405 },
406 'replies': [
407 {
408 'comment': {
409 'uri': 'at://did:plc:test/comment/3',
410 'cid': 'cid3',
411 'content': 'Level 3',
412 'createdAt': '2025-01-01T12:00:00Z',
413 'indexedAt': '2025-01-01T12:00:00Z',
414 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
415 'post': {
416 'uri': 'at://did:plc:test/post/123',
417 'cid': 'post-cid',
418 },
419 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
420 },
421 'hasMore': false,
422 },
423 ],
424 'hasMore': false,
425 },
426 ],
427 'hasMore': false,
428 };
429
430 final thread = ThreadViewComment.fromJson(json);
431
432 expect(thread.comment.content, 'Level 1');
433 expect(thread.replies![0].comment.content, 'Level 2');
434 expect(thread.replies![0].replies![0].comment.content, 'Level 3');
435 });
436
437 test('should handle empty content string', () {
438 final json = {
439 'uri': 'at://did:plc:test/comment/1',
440 'cid': 'cid1',
441 'content': '',
442 'createdAt': '2025-01-01T12:00:00Z',
443 'indexedAt': '2025-01-01T12:00:00Z',
444 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
445 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
446 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
447 };
448
449 final comment = CommentView.fromJson(json);
450
451 expect(comment.content, '');
452 });
453
454 test('should handle very long content', () {
455 final longContent = 'a' * 10000;
456 final json = {
457 'uri': 'at://did:plc:test/comment/1',
458 'cid': 'cid1',
459 'content': longContent,
460 'createdAt': '2025-01-01T12:00:00Z',
461 'indexedAt': '2025-01-01T12:00:00Z',
462 'author': {'did': 'did:plc:author', 'handle': 'test.user'},
463 'post': {'uri': 'at://did:plc:test/post/123', 'cid': 'post-cid'},
464 'stats': {'upvotes': 0, 'downvotes': 0, 'score': 0},
465 };
466
467 final comment = CommentView.fromJson(json);
468
469 expect(comment.content.length, 10000);
470 });
471
472 test('should handle negative vote counts', () {
473 final json = {'upvotes': 5, 'downvotes': 20, 'score': -15};
474
475 final stats = CommentStats.fromJson(json);
476
477 expect(stats.upvotes, 5);
478 expect(stats.downvotes, 20);
479 expect(stats.score, -15);
480 });
481 });
482}