Files
pangolin/client/test/unit/payment_api_test.dart
T
wangjia 272eca04ce feat(client): pay v2 支付渠道 switch + 订单列表/详情端到端
购买/支付(Phase B):
- 新 SegSwitch 段控 widget(镜像 .segswitch 原子)
- 购买页重构:顶部支付渠道 switch(默认 USDT→加密货币 / 人民币→支付宝),套餐价随渠道分币种显示,删底部弹窗选方式;点购买直达支付页
- 支付页加订单信息卡(套餐/渠道/金额按币种)
- models:PayChannel + PayCatalogItem.priceUsdtMicro/priceLabel(ch)/perMonthLabel(ch)

订单(P0,全新):
- payment_api.listOrders() + orders_provider(列表 + 详情 family)
- OrdersScreen 列表→详情(状态 pill / 可复制订单号 / 继续支付·重新购买)
- 导航:NavView.orders + desktop_shell 分支 + account 入口

配套:
- l10n +19 getter(支付渠道 4 + 订单 15)×6 语言,l10n 闸绿
- 图标单源迁移 lucide_icons_flutter + pangolin_icons.dart(codegen)
- 联系渠道订正(Telegram/邮件 support@yanmeiai.com/官网) + 前端去广告&时长
- 测试:购买 switch 分币种 + 订单列表/空态/详情 + 假 API listOrders;golden 重录
- flutter analyze 0 error,flutter test 226 全绿

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-12 00:07:15 +08:00

75 lines
3.0 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:pangolin_vpn/services/api_client.dart';
import 'package:pangolin_vpn/services/auth_api.dart';
import 'package:pangolin_vpn/models/payment.dart';
import 'package:pangolin_vpn/services/payment_api.dart';
ApiClient _client(MockClient mock) => ApiClient(
baseUrl: 'http://x',
getToken: () => 'tok1',
refresh: () async => false,
client: mock,
);
void main() {
test('catalog 解析三档', () async {
final api = PaymentApi(_client(MockClient((req) async {
expect(req.url.path, '/v1/pay/catalog');
return http.Response(
'{"items":[{"sku":"pro_month","plan":"pro","days":31,"price_minor":2999,"currency":"CNY"},'
'{"sku":"pro_quarter","plan":"pro","days":92,"price_minor":6888,"currency":"CNY"},'
'{"sku":"pro_year","plan":"pro","days":366,"price_minor":19999,"currency":"CNY"}]}',
200);
})));
final items = await api.catalog();
expect(items.length, 3);
expect(items.first.sku, 'pro_month');
expect(items.first.priceLabel(PayChannel.cny), '¥29.99');
});
test('createOrder 只传 sku+method+metadata,解析 session', () async {
final api = PaymentApi(_client(MockClient((req) async {
expect(req.url.path, '/v1/pay/orders');
expect(req.body.contains('"amount'), isFalse, reason: '客户端绝不传金额');
return http.Response(
'{"order_no":"pay1","session":{"render_type":"crypto_address",'
'"payload":{"address":"Txx","amount":"4.201234","amount_minor":4201234,'
'"currency":"USDT","network":"TRC20"},"expires_at":"2026-07-10T12:00:00Z"}}',
200);
})));
final order = await api.createOrder(sku: 'pro_month', method: 'crypto');
expect(order.orderNo, 'pay1');
expect(order.session.renderType, 'crypto_address');
expect(order.session.payload['address'], 'Txx');
expect(order.session.expiresAt, isNotNull);
});
test('orderStatus 解析 activated/expires_at', () async {
final api = PaymentApi(_client(MockClient((req) async => http.Response(
'{"order_no":"pay1","pay_status":"succeeded","activated":true,'
'"expires_at":"2026-08-10T12:00:00Z"}',
200))));
final st = await api.orderStatus('pay1');
expect(st.activated, isTrue);
expect(st.payStatus, 'succeeded');
expect(st.expiresAt, isNotNull);
});
test('retry 409 CURRENCY_MISMATCH 抛带 code 的异常', () async {
final api = PaymentApi(_client(MockClient((req) async => http.Response(
'{"code":"CURRENCY_MISMATCH","message_zh":"该支付方式结算币种与订单不符,请重新下单",'
'"message_en":"mismatch"}',
409,
headers: {'content-type': 'application/json; charset=utf-8'}))));
try {
await api.retry('pay1', method: 'alipay');
fail('应抛异常');
} on AuthApiException catch (e) {
expect(e.statusCode, 409);
expect(e.code, 'CURRENCY_MISMATCH');
}
});
}