252779c607
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 24s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 19s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 45s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 37s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 15s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m41s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 12m35s
根因(购买按钮全端「点了没反应」): - 生产 pay-v2 的 CNY 渠道是哪吒(nezha)聚合(底层支付宝/微信),客户端却给 CNY 发 method=alipay → pay 不认 → POST /api/v2/orders 400。USDT/crypto 一直正常。 - 且 _buy 只在 awaitingPayment 时导航,下单失败(phase=failed)既不导航也不弹错, 非 Auth 异常还直接从 start() 抛出未捕获 → 用户看着「没反应」。 改: - PayChannelX.method:cny 'alipay' → 'nezha'(usdt 仍 crypto) - payment_provider._metadata 法币条件同步 nezha;start() 补 catch(_) 兜非 Auth 异常 落 failed(状态机不再卡 creating) - purchase_page._buy:phase==failed 时 showPangolinToast 显式提示 - payment_page 换支付方式的另一渠道 alipay → nezha - 测试钉:PayChannel.cny.method == 'nezha'(发错 method 会被 pay 400) nezha 返回 render_type=redirect(payload.url),客户端现成 redirect 分支直接接; orders_page 早有 'nezha'→payMethodNezha 标签。服务端无需改(method 透传、 SettlementCurrency 只区分 crypto vs 法币)。 验证:flutter analyze 干净;payment 相关 18 测试全过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.4 KiB
Dart
83 lines
3.4 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('PayChannel 渠道→method 映射:usdt→crypto / cny→nezha(生产渠道码)', () {
|
|
// 回归钉:CNY 生产走哪吒(nezha)聚合,不是直连 alipay——发错 method 会被 pay 回 400。
|
|
expect(PayChannel.usdt.method, 'crypto');
|
|
expect(PayChannel.cny.method, 'nezha');
|
|
expect(PayChannel.usdt.currency, 'USDT');
|
|
expect(PayChannel.cny.currency, 'CNY');
|
|
});
|
|
|
|
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');
|
|
}
|
|
});
|
|
}
|