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/inventory_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 InventoryRepository repo; setUp(() { dio = Dio(BaseOptions(baseUrl: _baseUrl)); adapter = DioAdapter(dio: dio, matcher: const FullHttpRequestMatcher()); repo = InventoryRepository(_TestApiClient(dio)); }); // --------------------------------------------------------------------------- // listInventory() // --------------------------------------------------------------------------- group('InventoryRepository.listInventory()', () { test('returns PageResult with inventory items on 200', () async { adapter.onGet( '/inventory', (server) => server.reply(200, { 'data': [ { 'id': 1, 'warehouse_id': 1, 'warehouse_name': '主仓库', 'product_id': 1, 'product_name': '五粮液', 'product_code': 'P001', 'spec': '500ml', 'unit': '瓶', 'brand': '五粮液集团', 'min_stock': 10, 'quantity': 50.0, } ], 'total': 1, 'page': 1, 'page_size': 50, }), queryParameters: {'page': 1, 'page_size': 50}, ); final result = await repo.listInventory(); expect(result.total, 1); expect(result.data.length, 1); expect(result.data.first.productName, '五粮液'); expect(result.data.first.quantity, 50.0); expect(result.data.first.warehouseId, 1); }); test('returns empty list without error', () async { adapter.onGet( '/inventory', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 50, }), queryParameters: {'page': 1, 'page_size': 50}, ); final result = await repo.listInventory(); expect(result.data, isEmpty); expect(result.total, 0); }); test('passes warehouseId and keyword params', () async { adapter.onGet( '/inventory', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 50, }), queryParameters: { 'page': 1, 'page_size': 50, 'warehouse_id': 1, 'keyword': '五粮液', }, ); final result = await repo.listInventory(warehouseId: 1, keyword: '五粮液'); expect(result.data, isEmpty); }); test('401 throws AppException', () async { adapter.onGet( '/inventory', (server) => server.reply(401, {'error': 'unauthorized'}), queryParameters: {'page': 1, 'page_size': 50}, ); expect( () => repo.listInventory(), throwsA(predicate((e) => e.statusCode == 401)), ); }); test('400 throws AppException with server error message', () async { adapter.onGet( '/inventory', (server) => server.reply(400, {'error': 'invalid warehouse_id'}), queryParameters: {'page': 1, 'page_size': 50}, ); expect( () => repo.listInventory(), throwsA( predicate( (e) => e.message == 'invalid warehouse_id' && e.statusCode == 400, ), ), ); }); test('network timeout throws AppException', () async { final badDio = Dio(BaseOptions( baseUrl: 'http://localhost:19999', connectTimeout: const Duration(milliseconds: 100), )); final badRepo = InventoryRepository(_TestApiClient(badDio)); try { await badRepo.listInventory(); fail('Expected AppException'); } on AppException catch (e) { expect(e.message, isNotEmpty); } }); }); // --------------------------------------------------------------------------- // listLogs() // --------------------------------------------------------------------------- group('InventoryRepository.listLogs()', () { test('returns PageResult with inventory logs on 200', () async { adapter.onGet( '/inventory/logs', (server) => server.reply(200, { 'data': [ { 'warehouse_id': 1, 'product_id': 1, 'warehouse': {'name': '主仓库'}, 'product': {'name': '五粮液'}, 'direction': 'in', 'quantity': 20.0, 'qty_before': 30.0, 'qty_after': 50.0, 'ref_type': 'stock_in', 'ref_id': 5, 'created_at': '2024-01-10T10:00:00Z', } ], 'total': 1, 'page': 1, 'page_size': 50, }), queryParameters: {'page': 1, 'page_size': 50}, ); final result = await repo.listLogs(); expect(result.total, 1); expect(result.data.length, 1); expect(result.data.first.direction, 'in'); expect(result.data.first.quantity, 20.0); expect(result.data.first.qtyBefore, 30.0); expect(result.data.first.qtyAfter, 50.0); }); test('returns empty list without error', () async { adapter.onGet( '/inventory/logs', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 50, }), queryParameters: {'page': 1, 'page_size': 50}, ); final result = await repo.listLogs(); expect(result.data, isEmpty); }); test('passes warehouseId and productId params', () async { adapter.onGet( '/inventory/logs', (server) => server.reply(200, { 'data': [], 'total': 0, 'page': 1, 'page_size': 50, }), queryParameters: { 'page': 1, 'page_size': 50, 'warehouse_id': 1, 'product_id': 1, }, ); final result = await repo.listLogs(warehouseId: 1, productId: 1); expect(result.data, isEmpty); }); test('401 throws AppException', () async { adapter.onGet( '/inventory/logs', (server) => server.reply(401, {'error': 'unauthorized'}), queryParameters: {'page': 1, 'page_size': 50}, ); expect( () => repo.listLogs(), throwsA(predicate((e) => e.statusCode == 401)), ); }); test('400 throws AppException with server error message', () async { adapter.onGet( '/inventory/logs', (server) => server.reply(400, {'error': 'invalid product_id'}), queryParameters: {'page': 1, 'page_size': 50}, ); expect( () => repo.listLogs(), throwsA( predicate( (e) => e.message == 'invalid product_id' && e.statusCode == 400, ), ), ); }); }); }