at main 11 kB view raw
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( 182 'should throw ApiException on invalid response (null data)', 183 () async { 184 when( 185 mockDio.post<Map<String, dynamic>>( 186 '/xrpc/social.coves.community.comment.create', 187 data: anyNamed('data'), 188 ), 189 ).thenAnswer( 190 (_) async => Response( 191 requestOptions: RequestOptions(path: ''), 192 statusCode: 200, 193 data: null, 194 ), 195 ); 196 197 expect( 198 () => commentService.createComment( 199 rootUri: 'at://did:plc:author/post/123', 200 rootCid: 'rootCid', 201 parentUri: 'at://did:plc:author/post/123', 202 parentCid: 'parentCid', 203 content: 'Test comment', 204 ), 205 throwsA( 206 isA<ApiException>().having( 207 (e) => e.message, 208 'message', 209 contains('no data'), 210 ), 211 ), 212 ); 213 }, 214 ); 215 216 test( 217 'should throw ApiException on invalid response (missing uri)', 218 () async { 219 when( 220 mockDio.post<Map<String, dynamic>>( 221 '/xrpc/social.coves.community.comment.create', 222 data: anyNamed('data'), 223 ), 224 ).thenAnswer( 225 (_) async => Response( 226 requestOptions: RequestOptions(path: ''), 227 statusCode: 200, 228 data: {'cid': 'bafy123'}, 229 ), 230 ); 231 232 expect( 233 () => commentService.createComment( 234 rootUri: 'at://did:plc:author/post/123', 235 rootCid: 'rootCid', 236 parentUri: 'at://did:plc:author/post/123', 237 parentCid: 'parentCid', 238 content: 'Test comment', 239 ), 240 throwsA( 241 isA<ApiException>().having( 242 (e) => e.message, 243 'message', 244 contains('missing uri'), 245 ), 246 ), 247 ); 248 }, 249 ); 250 251 test( 252 'should throw ApiException on invalid response (empty uri)', 253 () async { 254 when( 255 mockDio.post<Map<String, dynamic>>( 256 '/xrpc/social.coves.community.comment.create', 257 data: anyNamed('data'), 258 ), 259 ).thenAnswer( 260 (_) async => Response( 261 requestOptions: RequestOptions(path: ''), 262 statusCode: 200, 263 data: {'uri': '', 'cid': 'bafy123'}, 264 ), 265 ); 266 267 expect( 268 () => commentService.createComment( 269 rootUri: 'at://did:plc:author/post/123', 270 rootCid: 'rootCid', 271 parentUri: 'at://did:plc:author/post/123', 272 parentCid: 'parentCid', 273 content: 'Test comment', 274 ), 275 throwsA( 276 isA<ApiException>().having( 277 (e) => e.message, 278 'message', 279 contains('missing uri'), 280 ), 281 ), 282 ); 283 }, 284 ); 285 286 test('should throw ApiException on server error', () async { 287 when( 288 mockDio.post<Map<String, dynamic>>( 289 '/xrpc/social.coves.community.comment.create', 290 data: anyNamed('data'), 291 ), 292 ).thenThrow( 293 DioException( 294 requestOptions: RequestOptions(path: ''), 295 type: DioExceptionType.badResponse, 296 response: Response( 297 requestOptions: RequestOptions(path: ''), 298 statusCode: 500, 299 data: {'error': 'Internal server error'}, 300 ), 301 message: 'Internal server error', 302 ), 303 ); 304 305 expect( 306 () => commentService.createComment( 307 rootUri: 'at://did:plc:author/post/123', 308 rootCid: 'rootCid', 309 parentUri: 'at://did:plc:author/post/123', 310 parentCid: 'parentCid', 311 content: 'Test comment', 312 ), 313 throwsA(isA<ApiException>()), 314 ); 315 }); 316 317 test('should send correct parent for nested reply', () async { 318 when( 319 mockDio.post<Map<String, dynamic>>( 320 '/xrpc/social.coves.community.comment.create', 321 data: anyNamed('data'), 322 ), 323 ).thenAnswer( 324 (_) async => Response( 325 requestOptions: RequestOptions(path: ''), 326 statusCode: 200, 327 data: { 328 'uri': 'at://did:plc:test/social.coves.community.comment/reply1', 329 'cid': 'bafyReply', 330 }, 331 ), 332 ); 333 334 await commentService.createComment( 335 rootUri: 'at://did:plc:author/social.coves.post.record/post123', 336 rootCid: 'postCid', 337 parentUri: 338 'at://did:plc:commenter/social.coves.community.comment/comment1', 339 parentCid: 'commentCid', 340 content: 'This is a nested reply', 341 ); 342 343 verify( 344 mockDio.post<Map<String, dynamic>>( 345 '/xrpc/social.coves.community.comment.create', 346 data: { 347 'reply': { 348 'root': { 349 'uri': 'at://did:plc:author/social.coves.post.record/post123', 350 'cid': 'postCid', 351 }, 352 'parent': { 353 'uri': 354 'at://did:plc:commenter/social.coves.community.comment/' 355 'comment1', 356 'cid': 'commentCid', 357 }, 358 }, 359 'content': 'This is a nested reply', 360 }, 361 ), 362 ).called(1); 363 }); 364 }); 365 }); 366}