Harden geofence automation and home editing

This commit is contained in:
Artem Kokos
2026-05-15 11:26:23 +07:00
parent 50748c6945
commit 8ffaa14b60
21 changed files with 718 additions and 160 deletions

View File

@@ -189,7 +189,10 @@ void main() {
expect(savedHome.latitude, 55.75);
expect(savedHome.longitude, 37.61);
expect(savedHome.geofenceEnabled, isFalse);
expect(savedHome.geofenceRadiusMeters, HomeConfig.defaultGeofenceRadiusMeters);
expect(
savedHome.geofenceRadiusMeters,
HomeConfig.defaultGeofenceRadiusMeters,
);
expect(savedApiKey, 'secret-key');
});
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ignis_app/features/settings/models/geofence_system_state.dart';
import 'package:ignis_app/features/settings/providers/settings_providers.dart';
import 'package:ignis_app/features/settings/services/geofence_system_status_service.dart';
import 'package:ignis_app/providers/providers.dart';
import 'package:ignis_app/services/settings_service.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@@ -0,0 +1,64 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ignis_app/features/homes/services/home_connection_change.dart';
import 'package:ignis_app/models/home_config.dart';
void main() {
test('new home always requires connection validation', () {
expect(
hasHomeConnectionChanges(
originalHome: null,
normalizedUrl: 'https://ignis.akokos.ru',
apiKey: 'secret-key',
originalApiKey: '',
),
isTrue,
);
});
test('local-only home edits do not require connection validation', () {
final originalHome = HomeConfig(
id: 'home-1',
name: 'Квартира',
url: 'https://ignis.akokos.ru',
latitude: 55.75,
longitude: 37.61,
);
expect(
hasHomeConnectionChanges(
originalHome: originalHome,
normalizedUrl: originalHome.url,
apiKey: 'saved-key',
originalApiKey: 'saved-key',
),
isFalse,
);
});
test('url or api key changes still require connection validation', () {
final originalHome = HomeConfig(
id: 'home-1',
name: 'Квартира',
url: 'https://ignis.akokos.ru',
);
expect(
hasHomeConnectionChanges(
originalHome: originalHome,
normalizedUrl: 'https://new.ignis.akokos.ru',
apiKey: 'saved-key',
originalApiKey: 'saved-key',
),
isTrue,
);
expect(
hasHomeConnectionChanges(
originalHome: originalHome,
normalizedUrl: originalHome.url,
apiKey: 'new-key',
originalApiKey: 'saved-key',
),
isTrue,
);
});
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ignis_app/features/settings/models/notification_permission_status.dart';
import 'package:ignis_app/features/settings/providers/settings_providers.dart';
import 'package:ignis_app/features/settings/services/notification_permission_status_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();

View File

@@ -0,0 +1,127 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:geolocator/geolocator.dart';
import 'package:ignis_app/features/homes/providers/location_providers.dart';
import 'package:ignis_app/features/homes/services/location_platform_service.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'location watcher does not auto-request permission when denied',
() async {
final service = _FakeLocationPlatformService();
final container = ProviderContainer(
overrides: [locationPlatformServiceProvider.overrideWithValue(service)],
);
addTearDown(() async {
await service.dispose();
container.dispose();
});
final notifier = container.read(userLocationProvider.notifier);
await notifier.startWatching();
await notifier.refresh();
expect(service.requestPermissionCalls, 0);
expect(service.getPositionStreamCalls, 0);
expect(
container.read(userLocationProvider).issue,
UserLocationIssue.permissionDenied,
);
},
);
test('location watcher resumes after permission is granted later', () async {
final service = _FakeLocationPlatformService();
final container = ProviderContainer(
overrides: [locationPlatformServiceProvider.overrideWithValue(service)],
);
addTearDown(() async {
await service.dispose();
container.dispose();
});
final notifier = container.read(userLocationProvider.notifier);
await notifier.startWatching();
service.permission = LocationPermission.always;
await notifier.ensureWatchingStarted();
expect(service.requestPermissionCalls, 0);
expect(service.getPositionStreamCalls, 1);
});
test('explicit location request asks Android and starts watching', () async {
final service = _FakeLocationPlatformService();
service.requestPermissionResult = LocationPermission.always;
final container = ProviderContainer(
overrides: [locationPlatformServiceProvider.overrideWithValue(service)],
);
addTearDown(() async {
await service.dispose();
container.dispose();
});
final notifier = container.read(userLocationProvider.notifier);
await notifier.startWatching();
await notifier.requestPermission();
expect(service.requestPermissionCalls, 1);
expect(service.getPositionStreamCalls, 1);
});
}
class _FakeLocationPlatformService implements LocationPlatformService {
final StreamController<Position> _positionController =
StreamController<Position>.broadcast();
bool locationServiceEnabled = true;
LocationPermission permission = LocationPermission.denied;
LocationPermission requestPermissionResult = LocationPermission.denied;
int requestPermissionCalls = 0;
int getPositionStreamCalls = 0;
Future<void> dispose() async {
await _positionController.close();
}
@override
Future<LocationPermission> checkPermission() async => permission;
@override
Stream<Position> getPositionStream({
required LocationSettings locationSettings,
}) {
getPositionStreamCalls += 1;
return _positionController.stream;
}
@override
Future<Position> getCurrentPosition({
required LocationSettings locationSettings,
}) {
throw UnimplementedError();
}
@override
Future<Position?> getLastKnownPosition() async => null;
@override
Future<bool> isLocationServiceEnabled() async => locationServiceEnabled;
@override
Future<bool> openAppSettings() async => true;
@override
Future<bool> openLocationSettings() async => true;
@override
Future<LocationPermission> requestPermission() async {
requestPermissionCalls += 1;
permission = requestPermissionResult;
return permission;
}
}