317 lines
11 KiB
Dart
317 lines
11 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:pj_supermarket_cloud/common/index.dart';
|
|
import 'package:sqflite/sqflite.dart' as sqlite;
|
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|
|
|
class _DiscountTestApp extends AppControl {
|
|
@override
|
|
void onClose() {}
|
|
}
|
|
|
|
/// 所有请求在这里结束,不访问真实优惠、钱包或支付接口。
|
|
class _DiscountAdapter implements dio.HttpClientAdapter {
|
|
_DiscountAdapter(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 _json(Object? data, {int code = 0}) =>
|
|
dio.ResponseBody.fromString(
|
|
jsonEncode({'code': code, 'msg': 'test response', 'data': data}),
|
|
200,
|
|
headers: {
|
|
'content-type': ['application/json']
|
|
},
|
|
);
|
|
|
|
DiscountsItem _rule({
|
|
bool allProducts = true,
|
|
int base = 0,
|
|
double rate = 1.5,
|
|
ProductHourType hour = ProductHourType.MORNING,
|
|
}) =>
|
|
DiscountsItem(
|
|
name: 'test rule',
|
|
hourType: hour,
|
|
type: null,
|
|
basePrice: base,
|
|
specialDishEnable: allProducts,
|
|
extraPer: rate,
|
|
);
|
|
|
|
ShopCardEntity _cart(int price, {bool increased = true}) => ShopCardEntity(
|
|
orderId: -1,
|
|
productServiceUniqueCode: '$price',
|
|
productName: 'test',
|
|
productPic: '',
|
|
productPrice: price,
|
|
nature: ProductNature.DEFAULT,
|
|
productNumber: 1,
|
|
productWidget: 0,
|
|
totalPrice: price,
|
|
isPriceIncreased: increased,
|
|
);
|
|
|
|
void main() {
|
|
final binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
late Directory directory;
|
|
late AppDatabase database;
|
|
late _DiscountTestApp app;
|
|
late dio.HttpClientAdapter originalAdapter;
|
|
late String originalBaseUrl;
|
|
final muteToasts = dio.InterceptorsWrapper(onRequest: (options, handler) {
|
|
// 保持测试无界面;另行断言生产 API 自己设置了 rejectBusinessError 标记。
|
|
options.extra[HttpManage.silentRequestKey] = true;
|
|
handler.next(options);
|
|
});
|
|
|
|
setUpAll(() async {
|
|
directory = await Directory.systemTemp.createTemp('pj-discounts-test-');
|
|
for (final channel in [
|
|
'plugins.flutter.io/path_provider',
|
|
'external_path'
|
|
]) {
|
|
binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
|
MethodChannel(channel),
|
|
(_) async => directory.path,
|
|
);
|
|
}
|
|
sqfliteFfiInit();
|
|
sqlite.databaseFactory = databaseFactoryFfiNoIsolate;
|
|
database = await $FloorAppDatabase.inMemoryDatabaseBuilder().build();
|
|
app = _DiscountTestApp()..dataBase = database;
|
|
Get.put<AppControl>(app);
|
|
await database.getSettingDao.save(SettingEntity(
|
|
sysJson: jsonEncode(
|
|
SysSetting(sysType: SysType.CANTEEN, serviceId: '10').toJson()),
|
|
userJson: jsonEncode(
|
|
(UserSetting(username: '', password: '')..tenantId = '1').toJson()),
|
|
));
|
|
await logger.init;
|
|
final client = HttpManage().dio;
|
|
originalAdapter = client.httpClientAdapter;
|
|
originalBaseUrl = client.options.baseUrl;
|
|
client.options.baseUrl = 'https://discount-test.invalid/api';
|
|
client.interceptors.add(muteToasts);
|
|
});
|
|
|
|
setUp(() async {
|
|
app.isNetwork.value = true;
|
|
await database.getUserDao.delAll();
|
|
await database.getUserDao.save(UserEntity(
|
|
userId: '100',
|
|
nickname: 'test',
|
|
walletBalance: 1000,
|
|
discountsList: [_rule()],
|
|
discountsUpdateTime: DateTime(2026, 1, 1),
|
|
));
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
final client = HttpManage().dio;
|
|
client.interceptors.remove(muteToasts);
|
|
client.httpClientAdapter = originalAdapter;
|
|
client.options.baseUrl = originalBaseUrl;
|
|
await logger.close();
|
|
await AppSettingTool.removeSettingCache();
|
|
await database.close();
|
|
Get.delete<AppControl>(force: true);
|
|
await directory.delete(recursive: true);
|
|
});
|
|
|
|
test('空优惠成功后清除离线旧规则,后续同结算不再查网络', () async {
|
|
final adapter = _DiscountAdapter((_) async => _json([]));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
final session = DiscountsTool.createSession();
|
|
expect(
|
|
await DiscountsTool.updateUserDiscountsList(
|
|
userId: '100', session: session),
|
|
isEmpty);
|
|
expect(
|
|
await DiscountsTool.updateUserDiscountsList(
|
|
userId: '100', session: session),
|
|
isEmpty);
|
|
expect(adapter.requests, hasLength(1));
|
|
final stored = await database.getUserDao.findByUserId('100');
|
|
expect(stored!.discountsList, isEmpty);
|
|
expect(stored.discountsUpdateTime, isNot(DateTime(2026, 1, 1)));
|
|
app.isNetwork.value = false;
|
|
expect(await DiscountsTool.updateUserDiscountsList(userId: '100'), isEmpty);
|
|
expect(adapter.requests, hasLength(1));
|
|
});
|
|
|
|
test('在线业务拒绝严格抛错、不按零优惠、不回退旧缓存,重试重新请求', () async {
|
|
final adapter = _DiscountAdapter((_) async => _json(null, code: 912));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
final session = DiscountsTool.createSession();
|
|
for (var i = 0; i < 2; i++) {
|
|
await expectLater(
|
|
DiscountsTool.computedExtraPrice(
|
|
userId: '100',
|
|
shopCardList: [_cart(1000)],
|
|
hourType: ProductHourType.MORNING.code,
|
|
session: session),
|
|
throwsA(isA<dio.DioException>()),
|
|
);
|
|
}
|
|
expect(adapter.requests, hasLength(2));
|
|
expect(
|
|
adapter.requests.every((request) =>
|
|
request.extra[HttpManage.rejectBusinessErrorKey] == true),
|
|
isTrue);
|
|
expect((await database.getUserDao.findByUserId('100'))!.discountsList,
|
|
hasLength(1));
|
|
});
|
|
|
|
test('异常空返回不能当作成功的空优惠', () async {
|
|
final adapter = _DiscountAdapter((_) async => _json(null));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
await expectLater(DiscountsTool.updateUserDiscountsList(userId: '100'),
|
|
throwsFormatException);
|
|
expect((await database.getUserDao.findByUserId('100'))!.discountsList,
|
|
hasLength(1));
|
|
});
|
|
|
|
test('等待规则时冻结商品金额,同结算改变数量仅重算不再次查询', () async {
|
|
final source = Completer<dio.ResponseBody>();
|
|
final adapter = _DiscountAdapter((_) => source.future);
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
final session = DiscountsTool.createSession();
|
|
final cart = [_cart(1000)];
|
|
final first = DiscountsTool.computedExtraPrice(
|
|
userId: '100',
|
|
shopCardList: cart,
|
|
hourType: ProductHourType.MORNING.code,
|
|
session: session);
|
|
cart.single.totalPrice = 2000;
|
|
source.complete(_json([_rule().toJson()]));
|
|
expect(await first, 500);
|
|
expect(
|
|
await DiscountsTool.computedExtraPrice(
|
|
userId: '100',
|
|
shopCardList: cart,
|
|
hourType: ProductHourType.MORNING.code,
|
|
session: session),
|
|
1000);
|
|
expect(adapter.requests, hasLength(1));
|
|
});
|
|
|
|
test('明确离线继续使用原本缓存而不访问网络', () async {
|
|
final adapter =
|
|
_DiscountAdapter((_) async => throw StateError('must not request'));
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
app.isNetwork.value = false;
|
|
expect(
|
|
await DiscountsTool.computedExtraPrice(
|
|
userId: '100',
|
|
shopCardList: [_cart(1000)],
|
|
hourType: ProductHourType.MORNING.code),
|
|
500);
|
|
expect(adapter.requests, isEmpty);
|
|
});
|
|
|
|
test('刷新人员余额时保留已有优惠规则和更新时间', () async {
|
|
await SyncUserTool.storageUser(
|
|
user: ServiceUser(userId: '100', username: 'test', walletBalance: 900));
|
|
final stored = await database.getUserDao.findByUserId('100');
|
|
expect(stored!.walletBalance, 900);
|
|
expect(stored.discountsList, hasLength(1));
|
|
expect(stored.discountsUpdateTime, DateTime(2026, 1, 1));
|
|
});
|
|
|
|
test('旧结算网络结果迟到时不能把新结算的空优惠覆盖回旧规则', () async {
|
|
final started = Completer<void>();
|
|
final oldResponse = Completer<dio.ResponseBody>();
|
|
var calls = 0;
|
|
final adapter = _DiscountAdapter((_) {
|
|
if (++calls == 1) {
|
|
started.complete();
|
|
return oldResponse.future;
|
|
}
|
|
return Future.value(_json([]));
|
|
});
|
|
HttpManage().dio.httpClientAdapter = adapter;
|
|
final session = DiscountsTool.createSession();
|
|
final old =
|
|
DiscountsTool.updateUserDiscountsList(userId: '100', session: session);
|
|
await started.future;
|
|
session.invalidate();
|
|
expect(
|
|
await DiscountsTool.updateUserDiscountsList(
|
|
userId: '100', session: session),
|
|
isEmpty);
|
|
oldResponse.complete(_json([_rule(rate: 2).toJson()]));
|
|
expect(await old, hasLength(1));
|
|
expect((await database.getUserDao.findByUserId('100'))!.discountsList,
|
|
isEmpty);
|
|
});
|
|
|
|
test('人员读取旧快照期间更新优惠,余额与新规则不会互相覆盖', () async {
|
|
final updatedAt = DateTime(2026, 10, 23, 16, 0);
|
|
// 人员刷新先发起读取;随后的规则 UPDATE 可以排在其读取和写入之间。
|
|
final profile = SyncUserTool.storageUser(
|
|
user: ServiceUser(
|
|
userId: '100',
|
|
username: 'test',
|
|
walletBalance: 700,
|
|
));
|
|
final discounts = database.getUserDao.updateDiscounts(
|
|
'100',
|
|
jsonEncode([_rule(rate: 2).toJson()]),
|
|
updatedAt,
|
|
);
|
|
await Future.wait([profile, discounts]);
|
|
final stored = (await database.getUserDao.findByUserId('100'))!;
|
|
expect(stored.walletBalance, 700);
|
|
expect(stored.discountsList!.single.extraPer, 2);
|
|
expect(stored.discountsUpdateTime, updatedAt);
|
|
|
|
// 反向更新规则同样不能把更新后的余额改回请求前的值。
|
|
await database.getUserDao.updateDiscounts('100', '[]', updatedAt);
|
|
final cleared = (await database.getUserDao.findByUserId('100'))!;
|
|
expect(cleared.walletBalance, 700);
|
|
expect(cleared.discountsList, isEmpty);
|
|
});
|
|
|
|
test('原收费算法保留餐段、特色菜范围、基数边界和逐规则四舍五入', () {
|
|
final rules = [
|
|
_rule(base: 1000, rate: 1.5),
|
|
_rule(allProducts: false, base: 0, rate: 1.1),
|
|
_rule(rate: 0.8),
|
|
_rule(hour: ProductHourType.NOON),
|
|
];
|
|
expect(
|
|
DiscountsTool.computeExtraPriceFromRules(
|
|
discountsItems: rules,
|
|
hourType: ProductHourType.MORNING.code,
|
|
totalPrice: 1000,
|
|
increasedPrice: 555),
|
|
56);
|
|
expect(
|
|
DiscountsTool.computeExtraPriceFromRules(
|
|
discountsItems: rules,
|
|
hourType: ProductHourType.MORNING.code,
|
|
totalPrice: 1001,
|
|
increasedPrice: 555),
|
|
557);
|
|
});
|
|
}
|