Files
pangolin/client/test/unit/nodes_provider_test.dart
T
wangjia ba53a0d478
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled
feat(client): P1 真实数据接入地基(#6 6A)
- 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>
2026-06-18 23:32:57 +08:00

72 lines
2.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 加载中时退回 kDemoNodeseffectiveNodeProvider 立即可用。
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');
});
}