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/stock_out_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); } Map _orderJson({ int id = 1, String orderNo = 'SO2024010001', String status = 'draft', }) => { 'id': id, 'order_no': orderNo, 'type': 'sale', 'warehouse_id': 1, 'warehouse_name': '主仓库', 'partner_id': 2, 'partner_name': '王记饭店', 'operator_id': null, 'status': status, 'order_date': '2024-01-05', 'total_amount': 2000.0, 'remark': null, 'items': [], }; void main() { late Dio dio; late DioAdapter adapter; late StockOutRepository repo; setUp(() { dio = Dio(BaseOptions(baseUrl: _baseUrl)); adapter = DioAdapter(dio: dio, matcher: const FullHttpRequestMatcher()); repo = StockOutRepository(_TestApiClient(dio)); }); // --------------------------------------------------------------------------- // list() // --------------------------------------------------------------------------- group('StockOutRepository.list()', () { test('returns PageResult with orders on 200', () async { adapter.onGet( '/stock-out/orders', (server) => server.reply(200, { 'data': [_orderJson()], 'total': 1, 'page': 1, 'page_size': 20, }), queryParameters: {'page': 1, 'page_size': 20}, ); final result = await repo.list(); expect(result.total, 1); expect(result.data.length, 1); expect(result.data.first.orderNo, 'SO2024010001'); expect(result.data.first.status, 'draft'); }); test('returns empty list without error', () async { adapter.onGet( '/stock-out/orders', (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); }); test('passes status and date params when provided', () async { adapter.onGet( '/stock-out/orders', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 20, }), queryParameters: { 'page': 1, 'page_size': 20, 'status': 'approved', 'start_date': '2024-01-01', 'end_date': '2024-01-31', }, ); final result = await repo.list( status: 'approved', startDate: '2024-01-01', endDate: '2024-01-31', ); expect(result.data, isEmpty); }); test('401 throws AppException', () async { adapter.onGet( '/stock-out/orders', (server) => server.reply(401, {'error': 'unauthorized'}), queryParameters: {'page': 1, 'page_size': 20}, ); expect( () => repo.list(), throwsA(predicate((e) => e.statusCode == 401)), ); }); test('400 throws AppException with error message', () async { adapter.onGet( '/stock-out/orders', (server) => server.reply(400, {'error': 'invalid filter'}), queryParameters: {'page': 1, 'page_size': 20}, ); expect( () => repo.list(), throwsA( predicate( (e) => e.message == 'invalid filter' && e.statusCode == 400, ), ), ); }); test('network timeout throws AppException', () async { final badDio = Dio(BaseOptions( baseUrl: 'http://localhost:19999', connectTimeout: const Duration(milliseconds: 100), )); final badRepo = StockOutRepository(_TestApiClient(badDio)); try { await badRepo.list(); fail('Expected AppException'); } on AppException catch (e) { expect(e.message, isNotEmpty); } }); }); // --------------------------------------------------------------------------- // get() // --------------------------------------------------------------------------- group('StockOutRepository.get()', () { test('returns order detail on 200', () async { adapter.onGet( '/stock-out/orders/1', (server) => server.reply(200, {'data': _orderJson(id: 1)}), ); final order = await repo.get(1); expect(order.id, 1); expect(order.orderNo, 'SO2024010001'); }); test('404 throws AppException', () async { adapter.onGet( '/stock-out/orders/999', (server) => server.reply(404, {'error': 'order not found'}), ); expect( () => repo.get(999), throwsA(predicate((e) => e.statusCode == 404)), ); }); }); // --------------------------------------------------------------------------- // create() // --------------------------------------------------------------------------- group('StockOutRepository.create()', () { test('returns created order on 201', () async { final payload = { 'warehouse_id': 1, 'partner_id': 2, 'items': [ {'product_id': 1, 'quantity': 5.0, 'unit_price': 200.0, 'total_price': 1000.0} ] }; adapter.onPost( '/stock-out/orders', (server) => server.reply(201, {'data': _orderJson(id: 10)}), data: payload, ); final order = await repo.create(payload); expect(order.id, 10); }); test('400 response throws AppException', () async { adapter.onPost( '/stock-out/orders', (server) => server.reply(400, {'error': 'insufficient stock'}), data: {'warehouse_id': 1, 'items': []}, ); expect( () => repo.create({'warehouse_id': 1, 'items': []}), throwsA(isA()), ); }); }); // --------------------------------------------------------------------------- // submit() / approve() / reject() // --------------------------------------------------------------------------- group('StockOutRepository workflow actions', () { test('submit completes without error on 200', () async { adapter.onPut( '/stock-out/orders/1/submit', (server) => server.reply(200, {'message': 'submitted'}), ); await expectLater(repo.submit(1), completes); }); test('submit 400 throws AppException', () async { adapter.onPut( '/stock-out/orders/1/submit', (server) => server.reply(400, {'error': 'already submitted'}), ); expect( () => repo.submit(1), throwsA( predicate( (e) => e.message == 'already submitted' && e.statusCode == 400, ), ), ); }); test('approve completes without error on 200', () async { adapter.onPut( '/stock-out/orders/1/approve', (server) => server.reply(200, {'message': 'approved'}), ); await expectLater(repo.approve(1), completes); }); test('approve 400 (insufficient stock) throws AppException', () async { adapter.onPut( '/stock-out/orders/1/approve', (server) => server.reply(400, {'error': 'insufficient stock for product 茅台'}), ); expect( () => repo.approve(1), throwsA( predicate( (e) => e.message.contains('insufficient stock') && e.statusCode == 400, ), ), ); }); test('reject completes without error on 200', () async { adapter.onPut( '/stock-out/orders/1/reject', (server) => server.reply(200, {'message': 'rejected'}), ); await expectLater(repo.reject(1), completes); }); test('reject 400 throws AppException', () async { adapter.onPut( '/stock-out/orders/1/reject', (server) => server.reply(400, {'error': 'cannot reject approved order'}), ); expect( () => repo.reject(1), throwsA(isA()), ); }); }); }