refactor: extract API actions into BoxingApiActions with error handling
Centralize all API call logic (fetchBoxInfo, saveBoxRecord, updateBoxRecord, deleteBoxRecord) into BoxingApiActions class with unified error categorization (network, duplicate, missingApiUrl). Add helper methods to reduce repetitive error handling in boxing_page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
159
test/pages/boxing/boxing_api_actions_test.dart
Normal file
159
test/pages/boxing/boxing_api_actions_test.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:pad_scanner/pages/boxing/boxing_api_actions.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
|
||||
void main() {
|
||||
group('BoxingApiActions', () {
|
||||
test('returns missingApiUrl when API URL is empty', () async {
|
||||
final actions = BoxingApiActions(
|
||||
apiService: ApiService(
|
||||
client: _MockClient((_) async {
|
||||
fail('ApiService should not be called without a base URL');
|
||||
}),
|
||||
),
|
||||
loadApiUrl: () async => '',
|
||||
);
|
||||
|
||||
final result = await actions.fetchBoxInfo('26B1');
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.errorKind, BoxingActionErrorKind.missingApiUrl);
|
||||
expect(result.errorMessage, BoxingApiActions.missingApiUrlMessage);
|
||||
});
|
||||
|
||||
test('fetchBoxInfo passes through successful data', () async {
|
||||
final actions = BoxingApiActions(
|
||||
apiService: ApiService(
|
||||
client: _MockClient((request) async {
|
||||
expect(request.url.path, '/CargoTrace/box/info');
|
||||
expect(request.url.queryParameters['zongpai_no'], '26B1');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'zongpai_no': '26B1',
|
||||
'paichan_no': 'W00009',
|
||||
'work_order_no': 'WO001',
|
||||
'quantity': 8,
|
||||
'current_zongpai_boxes': [
|
||||
{'box_item_id': 1, 'box_no': 2, 'quantity': 3},
|
||||
],
|
||||
'existing_boxes': [],
|
||||
'max_box_no': 2,
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
loadApiUrl: () async => 'http://localhost',
|
||||
);
|
||||
|
||||
final result = await actions.fetchBoxInfo('26B1');
|
||||
|
||||
expect(result.success, isTrue);
|
||||
expect(result.data?.paichanNo, 'W00009');
|
||||
expect(result.data?.currentZongpaiBoxes.single.boxNo, 2);
|
||||
});
|
||||
|
||||
test('saveBoxRecord maps duplicate and preserves box number', () async {
|
||||
final actions = BoxingApiActions(
|
||||
apiService: ApiService(
|
||||
client: _MockClient((_) async {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'error_code': 'DUPLICATE_BOX_ITEM',
|
||||
'message': '重复装箱',
|
||||
'box_no': 7,
|
||||
}),
|
||||
409,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
loadApiUrl: () async => 'http://localhost',
|
||||
);
|
||||
|
||||
final result = await actions.saveBoxRecord(
|
||||
zongpaiNo: '26B1',
|
||||
boxNo: 7,
|
||||
quantity: 2,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.errorKind, BoxingActionErrorKind.duplicate);
|
||||
expect(result.boxNo, 7);
|
||||
});
|
||||
|
||||
test('updateBoxRecord maps network errors', () async {
|
||||
final actions = BoxingApiActions(
|
||||
apiService: ApiService(
|
||||
client: _MockClient((_) async {
|
||||
throw Exception('Connection refused');
|
||||
}),
|
||||
),
|
||||
loadApiUrl: () async => 'http://localhost',
|
||||
);
|
||||
|
||||
final result = await actions.updateBoxRecord(
|
||||
boxItemId: 1,
|
||||
boxNo: 2,
|
||||
quantity: 3,
|
||||
);
|
||||
|
||||
expect(result.success, isFalse);
|
||||
expect(result.errorKind, BoxingActionErrorKind.network);
|
||||
expect(result.errorMessage, BoxingApiActions.networkErrorMessage);
|
||||
});
|
||||
|
||||
test('deleteBoxRecord passes through success and backend errors', () async {
|
||||
var calls = 0;
|
||||
final actions = BoxingApiActions(
|
||||
apiService: ApiService(
|
||||
client: _MockClient((_) async {
|
||||
calls += 1;
|
||||
if (calls == 1) {
|
||||
return http.Response(
|
||||
jsonEncode({'box_item_id': 1, 'deleted': true}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}
|
||||
return http.Response(
|
||||
jsonEncode({'message': '指定装箱明细不存在'}),
|
||||
404,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
loadApiUrl: () async => 'http://localhost',
|
||||
);
|
||||
|
||||
final ok = await actions.deleteBoxRecord(boxItemId: 1);
|
||||
final missing = await actions.deleteBoxRecord(boxItemId: 999);
|
||||
|
||||
expect(ok.success, isTrue);
|
||||
expect(missing.success, isFalse);
|
||||
expect(missing.errorKind, BoxingActionErrorKind.backend);
|
||||
expect(missing.errorMessage, '指定装箱明细不存在');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _MockClient extends http.BaseClient {
|
||||
final Future<http.Response> Function(http.BaseRequest) _handler;
|
||||
|
||||
_MockClient(this._handler);
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
final response = await _handler(request);
|
||||
return http.StreamedResponse(
|
||||
http.ByteStream.fromBytes(response.bodyBytes),
|
||||
response.statusCode,
|
||||
headers: response.headers,
|
||||
reasonPhrase: response.reasonPhrase,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user