import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:fruitshorts/core/data/remote/api_client.dart'; import 'package:fruitshorts/core/data/remote/auth_api.dart'; import 'package:fruitshorts/core/data/services/auth_controller.dart'; import 'package:fruitshorts/core/data/services/auth_session_store.dart'; import '../support/fake_http_adapter.dart'; /// 用假的 dio adapter 覆盖四条关键路径:注册成功、401 → 刷新 → 重试、刷新失败 → 清会话、登出。 /// /// 网络层从 http 换成 dio 后,这里的**断言口径一条没变**,只是注入方式从 /// `http.testing.MockClient` 换成 `FakeHttpAdapter`。 void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); }); String tokenBody({ required String accessToken, required String refreshToken, String uid = 'U1', }) { return jsonEncode({ 'code': 0, 'message': 'ok', 'data': { 'accessToken': accessToken, 'refreshToken': refreshToken, 'expiresIn': 7200, 'user': {'uid': uid, 'nickname': 'Tester', 'avatarUrl': ''}, }, }); } ApiClient clientWith( AuthController Function() auth, { required FakeHttpAdapter adapter, }) { final store = AuthSessionStore(); return ApiClient( baseUrl: 'http://127.0.0.1:8081', adapter: adapter, accessTokenReader: () => auth().accessToken, tokenRefresher: () => auth().refresh(), onUnauthorized: () => auth().clear(), deviceIdReader: store.deviceId, ); } test('should_store_session_and_expose_signed_in_state_when_register_succeeds', () async { late AuthController auth; final adapter = FakeHttpAdapter((options) async { expect(options.uri.path, '/app/auth/register'); expect(options.headers['X-Device-Id'], isNotEmpty); expect(options.headers.containsKey('Authorization'), isFalse); return FakeHttpResponse(tokenBody(accessToken: 'a1', refreshToken: 'r1')); }); final store = AuthSessionStore(); auth = AuthController( AuthApi(clientWith(() => auth, adapter: adapter)), store, ); expect(auth.isSignedIn, isFalse); await auth.register(email: 'a@b.com', password: 'abcd1234'); expect(auth.isSignedIn, isTrue); expect(auth.accessToken, 'a1'); expect(auth.user?.nickname, 'Tester'); // 落盘:新建一个 store 也能读回来 expect((await AuthSessionStore().read())?.refreshToken, 'r1'); }); test('should_refresh_once_and_retry_with_new_token_when_response_code_is_401', () async { var calls = 0; late AuthController auth; final adapter = FakeHttpAdapter((options) async { calls++; if (options.uri.path == '/app/auth/refresh') { return FakeHttpResponse(tokenBody(accessToken: 'a2', refreshToken: 'r2')); } if (options.uri.path != '/app/me') { // 注册等其它请求:直接发第一版令牌 return FakeHttpResponse(tokenBody(accessToken: 'a1', refreshToken: 'r1')); } final token = options.headers['Authorization']; if (token == 'Bearer a1') { return FakeHttpResponse( jsonEncode({'code': 401, 'message': '未登录或登录已过期', 'data': null}), ); } expect(token, 'Bearer a2'); return FakeHttpResponse( jsonEncode({ 'code': 0, 'message': 'ok', 'data': {'hit': true}, }), ); }); final store = AuthSessionStore(); auth = AuthController( AuthApi(clientWith(() => auth, adapter: adapter)), store, ); await auth.register(email: 'a@b.com', password: 'abcd1234'); calls = 0; final api = clientWith(() => auth, adapter: adapter); final data = await api.get('/app/me'); expect((data as Map)['hit'], isTrue); expect(calls, 3); // 原请求(401) + 刷新 + 重试 expect(auth.accessToken, 'a2'); }); test('should_clear_session_when_refresh_itself_fails', () async { late AuthController auth; final adapter = FakeHttpAdapter((options) async { if (options.uri.path == '/app/auth/refresh') { return FakeHttpResponse( jsonEncode({'code': 401, 'message': '登录已过期', 'data': null}), ); } if (options.uri.path == '/app/auth/register') { return FakeHttpResponse(tokenBody(accessToken: 'a1', refreshToken: 'r1')); } return FakeHttpResponse( jsonEncode({'code': 401, 'message': '未登录或登录已过期', 'data': null}), ); }); final store = AuthSessionStore(); auth = AuthController( AuthApi(clientWith(() => auth, adapter: adapter)), store, ); await auth.register(email: 'a@b.com', password: 'abcd1234'); final api = clientWith(() => auth, adapter: adapter); await expectLater(api.get('/app/me'), throwsA(isA())); expect(auth.isSignedIn, isFalse); expect(await AuthSessionStore().read(), isNull); }); test('should_clear_local_session_when_sign_out_hits_server_error', () async { late AuthController auth; final adapter = FakeHttpAdapter((options) async { if (options.uri.path == '/app/auth/logout') { return FakeHttpResponse('{}', 500); } return FakeHttpResponse(tokenBody(accessToken: 'a1', refreshToken: 'r1')); }); final store = AuthSessionStore(); auth = AuthController( AuthApi(clientWith(() => auth, adapter: adapter)), store, ); await auth.register(email: 'a@b.com', password: 'abcd1234'); await auth.signOut(); expect(auth.isSignedIn, isFalse); expect(await AuthSessionStore().read(), isNull); }); }