1import 'package:coves_flutter/models/coves_session.dart'; 2import 'package:coves_flutter/services/api_exceptions.dart'; 3import 'package:coves_flutter/services/comment_service.dart'; 4import 'package:dio/dio.dart'; 5import 'package:flutter_test/flutter_test.dart'; 6import 'package:mockito/annotations.dart'; 7import 'package:mockito/mockito.dart'; 8 9import 'comment_service_test.mocks.dart'; 10 11@GenerateMocks([Dio]) 12void main() { 13 group('CommentService', () { 14 group('CreateCommentResponse', () { 15 test('should create response with uri and cid', () { 16 const response = CreateCommentResponse( 17 uri: 'at://did:plc:test/social.coves.community.comment/123', 18 cid: 'bafy123', 19 ); 20 21 expect( 22 response.uri, 23 'at://did:plc:test/social.coves.community.comment/123', 24 ); 25 expect(response.cid, 'bafy123'); 26 }); 27 }); 28 29 group('createComment', () { 30 late MockDio mockDio; 31 late CommentService commentService; 32 late CovesSession testSession; 33 34 setUp(() { 35 mockDio = MockDio(); 36 testSession = CovesSession( 37 token: 'test-token', 38 did: 'did:plc:test', 39 sessionId: 'test-session-id', 40 handle: 'test.user', 41 ); 42 43 // Setup default interceptors behavior 44 when(mockDio.interceptors).thenReturn(Interceptors()); 45 46 commentService = CommentService( 47 sessionGetter: () async => testSession, 48 tokenRefresher: () async => true, 49 signOutHandler: () async {}, 50 dio: mockDio, 51 ); 52 }); 53 54 test('should create comment successfully', () async { 55 when( 56 mockDio.post<Map<String, dynamic>>( 57 '/xrpc/social.coves.community.comment.create', 58 data: anyNamed('data'), 59 ), 60 ).thenAnswer( 61 (_) async => Response( 62 requestOptions: RequestOptions(path: ''), 63 statusCode: 200, 64 data: { 65 'uri': 'at://did:plc:test/social.coves.community.comment/abc123', 66 'cid': 'bafy123', 67 }, 68 ), 69 ); 70 71 final response = await commentService.createComment( 72 rootUri: 'at://did:plc:author/social.coves.post.record/post123', 73 rootCid: 'rootCid123', 74 parentUri: 'at://did:plc:author/social.coves.post.record/post123', 75 parentCid: 'parentCid123', 76 content: 'This is a test comment', 77 ); 78 79 expect( 80 response.uri, 81 'at://did:plc:test/social.coves.community.comment/abc123', 82 ); 83 expect(response.cid, 'bafy123'); 84 85 verify( 86 mockDio.post<Map<String, dynamic>>( 87 '/xrpc/social.coves.community.comment.create', 88 data: { 89 'reply': { 90 'root': { 91 'uri': 'at://did:plc:author/social.coves.post.record/post123', 92 'cid': 'rootCid123', 93 }, 94 'parent': { 95 'uri': 'at://did:plc:author/social.coves.post.record/post123', 96 'cid': 'parentCid123', 97 }, 98 }, 99 'content': 'This is a test comment', 100 }, 101 ), 102 ).called(1); 103 }); 104 105 test('should throw AuthenticationException when no session', () async { 106 final serviceWithoutSession = CommentService( 107 sessionGetter: () async => null, 108 tokenRefresher: () async => true, 109 signOutHandler: () async {}, 110 dio: mockDio, 111 ); 112 113 expect( 114 () => serviceWithoutSession.createComment( 115 rootUri: 'at://did:plc:author/post/123', 116 rootCid: 'rootCid', 117 parentUri: 'at://did:plc:author/post/123', 118 parentCid: 'parentCid', 119 content: 'Test comment', 120 ), 121 throwsA(isA<AuthenticationException>()), 122 ); 123 }); 124 125 test('should throw ApiException on network error', () async { 126 when( 127 mockDio.post<Map<String, dynamic>>( 128 '/xrpc/social.coves.community.comment.create', 129 data: anyNamed('data'), 130 ), 131 ).thenThrow( 132 DioException( 133 requestOptions: RequestOptions(path: ''), 134 type: DioExceptionType.connectionError, 135 message: 'Connection failed', 136 ), 137 ); 138 139 expect( 140 () => commentService.createComment( 141 rootUri: 'at://did:plc:author/post/123', 142 rootCid: 'rootCid', 143 parentUri: 'at://did:plc:author/post/123', 144 parentCid: 'parentCid', 145 content: 'Test comment', 146 ), 147 throwsA(isA<ApiException>()), 148 ); 149 }); 150 151 test('should throw AuthenticationException on 401 response', () async { 152 when( 153 mockDio.post<Map<String, dynamic>>( 154 '/xrpc/social.coves.community.comment.create', 155 data: anyNamed('data'), 156 ), 157 ).thenThrow( 158 DioException( 159 requestOptions: RequestOptions(path: ''), 160 type: DioExceptionType.badResponse, 161 response: Response( 162 requestOptions: RequestOptions(path: ''), 163 statusCode: 401, 164 data: {'error': 'Unauthorized'}, 165 ), 166 ), 167 ); 168 169 expect( 170 () => commentService.createComment( 171 rootUri: 'at://did:plc:author/post/123', 172 rootCid: 'rootCid', 173 parentUri: 'at://did:plc:author/post/123', 174 parentCid: 'parentCid', 175 content: 'Test comment', 176 ), 177 throwsA(isA<AuthenticationException>()), 178 ); 179 }); 180 181 test('should throw ApiException on invalid response (null data)', () async { 182 when( 183 mockDio.post<Map<String, dynamic>>( 184 '/xrpc/social.coves.community.comment.create', 185 data: anyNamed('data'), 186 ), 187 ).thenAnswer( 188 (_) async => Response( 189 requestOptions: RequestOptions(path: ''), 190 statusCode: 200, 191 data: null, 192 ), 193 ); 194 195 expect( 196 () => commentService.createComment( 197 rootUri: 'at://did:plc:author/post/123', 198 rootCid: 'rootCid', 199 parentUri: 'at://did:plc:author/post/123', 200 parentCid: 'parentCid', 201 content: 'Test comment', 202 ), 203 throwsA( 204 isA<ApiException>().having( 205 (e) => e.message, 206 'message', 207 contains('no data'), 208 ), 209 ), 210 ); 211 }); 212 213 test('should throw ApiException on invalid response (missing uri)', () async { 214 when( 215 mockDio.post<Map<String, dynamic>>( 216 '/xrpc/social.coves.community.comment.create', 217 data: anyNamed('data'), 218 ), 219 ).thenAnswer( 220 (_) async => Response( 221 requestOptions: RequestOptions(path: ''), 222 statusCode: 200, 223 data: {'cid': 'bafy123'}, 224 ), 225 ); 226 227 expect( 228 () => commentService.createComment( 229 rootUri: 'at://did:plc:author/post/123', 230 rootCid: 'rootCid', 231 parentUri: 'at://did:plc:author/post/123', 232 parentCid: 'parentCid', 233 content: 'Test comment', 234 ), 235 throwsA( 236 isA<ApiException>().having( 237 (e) => e.message, 238 'message', 239 contains('missing uri'), 240 ), 241 ), 242 ); 243 }); 244 245 test('should throw ApiException on invalid response (empty uri)', () async { 246 when( 247 mockDio.post<Map<String, dynamic>>( 248 '/xrpc/social.coves.community.comment.create', 249 data: anyNamed('data'), 250 ), 251 ).thenAnswer( 252 (_) async => Response( 253 requestOptions: RequestOptions(path: ''), 254 statusCode: 200, 255 data: {'uri': '', 'cid': 'bafy123'}, 256 ), 257 ); 258 259 expect( 260 () => commentService.createComment( 261 rootUri: 'at://did:plc:author/post/123', 262 rootCid: 'rootCid', 263 parentUri: 'at://did:plc:author/post/123', 264 parentCid: 'parentCid', 265 content: 'Test comment', 266 ), 267 throwsA( 268 isA<ApiException>().having( 269 (e) => e.message, 270 'message', 271 contains('missing uri'), 272 ), 273 ), 274 ); 275 }); 276 277 test('should throw ApiException on server error', () async { 278 when( 279 mockDio.post<Map<String, dynamic>>( 280 '/xrpc/social.coves.community.comment.create', 281 data: anyNamed('data'), 282 ), 283 ).thenThrow( 284 DioException( 285 requestOptions: RequestOptions(path: ''), 286 type: DioExceptionType.badResponse, 287 response: Response( 288 requestOptions: RequestOptions(path: ''), 289 statusCode: 500, 290 data: {'error': 'Internal server error'}, 291 ), 292 message: 'Internal server error', 293 ), 294 ); 295 296 expect( 297 () => commentService.createComment( 298 rootUri: 'at://did:plc:author/post/123', 299 rootCid: 'rootCid', 300 parentUri: 'at://did:plc:author/post/123', 301 parentCid: 'parentCid', 302 content: 'Test comment', 303 ), 304 throwsA(isA<ApiException>()), 305 ); 306 }); 307 308 test('should send correct parent for nested reply', () async { 309 when( 310 mockDio.post<Map<String, dynamic>>( 311 '/xrpc/social.coves.community.comment.create', 312 data: anyNamed('data'), 313 ), 314 ).thenAnswer( 315 (_) async => Response( 316 requestOptions: RequestOptions(path: ''), 317 statusCode: 200, 318 data: { 319 'uri': 'at://did:plc:test/social.coves.community.comment/reply1', 320 'cid': 'bafyReply', 321 }, 322 ), 323 ); 324 325 await commentService.createComment( 326 rootUri: 'at://did:plc:author/social.coves.post.record/post123', 327 rootCid: 'postCid', 328 parentUri: 329 'at://did:plc:commenter/social.coves.community.comment/comment1', 330 parentCid: 'commentCid', 331 content: 'This is a nested reply', 332 ); 333 334 verify( 335 mockDio.post<Map<String, dynamic>>( 336 '/xrpc/social.coves.community.comment.create', 337 data: { 338 'reply': { 339 'root': { 340 'uri': 'at://did:plc:author/social.coves.post.record/post123', 341 'cid': 'postCid', 342 }, 343 'parent': { 344 'uri': 345 'at://did:plc:commenter/social.coves.community.comment/' 346 'comment1', 347 'cid': 'commentCid', 348 }, 349 }, 350 'content': 'This is a nested reply', 351 }, 352 ), 353 ).called(1); 354 }); 355 }); 356 }); 357}