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}