fix:参考金值,研听,研值工程通用通用工程公共能力,重点对比网络请求、登录认证、状态管理、本地存储、WebView、推送、版本更新、一键登录、主题/UI 等模块,确定后续统一整改方案

This commit is contained in:
jingyun committed 2026-09-10 09:52:36 +08:00
1 parent 6997a65c96
commit d364c6d908
46 files changed
+2060 -395

No files matched your search

+97 -5
View File
@@ -1,9 +1,14 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart';
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart';
import 'package:jinzhi_flutter_app/features/auth/application/auth_session_coordinator.dart';
import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart';
import 'package:jinzhi_flutter_app/features/assets/data/mock_asset_repository.dart';
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
import 'package:shared_preferences/shared_preferences.dart';
@@ -61,7 +66,7 @@ void main() {
await fixture.controller.retryBootstrap();
expect(fixture.controller.state.value, isA<GuestAuthState>());
expect(fixture.controller.state.value, isA<SessionExpiredAuthState>());
expect(await fixture.identity.readJwt(), isNull);
});
@@ -81,6 +86,59 @@ void main() {
expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse);
});
test('restores cached profile before the background profile reply', () async {
final fixture = await _Fixture.create();
await fixture.identity.saveJwt('token');
await fixture.preferences.setString(storageUserId, 'cached-user');
await fixture.preferences.setString(storageUserPhone, '13900139000');
fixture.profileCompleter = Completer<void>();
final bootstrap = fixture.controller.retryBootstrap();
await Future<void>.delayed(Duration.zero);
final stale = fixture.controller.state.value! as LoggedInAuthState;
expect(stale.user.id, 'cached-user');
expect(stale.isVerified, isFalse);
fixture.profileCompleter!.complete();
await bootstrap;
expect(
(fixture.controller.state.value! as LoggedInAuthState).isVerified,
isTrue,
);
});
test('keeps cached login on 403, 429, 5xx, and decode failures', () async {
for (final failure in [403, 429, 503, -1]) {
final fixture = await _Fixture.create();
await fixture.identity.saveJwt('token');
await fixture.preferences.setString(storageUserId, 'cached-user');
fixture.profileFailureStatus = failure;
await fixture.controller.retryBootstrap();
expect(fixture.controller.state.value, isA<LoggedInAuthState>());
expect(await fixture.identity.readJwt(), 'token');
}
});
test('logout preserves assets while account deletion removes them', () async {
final logoutFixture = await _Fixture.create();
await logoutFixture.identity.saveJwt('token');
await logoutFixture.preferences.setString(storageJinzhiHoldings, 'assets');
await logoutFixture.controller.logout();
expect(
logoutFixture.preferences.getString(storageJinzhiHoldings),
'assets',
);
final deleteFixture = await _Fixture.create();
await deleteFixture.identity.saveJwt('token');
await deleteFixture.preferences.setString(storageJinzhiHoldings, 'assets');
await deleteFixture.controller.deleteAccount('123456');
expect(deleteFixture.preferences.getString(storageJinzhiHoldings), isNull);
});
test('keeps the verified one-key login state shape', () async {
final fixture = await _Fixture.create();
@@ -106,20 +164,47 @@ class _Fixture {
bool failDeletion = false;
bool failProfile = false;
bool profileUnauthorized = false;
int? profileFailureStatus;
Completer<void>? profileCompleter;
String? deletedWithCode;
static Future<_Fixture> create() async {
final preferences = await SharedPreferences.getInstance();
final identity = SecureIdentityStorage(
preferences,
PreferencesStorage(preferences),
secureStorage: _MemorySecureStorage(),
);
late _Fixture fixture;
final dio = Dio()
..interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
onRequest: (options, handler) async {
if (options.path == '/api/v1/me' && options.method == 'GET') {
if (fixture.profileCompleter case final completer?) {
await completer.future;
}
if (fixture.profileFailureStatus case final status?) {
if (status == -1) {
handler.resolve(
Response<List<int>>(
requestOptions: options,
statusCode: 200,
data: const [255],
),
);
return;
}
handler.reject(
DioException(
requestOptions: options,
response: Response<void>(
requestOptions: options,
statusCode: status,
),
),
);
return;
}
if (fixture.failProfile) {
handler.reject(DioException(requestOptions: options));
return;
@@ -185,10 +270,17 @@ class _Fixture {
},
),
);
final preferencesStorage = PreferencesStorage(preferences);
final sessionStorage = AuthSessionStorage(identity);
final coordinator = AuthSessionCoordinator(
sessionStorage,
preferencesStorage,
MockAssetRepository(preferencesStorage),
);
final controller = AuthController(
dio,
identityStorage: identity,
preferences: preferences,
preferences: preferencesStorage,
coordinator: coordinator,
autoBootstrap: false,
);
fixture = _Fixture(
@@ -0,0 +1,126 @@
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/common/network/api_exception.dart';
import 'package:jinzhi_flutter_app/common/storage/preferences_storage.dart';
import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart';
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
import 'package:jinzhi_flutter_app/features/auth/application/auth_session_coordinator.dart';
import 'package:jinzhi_flutter_app/features/assets/data/mock_asset_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
test(
'concurrent unauthorized responses expire one session only once',
() async {
SharedPreferences.setMockInitialValues({});
final preferences = PreferencesStorage(
await SharedPreferences.getInstance(),
);
final identity = SecureIdentityStorage(
preferences,
secureStorage: _MemorySecureStorage(),
);
final session = AuthSessionStorage(identity);
final assets = MockAssetRepository(preferences);
final coordinator = AuthSessionCoordinator(session, preferences, assets);
await coordinator.startSession('token');
final generation = coordinator.generation;
var notifications = 0;
coordinator.addListener((_) => notifications++);
final request = RequestOptions(
path: '/protected',
extra: {authSessionGenerationExtra: generation},
);
final exception = ApiException(
kind: ApiExceptionKind.backend,
httpStatus: 400,
reason: 'TOKEN_EXPIRED',
requestOptions: request,
);
await Future.wait([
coordinator.expireIfNeeded(exception),
coordinator.expireIfNeeded(exception),
]);
expect(notifications, 1);
expect(await identity.readJwt(), isNull);
expect(await coordinator.expireIfNeeded(exception), isFalse);
},
);
test('account deletion upgrades an in-flight expiration', () async {
SharedPreferences.setMockInitialValues({
storageUserId: 'user-1',
storageUserPhone: '13800138000',
storageJinzhiHoldings: 'legacy-assets',
});
final preferences = PreferencesStorage(
await SharedPreferences.getInstance(),
);
final secure = _MemorySecureStorage({
secureBiometricCredential: 'biometric',
});
final identity = SecureIdentityStorage(preferences, secureStorage: secure);
final session = AuthSessionStorage(identity);
final assets = MockAssetRepository(preferences);
final accountScope = MockAssetRepository.accountScope(userId: 'user-1');
await assets.saveHoldings(accountScope, assets.loadHoldings(accountScope));
final coordinator = AuthSessionCoordinator(session, preferences, assets);
await coordinator.startSession('token');
final notifications = <SessionEndReason>[];
coordinator.addListener(notifications.add);
final expiring = coordinator.end(SessionEndReason.expired);
final deleting = coordinator.accountDeleted();
await Future.wait([expiring, deleting]);
expect(notifications, [SessionEndReason.accountDeleted]);
expect(await secure.read(secureBiometricCredential), isNull);
expect(assets.loadHoldings(accountScope), isEmpty);
expect(preferences.containsKey(storageJinzhiHoldings), isFalse);
});
test('a new login waits for an in-flight session end', () async {
SharedPreferences.setMockInitialValues({});
final preferences = PreferencesStorage(
await SharedPreferences.getInstance(),
);
final identity = SecureIdentityStorage(
preferences,
secureStorage: _MemorySecureStorage(),
);
final session = AuthSessionStorage(identity);
final coordinator = AuthSessionCoordinator(
session,
preferences,
MockAssetRepository(preferences),
);
final accountScope = MockAssetRepository.accountScope(userId: 'user-1');
await preferences.setString(storageUserId, 'user-1');
final assets = MockAssetRepository(preferences);
await assets.saveHoldings(accountScope, assets.loadHoldings(accountScope));
await coordinator.startSession('old-token');
final ending = coordinator.logout();
final starting = coordinator.startSession('new-token');
await Future.wait([ending, starting]);
expect(session.accessToken, 'new-token');
expect(await identity.readJwt(), 'new-token');
expect(assets.loadHoldings(accountScope), isNotEmpty);
});
}
class _MemorySecureStorage implements SecureKeyValueStorage {
_MemorySecureStorage([Map<String, String>? initial]) : values = {...?initial};
final Map<String, String> values;
@override
Future<void> delete(String key) async => values.remove(key);
@override
Future<String?> read(String key) async => values[key];
@override
Future<void> write(String key, String value) async => values[key] = value;
}