refactor(auth): Migrate session storage to flutter_secure_storage
This commit is contained in:
@@ -23,7 +23,8 @@ Important areas:
|
|||||||
and client synchronization.
|
and client synchronization.
|
||||||
- `lib/features/launcher/data` contains API, installation, and log services.
|
- `lib/features/launcher/data` contains API, installation, and log services.
|
||||||
- `lib/features/preferences` stores selected install directory and launcher
|
- `lib/features/preferences` stores selected install directory and launcher
|
||||||
session data.
|
session data. Launcher sessions must use secure storage; non-sensitive
|
||||||
|
preferences may use `shared_preferences`.
|
||||||
- `lib/service_container.dart` and `lib/service_container.config.dart` provide
|
- `lib/service_container.dart` and `lib/service_container.config.dart` provide
|
||||||
`get_it`/`injectable` dependency wiring.
|
`get_it`/`injectable` dependency wiring.
|
||||||
- `docs/launcher_web_api_spec.md` documents the current launcher Web API and
|
- `docs/launcher_web_api_spec.md` documents the current launcher Web API and
|
||||||
@@ -52,9 +53,6 @@ flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host
|
|||||||
See `docs/linting.md` for the required format, analyze, test, and Lefthook
|
See `docs/linting.md` for the required format, analyze, test, and Lefthook
|
||||||
pre-commit workflow.
|
pre-commit workflow.
|
||||||
|
|
||||||
In the current shell on 2026-06-18, `flutter` was not recognized by
|
|
||||||
`Get-Command flutter` or `where.exe flutter`.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The launcher API base URL is compile-time configuration:
|
The launcher API base URL is compile-time configuration:
|
||||||
@@ -69,6 +67,8 @@ or local-only API URLs.
|
|||||||
## Architecture Notes
|
## Architecture Notes
|
||||||
|
|
||||||
- All backend calls should go through `LauncherApiClient`.
|
- All backend calls should go through `LauncherApiClient`.
|
||||||
|
- Launcher bearer tokens must be persisted through `flutter_secure_storage`.
|
||||||
|
Do not store sessions or other secrets in `shared_preferences`.
|
||||||
- File sync behavior should stay in `ClientSyncUseCase` and
|
- File sync behavior should stay in `ClientSyncUseCase` and
|
||||||
`GameInstallationService`.
|
`GameInstallationService`.
|
||||||
- The server manifest is the source of truth for client files.
|
- The server manifest is the source of truth for client files.
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ The launcher is responsible for:
|
|||||||
- `flutter_bloc` for launcher UI state
|
- `flutter_bloc` for launcher UI state
|
||||||
- `get_it` and `injectable` for dependency injection
|
- `get_it` and `injectable` for dependency injection
|
||||||
- `dio` for Web API and file download requests
|
- `dio` for Web API and file download requests
|
||||||
- `shared_preferences` for launcher preferences
|
- `flutter_secure_storage` for launcher sessions
|
||||||
|
- `shared_preferences` for non-sensitive launcher preferences
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ See `docs/launcher_web_api_spec.md` for the API and sync contract.
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- `AGENTS.md`: architecture, commands, and repository rules for contributors
|
- `AGENTS.md`: architecture, commands, and repository rules for contributors
|
||||||
- `TODO.md`: refactoring, verification, and Flutter upgrade tasks
|
- `TODO.md`: remaining verification and maintenance tasks
|
||||||
- `docs/linting.md`: format, analyze, test, and Lefthook workflow
|
- `docs/linting.md`: format, analyze, test, and Lefthook workflow
|
||||||
- `docs/project_review.md`: project review with strengths, weaknesses, and
|
- `docs/project_review.md`: project review with strengths, weaknesses, and
|
||||||
upgrade feasibility notes
|
upgrade feasibility notes
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] Run a Windows build smoke test:
|
||||||
|
`flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`.
|
||||||
+14
-20
@@ -51,8 +51,9 @@ most important file verification behavior.
|
|||||||
normalized to UTF-8.
|
normalized to UTF-8.
|
||||||
- At initial review, custom title-bar widgets were duplicated between login and
|
- At initial review, custom title-bar widgets were duplicated between login and
|
||||||
home screens. They have since been extracted into a shared widget.
|
home screens. They have since been extracted into a shared widget.
|
||||||
- Session tokens are persisted through `shared_preferences`. That is simple and
|
- Session tokens were originally persisted through `shared_preferences`. They
|
||||||
testable, but it is not hardened storage for bearer tokens.
|
now use `flutter_secure_storage`, while `shared_preferences` remains limited
|
||||||
|
to non-sensitive launcher preferences.
|
||||||
- The dependency injection output file `lib/service_container.config.dart` is
|
- The dependency injection output file `lib/service_container.config.dart` is
|
||||||
committed, but the regeneration command was not previously documented.
|
committed, but the regeneration command was not previously documented.
|
||||||
- UI/widget/integration coverage is still light compared with the risk in login,
|
- UI/widget/integration coverage is still light compared with the risk in login,
|
||||||
@@ -69,37 +70,30 @@ Assumption: `3.44.2` means Flutter SDK `3.44.2`.
|
|||||||
|
|
||||||
Current local indicators:
|
Current local indicators:
|
||||||
|
|
||||||
- `.fvmrc` pins Flutter `3.35.2`.
|
- `.fvmrc` is absent.
|
||||||
- `pubspec.yaml` declares Dart SDK `^3.9.0`.
|
- `pubspec.yaml` declares Dart SDK `^3.9.0`.
|
||||||
- `pubspec.lock` declares Flutter SDK `>=3.38.0`.
|
- `pubspec.lock` declares Flutter SDK `>=3.38.0`.
|
||||||
- The project policy is latest stable Flutter SDK only; FVM-pinned workflows are
|
- The project policy is latest stable Flutter SDK only; FVM-pinned workflows are
|
||||||
not supported.
|
not supported.
|
||||||
- The local shell cannot find `flutter`: both `Get-Command flutter` and
|
- The local shell now finds Flutter `3.44.2` on the stable channel.
|
||||||
`where.exe flutter` fail.
|
|
||||||
|
|
||||||
Because `flutter` is not available in this shell, the upgrade cannot be fully
|
Flutter `3.44.2` has been validated for dependency resolution, formatting,
|
||||||
validated here. If Flutter is installed, restart the shell or fix PATH so
|
analysis, and tests. A production-like Windows build smoke test remains open.
|
||||||
`flutter --version` works. The code and lockfile already suggest that the
|
|
||||||
project has moved beyond the stale `.fvmrc` pin, so the upgrade is plausible,
|
|
||||||
but it remains a tooling validation task.
|
|
||||||
|
|
||||||
Recommended upgrade path:
|
Recommended upgrade path:
|
||||||
|
|
||||||
1. Install or expose the latest stable Flutter SDK on PATH.
|
1. Keep the latest stable Flutter SDK on PATH.
|
||||||
2. Confirm whether the latest stable Flutter SDK is `3.44.2` or newer.
|
2. Run `flutter pub get`.
|
||||||
3. Remove or ignore stale FVM pinning if it conflicts with the latest-SDK
|
3. Regenerate dependency injection only if dependency resolution or generator
|
||||||
policy.
|
|
||||||
4. Run `flutter pub get`.
|
|
||||||
5. Regenerate dependency injection only if dependency resolution or generator
|
|
||||||
output requires it:
|
output requires it:
|
||||||
`dart run build_runner build --delete-conflicting-outputs`.
|
`dart run build_runner build --delete-conflicting-outputs`.
|
||||||
6. Run `flutter analyze`.
|
4. Run `flutter analyze`.
|
||||||
7. Run a Windows build smoke test:
|
5. Run a Windows build smoke test:
|
||||||
`flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`.
|
`flutter build windows --dart-define=MOONWELL_API_BASE_URL=https://host`.
|
||||||
|
|
||||||
## Recommended Refactoring Direction
|
## Recommended Refactoring Direction
|
||||||
|
|
||||||
- Decide whether bearer tokens can remain in `shared_preferences`; if not,
|
- Keep secure session persistence covered by tests when login or session
|
||||||
migrate session storage to platform-secure storage.
|
restore behavior changes.
|
||||||
- Add CI or a documented local verification workflow that runs analysis and a
|
- Add CI or a documented local verification workflow that runs analysis and a
|
||||||
Windows build smoke test with the latest stable Flutter SDK.
|
Windows build smoke test with the latest stable Flutter SDK.
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:moonwell_launcher/config.dart';
|
import 'package:moonwell_launcher/config.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
@lazySingleton
|
||||||
class LauncherLogService {
|
class LauncherLogService {
|
||||||
static Future<void> _pendingWrite = Future<void>.value();
|
static Future<void> _pendingWrite = Future<void>.value();
|
||||||
|
|
||||||
|
|||||||
+29
-15
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
|
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
|
||||||
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
import 'package:moonwell_launcher/features/preferences/domain/repositories/preferences_repository.dart';
|
||||||
@@ -9,12 +10,15 @@ const _outputDirKey = 'output_dir';
|
|||||||
const _launcherSessionKey = 'launcher_session';
|
const _launcherSessionKey = 'launcher_session';
|
||||||
|
|
||||||
@LazySingleton(as: PreferencesRepository, env: ['flutter'])
|
@LazySingleton(as: PreferencesRepository, env: ['flutter'])
|
||||||
class SharedPrefsPreferencesRepository implements PreferencesRepository {
|
class LocalPreferencesRepository implements PreferencesRepository {
|
||||||
final SharedPreferences _preferences;
|
final SharedPreferences _preferences;
|
||||||
|
final FlutterSecureStorage _secureStorage;
|
||||||
|
|
||||||
const SharedPrefsPreferencesRepository({
|
const LocalPreferencesRepository({
|
||||||
required SharedPreferences preferences,
|
required SharedPreferences preferences,
|
||||||
}) : _preferences = preferences;
|
required FlutterSecureStorage secureStorage,
|
||||||
|
}) : _preferences = preferences,
|
||||||
|
_secureStorage = secureStorage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Uri?> getOutputDir() {
|
Future<Uri?> getOutputDir() {
|
||||||
@@ -33,35 +37,45 @@ class SharedPrefsPreferencesRepository implements PreferencesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<LauncherSession?> getLauncherSession() {
|
Future<LauncherSession?> getLauncherSession() async {
|
||||||
final rawSession = _preferences.getString(_launcherSessionKey);
|
await _maybeClearLegacyLauncherSession();
|
||||||
|
|
||||||
|
final rawSession = await _secureStorage.read(key: _launcherSessionKey);
|
||||||
if (rawSession == null || rawSession.isEmpty) {
|
if (rawSession == null || rawSession.isEmpty) {
|
||||||
return Future.value(null);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(rawSession);
|
final decoded = jsonDecode(rawSession);
|
||||||
if (decoded is! Map) {
|
if (decoded is! Map) {
|
||||||
return Future.value(null);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final json = decoded.map((key, value) => MapEntry(key.toString(), value));
|
final json = decoded.map((key, value) => MapEntry(key.toString(), value));
|
||||||
return Future.value(LauncherSession.fromJson(json));
|
return LauncherSession.fromJson(json);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return Future.value(null);
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> setLauncherSession(LauncherSession session) {
|
Future<void> setLauncherSession(LauncherSession session) async {
|
||||||
return _preferences.setString(
|
await _secureStorage.write(
|
||||||
_launcherSessionKey,
|
key: _launcherSessionKey,
|
||||||
jsonEncode(session.toJson()),
|
value: jsonEncode(session.toJson()),
|
||||||
);
|
);
|
||||||
|
await _maybeClearLegacyLauncherSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> clearLauncherSession() {
|
Future<void> clearLauncherSession() async {
|
||||||
return _preferences.remove(_launcherSessionKey);
|
await _secureStorage.delete(key: _launcherSessionKey);
|
||||||
|
await _maybeClearLegacyLauncherSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _maybeClearLegacyLauncherSession() async {
|
||||||
|
if (_preferences.containsKey(_launcherSessionKey)) {
|
||||||
|
await _preferences.remove(_launcherSessionKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// dart format width=80
|
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// dart format width=80
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// InjectableConfigGenerator
|
// InjectableConfigGenerator
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
|
|
||||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart' as _i558;
|
||||||
import 'package:get_it/get_it.dart' as _i174;
|
import 'package:get_it/get_it.dart' as _i174;
|
||||||
import 'package:injectable/injectable.dart' as _i526;
|
import 'package:injectable/injectable.dart' as _i526;
|
||||||
import 'package:shared_preferences/shared_preferences.dart' as _i460;
|
import 'package:shared_preferences/shared_preferences.dart' as _i460;
|
||||||
@@ -16,10 +17,12 @@ import 'package:shared_preferences/shared_preferences.dart' as _i460;
|
|||||||
import 'features/launcher/application/client_sync_use_case.dart' as _i757;
|
import 'features/launcher/application/client_sync_use_case.dart' as _i757;
|
||||||
import 'features/launcher/data/game_installation_service.dart' as _i315;
|
import 'features/launcher/data/game_installation_service.dart' as _i315;
|
||||||
import 'features/launcher/data/launcher_api_client.dart' as _i703;
|
import 'features/launcher/data/launcher_api_client.dart' as _i703;
|
||||||
import 'features/preferences/data/repositories/shared_prefs_preferences_repository.dart'
|
import 'features/launcher/data/launcher_log_service.dart' as _i43;
|
||||||
as _i662;
|
import 'features/preferences/data/repositories/local_preferences_repository.dart'
|
||||||
|
as _i658;
|
||||||
import 'features/preferences/domain/repositories/preferences_repository.dart'
|
import 'features/preferences/domain/repositories/preferences_repository.dart'
|
||||||
as _i44;
|
as _i44;
|
||||||
|
import 'third_party/flutter_secure_storage.dart' as _i782;
|
||||||
import 'third_party/shared_preferences.dart' as _i1006;
|
import 'third_party/shared_preferences.dart' as _i1006;
|
||||||
|
|
||||||
const String _flutter = 'flutter';
|
const String _flutter = 'flutter';
|
||||||
@@ -32,6 +35,7 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
}) async {
|
}) async {
|
||||||
final gh = _i526.GetItHelper(this, environment, environmentFilter);
|
final gh = _i526.GetItHelper(this, environment, environmentFilter);
|
||||||
final sharedPreferencesModule = _$SharedPreferencesModule();
|
final sharedPreferencesModule = _$SharedPreferencesModule();
|
||||||
|
final flutterSecureStorageModule = _$FlutterSecureStorageModule();
|
||||||
await gh.factoryAsync<_i460.SharedPreferences>(
|
await gh.factoryAsync<_i460.SharedPreferences>(
|
||||||
() => sharedPreferencesModule.prefs,
|
() => sharedPreferencesModule.prefs,
|
||||||
preResolve: true,
|
preResolve: true,
|
||||||
@@ -39,21 +43,33 @@ extension GetItInjectableX on _i174.GetIt {
|
|||||||
gh.lazySingleton<_i315.GameInstallationService>(
|
gh.lazySingleton<_i315.GameInstallationService>(
|
||||||
() => _i315.GameInstallationService(),
|
() => _i315.GameInstallationService(),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i703.LauncherApiClient>(() => _i703.LauncherApiClient());
|
gh.lazySingleton<_i43.LauncherLogService>(() => _i43.LauncherLogService());
|
||||||
|
gh.lazySingleton<_i558.FlutterSecureStorage>(
|
||||||
|
() => flutterSecureStorageModule.storage,
|
||||||
|
);
|
||||||
|
gh.lazySingleton<_i703.LauncherApiClient>(
|
||||||
|
() => _i703.LauncherApiClient(
|
||||||
|
launcherLogService: gh<_i43.LauncherLogService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
gh.lazySingleton<_i44.PreferencesRepository>(
|
||||||
|
() => _i658.LocalPreferencesRepository(
|
||||||
|
preferences: gh<_i460.SharedPreferences>(),
|
||||||
|
secureStorage: gh<_i558.FlutterSecureStorage>(),
|
||||||
|
),
|
||||||
|
registerFor: {_flutter},
|
||||||
|
);
|
||||||
gh.lazySingleton<_i757.ClientSyncUseCase>(
|
gh.lazySingleton<_i757.ClientSyncUseCase>(
|
||||||
() => _i757.ClientSyncUseCase(
|
() => _i757.ClientSyncUseCase(
|
||||||
launcherApiClient: gh<_i703.LauncherApiClient>(),
|
launcherApiClient: gh<_i703.LauncherApiClient>(),
|
||||||
installationService: gh<_i315.GameInstallationService>(),
|
installationService: gh<_i315.GameInstallationService>(),
|
||||||
|
launcherLogService: gh<_i43.LauncherLogService>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i44.PreferencesRepository>(
|
|
||||||
() => _i662.SharedPrefsPreferencesRepository(
|
|
||||||
preferences: gh<_i460.SharedPreferences>(),
|
|
||||||
),
|
|
||||||
registerFor: {_flutter},
|
|
||||||
);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _$SharedPreferencesModule extends _i1006.SharedPreferencesModule {}
|
class _$SharedPreferencesModule extends _i1006.SharedPreferencesModule {}
|
||||||
|
|
||||||
|
class _$FlutterSecureStorageModule extends _i782.FlutterSecureStorageModule {}
|
||||||
|
|||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
@module
|
||||||
|
abstract class FlutterSecureStorageModule {
|
||||||
|
@lazySingleton
|
||||||
|
FlutterSecureStorage get storage => const FlutterSecureStorage();
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
|
||||||
|
import 'package:moonwell_launcher/features/preferences/data/repositories/local_preferences_repository.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('LocalPreferencesRepository', () {
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||||
|
FlutterSecureStorage.setMockInitialValues(<String, String>{});
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<LocalPreferencesRepository> createRepository() async {
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
return LocalPreferencesRepository(
|
||||||
|
preferences: preferences,
|
||||||
|
secureStorage: const FlutterSecureStorage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('persists output directory through shared preferences', () async {
|
||||||
|
final repository = await createRepository();
|
||||||
|
final uri = Uri.file(r'C:\Games\MoonWell');
|
||||||
|
|
||||||
|
await repository.setOutputDir(uri);
|
||||||
|
|
||||||
|
expect(await repository.getOutputDir(), uri);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persists launcher session through secure storage', () async {
|
||||||
|
final repository = await createRepository();
|
||||||
|
final session = LauncherSession(
|
||||||
|
accessToken: 'token',
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await repository.setLauncherSession(session);
|
||||||
|
final restoredSession = await repository.getLauncherSession();
|
||||||
|
|
||||||
|
expect(restoredSession?.accessToken, 'token');
|
||||||
|
expect(restoredSession?.tokenType, 'Bearer');
|
||||||
|
expect(
|
||||||
|
restoredSession?.expiresAt?.toUtc(),
|
||||||
|
DateTime.parse('2030-01-01T00:00:00Z'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears persisted launcher session', () async {
|
||||||
|
final repository = await createRepository();
|
||||||
|
|
||||||
|
await repository.setLauncherSession(
|
||||||
|
const LauncherSession(
|
||||||
|
accessToken: 'token',
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresAt: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await repository.clearLauncherSession();
|
||||||
|
|
||||||
|
expect(await repository.getLauncherSession(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'removes legacy shared preferences session without restoring it',
|
||||||
|
() async {
|
||||||
|
final legacySession = LauncherSession(
|
||||||
|
accessToken: 'legacy-token',
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
|
||||||
|
);
|
||||||
|
SharedPreferences.setMockInitialValues(<String, Object>{
|
||||||
|
'launcher_session': jsonEncode(legacySession.toJson()),
|
||||||
|
});
|
||||||
|
final preferences = await SharedPreferences.getInstance();
|
||||||
|
final repository = LocalPreferencesRepository(
|
||||||
|
preferences: preferences,
|
||||||
|
secureStorage: const FlutterSecureStorage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await repository.getLauncherSession(), isNull);
|
||||||
|
expect(preferences.containsKey('launcher_session'), isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_session.dart';
|
|
||||||
import 'package:moonwell_launcher/features/preferences/data/repositories/shared_prefs_preferences_repository.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('SharedPrefsPreferencesRepository', () {
|
|
||||||
setUp(() {
|
|
||||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('persists launcher session between reads', () async {
|
|
||||||
final preferences = await SharedPreferences.getInstance();
|
|
||||||
final repository = SharedPrefsPreferencesRepository(
|
|
||||||
preferences: preferences,
|
|
||||||
);
|
|
||||||
final session = LauncherSession(
|
|
||||||
accessToken: 'token',
|
|
||||||
tokenType: 'Bearer',
|
|
||||||
expiresAt: DateTime.parse('2030-01-01T00:00:00Z'),
|
|
||||||
);
|
|
||||||
|
|
||||||
await repository.setLauncherSession(session);
|
|
||||||
final restoredSession = await repository.getLauncherSession();
|
|
||||||
|
|
||||||
expect(restoredSession?.accessToken, 'token');
|
|
||||||
expect(restoredSession?.tokenType, 'Bearer');
|
|
||||||
expect(
|
|
||||||
restoredSession?.expiresAt?.toUtc(),
|
|
||||||
DateTime.parse('2030-01-01T00:00:00Z'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clears persisted launcher session', () async {
|
|
||||||
final preferences = await SharedPreferences.getInstance();
|
|
||||||
final repository = SharedPrefsPreferencesRepository(
|
|
||||||
preferences: preferences,
|
|
||||||
);
|
|
||||||
|
|
||||||
await repository.setLauncherSession(
|
|
||||||
LauncherSession(
|
|
||||||
accessToken: 'token',
|
|
||||||
tokenType: 'Bearer',
|
|
||||||
expiresAt: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await repository.clearLauncherSession();
|
|
||||||
|
|
||||||
expect(await repository.getLauncherSession(), isNull);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user