import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; import 'package:jiu_client/core/api/api_client.dart'; import 'package:jiu_client/core/exceptions.dart'; import 'package:jiu_client/repositories/partner_repository.dart'; const _baseUrl = 'http://localhost:8080/api/v1'; class _TestApiClient extends ApiClient { final Dio _testDio; _TestApiClient(this._testDio) : super(token: 'test-token'); @override Future get(String path, {Map? params}) => _testDio.get(path, queryParameters: params); @override Future post(String path, {dynamic data}) => _testDio.post(path, data: data); @override Future put(String path, {dynamic data}) => _testDio.put(path, data: data); @override Future delete(String path) => _testDio.delete(path); } void main() { late Dio dio; late DioAdapter adapter; late PartnerRepository repo; setUp(() { dio = Dio(BaseOptions(baseUrl: _baseUrl)); adapter = DioAdapter(dio: dio, matcher: const FullHttpRequestMatcher()); repo = PartnerRepository(_TestApiClient(dio)); }); // --------------------------------------------------------------------------- // list() // --------------------------------------------------------------------------- group('PartnerRepository.list()', () { test('returns PageResult with partners on 200', () async { adapter.onGet( '/partners', (server) => server.reply(200, { 'data': [ { 'id': 1, 'code': 'S001', 'name': '张家酒水供应商', 'type': 'supplier', 'contact': '张三', 'phone': '13800138000', 'address': '北京市朝阳区', 'bank_account': null, 'remark': null, } ], 'total': 1, 'page': 1, 'page_size': 20, }), queryParameters: {'page': 1, 'page_size': 20, 'type': 'supplier'}, ); final result = await repo.list(type: 'supplier'); expect(result.total, 1); expect(result.data.length, 1); expect(result.data.first.name, '张家酒水供应商'); expect(result.data.first.type, 'supplier'); }); test('returns empty list without error', () async { adapter.onGet( '/partners', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 20, }), queryParameters: {'page': 1, 'page_size': 20}, ); final result = await repo.list(); expect(result.data, isEmpty); expect(result.total, 0); }); test('passes keyword query param', () async { adapter.onGet( '/partners', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 20, }), queryParameters: { 'page': 1, 'page_size': 20, 'keyword': '茅台', }, ); final result = await repo.list(keyword: '茅台'); expect(result.data, isEmpty); }); test('400 response throws AppException with server error message', () async { adapter.onGet( '/partners', (server) => server.reply(400, {'error': 'invalid type'}), queryParameters: {'page': 1, 'page_size': 20}, ); expect( () => repo.list(), throwsA( predicate( (e) => e.message == 'invalid type' && e.statusCode == 400, ), ), ); }); test('401 response throws AppException with status 401', () async { adapter.onGet( '/partners', (server) => server.reply(401, {'error': 'unauthorized'}), queryParameters: {'page': 1, 'page_size': 20}, ); expect( () => repo.list(), throwsA( predicate((e) => e.statusCode == 401), ), ); }); test('network timeout throws AppException with fallback message', () async { final badDio = Dio(BaseOptions( baseUrl: 'http://localhost:19999', connectTimeout: const Duration(milliseconds: 100), )); final badRepo = PartnerRepository(_TestApiClient(badDio)); try { await badRepo.list(); fail('Expected AppException'); } on AppException catch (e) { expect(e.message, isNotEmpty); } }); }); // --------------------------------------------------------------------------- // create() // --------------------------------------------------------------------------- group('PartnerRepository.create()', () { test('returns created Partner on 201', () async { final payload = {'name': '李记酒行', 'type': 'supplier'}; adapter.onPost( '/partners', (server) => server.reply(201, { 'data': { 'id': 2, 'code': null, 'name': '李记酒行', 'type': 'supplier', 'contact': null, 'phone': null, 'address': null, 'bank_account': null, 'remark': null, } }), data: payload, ); final partner = await repo.create(payload); expect(partner.id, 2); expect(partner.name, '李记酒行'); expect(partner.type, 'supplier'); }); test('400 response throws AppException', () async { adapter.onPost( '/partners', (server) => server.reply(400, {'error': 'name is required'}), data: {'name': '', 'type': 'supplier'}, ); expect( () => repo.create({'name': '', 'type': 'supplier'}), throwsA(isA()), ); }); }); // --------------------------------------------------------------------------- // update() // --------------------------------------------------------------------------- group('PartnerRepository.update()', () { test('returns updated Partner on 200', () async { final payload = {'name': '张家酒水(新)', 'type': 'supplier'}; adapter.onPut( '/partners/1', (server) => server.reply(200, { 'data': { 'id': 1, 'code': 'S001', 'name': '张家酒水(新)', 'type': 'supplier', 'contact': null, 'phone': null, 'address': null, 'bank_account': null, 'remark': null, } }), data: payload, ); final partner = await repo.update(1, payload); expect(partner.id, 1); expect(partner.name, '张家酒水(新)'); }); test('404 throws AppException', () async { adapter.onPut( '/partners/999', (server) => server.reply(404, {'error': 'partner not found'}), data: {'name': 'X', 'type': 'supplier'}, ); expect( () => repo.update(999, {'name': 'X', 'type': 'supplier'}), throwsA( predicate((e) => e.statusCode == 404), ), ); }); }); // --------------------------------------------------------------------------- // delete() // --------------------------------------------------------------------------- group('PartnerRepository.delete()', () { test('completes without error on 200', () async { adapter.onDelete( '/partners/1', (server) => server.reply(200, {'message': 'deleted'}), ); await expectLater(repo.delete(1), completes); }); test('404 response throws AppException', () async { adapter.onDelete( '/partners/999', (server) => server.reply(404, {'error': 'not found'}), ); expect( () => repo.delete(999), throwsA(isA()), ); }); }); }