fix: stabilize auth and home error flows
This commit is contained in:
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user