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}