feat(app): смена сервера API и очистка данных устройства при выходе

Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
This commit is contained in:
Dmitry
2026-09-19 22:14:14 +03:00
parent bfe74ca47c
commit a559d6de3e
17 changed files with 887 additions and 135 deletions
+120
View File
@@ -0,0 +1,120 @@
import 'package:drift/native.dart';
import 'package:fintracker_app/core/auth/auth_controller.dart';
import 'package:fintracker_app/core/auth/switch_server.dart';
import 'package:fintracker_app/core/auth/token_store.dart';
import 'package:fintracker_app/core/cache/response_cache.dart';
import 'package:fintracker_app/core/cache/response_cache_provider.dart';
import 'package:fintracker_app/core/config.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// No stored refresh token, so `logout()` never touches the network.
class _NoTokenStore extends TokenStore {
bool cleared = false;
@override
Future<String?> readRefresh() async => null;
@override
Future<void> clear() async => cleared = true;
}
void main() {
late ResponseCacheDatabase db;
late _NoTokenStore tokens;
ProviderContainer container({String? storedUrl}) {
final c = ProviderContainer(
overrides: [
responseCacheDbProvider.overrideWithValue(db),
tokenStoreProvider.overrideWithValue(tokens),
initialApiBaseUrlProvider.overrideWithValue(storedUrl),
],
);
addTearDown(c.dispose);
return c;
}
setUp(() async {
SharedPreferences.setMockInitialValues({});
db = ResponseCacheDatabase.forTesting(NativeDatabase.memory());
tokens = _NoTokenStore();
await db.put('/networth/breakdown', {'totalRub': '100'});
});
tearDown(() => db.close());
test('logout drops the token and every cached response', () async {
final c = container();
await c.read(authControllerProvider.notifier).logout();
expect(tokens.cleared, isTrue);
expect(await db.get('/networth/breakdown'), isNull);
expect(c.read(authControllerProvider).status, AuthStatus.signedOut);
});
group('API base URL', () {
test('a saved address wins over the default', () {
expect(
container(storedUrl: 'https://fin.example.com')
.read(apiBaseUrlProvider),
'https://fin.example.com',
);
expect(container().read(apiBaseUrlProvider), defaultApiBaseUrl());
});
test('normalize accepts http(s) origins and trims the trailing slash', () {
expect(
ApiBaseUrlController.normalize(' https://fin.example.com/ '),
'https://fin.example.com',
);
expect(
ApiBaseUrlController.normalize('http://10.0.0.5:8000'),
'http://10.0.0.5:8000',
);
expect(ApiBaseUrlController.normalize('fin.example.com'), isNull);
expect(ApiBaseUrlController.normalize('ftp://fin.example.com'), isNull);
expect(ApiBaseUrlController.normalize(''), isNull);
});
test(
'switching servers signs out, clears the cache and persists the address',
() async {
final c = container();
await switchApiBaseUrl(c, 'https://other.example.com');
expect(c.read(apiBaseUrlProvider), 'https://other.example.com');
expect(await loadStoredApiBaseUrl(), 'https://other.example.com');
expect(tokens.cleared, isTrue);
expect(await db.get('/networth/breakdown'), isNull);
},
);
test(
'saving the address already in use keeps the session and the cache',
() async {
final c = container(storedUrl: 'https://fin.example.com');
await switchApiBaseUrl(c, 'https://fin.example.com');
expect(tokens.cleared, isFalse);
expect(await db.get('/networth/breakdown'), isNotNull);
},
);
test(
'null goes back to the default and forgets the saved address',
() async {
final c = container(storedUrl: 'https://fin.example.com');
await switchApiBaseUrl(c, null);
expect(c.read(apiBaseUrlProvider), defaultApiBaseUrl());
expect(await loadStoredApiBaseUrl(), isNull);
},
);
});
}