233 lines
7.9 KiB
Dart
233 lines
7.9 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:dio/dio.dart' as dio;
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:logger/logger.dart';
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
import 'package:pj_supermarket_cloud/common/utils/order/payment_timing_trace.dart';
|
|
import 'package:sqflite/sqflite.dart' as sqlite;
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
|
|
class _PaymentHttpTestApp extends AppControl {
|
|
@override
|
|
void onClose() {}
|
|
}
|
|
|
|
/// All requests end here; the tests never open a network connection.
|
|
class _PaymentHttpAdapter implements dio.HttpClientAdapter {
|
|
_PaymentHttpAdapter(this.respond);
|
|
|
|
final Future<dio.ResponseBody> Function(dio.RequestOptions) respond;
|
|
final requests = <dio.RequestOptions>[];
|
|
|
|
@override
|
|
Future<dio.ResponseBody> fetch(
|
|
dio.RequestOptions options,
|
|
Stream<Uint8List>? requestStream,
|
|
Future<void>? cancelFuture,
|
|
) {
|
|
requests.add(options);
|
|
return respond(options);
|
|
}
|
|
|
|
@override
|
|
void close({bool force = false}) {}
|
|
}
|
|
|
|
dio.ResponseBody _jsonResponse(Object data) => dio.ResponseBody.fromString(
|
|
jsonEncode(data),
|
|
200,
|
|
headers: {
|
|
'content-type': ['application/json'],
|
|
},
|
|
);
|
|
|
|
void main() {
|
|
final binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
late Directory temporaryDirectory;
|
|
late AppDatabase database;
|
|
late dio.HttpClientAdapter originalAdapter;
|
|
late String originalBaseUrl;
|
|
final warningMessages = <String>[];
|
|
void capture(LogEvent event) {
|
|
if (event.level == Level.warning &&
|
|
event.message.toString().contains('|支付请求耗时|')) {
|
|
warningMessages.add(event.message.toString());
|
|
}
|
|
}
|
|
|
|
setUpAll(() async {
|
|
temporaryDirectory =
|
|
await Directory.systemTemp.createTemp('pj-payment-http-timing-');
|
|
for (final channel in [
|
|
'plugins.flutter.io/path_provider',
|
|
'external_path'
|
|
]) {
|
|
binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
MethodChannel(channel),
|
|
(_) async => temporaryDirectory.path,
|
|
);
|
|
}
|
|
sqfliteFfiInit();
|
|
sqlite.databaseFactory = databaseFactoryFfiNoIsolate;
|
|
database = await $FloorAppDatabase.inMemoryDatabaseBuilder().build();
|
|
Get.put<AppControl>(_PaymentHttpTestApp()..dataBase = database);
|
|
await database.getSettingDao.save(SettingEntity(
|
|
sysJson: jsonEncode(SysSetting(sysType: SysType.CANTEEN).toJson()),
|
|
userJson: jsonEncode((UserSetting(username: '', password: '')
|
|
..accessToken = 'test-auth-secret'
|
|
..tenantId = '12')
|
|
.toJson()),
|
|
));
|
|
await logger.init;
|
|
final client = HttpManage().dio;
|
|
originalAdapter = client.httpClientAdapter;
|
|
originalBaseUrl = client.options.baseUrl;
|
|
// Keep the configured gateway prefix, while using a deliberately fake host.
|
|
client.options.baseUrl = 'https://payment-test.invalid/api';
|
|
Logger.addLogListener(capture);
|
|
});
|
|
|
|
setUp(warningMessages.clear);
|
|
|
|
tearDownAll(() async {
|
|
Logger.removeLogListener(capture);
|
|
HttpManage().dio
|
|
..httpClientAdapter = originalAdapter
|
|
..options.baseUrl = originalBaseUrl;
|
|
await logger.close();
|
|
await AppSettingTool.removeSettingCache();
|
|
await database.close();
|
|
Get.delete<AppControl>(force: true);
|
|
await temporaryDirectory.delete(recursive: true);
|
|
});
|
|
|
|
test('slow request keeps its result and reports only path and timing',
|
|
() async {
|
|
final adapter = _PaymentHttpAdapter((_) async {
|
|
await Future<void>.delayed(const Duration(milliseconds: 1050));
|
|
return _jsonResponse({
|
|
'code': 0,
|
|
'data': {'message': 'response-secret'},
|
|
});
|
|
});
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
final trace = PaymentTimingTrace(id: 'test_payment', log: (_) {});
|
|
addTearDown(() => trace.finish(outcome: 'failed'));
|
|
final response = await trace.run(() => HttpManage().dio.post<dynamic>(
|
|
'/hard/hard-api/ad/create-order',
|
|
queryParameters: {'qrcode': 'query-secret'},
|
|
data: {'cardNo': 'body-secret'},
|
|
));
|
|
trace.finish(outcome: 'success');
|
|
|
|
expect(response.data, {'message': 'response-secret'});
|
|
expect(adapter.requests, hasLength(1));
|
|
expect(adapter.requests.single.headers['Authorization'],
|
|
'Bearer test-auth-secret');
|
|
expect(adapter.requests.single.headers['TENANT-ID'], '12');
|
|
expect(warningMessages, hasLength(1));
|
|
final message = warningMessages.single;
|
|
expect(message, contains('|trace:test_payment|'));
|
|
expect(message, contains('|path:/hard/hard-api/ad/create-order|'));
|
|
expect(message, contains('|result:success|'));
|
|
expect(message, contains('|settingsMs:'));
|
|
expect(message, contains('|prepareMs:'));
|
|
expect(message, contains('|waitMs:'));
|
|
expect(message, contains('|handlerMs:'));
|
|
final waitMs =
|
|
int.parse(RegExp(r'\|waitMs:(\d+)\|').firstMatch(message)!.group(1)!);
|
|
expect(waitMs, greaterThanOrEqualTo(1000));
|
|
for (final secret in [
|
|
'test-auth-secret',
|
|
'query-secret',
|
|
'body-secret',
|
|
'response-secret',
|
|
'payment-test.invalid',
|
|
]) {
|
|
expect(message, isNot(contains(secret)));
|
|
}
|
|
});
|
|
|
|
test('fast successful payment request does not add an HTTP warning',
|
|
() async {
|
|
final adapter = _PaymentHttpAdapter((_) async => _jsonResponse({
|
|
'code': 0,
|
|
'data': <Object>[],
|
|
}));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
|
|
final response = await HttpManage().dio.get<dynamic>(
|
|
'/wallet/wallet-setting/ad/get-discounts-list',
|
|
);
|
|
|
|
expect(response.data, isEmpty);
|
|
expect(adapter.requests, hasLength(1));
|
|
expect(warningMessages, isEmpty);
|
|
});
|
|
|
|
test('business rejection is unchanged and emits one diagnostic', () async {
|
|
final adapter = _PaymentHttpAdapter((_) async => _jsonResponse({
|
|
'code': 912,
|
|
'msg': 'private business rejection',
|
|
'data': null,
|
|
}));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
|
|
await expectLater(
|
|
HttpManage().dio.get<dynamic>(
|
|
'/wallet/wallet-setting/ad/get-discounts-list',
|
|
options: dio.Options(extra: {HttpManage.silentRequestKey: true}),
|
|
),
|
|
throwsA(isA<dio.DioException>()
|
|
.having(
|
|
(error) => error.type, 'type', dio.DioExceptionType.badResponse)
|
|
.having((error) => error.error, 'error', 'private business rejection')
|
|
.having((error) => error.response?.statusCode, 'status', 200)),
|
|
);
|
|
expect(adapter.requests, hasLength(1));
|
|
expect(warningMessages, hasLength(1));
|
|
expect(warningMessages.single, contains('|result:businessError|'));
|
|
expect(warningMessages.single, contains('|code:912|'));
|
|
expect(
|
|
warningMessages.single, isNot(contains('private business rejection')));
|
|
});
|
|
|
|
test('transport error propagates once and unrelated routes are excluded',
|
|
() async {
|
|
final adapter = _PaymentHttpAdapter((options) async {
|
|
throw dio.DioException(
|
|
requestOptions: options,
|
|
type: dio.DioExceptionType.receiveTimeout,
|
|
message: 'private transport details',
|
|
);
|
|
});
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
for (final path in [
|
|
'/hard/hard-api/ad/get-user',
|
|
'/hard/hard-api/ad/unrelated-test',
|
|
]) {
|
|
await expectLater(
|
|
HttpManage().dio.get<dynamic>(
|
|
path,
|
|
options: dio.Options(extra: {HttpManage.silentRequestKey: true}),
|
|
),
|
|
throwsA(isA<dio.DioException>().having(
|
|
(error) => error.type,
|
|
'type',
|
|
dio.DioExceptionType.receiveTimeout,
|
|
)),
|
|
);
|
|
}
|
|
expect(adapter.requests, hasLength(2));
|
|
expect(warningMessages, hasLength(1));
|
|
expect(warningMessages.single, contains('|errorType:receiveTimeout|'));
|
|
expect(
|
|
warningMessages.single, isNot(contains('private transport details')));
|
|
});
|
|
}
|