fix: stabilize auth and home error flows

This commit is contained in:
Artem Kokos
2026-04-27 23:11:45 +07:00
parent c2d7ce5bdc
commit eed04e9122
5 changed files with 247 additions and 41 deletions

View File

@@ -2,9 +2,30 @@ import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ignis_app/app/load_state.dart';
import 'package:ignis_app/models/home_config.dart';
import 'package:ignis_app/models/ignis_group.dart';
import 'package:ignis_app/providers/providers.dart';
import 'package:ignis_app/services/credentials_storage.dart';
import 'package:ignis_app/services/api_client.dart';
import 'package:ignis_app/services/settings_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
class InMemoryCredentialsStorage implements CredentialsStorage {
final Map<String, String> _apiKeys = {};
@override
Future<String?> getApiKey(String homeId) async => _apiKeys[homeId];
@override
Future<void> setApiKey(String homeId, String apiKey) async {
_apiKeys[homeId] = apiKey;
}
@override
Future<void> deleteApiKey(String homeId) async {
_apiKeys.remove(homeId);
}
}
class FakeIgnisApi extends IgnisApi {
Object? groupsData;
@@ -183,9 +204,18 @@ class FakeIgnisApi extends IgnisApi {
}
void main() {
ProviderContainer containerWith(FakeIgnisApi api) {
TestWidgetsFlutterBinding.ensureInitialized();
ProviderContainer containerWith(
FakeIgnisApi api, {
SettingsService? settingsService,
}) {
final overrides = [apiProvider.overrideWithValue(api)];
if (settingsService != null) {
overrides.add(settingsServiceProvider.overrideWithValue(settingsService));
}
final container = ProviderContainer(
overrides: [apiProvider.overrideWithValue(api)],
overrides: overrides,
);
addTearDown(container.dispose);
return container;
@@ -448,11 +478,32 @@ void main() {
await container.read(authInfoProvider.notifier).load();
final state = container.read(authInfoProvider);
expect(state?.isAdmin, isTrue);
expect(state?.name, 'owner');
expect(state.status, LoadStatus.data);
expect(state.data?.isAdmin, isTrue);
expect(state.data?.name, 'owner');
expect(container.read(authInfoProvider.notifier).isAdmin, isTrue);
});
test('auth info load error clears stale auth state and exposes message', () async {
final api = FakeIgnisApi(authData: {'is_admin': true, 'name': 'owner'});
final container = containerWith(api);
await container.read(authInfoProvider.notifier).load();
api.authError = DioException(
requestOptions: RequestOptions(path: '/auth/me'),
type: DioExceptionType.connectionError,
message: 'No route to host',
);
await container.read(authInfoProvider.notifier).load();
final state = container.read(authInfoProvider);
expect(state.status, LoadStatus.error);
expect(state.data, isNull);
expect(state.errorMessage, contains('Backend недоступен'));
expect(container.read(authInfoProvider.notifier).isAdmin, isFalse);
});
test('api keys load exposes empty state', () async {
final api = FakeIgnisApi(apiKeysData: {'keys': <Object>[]});
final container = containerWith(api);
@@ -630,4 +681,54 @@ void main() {
expect(error.message, contains('Backend недоступен'));
expect(api.controlGroupParams, containsPair('brightness', 42));
});
test('home selection rolls back current home and auth state on auth failure', () async {
SharedPreferences.setMockInitialValues({
'ignis_homes':
'[{"id":"home-1","name":"Home 1","url":"https://one.example","geofenceEnabled":false},'
'{"id":"home-2","name":"Home 2","url":"https://two.example","geofenceEnabled":false}]',
'ignis_current_home_id': 'home-1',
});
final settingsService = SettingsService(
credentialsStorage: InMemoryCredentialsStorage(),
);
await settingsService.setHomeApiKey('home-1', 'key-1');
await settingsService.setHomeApiKey('home-2', 'key-2');
final api = FakeIgnisApi(authData: {'is_admin': true, 'name': 'owner'});
final container = containerWith(api, settingsService: settingsService);
await container.read(currentHomeProvider.notifier).load();
await container.read(authInfoProvider.notifier).load();
final secondHome = HomeConfig(
id: 'home-2',
name: 'Home 2',
url: 'https://two.example',
);
final authError = DioException(
requestOptions: RequestOptions(path: '/auth/me'),
type: DioExceptionType.badResponse,
response: Response(
requestOptions: RequestOptions(path: '/auth/me'),
statusCode: 403,
),
);
api.authError = authError;
await expectLater(
container.read(currentHomeProvider.notifier).select(secondHome),
throwsA(same(authError)),
);
final currentHome = container.read(currentHomeProvider);
final authState = container.read(authInfoProvider);
final prefs = await SharedPreferences.getInstance();
expect(currentHome?.id, 'home-1');
expect(authState.status, LoadStatus.data);
expect(authState.data?.isAdmin, isTrue);
expect(prefs.getString('ignis_current_home_id'), 'home-1');
});
}