// Author: fruit.com.cn import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/config/app_config.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import '../support/fake_http_adapter.dart'; /// 网络层换 dio 之后,把「对外契约」逐条钉住:解包、错误码映射、请求头、查询参数、跳过重试。 void main() { String envelope(Object? data, {int code = 0, String message = 'ok'}) { return jsonEncode({'code': code, 'message': message, 'data': data}); } test('should_return_data_field_when_envelope_code_is_zero', () async { final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope({'id': 7})), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, ); final data = await client.get('/app/dramas/7'); expect((data as Map)['id'], 7); }); test( 'should_throw_api_exception_with_server_message_when_envelope_code_is_not_zero', () async { final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope(null, code: 40001, message: '邮箱格式不正确')), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, ); await expectLater( client.post('/app/auth/register', body: {'target': 'bad'}), throwsA( isA() .having((e) => e.code, 'code', 40001) .having((e) => e.message, 'message', '邮箱格式不正确'), ), ); }, ); test( 'should_map_to_api_exception_with_http_status_when_body_has_no_envelope', () async { final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse('oops', 502), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, ); await expectLater( client.get('/app/home/feed'), throwsA(isA().having((e) => e.code, 'code', 502)), ); }, ); test( 'should_send_bearer_token_and_device_header_when_both_are_available', () async { final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope(null)), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, accessTokenReader: () => 'token-1', deviceIdReader: () async => 'device-9', ); await client.get('/app/me'); final request = adapter.requests.single; expect(request.headers['Authorization'], 'Bearer token-1'); expect(request.headers['X-Device-Id'], 'device-9'); expect(request.headers['Accept'], 'application/json'); }, ); test('should_put_query_map_into_request_uri_when_query_is_given', () async { final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope(null)), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, ); await client.get('/app/search', query: {'keyword': '短剧', 'page': '2'}); final uri = adapter.requests.single.uri; expect(uri.path, '/app/search'); expect(uri.queryParameters['keyword'], '短剧'); expect(uri.queryParameters['page'], '2'); }); test('should_skip_refresh_when_call_opts_out_of_auth_retry', () async { var refreshed = false; final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope(null, code: 401, message: '登录已过期')), ); final client = ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, tokenRefresher: () async { refreshed = true; return true; }, ); await expectLater( client.post( '/app/auth/refresh', body: {'refreshToken': 'r1'}, skipAuthRetry: true, ), throwsA(isA().having((e) => e.code, 'code', 401)), ); expect(refreshed, isFalse); expect(adapter.requests, hasLength(1)); }); test( 'should_follow_runtime_endpoint_applied_after_client_creation', () async { const runtimeBaseUrl = 'http://runtime.test:8081'; final adapter = FakeHttpAdapter( (_) async => FakeHttpResponse(envelope(null)), ); final client = ApiClient(adapter: adapter); AppConfig.applyRuntimeBaseUrl(runtimeBaseUrl); addTearDown(AppConfig.clearRuntimeBaseUrl); await client.get('/app/home/feed'); expect(adapter.requests.single.uri.origin, runtimeBaseUrl); }, ); }