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>
63 lines
2.0 KiB
Dart
63 lines
2.0 KiB
Dart
// api_client_test.dart — ApiClient 鉴权 + 401 自动刷新重试。
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/testing.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:pangolin_vpn/services/api_client.dart';
|
|
import 'package:pangolin_vpn/services/auth_api.dart';
|
|
|
|
void main() {
|
|
group('ApiClient', () {
|
|
test('GET 附带 Bearer 并解析 JSON', () async {
|
|
late String seenAuth;
|
|
final c = ApiClient(
|
|
baseUrl: 'http://x',
|
|
getToken: () => 'tok1',
|
|
refresh: () async => false,
|
|
client: MockClient((req) async {
|
|
seenAuth = req.headers['Authorization'] ?? '';
|
|
return http.Response('{"email":"a@b.c"}', 200);
|
|
}),
|
|
);
|
|
final body = await c.getJson('/v1/me');
|
|
expect(seenAuth, 'Bearer tok1');
|
|
expect(body['email'], 'a@b.c');
|
|
});
|
|
|
|
test('401 → refresh 成功 → 用新令牌重试一次', () async {
|
|
var token = 'old';
|
|
var calls = 0;
|
|
final c = ApiClient(
|
|
baseUrl: 'http://x',
|
|
getToken: () => token,
|
|
refresh: () async {
|
|
token = 'new';
|
|
return true;
|
|
},
|
|
client: MockClient((req) async {
|
|
calls++;
|
|
if (req.headers['Authorization'] == 'Bearer old') {
|
|
return http.Response('{"message_zh":"expired"}', 401);
|
|
}
|
|
return http.Response('{"ok":true}', 200);
|
|
}),
|
|
);
|
|
final body = await c.getJson('/v1/me');
|
|
expect(calls, 2); // 首次 401 + 刷新后重试
|
|
expect(body['ok'], true);
|
|
});
|
|
|
|
test('401 → refresh 失败 → 抛 AuthApiException(401)', () async {
|
|
final c = ApiClient(
|
|
baseUrl: 'http://x',
|
|
getToken: () => 'old',
|
|
refresh: () async => false,
|
|
client: MockClient((req) async => http.Response('{"message_zh":"expired"}', 401)),
|
|
);
|
|
expect(
|
|
() => c.getJson('/v1/me'),
|
|
throwsA(isA<AuthApiException>().having((e) => e.statusCode, 'status', 401)),
|
|
);
|
|
});
|
|
});
|
|
}
|