at main 13 kB view raw
1import 'package:coves_flutter/models/community.dart'; 2import 'package:coves_flutter/services/api_exceptions.dart'; 3import 'package:coves_flutter/services/coves_api_service.dart'; 4import 'package:dio/dio.dart'; 5import 'package:flutter_test/flutter_test.dart'; 6import 'package:http_mock_adapter/http_mock_adapter.dart'; 7 8void main() { 9 TestWidgetsFlutterBinding.ensureInitialized(); 10 11 group('CovesApiService - listCommunities', () { 12 late Dio dio; 13 late DioAdapter dioAdapter; 14 late CovesApiService apiService; 15 16 setUp(() { 17 dio = Dio(BaseOptions(baseUrl: 'https://api.test.coves.social')); 18 dioAdapter = DioAdapter(dio: dio); 19 apiService = CovesApiService( 20 dio: dio, 21 tokenGetter: () async => 'test-token', 22 ); 23 }); 24 25 tearDown(() { 26 apiService.dispose(); 27 }); 28 29 test('should successfully fetch communities', () async { 30 final mockResponse = { 31 'communities': [ 32 { 33 'did': 'did:plc:community1', 34 'name': 'test-community-1', 35 'displayName': 'Test Community 1', 36 'subscriberCount': 100, 37 'memberCount': 50, 38 }, 39 { 40 'did': 'did:plc:community2', 41 'name': 'test-community-2', 42 'displayName': 'Test Community 2', 43 'subscriberCount': 200, 44 'memberCount': 100, 45 }, 46 ], 47 'cursor': 'next-cursor', 48 }; 49 50 dioAdapter.onGet( 51 '/xrpc/social.coves.community.list', 52 (server) => server.reply(200, mockResponse), 53 queryParameters: { 54 'limit': 50, 55 'sort': 'popular', 56 }, 57 ); 58 59 final response = await apiService.listCommunities(); 60 61 expect(response, isA<CommunitiesResponse>()); 62 expect(response.communities.length, 2); 63 expect(response.cursor, 'next-cursor'); 64 expect(response.communities[0].did, 'did:plc:community1'); 65 expect(response.communities[0].name, 'test-community-1'); 66 expect(response.communities[1].did, 'did:plc:community2'); 67 }); 68 69 test('should handle empty communities response', () async { 70 final mockResponse = { 71 'communities': [], 72 'cursor': null, 73 }; 74 75 dioAdapter.onGet( 76 '/xrpc/social.coves.community.list', 77 (server) => server.reply(200, mockResponse), 78 queryParameters: { 79 'limit': 50, 80 'sort': 'popular', 81 }, 82 ); 83 84 final response = await apiService.listCommunities(); 85 86 expect(response.communities, isEmpty); 87 expect(response.cursor, null); 88 }); 89 90 test('should handle null communities array', () async { 91 final mockResponse = { 92 'communities': null, 93 'cursor': null, 94 }; 95 96 dioAdapter.onGet( 97 '/xrpc/social.coves.community.list', 98 (server) => server.reply(200, mockResponse), 99 queryParameters: { 100 'limit': 50, 101 'sort': 'popular', 102 }, 103 ); 104 105 final response = await apiService.listCommunities(); 106 107 expect(response.communities, isEmpty); 108 }); 109 110 test('should fetch communities with custom limit', () async { 111 final mockResponse = { 112 'communities': [], 113 'cursor': null, 114 }; 115 116 dioAdapter.onGet( 117 '/xrpc/social.coves.community.list', 118 (server) => server.reply(200, mockResponse), 119 queryParameters: { 120 'limit': 25, 121 'sort': 'popular', 122 }, 123 ); 124 125 final response = await apiService.listCommunities(limit: 25); 126 127 expect(response, isA<CommunitiesResponse>()); 128 }); 129 130 test('should fetch communities with cursor for pagination', () async { 131 const cursor = 'pagination-cursor-123'; 132 133 final mockResponse = { 134 'communities': [ 135 { 136 'did': 'did:plc:community3', 137 'name': 'paginated-community', 138 }, 139 ], 140 'cursor': 'next-cursor-456', 141 }; 142 143 dioAdapter.onGet( 144 '/xrpc/social.coves.community.list', 145 (server) => server.reply(200, mockResponse), 146 queryParameters: { 147 'limit': 50, 148 'sort': 'popular', 149 'cursor': cursor, 150 }, 151 ); 152 153 final response = await apiService.listCommunities(cursor: cursor); 154 155 expect(response.communities.length, 1); 156 expect(response.cursor, 'next-cursor-456'); 157 }); 158 159 test('should fetch communities with custom sort', () async { 160 final mockResponse = { 161 'communities': [], 162 'cursor': null, 163 }; 164 165 dioAdapter.onGet( 166 '/xrpc/social.coves.community.list', 167 (server) => server.reply(200, mockResponse), 168 queryParameters: { 169 'limit': 50, 170 'sort': 'new', 171 }, 172 ); 173 174 final response = await apiService.listCommunities(sort: 'new'); 175 176 expect(response, isA<CommunitiesResponse>()); 177 }); 178 179 test('should handle 401 unauthorized error', () async { 180 dioAdapter.onGet( 181 '/xrpc/social.coves.community.list', 182 (server) => server.reply(401, { 183 'error': 'Unauthorized', 184 'message': 'Invalid token', 185 }), 186 queryParameters: { 187 'limit': 50, 188 'sort': 'popular', 189 }, 190 ); 191 192 expect( 193 () => apiService.listCommunities(), 194 throwsA(isA<AuthenticationException>()), 195 ); 196 }); 197 198 test('should handle 500 server error', () async { 199 dioAdapter.onGet( 200 '/xrpc/social.coves.community.list', 201 (server) => server.reply(500, { 202 'error': 'InternalServerError', 203 'message': 'Database error', 204 }), 205 queryParameters: { 206 'limit': 50, 207 'sort': 'popular', 208 }, 209 ); 210 211 expect( 212 () => apiService.listCommunities(), 213 throwsA(isA<ServerException>()), 214 ); 215 }); 216 217 test('should handle network timeout', () async { 218 dioAdapter.onGet( 219 '/xrpc/social.coves.community.list', 220 (server) => server.throws( 221 408, 222 DioException.connectionTimeout( 223 timeout: const Duration(seconds: 30), 224 requestOptions: RequestOptions(), 225 ), 226 ), 227 queryParameters: { 228 'limit': 50, 229 'sort': 'popular', 230 }, 231 ); 232 233 expect( 234 () => apiService.listCommunities(), 235 throwsA(isA<NetworkException>()), 236 ); 237 }); 238 }); 239 240 group('CovesApiService - createPost', () { 241 late Dio dio; 242 late DioAdapter dioAdapter; 243 late CovesApiService apiService; 244 245 setUp(() { 246 dio = Dio(BaseOptions(baseUrl: 'https://api.test.coves.social')); 247 dioAdapter = DioAdapter(dio: dio); 248 apiService = CovesApiService( 249 dio: dio, 250 tokenGetter: () async => 'test-token', 251 ); 252 }); 253 254 tearDown(() { 255 apiService.dispose(); 256 }); 257 258 test('should successfully create a post with all fields', () async { 259 final mockResponse = { 260 'uri': 'at://did:plc:user/social.coves.community.post/123', 261 'cid': 'bafyreicid123', 262 }; 263 264 dioAdapter.onPost( 265 '/xrpc/social.coves.community.post.create', 266 (server) => server.reply(200, mockResponse), 267 data: { 268 'community': 'did:plc:community1', 269 'title': 'Test Post Title', 270 'content': 'Test post content', 271 'embed': { 272 'uri': 'https://example.com/article', 273 'title': 'Article Title', 274 }, 275 'langs': ['en'], 276 'labels': { 277 'values': [ 278 {'val': 'nsfw'}, 279 ], 280 }, 281 }, 282 ); 283 284 final response = await apiService.createPost( 285 community: 'did:plc:community1', 286 title: 'Test Post Title', 287 content: 'Test post content', 288 embed: const ExternalEmbedInput( 289 uri: 'https://example.com/article', 290 title: 'Article Title', 291 ), 292 langs: ['en'], 293 labels: const SelfLabels(values: [SelfLabel(val: 'nsfw')]), 294 ); 295 296 expect(response, isA<CreatePostResponse>()); 297 expect(response.uri, 'at://did:plc:user/social.coves.community.post/123'); 298 expect(response.cid, 'bafyreicid123'); 299 }); 300 301 test('should successfully create a minimal post', () async { 302 final mockResponse = { 303 'uri': 'at://did:plc:user/social.coves.community.post/456', 304 'cid': 'bafyreicid456', 305 }; 306 307 dioAdapter.onPost( 308 '/xrpc/social.coves.community.post.create', 309 (server) => server.reply(200, mockResponse), 310 data: { 311 'community': 'did:plc:community1', 312 'title': 'Just a title', 313 }, 314 ); 315 316 final response = await apiService.createPost( 317 community: 'did:plc:community1', 318 title: 'Just a title', 319 ); 320 321 expect(response, isA<CreatePostResponse>()); 322 expect(response.uri, 'at://did:plc:user/social.coves.community.post/456'); 323 }); 324 325 test('should successfully create a link post', () async { 326 final mockResponse = { 327 'uri': 'at://did:plc:user/social.coves.community.post/789', 328 'cid': 'bafyreicid789', 329 }; 330 331 dioAdapter.onPost( 332 '/xrpc/social.coves.community.post.create', 333 (server) => server.reply(200, mockResponse), 334 data: { 335 'community': 'did:plc:community1', 336 'embed': { 337 'uri': 'https://example.com/article', 338 }, 339 }, 340 ); 341 342 final response = await apiService.createPost( 343 community: 'did:plc:community1', 344 embed: const ExternalEmbedInput(uri: 'https://example.com/article'), 345 ); 346 347 expect(response, isA<CreatePostResponse>()); 348 }); 349 350 test('should handle 401 unauthorized error', () async { 351 dioAdapter.onPost( 352 '/xrpc/social.coves.community.post.create', 353 (server) => server.reply(401, { 354 'error': 'Unauthorized', 355 'message': 'Authentication required', 356 }), 357 data: { 358 'community': 'did:plc:community1', 359 'title': 'Test', 360 }, 361 ); 362 363 expect( 364 () => apiService.createPost( 365 community: 'did:plc:community1', 366 title: 'Test', 367 ), 368 throwsA(isA<AuthenticationException>()), 369 ); 370 }); 371 372 test('should handle 404 community not found', () async { 373 dioAdapter.onPost( 374 '/xrpc/social.coves.community.post.create', 375 (server) => server.reply(404, { 376 'error': 'NotFound', 377 'message': 'Community not found', 378 }), 379 data: { 380 'community': 'did:plc:nonexistent', 381 'title': 'Test', 382 }, 383 ); 384 385 expect( 386 () => apiService.createPost( 387 community: 'did:plc:nonexistent', 388 title: 'Test', 389 ), 390 throwsA(isA<NotFoundException>()), 391 ); 392 }); 393 394 test('should handle 400 validation error', () async { 395 dioAdapter.onPost( 396 '/xrpc/social.coves.community.post.create', 397 (server) => server.reply(400, { 398 'error': 'ValidationError', 399 'message': 'Title exceeds maximum length', 400 }), 401 data: { 402 'community': 'did:plc:community1', 403 'title': 'a' * 1000, // Very long title 404 }, 405 ); 406 407 expect( 408 () => apiService.createPost( 409 community: 'did:plc:community1', 410 title: 'a' * 1000, 411 ), 412 throwsA(isA<ApiException>()), 413 ); 414 }); 415 416 test('should handle 500 server error', () async { 417 dioAdapter.onPost( 418 '/xrpc/social.coves.community.post.create', 419 (server) => server.reply(500, { 420 'error': 'InternalServerError', 421 'message': 'Database error', 422 }), 423 data: { 424 'community': 'did:plc:community1', 425 'title': 'Test', 426 }, 427 ); 428 429 expect( 430 () => apiService.createPost( 431 community: 'did:plc:community1', 432 title: 'Test', 433 ), 434 throwsA(isA<ServerException>()), 435 ); 436 }); 437 438 test('should handle network timeout', () async { 439 dioAdapter.onPost( 440 '/xrpc/social.coves.community.post.create', 441 (server) => server.throws( 442 408, 443 DioException.connectionTimeout( 444 timeout: const Duration(seconds: 30), 445 requestOptions: RequestOptions(), 446 ), 447 ), 448 data: { 449 'community': 'did:plc:community1', 450 'title': 'Test', 451 }, 452 ); 453 454 expect( 455 () => apiService.createPost( 456 community: 'did:plc:community1', 457 title: 'Test', 458 ), 459 throwsA(isA<NetworkException>()), 460 ); 461 }); 462 }); 463}