glp-companion/test/features/settings/integrations_controller_tes...

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);
});
}