Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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('pushes fastest server when permission is missing', () async {
final pushes = await _pushesForVpnStatus(VPNStatus.missingPermission);
expect(pushes, hasLength(1));
});

test('pushes fastest server when no status event has arrived yet', () async {
final pushes = await _pushesForVpnStatus(null);
expect(pushes, hasLength(1));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});

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