已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
61
apps/user_app/test/data/avatar_upload_test.dart
Normal file
61
apps/user_app/test/data/avatar_upload_test.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// 功能描述:验证 multipart 文件上传鉴权、会话过期与资源路径解析。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('上传携带鉴权和文件内容,不使用 JSON 包装二进制', () async {
|
||||
final api = ApiClient(
|
||||
() => 'test-token',
|
||||
baseUrl: 'https://api.example.com',
|
||||
client: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'test-token');
|
||||
expect(request.url.path, '/upload/avatar');
|
||||
expect(request.headers['content-type'], startsWith('multipart/form-data; boundary='));
|
||||
expect(request.body, contains('name="file"; filename="avatar.png"'));
|
||||
return http.Response('{"code":0,"details":{"uri":"/uploads/avatars/test.png"}}', 200);
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
await ClientRepository(api).uploadAvatar(Uint8List.fromList([1, 2, 3]), 'avatar.png'),
|
||||
'/uploads/avatars/test.png',
|
||||
);
|
||||
});
|
||||
test('超过大小限制不会发起请求;上传过期使会话失效', () async {
|
||||
var requests = 0;
|
||||
final rejected = <String>[];
|
||||
final api = ApiClient(
|
||||
() => 'expired',
|
||||
onUnauthorized: rejected.add,
|
||||
client: MockClient((request) async {
|
||||
requests++;
|
||||
return http.Response('', 401);
|
||||
}),
|
||||
);
|
||||
await expectLater(
|
||||
api.uploadAvatar(Uint8List(2 * 1024 * 1024 + 1), 'x.png'),
|
||||
throwsA(isA<ApiException>()),
|
||||
);
|
||||
expect(requests, 0);
|
||||
await expectLater(
|
||||
api.uploadAvatar(Uint8List.fromList([1]), 'x.png'),
|
||||
throwsA(isA<SessionExpiredException>()),
|
||||
);
|
||||
expect(rejected, ['expired']);
|
||||
});
|
||||
test('图片相对路径使用配置的 API 域名,拒绝非网络协议', () {
|
||||
final repo = ClientRepository(ApiClient(() => '', baseUrl: 'https://api.example.com'));
|
||||
expect(
|
||||
repo.resolveImageUrl('/uploads/product.png'),
|
||||
'https://api.example.com/uploads/product.png',
|
||||
);
|
||||
expect(repo.resolveImageUrl('https://cdn.example.com/a.png'), 'https://cdn.example.com/a.png');
|
||||
for (final value in ['//other.example.com/a', 'javascript:x', 'file:///secret']) {
|
||||
expect(repo.resolveImageUrl(value), '');
|
||||
}
|
||||
});
|
||||
}
|
||||
77
apps/user_app/test/data/cart_test.dart
Normal file
77
apps/user_app/test/data/cart_test.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
// 功能描述:购物车公开标识、整数金额及登录失效契约回归;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/cart_item.dart';
|
||||
|
||||
void main() {
|
||||
test('购物车迟到响应不跨账号继续加购或更新界面', () async {
|
||||
var token = 'old';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((_) async {
|
||||
token = 'new';
|
||||
return http.Response('{"code":0,"details":{}}', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.cartItem('p1'), throwsA(isA<SessionExpiredException>()));
|
||||
});
|
||||
test('条件数量更新携带原版本,图片仅接受可信协议', () async {
|
||||
final value = <String, Object?>{
|
||||
'product_identity': 'p1',
|
||||
'name': '商品',
|
||||
'price_amount': 1999,
|
||||
'stock_quantity': 4,
|
||||
'quantity': 2,
|
||||
'selected': false,
|
||||
'available': true,
|
||||
'revision': 'revision',
|
||||
'image_url': 'javascript:alert(1)',
|
||||
};
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test-token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'test-token');
|
||||
if (request.method == 'PUT') {
|
||||
expect(jsonDecode(request.body), {
|
||||
'quantity': 2,
|
||||
'selected': false,
|
||||
'revision': 'revision',
|
||||
});
|
||||
}
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': value}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final item = await repo.cartItem('p1');
|
||||
expect(item.product.imageUrl, '');
|
||||
expect(item.amount, 3998);
|
||||
expect((await repo.setCartItem(item, quantity: 2, selected: false)).selected, isFalse);
|
||||
expect(
|
||||
() => CartItem.fromJson({...value, 'price_amount': 1.5}, (s) => s),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
test('购物车接口401清理会话', () async {
|
||||
var expired = false;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test',
|
||||
onUnauthorized: (_) => expired = true,
|
||||
client: MockClient((_) async => http.Response('{"code":1301}', 401)),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.cart(), throwsA(isA<SessionExpiredException>()));
|
||||
expect(expired, isTrue);
|
||||
});
|
||||
}
|
||||
55
apps/user_app/test/data/change_login_password_test.dart
Normal file
55
apps/user_app/test/data/change_login_password_test.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
// 功能描述:改密请求契约、成功确认和迟到会话响应保护;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('改密使用本人PUT请求,只有changed=true被认为成功', () async {
|
||||
var success = true;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test-token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.method, 'PUT');
|
||||
expect(request.url.path, '/heqi/client/v1/user/auth/password');
|
||||
expect(jsonDecode(request.body), {
|
||||
'current_password': 'old-value',
|
||||
'new_password': 'new-value',
|
||||
});
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': {'changed': success},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await repo.changeLoginPassword('old-value', 'new-value');
|
||||
success = false;
|
||||
await expectLater(
|
||||
repo.changeLoginPassword('old-value', 'new-value'),
|
||||
throwsA(isA<ApiException>()),
|
||||
);
|
||||
});
|
||||
test('切换账户后丢弃旧改密响应', () async {
|
||||
var token = 'first';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((_) async {
|
||||
token = 'second';
|
||||
return http.Response('{"code":0,"details":{"changed":true}}', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
repo.changeLoginPassword('old', 'next'),
|
||||
throwsA(isA<SessionExpiredException>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
126
apps/user_app/test/data/favorite_test.dart
Normal file
126
apps/user_app/test/data/favorite_test.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
// 功能描述:收藏版本契约、跨入口同步及账号切换迟到响应保护;版本:1.0.0。
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/product_favorite.dart';
|
||||
import 'package:user_app/ui/features/shop/favorite_button.dart';
|
||||
|
||||
http.Response _response(Object details) => http.Response(
|
||||
jsonEncode({'code': 0, 'details': details}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('两个入口同步服务端收藏状态,失败读取后不反转图标', (tester) async {
|
||||
var active = false, version = 0, fail = false;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test',
|
||||
client: MockClient((request) async {
|
||||
if (request.method == 'PUT') {
|
||||
final body = jsonDecode(request.body) as Map;
|
||||
expect(body['revision'], version == 0 ? '' : 'f:$version');
|
||||
if (fail) return http.Response('{"code":500}', 200);
|
||||
active = body['favorite'] as bool;
|
||||
version++;
|
||||
}
|
||||
return _response({
|
||||
'product_identity': 'p1',
|
||||
'favorite': active,
|
||||
'revision': version == 0 ? '' : 'f:$version',
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
for (var i = 0; i < 2; i++)
|
||||
FavoriteButton(repository: repo, identity: 'p1', redirect: '/shop', name: '商品'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('收藏 商品'), findsNWidgets(2));
|
||||
await tester.tap(find.byTooltip('收藏 商品').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('取消收藏 商品'), findsNWidgets(2));
|
||||
fail = true;
|
||||
await tester.tap(find.byTooltip('取消收藏 商品').first);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byTooltip('取消收藏 商品'), findsNWidgets(2));
|
||||
});
|
||||
test('账号切换后不广播前一账号收藏结果', () async {
|
||||
var token = 'account-a';
|
||||
final pending = Completer<http.Response>();
|
||||
final events = <ProductFavoriteState>[];
|
||||
final repo = ClientRepository(
|
||||
ApiClient(() => token, client: MockClient((_) => pending.future)),
|
||||
);
|
||||
final subscription = repo.favoriteChanges.listen(events.add);
|
||||
addTearDown(subscription.cancel);
|
||||
final result = repo.setFavorite(
|
||||
const ProductFavoriteState(productIdentity: 'p1', active: false, revision: ''),
|
||||
true,
|
||||
);
|
||||
final assertion = expectLater(result, throwsA(isA<SessionExpiredException>()));
|
||||
token = 'account-b';
|
||||
pending.complete(_response({'product_identity': 'p1', 'favorite': true, 'revision': 'f:1'}));
|
||||
await assertion;
|
||||
expect(events, isEmpty);
|
||||
});
|
||||
test('收藏分页、图片URL及金额数据校验', () async {
|
||||
var reads = 0;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test',
|
||||
client: MockClient((request) async {
|
||||
reads++;
|
||||
final page = int.parse(request.url.queryParameters['page']!);
|
||||
return _response({
|
||||
'items': [
|
||||
{
|
||||
'product_identity': 'p$page',
|
||||
'favorite': true,
|
||||
'revision': 'r$page',
|
||||
'name': '商品$page',
|
||||
'price_amount': 16900,
|
||||
'stock_quantity': 0,
|
||||
'available': false,
|
||||
'category_name': '配件',
|
||||
'image_url': 'javascript:bad',
|
||||
'specifications': ['家用'],
|
||||
},
|
||||
],
|
||||
'has_more': page == 1,
|
||||
'page': page,
|
||||
'page_size': 30,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
final items = await repo.favorites();
|
||||
expect(reads, 2);
|
||||
expect(items.length, 2);
|
||||
expect(items.first.product.imageUrl, '');
|
||||
expect(items.first.available, isFalse);
|
||||
expect(
|
||||
() => ProductFavoriteState.fromJson({
|
||||
'product_identity': 'p1',
|
||||
'favorite': 'true',
|
||||
'revision': '',
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
}
|
||||
34
apps/user_app/test/data/gas_contract_pdf_test.dart
Normal file
34
apps/user_app/test/data/gas_contract_pdf_test.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
// 功能描述:PDF鉴权、缺失、错误页与跨会话保护;版本:1.0.0。
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('PDF只接收有效字节并拒绝缺失、JSON错误及旧会话', () async {
|
||||
var mode = 0;
|
||||
var token = 'token';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'token');
|
||||
expect(request.headers['accept'], 'application/pdf');
|
||||
expect(request.url.path.endsWith('/gas/contracts/c1/attachment'), isTrue);
|
||||
if (mode == 1) return http.Response('', 404);
|
||||
if (mode == 2) return http.Response('{"code":1112}', 200);
|
||||
if (mode == 3) token = 'changed';
|
||||
return http.Response('%PDF-1.7\n%%EOF', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(String.fromCharCodes(await repo.gasContractPdf('c1')), startsWith('%PDF-'));
|
||||
mode = 1;
|
||||
await expectLater(repo.gasContractPdf('c1'), throwsA(isA<ApiException>()));
|
||||
mode = 2;
|
||||
await expectLater(repo.gasContractPdf('c1'), throwsA(isA<ApiException>()));
|
||||
mode = 3;
|
||||
await expectLater(repo.gasContractPdf('c1'), throwsA(isA<SessionExpiredException>()));
|
||||
});
|
||||
}
|
||||
48
apps/user_app/test/data/gas_contracts_test.dart
Normal file
48
apps/user_app/test/data/gas_contracts_test.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
// 功能描述:合同列表类型、会话隔离及真实状态回归;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('合同草稿保持真实状态并拦截跨会话迟到响应', () async {
|
||||
var token = 'first';
|
||||
var changeSession = false;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((request) async {
|
||||
expect(request.url.path.endsWith('/gas/contracts'), isTrue);
|
||||
if (changeSession) token = 'second';
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': [
|
||||
{
|
||||
'identity': 'c1',
|
||||
'contract_no': 'HT1',
|
||||
'title': '合同',
|
||||
'terms': '条款',
|
||||
'contract_status': 0,
|
||||
'station_name': '气站',
|
||||
'has_attachment': false,
|
||||
'signed_at': '0001-01-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final contracts = await repo.gasContracts();
|
||||
expect(contracts.single.statusName, '草稿');
|
||||
expect(contracts.single.signedAt, isNull);
|
||||
expect(contracts.single.stationName, '气站');
|
||||
changeSession = true;
|
||||
await expectLater(repo.gasContracts(), throwsA(isA<SessionExpiredException>()));
|
||||
});
|
||||
}
|
||||
69
apps/user_app/test/data/gas_order_detail_test.dart
Normal file
69
apps/user_app/test/data/gas_order_detail_test.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
// 功能描述:供气与合同鉴权、货币精度及签收请求契约回归;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/gas_order_detail.dart';
|
||||
|
||||
void main() {
|
||||
final data = <String, Object?>{
|
||||
'identity': 'gas',
|
||||
'order_no': 'G1',
|
||||
'status_name': '配送中',
|
||||
'allowed_actions': [],
|
||||
'items': [
|
||||
{'identity': 'line', 'quantity': 1, 'sale_amount': 12800},
|
||||
],
|
||||
'delivery_fee': 500,
|
||||
'product_amount': 12800,
|
||||
'discount_amount': 0,
|
||||
'payable_amount': 13300,
|
||||
'timeline': [],
|
||||
'payments': [],
|
||||
};
|
||||
test('供气详情不从履约状态推断支付,签收只发送原请求号', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'token');
|
||||
if (request.method == 'POST') {
|
||||
expect(request.url.path.endsWith('/gas/orders/gas/confirm-receipt'), isTrue);
|
||||
expect(jsonDecode(request.body), {'request_no': 'request'});
|
||||
return http.Response('{"code":0,"details":{"confirmed":true}}', 200);
|
||||
}
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': data}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final value = await repo.gasOrderDetail('gas');
|
||||
expect(value.payments, isEmpty);
|
||||
expect(value.deliveryFee, 500);
|
||||
expect(value.items.single.quantity, 1);
|
||||
await repo.confirmGasReceipt('gas', requestNo: 'request');
|
||||
await expectLater(repo.gasOrderDetail('other'), throwsA(isA<ApiException>()));
|
||||
expect(
|
||||
() => GasOrderDetail.fromJson({...data, 'delivery_fee': 1.2}, (s) => s),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
test('供气和合同读取401触发会话清理', () async {
|
||||
var expired = 0;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
onUnauthorized: (_) => expired++,
|
||||
client: MockClient((_) async => http.Response('{"code":1301}', 401)),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.gasOrderDetail('gas'), throwsA(isA<SessionExpiredException>()));
|
||||
await expectLater(repo.gasContractDetail('contract'), throwsA(isA<SessionExpiredException>()));
|
||||
expect(expired, 2);
|
||||
});
|
||||
}
|
||||
84
apps/user_app/test/data/payment_password_test.dart
Normal file
84
apps/user_app/test/data/payment_password_test.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
// 功能描述:支付密码状态和OTP申请/改密请求契约与会话保护;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
|
||||
void main() {
|
||||
test('缺失支付密码状态保持未知', () {
|
||||
expect(WalletSummary.fromJson({}).paymentPasswordSet, isNull);
|
||||
});
|
||||
test('验证码只发当前资料手机号,区分首次与重置,不把Mock报告为已发送', () async {
|
||||
final purposes = <String>[];
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((r) async {
|
||||
if (r.url.path.endsWith('/auth/profile')) {
|
||||
return http.Response('{"code":0,"details":{"phone":"13800000001"}}', 200);
|
||||
}
|
||||
expect(r.url.path, '/heqi/client/v1/user/auth/verification-code');
|
||||
final body = jsonDecode(r.body) as Map;
|
||||
expect(body['phone'], '13800000001');
|
||||
purposes.add(body['purpose'] as String);
|
||||
return http.Response(
|
||||
'{"code":0,"details":{"request_identity":"request","delivery_mode":"mock","delivery_status":"not_sent","retry_after":60}}',
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final first = await repo.requestPaymentPasswordCode(resetting: false);
|
||||
await repo.requestPaymentPasswordCode(resetting: true);
|
||||
expect(first.delivered, false);
|
||||
expect(first.maskedPhone, '138****0001');
|
||||
expect(purposes, ['set_payment_password', 'reset_payment_password']);
|
||||
});
|
||||
test('改密必须确认服务端成功,缓存解锁失败独立返回', () async {
|
||||
var changed = true;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((r) async {
|
||||
expect(r.method, 'PUT');
|
||||
expect(r.url.path, '/heqi/client/v1/user/wallet/payment-password');
|
||||
expect(jsonDecode(r.body), {'new_password': '123456', 'current_password': '654321'});
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': {'changed': changed, 'lock_cleared': false},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(await repo.setPaymentPassword(newPassword: '123456', currentPassword: '654321'), false);
|
||||
changed = false;
|
||||
await expectLater(
|
||||
repo.setPaymentPassword(newPassword: '123456', currentPassword: '654321'),
|
||||
throwsA(isA<ApiException>()),
|
||||
);
|
||||
});
|
||||
test('切换会话后不继续为旧账户申请验证码', () async {
|
||||
var token = 'first', calls = 0;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((r) async {
|
||||
calls++;
|
||||
token = 'second';
|
||||
return http.Response('{"code":0,"details":{"phone":"13800000001"}}', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
repo.requestPaymentPasswordCode(resetting: false),
|
||||
throwsA(isA<SessionExpiredException>()),
|
||||
);
|
||||
expect(calls, 1);
|
||||
});
|
||||
}
|
||||
70
apps/user_app/test/data/primary_repository_test.dart
Normal file
70
apps/user_app/test/data/primary_repository_test.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
// 功能描述:验证新旧分页契约兼容、异常分页与非法金额拒绝。版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/repositories/primary_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
/// 创建返回受控响应的真实 Repository,覆盖 HTTP 解码到领域转换链路。
|
||||
ClientRepository source(Object? Function(http.Request) reply) => ClientRepository(
|
||||
ApiClient(
|
||||
() => '',
|
||||
client: MockClient(
|
||||
(request) async => http.Response(
|
||||
jsonEncode({'code': 0, 'details': reply(request)}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
),
|
||||
),
|
||||
baseUrl: 'http://fixture.test',
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
test('分页连续读取且公开商品不发送认证头', () async {
|
||||
final pages = <String>[];
|
||||
final repository = source((request) {
|
||||
expect(request.headers.containsKey('authorization'), isFalse);
|
||||
final page = request.url.queryParameters['page']!;
|
||||
pages.add(page);
|
||||
return {
|
||||
'items': [
|
||||
{'identity': page, 'name': '商品$page', 'price_amount': 16900, 'stock_quantity': 1},
|
||||
],
|
||||
'has_more': page == '1',
|
||||
};
|
||||
});
|
||||
final values = await PrimaryRepository(repository).products();
|
||||
expect(pages, ['1', '2']);
|
||||
expect(values.map((p) => p.identity), ['1', '2']);
|
||||
expect(values.first.price, 16900);
|
||||
});
|
||||
test('兼容旧数组响应', () async {
|
||||
final repository = source(
|
||||
(_) => [
|
||||
{'identity': 'legacy', 'name': '商品', 'price_amount': 100, 'stock_quantity': 2},
|
||||
],
|
||||
);
|
||||
expect((await PrimaryRepository(repository).products()).single.identity, 'legacy');
|
||||
});
|
||||
test('空页仍声明更多数据时返回异常而不是循环或空成功', () async {
|
||||
final repository = source((_) => {'items': <Object?>[], 'has_more': true});
|
||||
await expectLater(
|
||||
repository.products(),
|
||||
throwsA(isA<ApiException>().having((e) => e.code, 'code', 1714)),
|
||||
);
|
||||
});
|
||||
test('浮点金额不转换为看似有效的整数分', () async {
|
||||
final repository = source(
|
||||
(_) => [
|
||||
{'identity': 'bad', 'price_amount': 1.25, 'stock_quantity': 1},
|
||||
],
|
||||
);
|
||||
await expectLater(
|
||||
PrimaryRepository(repository).products(),
|
||||
throwsA(isA<ApiException>().having((e) => e.code, 'code', 1714)),
|
||||
);
|
||||
});
|
||||
}
|
||||
75
apps/user_app/test/data/product_detail_test.dart
Normal file
75
apps/user_app/test/data/product_detail_test.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
// 功能描述:商品详情匿名请求、图片路径解析、错误映射和畸形金额拒绝。
|
||||
// 版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('公开详情不发送登录令牌,解析图片和参数', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'private-token',
|
||||
baseUrl: 'https://api.example.com',
|
||||
client: MockClient((request) async {
|
||||
expect(request.url.path, '/heqi/client/v1/user/public/products/p1');
|
||||
expect(request.headers.containsKey('Authorization'), false);
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': {
|
||||
'identity': 'p1',
|
||||
'name': '商品',
|
||||
'price_amount': 123,
|
||||
'stock_quantity': 1,
|
||||
'images': [
|
||||
{'image_url': '/product.png'},
|
||||
{'image_url': 'javascript:bad'},
|
||||
],
|
||||
'attributes': [
|
||||
{'name': '规格', 'value': '15kg'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
200,
|
||||
headers: const {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final detail = await repo.productDetail('p1');
|
||||
expect(detail.images, ['https://api.example.com/product.png']);
|
||||
expect(detail.attributes.single.value, '15kg');
|
||||
expect(detail.price, 123);
|
||||
});
|
||||
test('不存在映射中文;错误价格不显示为可购买商品', () async {
|
||||
var body = {'code': 1112, 'details': 'Record Not Found'};
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => '',
|
||||
baseUrl: 'https://api.example.com',
|
||||
client: MockClient(
|
||||
(request) async => http.Response(
|
||||
jsonEncode(body),
|
||||
200,
|
||||
headers: const {'content-type': 'application/json; charset=utf-8'},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
repo.productDetail('missing'),
|
||||
throwsA(isA<ApiException>().having((error) => error.message, '提示', '商品已下架或不存在')),
|
||||
);
|
||||
body = {
|
||||
'code': 0,
|
||||
'details': {'identity': 'p1', 'name': '商品', 'price_amount': -1, 'stock_quantity': 1},
|
||||
};
|
||||
await expectLater(
|
||||
repo.productDetail('p1'),
|
||||
throwsA(isA<ApiException>().having((error) => error.code, '代码', 1714)),
|
||||
);
|
||||
});
|
||||
}
|
||||
92
apps/user_app/test/data/product_recommendations_test.dart
Normal file
92
apps/user_app/test/data/product_recommendations_test.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// 功能描述:推荐分页、整数金额、会话归属与登录失效回归;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/product_recommendations.dart';
|
||||
|
||||
void main() {
|
||||
final data = <String, Object?>{
|
||||
'page': 2,
|
||||
'has_more': false,
|
||||
'items': [
|
||||
{
|
||||
'identity': 'p',
|
||||
'name': '商品',
|
||||
'price_amount': 1999,
|
||||
'stock_quantity': 2,
|
||||
'image_url': 'javascript:alert(1)',
|
||||
},
|
||||
],
|
||||
};
|
||||
test('按入口分页,真实整数金额和图片协议校验', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'test',
|
||||
client: MockClient((request) async {
|
||||
expect(request.url.queryParameters, {
|
||||
'source': 'favorites',
|
||||
'page': '2',
|
||||
'page_size': '2',
|
||||
});
|
||||
expect(request.headers['authorization'], 'test');
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': data}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final result = await repo.recommendations(source: 'favorites', page: 2);
|
||||
expect(result.items.single.price, 1999);
|
||||
expect(result.items.single.imageUrl, '');
|
||||
expect(
|
||||
() => ProductRecommendations.fromJson({
|
||||
...data,
|
||||
'items': [
|
||||
{'identity': 'p', 'name': '商品', 'price_amount': 1.9, 'stock_quantity': 2},
|
||||
],
|
||||
}, (s) => s),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => ProductRecommendations.fromJson({...data, 'items': [], 'has_more': true}, (s) => s),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
test('迟到的旧账号推荐被丢弃,401交由会话退出处理', () async {
|
||||
var token = 'old', expired = false;
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((_) async {
|
||||
token = 'new';
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': data}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
repo.recommendations(source: 'cart', page: 2),
|
||||
throwsA(isA<SessionExpiredException>()),
|
||||
);
|
||||
final unauthorized = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
onUnauthorized: (_) => expired = true,
|
||||
client: MockClient((_) async => http.Response('{"code":1301}', 401)),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
unauthorized.recommendations(source: 'cart'),
|
||||
throwsA(isA<SessionExpiredException>()),
|
||||
);
|
||||
expect(expired, isTrue);
|
||||
});
|
||||
}
|
||||
45
apps/user_app/test/data/recharge_agreement_test.dart
Normal file
45
apps/user_app/test/data/recharge_agreement_test.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
// 功能描述:充值协议按实际版本提交并校验确认结果;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/recharge.dart';
|
||||
import 'package:user_app/domain/models/client_models.dart';
|
||||
|
||||
class _Repo extends ClientRepository {
|
||||
_Repo(super.api);
|
||||
@override
|
||||
Future<UserProfile> profile() async =>
|
||||
const UserProfile(identity: 'alice', name: '', phone: '', avatar: '');
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('确认使用稳定账号版本请求号,拒绝错误版本响应', () async {
|
||||
var version = 2;
|
||||
final repo = _Repo(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.method, 'POST');
|
||||
expect(request.url.path, '/heqi/client/v1/user/contents/read-confirmations');
|
||||
final body = jsonDecode(request.body) as Map;
|
||||
expect(body['content_version'], 2);
|
||||
expect(body['request_no'], 'consent:alice:agreement:2');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': {'identity': 'read', 'content_version': version},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
const agreement = RechargeAgreement('agreement', 2, '充值协议', '正文');
|
||||
await repo.confirmRechargeAgreement(agreement);
|
||||
version = 3;
|
||||
await expectLater(repo.confirmRechargeAgreement(agreement), throwsA(isA<ApiException>()));
|
||||
});
|
||||
}
|
||||
21
apps/user_app/test/data/repair_draft_store_test.dart
Normal file
21
apps/user_app/test/data/repair_draft_store_test.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
// 功能描述:安全存储中的账户/环境草稿隔离与独立清理。
|
||||
// 版本:1.0.0。
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:user_app/data/services/repair_draft_store.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test('账户和API环境分别隔离,删除不影响其他草稿', () async {
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
final store = SecureRepairDraftStore();
|
||||
await store.write('dev#alice', {'schema': 1, 'request_no': 'a'});
|
||||
await store.write('dev#bob', {'schema': 1, 'request_no': 'b'});
|
||||
await store.write('other#alice', {'schema': 1, 'request_no': 'c'});
|
||||
expect((await store.read('dev#alice'))!['request_no'], 'a');
|
||||
await store.delete('dev#alice');
|
||||
expect(await store.read('dev#alice'), isNull);
|
||||
expect((await store.read('dev#bob'))!['request_no'], 'b');
|
||||
expect((await store.read('other#alice'))!['request_no'], 'c');
|
||||
});
|
||||
}
|
||||
70
apps/user_app/test/data/shop_order_detail_test.dart
Normal file
70
apps/user_app/test/data/shop_order_detail_test.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
// 功能描述:订单详情价格精度、公开标识和会话归属校验;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/shop_order_detail.dart';
|
||||
|
||||
void main() {
|
||||
final data = <String, Object?>{
|
||||
'identity': 'order',
|
||||
'order_no': 'EC123',
|
||||
'status_name': '待付款',
|
||||
'allowed_actions': ['cancel'],
|
||||
'items': [
|
||||
{
|
||||
'identity': 'item',
|
||||
'name': '历史商品',
|
||||
'quantity': 2,
|
||||
'sale_amount': 1999,
|
||||
'image_url': 'javascript:alert(1)',
|
||||
},
|
||||
],
|
||||
'product_amount': 3998,
|
||||
'discount_amount': 0,
|
||||
'payable_amount': 3998,
|
||||
};
|
||||
test('成交单价为整数分,详情标识不匹配不得展示', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.headers['authorization'], 'token');
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': data}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final order = await repo.shopOrderDetail('order');
|
||||
expect(order.items.single.amount, 3998);
|
||||
expect(order.items.single.imageUrl, '');
|
||||
expect(order.summary.items.single.identity, 'item');
|
||||
await expectLater(repo.shopOrderDetail('other'), throwsA(isA<ApiException>()));
|
||||
expect(
|
||||
() => ShopOrderDetail.fromJson({...data, 'payable_amount': 39.98}, (s) => s),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
test('切换账号丢弃迟到订单响应', () async {
|
||||
var token = 'old';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((_) async {
|
||||
token = 'new';
|
||||
return http.Response(
|
||||
jsonEncode({'code': 0, 'details': data}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.shopOrderDetail('order'), throwsA(isA<SessionExpiredException>()));
|
||||
});
|
||||
}
|
||||
93
apps/user_app/test/data/wallet_account_test.dart
Normal file
93
apps/user_app/test/data/wallet_account_test.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
// 功能描述:银行卡与提现接口契约、金额精度和会话隔离测试;版本:1.0.0。
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/wallet_account.dart';
|
||||
|
||||
void main() {
|
||||
test('提现金额按整数分解析,不接受超两位小数和指数', () {
|
||||
expect(parseWithdrawalAmount('300'), 30000);
|
||||
expect(parseWithdrawalAmount('10.08'), 1008);
|
||||
expect(parseWithdrawalAmount('1.001'), isNull);
|
||||
expect(parseWithdrawalAmount('1e3'), isNull);
|
||||
});
|
||||
|
||||
test('银行卡列表只读取服务端脱敏字段', () async {
|
||||
var requestedPath = '';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((request) async {
|
||||
requestedPath = request.url.path;
|
||||
return http.Response.bytes(
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': [
|
||||
{
|
||||
'identity': 'b1',
|
||||
'card_no_masked': '**** **** **** 2868',
|
||||
'bank_name': '中国建设银行',
|
||||
'card_owner': '张先生',
|
||||
'bank_type': 'debit',
|
||||
'is_default': true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final bank = (await repo.walletBanks()).single;
|
||||
expect(requestedPath, '/heqi/client/v1/user/wallet/banks');
|
||||
expect(bank.maskedNumber, endsWith('2868'));
|
||||
expect(bank.typeName, '储蓄卡');
|
||||
expect(bank.isDefault, isTrue);
|
||||
});
|
||||
|
||||
test('提现请求使用公开银行卡标识、整数分和幂等号', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 'token',
|
||||
client: MockClient((request) async {
|
||||
expect(request.method, 'POST');
|
||||
expect(request.url.path, '/heqi/client/v1/user/wallet/withdrawals');
|
||||
expect(jsonDecode(request.body), {
|
||||
'bank_identity': 'b1',
|
||||
'amount': 30000,
|
||||
'request_no': 'request-1',
|
||||
'payment_password': '123456',
|
||||
});
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'code': 0,
|
||||
'details': {
|
||||
'identity': 'w1',
|
||||
'cash_no': 'WD1',
|
||||
'amount': 30000,
|
||||
'fee': 0,
|
||||
'apply_status': 10,
|
||||
'created_at': '2026-09-11T12:00:00+08:00',
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
final result = await repo.createWalletWithdrawal(
|
||||
bankIdentity: 'b1',
|
||||
amount: 30000,
|
||||
requestNo: 'request-1',
|
||||
paymentPassword: '123456',
|
||||
);
|
||||
expect(result.statusName, '待审核');
|
||||
});
|
||||
}
|
||||
73
apps/user_app/test/data/wallet_bill_test.dart
Normal file
73
apps/user_app/test/data/wallet_bill_test.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
// 功能描述:账单金额严格解析、筛选请求和过期会话保护;版本:1.0.0。
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:user_app/data/repositories/client_repository.dart';
|
||||
import 'package:user_app/data/services/api_client.dart';
|
||||
import 'package:user_app/domain/models/wallet_bill.dart';
|
||||
|
||||
void main() {
|
||||
test('钱包缺失或小数金额不能显示为零余额', () async {
|
||||
for (final data in [
|
||||
'{}',
|
||||
'{"balance":1.5,"withdrawal_balance":0}',
|
||||
'{"balance":0,"withdrawal_balance":1}',
|
||||
]) {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 't',
|
||||
client: MockClient((_) async => http.Response('{"code":0,"details":$data}', 200)),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.wallet(), throwsA(isA<ApiException>()));
|
||||
}
|
||||
});
|
||||
final valid = <String, Object?>{
|
||||
'identity': 'r1',
|
||||
'direction': 'expense',
|
||||
'amount': 100,
|
||||
'fee': 0,
|
||||
'balance_after': 500,
|
||||
'created_at': '2026-09-08T08:00:00Z',
|
||||
};
|
||||
test('金额和收支不推测、不截断为整数', () {
|
||||
expect(WalletBill.fromJson(valid).signedAmount, '-¥1.00');
|
||||
for (final patch in [
|
||||
{'amount': 1.5},
|
||||
{'amount': -1},
|
||||
{'fee': '0'},
|
||||
{'direction': 'unknown'},
|
||||
{'created_at': ''},
|
||||
]) {
|
||||
expect(() => WalletBill.fromJson({...valid, ...patch}), throwsFormatException);
|
||||
}
|
||||
});
|
||||
test('账单筛选和游标传递,不将错误响应当空列表', () async {
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => 't',
|
||||
client: MockClient((r) async {
|
||||
expect(r.url.queryParameters, {'direction': 'income', 'cursor': '50'});
|
||||
return http.Response('{"code":0,"details":{}}', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
repo.walletBills(direction: 'income', cursor: '50'),
|
||||
throwsA(isA<ApiException>()),
|
||||
);
|
||||
});
|
||||
test('切换账户后迟到的账单不可显示', () async {
|
||||
var token = 'alice';
|
||||
final repo = ClientRepository(
|
||||
ApiClient(
|
||||
() => token,
|
||||
client: MockClient((r) async {
|
||||
token = 'bob';
|
||||
return http.Response('{"code":0,"details":{"items":[],"next_cursor":""}}', 200);
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expectLater(repo.walletBills(), throwsA(isA<SessionExpiredException>()));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user