Files
pj_supermarket_cloud/test/payment_http_timing_test.dart
T
2026-10-23 15:53:43 +08:00

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')));
});
}