ba53a0d478
- api_config.dart:API 基址单源(消除各处重复 _kApiUrl) - api_client.dart:统一鉴权 HTTP 客户端,401→AuthNotifier.refresh→重试一次, 错误统一 AuthApiException;+ 单测覆盖 401 刷新重试 - 模型 me/device/plan/usage_point(对齐后端 snake_case 契约) - account_api.dart:/v1/me、/v1/plans、/v1/me/devices(列表+删)、/v1/usage、 /v1/redeem、/v1/ads/unlock 封装 - auth_provider:注入 AuthApi + refresh();account_providers 装配 ApiClient/AccountApi - 顺带修复 onboarding 提交遗留的测试 stub(_NullTokenStore 缺 isOnboarded/markOnboarded) flutter analyze 0 error;114 tests passed。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.9 KiB
Dart
72 lines
2.9 KiB
Dart
// nodes_provider_test.dart — 节点选择 / 智能选择派生
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:pangolin_vpn/l10n/app_text.dart';
|
||
import 'package:pangolin_vpn/models/node.dart';
|
||
import 'package:pangolin_vpn/services/token_store.dart';
|
||
import 'package:pangolin_vpn/state/auth_provider.dart';
|
||
import 'package:pangolin_vpn/state/nodes_provider.dart';
|
||
|
||
// ── Stub TokenStore(无平台依赖)──────────────────────────────────
|
||
|
||
class _NullTokenStore implements TokenStore {
|
||
const _NullTokenStore();
|
||
@override
|
||
Future<void> saveTokens({required String access, required String refresh}) async {}
|
||
@override
|
||
Future<String?> loadAccessToken() async => null;
|
||
@override
|
||
Future<String?> loadRefreshToken() async => null;
|
||
@override
|
||
Future<void> clear() async {}
|
||
@override
|
||
Future<void> markOnboarded() async {}
|
||
@override
|
||
Future<bool> isOnboarded() async => true;
|
||
}
|
||
|
||
// ── 辅助:带 stub 覆盖的 ProviderContainer ──────────────────────
|
||
|
||
ProviderContainer makeContainer({List<Override> overrides = const []}) =>
|
||
ProviderContainer(overrides: [
|
||
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
|
||
// effectiveNodeProvider 在 nodesProvider 加载时退回 kDemoNodes,
|
||
// 所以测试直接读 effectiveNodeProvider,无需等待 nodesProvider.future。
|
||
...overrides,
|
||
]);
|
||
|
||
void main() {
|
||
test('默认智能选择(AUTO)', () {
|
||
final c = makeContainer();
|
||
addTearDown(c.dispose);
|
||
expect(c.read(selectedNodeCodeProvider), kSmartNodeCode);
|
||
expect(c.read(isSmartSelectProvider), true);
|
||
});
|
||
|
||
test('智能选择取延迟最小节点(退回 kDemoNodes)', () {
|
||
final c = makeContainer();
|
||
addTearDown(c.dispose);
|
||
// nodesProvider 加载中时退回 kDemoNodes,effectiveNodeProvider 立即可用。
|
||
final node = c.read(effectiveNodeProvider);
|
||
final minPing = kDemoNodes.map((n) => n.ping).reduce((a, b) => a < b ? a : b);
|
||
expect(node.ping, minPing);
|
||
});
|
||
|
||
test('选中具体节点后生效节点随之改变', () {
|
||
final c = makeContainer();
|
||
addTearDown(c.dispose);
|
||
c.read(selectedNodeCodeProvider.notifier).state = 'JP';
|
||
expect(c.read(isSmartSelectProvider), false);
|
||
expect(c.read(effectiveNodeProvider).code, 'JP');
|
||
});
|
||
|
||
test('localizedSub 不串语言:无标签用拉丁地名', () {
|
||
const tw = Node(code: 'TW', nameZh: '台湾 台北', nameEn: 'Taipei', ping: 28);
|
||
expect(tw.localizedSub(AppLang.zh), 'Taipei');
|
||
expect(tw.localizedSub(AppLang.en), 'Taipei');
|
||
const hk = Node(code: 'HK', nameZh: '香港 · 流媒体', nameEn: 'Hong Kong', ping: 18, tag: NodeTag.streaming);
|
||
expect(hk.localizedSub(AppLang.zh), '流媒体优化');
|
||
expect(hk.localizedSub(AppLang.en), 'Streaming');
|
||
});
|
||
}
|