import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pangolin_vpn/models/payment.dart'; import 'package:pangolin_vpn/services/auth_api.dart'; import 'package:pangolin_vpn/services/payment_api.dart'; import 'package:pangolin_vpn/state/payment_provider.dart'; class _FakePaymentApi implements PaymentApi { _FakePaymentApi(); int createCalls = 0, cancelCalls = 0, statusCalls = 0; bool activated = false; Exception? retryError; String lastMethod = ''; @override Future> catalog() async => const []; @override Future createOrder({required String sku, required String method, Map? metadata}) async { createCalls++; lastMethod = method; return PayOrder( orderNo: 'pay$createCalls', session: PaySession(renderType: method == 'crypto' ? 'crypto_address' : 'redirect', payload: const {'url': 'https://x'}, expiresAt: null)); } @override Future orderStatus(String orderNo) async { statusCalls++; return PayOrderStatus(orderNo: orderNo, payStatus: activated ? 'succeeded' : 'pending', activated: activated, expiresAt: null); } @override Future retry(String orderNo, {required String method, Map? metadata}) async { if (retryError != null) throw retryError!; return PayOrder(orderNo: orderNo, session: PaySession(renderType: 'redirect', payload: const {'url': 'https://y'}, expiresAt: null)); } @override Future cancel(String orderNo) async { cancelCalls++; } } ProviderContainer _container(_FakePaymentApi api) { final c = ProviderContainer(overrides: [paymentApiProvider.overrideWithValue(api)]); addTearDown(c.dispose); return c; } void main() { const item = PayCatalogItem(sku: 'pro_month', plan: 'pro', days: 31, priceMinor: 2999, currency: 'CNY'); test('start → awaitingPayment,pollOnce 到 activated 变 succeeded', () async { final api = _FakePaymentApi(); final c = _container(api); final ctl = c.read(paymentFlowProvider.notifier); await ctl.start(item, 'crypto'); expect(c.read(paymentFlowProvider).phase, PaymentPhase.awaitingPayment); await ctl.pollOnce(); expect(c.read(paymentFlowProvider).phase, PaymentPhase.awaitingPayment); api.activated = true; await ctl.pollOnce(); expect(c.read(paymentFlowProvider).phase, PaymentPhase.succeeded); }); test('stopPolling() 只停 Timer,不远程 cancel、不重置 state', () async { final api = _FakePaymentApi(); final c = _container(api); final ctl = c.read(paymentFlowProvider.notifier); await ctl.start(item, 'crypto'); expect(c.read(paymentFlowProvider).phase, PaymentPhase.awaitingPayment); ctl.stopPolling(); expect(api.cancelCalls, 0); expect(c.read(paymentFlowProvider).phase, PaymentPhase.awaitingPayment); expect(c.read(paymentFlowProvider).order?.orderNo, 'pay1'); // Timer 已停:再等一拍轮询周期,statusCalls 不应继续增长(pollOnce 只由 // Timer 或测试显式调用驱动,这里断言的是"没有 Timer 在背后继续触发")。 final callsAfterStop = api.statusCalls; await Future.delayed(const Duration(milliseconds: 10)); expect(api.statusCalls, callsAfterStop); }); test('switchMethod 遇 CURRENCY_MISMATCH → cancel 旧单 + 新 method 重新下单', () async { final api = _FakePaymentApi(); final c = _container(api); final ctl = c.read(paymentFlowProvider.notifier); await ctl.start(item, 'crypto'); api.retryError = const AuthApiException( statusCode: 409, messageZh: 'x', messageEn: 'x', code: 'CURRENCY_MISMATCH'); await ctl.switchMethod('alipay'); expect(api.cancelCalls, 1); expect(api.createCalls, 2, reason: '换币种必须新单(pay 契约)'); expect(api.lastMethod, 'alipay'); expect(c.read(paymentFlowProvider).order?.orderNo, 'pay2'); }); }