Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/features/vpn/provider/available_servers_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:lantern/core/common/common.dart';
import 'package:lantern/core/models/available_servers.dart';
import 'package:lantern/core/models/server_location.dart';
import 'package:lantern/features/vpn/provider/server_location_notifier.dart';
import 'package:lantern/features/vpn/provider/vpn_status_notifier.dart';
Comment thread
jigar-f marked this conversation as resolved.
import 'package:lantern/lantern/lantern_service_notifier.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

Expand Down Expand Up @@ -102,6 +103,13 @@ class AvailableServersNotifier extends _$AvailableServersNotifier {
final fastest = servers.fastestLanternServer;
if (fastest == null) return;

// Don't push the fastest server if the VPN is active.
// It would override the server the user is connected to.
final vpnStatus = ref.read(vPNStatusProvider).value?.status;
if (vpnStatus != VPNStatus.disconnected) {
appLogger.debug('Skipping Smart Location push, VPN status is $vpnStatus');
return;
}
Comment thread
jigar-f marked this conversation as resolved.
final current = ref.read(serverLocationProvider);
if (current.serverType.toServerLocationType != ServerLocationType.auto) {
return;
Expand Down
1 change: 0 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ Future<void> _configureLocalTimeZone() async {
if (kIsWeb) return;

tz.initializeTimeZones();

try {
final timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName.identifier));
Expand Down
123 changes: 121 additions & 2 deletions test/features/vpn/provider/available_servers_notifier_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lantern/core/common/common.dart';
import 'package:lantern/core/models/available_servers.dart';
import 'package:lantern/core/utils/failure.dart';
import 'package:lantern/core/models/lantern_status.dart';
import 'package:lantern/core/models/server_location.dart';
import 'package:lantern/features/vpn/provider/available_servers_notifier.dart';
import 'package:lantern/features/vpn/provider/server_location_notifier.dart';
import 'package:lantern/features/vpn/provider/vpn_status_notifier.dart';
import 'package:lantern/lantern/lantern_service.dart';
import 'package:lantern/lantern/lantern_service_notifier.dart';

class _FakeLanternService implements LanternService {
_FakeLanternService({AvailableServers? servers})
: servers = servers ?? AvailableServers([]);

AvailableServers servers;
Completer<Either<Failure, AvailableServers>>? pendingFetch;
final pendingFetchStarted = Completer<void>();
int fetchCalls = 0;
Expand All @@ -24,13 +32,86 @@ class _FakeLanternService implements LanternService {
}
return pending.future;
}
return Future.value(right(AvailableServers([])));
return Future.value(right(servers));
}

@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

class _FakeVPNStatusNotifier extends VPNStatusNotifier {
_FakeVPNStatusNotifier(this._stream);

final Stream<LanternStatus> _stream;

@override
Stream<LanternStatus> build() => _stream;
}

class _FakeServerLocationNotifier extends ServerLocationNotifier {
final pushedLocations = <ServerLocation>[];

@override
ServerLocation build() => ServerLocation(
serverName: '',
serverType: ServerLocationType.auto.name,
autoLocation: const AutoLocation(
country: 'Germany',
countryCode: 'DE',
displayName: 'Germany - Berlin',
tag: 'old-tag',
),
);

@override
void updateServerLocation(ServerLocation entity) {
pushedLocations.add(entity);
}
}

Server _fastestLanternServer() => Server(
tag: 'fastest-tag',
type: 'lantern',
isLantern: true,
location: GeoLocation(
country: 'United States',
countryCode: 'US',
city: 'New York',
latitude: 0,
longitude: 0,
),
selectionHistory: SelectionHistory(lastSuccessDelayMs: 42),
);

/// Builds the notifier with the given VPN status (null = no status event
/// yet) and returns the recorded Smart Location pushes.
Future<List<ServerLocation>> _pushesForVpnStatus(VPNStatus? status) async {
final service = _FakeLanternService(
servers: AvailableServers([_fastestLanternServer()]),
);
final locationNotifier = _FakeServerLocationNotifier();
final statusStream = status == null
? StreamController<LanternStatus>().stream
: Stream.value(LanternStatus(status: status));
final container = ProviderContainer(
overrides: [
lanternServiceProvider.overrideWithValue(service),
vPNStatusProvider.overrideWith(() => _FakeVPNStatusNotifier(statusStream)),
serverLocationProvider.overrideWith(() => locationNotifier),
],
);
addTearDown(container.dispose);

// Riverpod pauses a provider's stream subscription while it has no
// listeners, so attach one before awaiting the first status event.
container.listen(vPNStatusProvider, (_, _) {});
if (status != null) {
await container.read(vPNStatusProvider.future);
}
await container.read(availableServersProvider.future);
return locationNotifier.pushedLocations;
}

void main() {
test(
'probe-settle refresh stops when disposed during the first fetch',
Expand All @@ -54,4 +135,42 @@ void main() {
expect(service.fetchCalls, 2);
},
);

group('Smart Location push VPN-status guard', () {
test('pushes fastest server when VPN is disconnected', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.disconnected);
expect(pushes, hasLength(1));
expect(pushes.single.autoLocation?.tag, 'fastest-tag');
});

test('skips push when permission is missing', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.missingPermission);
expect(pushes, isEmpty);
});

test('skips push when no status event has arrived yet', () async {
final pushes = await _pushesForVpnStatus(null);
expect(pushes, isEmpty);
});

test('skips push when VPN is connected', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.connected);
expect(pushes, isEmpty);
});

test('skips push when VPN is connecting', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.connecting);
expect(pushes, isEmpty);
});

test('skips push when VPN is disconnecting', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.disconnecting);
expect(pushes, isEmpty);
});

test('skips push when VPN status is error', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.error);
expect(pushes, isEmpty);
});
});
}
Loading