183 lines
6.2 KiB
Dart
183 lines
6.2 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:glp1_companion/core/platform/health_gateway.dart';
|
|
import 'package:glp1_companion/features/health_sync/presentation/controllers/biometric_controller.dart';
|
|
import 'package:glp1_companion/features/settings/presentation/controllers/integrations_controller.dart';
|
|
|
|
/// Phase 3B: connection-status orchestration for the Settings integrations
|
|
/// section (connect / disconnect round-trips, dynamic status indicators,
|
|
/// graceful degradation on unsupported platforms).
|
|
|
|
/// Scriptable [IHealthGateway] standing in for HealthKit / Health Connect.
|
|
///
|
|
/// Extends the interface so the Phase 3B defaults stay available; the
|
|
/// scripted members mirror what the mobile driver does on a real device.
|
|
class _ScriptedHealthGateway extends IHealthGateway {
|
|
_ScriptedHealthGateway({this.nativeSupported = true});
|
|
|
|
bool nativeSupported;
|
|
bool grantOnRequest = true;
|
|
HealthConnectionStatus nativeStatus = HealthConnectionStatus.notConfigured;
|
|
final requestedProviders = <HealthProviderType>[];
|
|
final disconnectedProviders = <HealthProviderType>[];
|
|
|
|
@override
|
|
bool get supportsNativeHealth => nativeSupported;
|
|
|
|
@override
|
|
NativeHealthPlatform? get nativePlatform =>
|
|
nativeSupported ? NativeHealthPlatform.healthConnect : null;
|
|
|
|
@override
|
|
Future<HealthConnectionStatus> checkStatus(
|
|
HealthProviderType provider,
|
|
) async {
|
|
if (provider == HealthProviderType.manualFallback) {
|
|
return HealthConnectionStatus.connected;
|
|
}
|
|
if (provider == HealthProviderType.enterpriseFhir) {
|
|
return HealthConnectionStatus.notConfigured;
|
|
}
|
|
if (!nativeSupported) return HealthConnectionStatus.unsupportedPlatform;
|
|
if (provider == HealthProviderType.healthConnect) return nativeStatus;
|
|
return HealthConnectionStatus.unsupportedPlatform;
|
|
}
|
|
|
|
@override
|
|
Future<bool> requestAuthorization(HealthProviderType provider) async {
|
|
requestedProviders.add(provider);
|
|
if (provider == activeProvider && grantOnRequest) {
|
|
nativeStatus = HealthConnectionStatus.connected;
|
|
}
|
|
return grantOnRequest;
|
|
}
|
|
|
|
@override
|
|
Future<void> disconnect(HealthProviderType provider) async {
|
|
disconnectedProviders.add(provider);
|
|
if (provider == activeProvider) {
|
|
nativeStatus = HealthConnectionStatus.notConfigured;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> requestPermissions() async => grantOnRequest;
|
|
|
|
@override
|
|
Future<List<HealthMetric>> fetchWeightHistory({
|
|
required DateTime start,
|
|
required DateTime end,
|
|
}) async =>
|
|
const [];
|
|
|
|
@override
|
|
Future<List<HealthMetric>> fetchActiveEnergyBurned({
|
|
required DateTime start,
|
|
required DateTime end,
|
|
}) async =>
|
|
const [];
|
|
|
|
@override
|
|
Future<bool> writeWeight({
|
|
required double weightLbs,
|
|
required DateTime timestamp,
|
|
}) async =>
|
|
false;
|
|
}
|
|
|
|
void main() {
|
|
late _ScriptedHealthGateway gateway;
|
|
late ProviderContainer container;
|
|
|
|
setUp(() {
|
|
gateway = _ScriptedHealthGateway();
|
|
container = ProviderContainer(
|
|
overrides: [healthGatewayProvider.overrideWithValue(gateway)],
|
|
);
|
|
addTearDown(container.dispose);
|
|
});
|
|
|
|
IntegrationsState state() => container.read(integrationsControllerProvider);
|
|
|
|
IntegrationsController controller() =>
|
|
container.read(integrationsControllerProvider.notifier);
|
|
|
|
test(
|
|
'unsupported platforms disable native providers and surface the '
|
|
'fallback notice', () async {
|
|
gateway = _ScriptedHealthGateway(nativeSupported: false);
|
|
container = ProviderContainer(
|
|
overrides: [healthGatewayProvider.overrideWithValue(gateway)],
|
|
);
|
|
addTearDown(container.dispose);
|
|
|
|
await controller().refreshStatuses();
|
|
|
|
final s = state();
|
|
expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform);
|
|
expect(s.healthConnectStatus, HealthConnectionStatus.unsupportedPlatform);
|
|
expect(s.fhirStatus, HealthConnectionStatus.notConfigured);
|
|
expect(s.nativeHealthUnsupported, isTrue);
|
|
expect(s.errorMessage, contains('manual-entry mode'));
|
|
expect(s.isRequestingPermissions, isFalse);
|
|
});
|
|
|
|
test('initial probe reports notConfigured native providers on mobile',
|
|
() async {
|
|
await controller().refreshStatuses();
|
|
|
|
final s = state();
|
|
expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform);
|
|
expect(s.healthConnectStatus, HealthConnectionStatus.notConfigured);
|
|
expect(s.fhirStatus, HealthConnectionStatus.notConfigured);
|
|
expect(s.errorMessage, isNull);
|
|
});
|
|
|
|
test('granted authorization flips only the addressed row to connected',
|
|
() async {
|
|
await controller().refreshStatuses();
|
|
await controller().connectHealthConnect();
|
|
|
|
final s = state();
|
|
expect(gateway.requestedProviders, [HealthProviderType.healthConnect]);
|
|
expect(s.healthConnectStatus, HealthConnectionStatus.connected);
|
|
expect(s.healthConnectConnected, isTrue);
|
|
// The Apple Health row is untouched on an Android-backed gateway.
|
|
expect(s.appleHealthStatus, HealthConnectionStatus.unsupportedPlatform);
|
|
expect(s.errorMessage, isNull);
|
|
expect(s.isRequestingPermissions, isFalse);
|
|
});
|
|
|
|
test('denied authorization records permissionDenied and an error message',
|
|
() async {
|
|
gateway.grantOnRequest = false;
|
|
|
|
await controller().connectHealthConnect();
|
|
|
|
final s = state();
|
|
expect(s.healthConnectStatus, HealthConnectionStatus.permissionDenied);
|
|
expect(s.healthConnectConnected, isFalse);
|
|
expect(s.errorMessage, contains('denied'));
|
|
expect(s.isRequestingPermissions, isFalse);
|
|
});
|
|
|
|
test('disconnect tears down the binding and re-probes the status', () async {
|
|
await controller().connectHealthConnect();
|
|
expect(state().healthConnectStatus, HealthConnectionStatus.connected);
|
|
|
|
await controller().disconnectProvider(HealthProviderType.healthConnect);
|
|
|
|
expect(gateway.disconnectedProviders, [HealthProviderType.healthConnect]);
|
|
expect(state().healthConnectStatus, HealthConnectionStatus.notConfigured);
|
|
});
|
|
|
|
test('clearError resets the transient banner', () async {
|
|
gateway.grantOnRequest = false;
|
|
await controller().connectHealthConnect();
|
|
expect(state().errorMessage, isNotNull);
|
|
|
|
controller().clearError();
|
|
expect(state().errorMessage, isNull);
|
|
});
|
|
}
|