From fab4b26160d3d99a773a671fac6a04eae0d121a0 Mon Sep 17 00:00:00 2001 From: Peter Jhong Date: Fri, 26 Dec 2025 15:32:14 +0800 Subject: [PATCH 1/2] refactor: extract DHCP Reservations logic into service layer with UI models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor DHCPReservationsProvider to comply with architecture guidelines by: - Create DHCPReservationUIModel for presentation layer usage - Extract JNAP operations into LocalNetworkSettingsService (shared service) - Create DHCPReservationsService for business logic - Remove JNAP model imports from Provider, State, and View layers - Implement comprehensive test coverage (28 tests): * DHCPReservationUIModel: 11 tests (100% coverage) * LocalNetworkSettingsService: 11 tests (90.9% coverage) * DHCPReservationsService: 6 tests (isConflict logic) This is Phase 1 of the refactoring. LocalNetworkSettingsService is designed to be shared with LocalNetworkSettingsProvider for future Phase 2 refactoring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../models/reservation_item_ui_model.dart | 62 ++ .../providers/dhcp_reservations_provider.dart | 16 +- .../providers/dhcp_reservations_state.dart | 9 +- .../services/dhcp_reservations_service.dart | 101 ++++ .../local_network_settings_service.dart | 130 +++++ .../views/dhcp_reservations_view.dart | 17 +- .../dhcp_reservations_notifier_mocks.dart | 19 +- .../local_network_settings_service_mocks.dart | 536 ++++++++++++++++++ .../dhcp_reservations_test_data.dart | 78 +++ .../local_network_settings_test_data.dart | 97 ++++ .../reservation_item_ui_model_test.dart | 164 ++++++ .../dhcp_reservations_service_test.dart | 88 +++ .../local_network_settings_service_test.dart | 321 +++++++++++ 13 files changed, 1612 insertions(+), 26 deletions(-) create mode 100644 lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart create mode 100644 lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart create mode 100644 lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart create mode 100644 test/mocks/local_network_settings_service_mocks.dart create mode 100644 test/mocks/test_data/dhcp_reservations_test_data.dart create mode 100644 test/mocks/test_data/local_network_settings_test_data.dart create mode 100644 test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart create mode 100644 test/page/advanced_settings/local_network_settings/services/dhcp_reservations_service_test.dart create mode 100644 test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart diff --git a/lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart b/lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart new file mode 100644 index 000000000..fe1249250 --- /dev/null +++ b/lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:equatable/equatable.dart'; + +/// UI Model for DHCP Reservation display and manipulation +/// +/// This is the presentation layer model used by both: +/// - DHCPReservationsProvider (reservation management) +/// - LocalNetworkSettingsProvider (LAN settings with reservations) +/// +/// Represents a single DHCP reservation with MAC address, IP address, and description. +class DHCPReservationUIModel extends Equatable { + final String macAddress; + final String ipAddress; + final String description; + + const DHCPReservationUIModel({ + required this.macAddress, + required this.ipAddress, + required this.description, + }); + + DHCPReservationUIModel copyWith({ + String? macAddress, + String? ipAddress, + String? description, + }) { + return DHCPReservationUIModel( + macAddress: macAddress ?? this.macAddress, + ipAddress: ipAddress ?? this.ipAddress, + description: description ?? this.description, + ); + } + + Map toMap() { + return { + 'macAddress': macAddress, + 'ipAddress': ipAddress, + 'description': description, + }; + } + + factory DHCPReservationUIModel.fromMap(Map map) { + return DHCPReservationUIModel( + macAddress: map['macAddress'] as String, + ipAddress: map['ipAddress'] as String, + description: map['description'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory DHCPReservationUIModel.fromJson(String source) => + DHCPReservationUIModel.fromMap( + json.decode(source) as Map); + + @override + List get props => [macAddress, ipAddress, description]; + + @override + bool get stringify => true; +} diff --git a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart index e3589ad3e..18932323c 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart'; import 'package:privacy_gui/page/instant_device/_instant_device.dart'; import 'package:privacy_gui/providers/preservable.dart'; import 'package:privacy_gui/providers/preservable_contract.dart'; @@ -48,17 +48,15 @@ class DHCPReservationsNotifier extends AutoDisposeNotifier @override Future performSave() async { - final settings = state.settings.current; - final reservations = settings.reservations + final service = ref.read(dhcpReservationsServiceProvider); + final reservedItems = state.settings.current.reservations .where((e) => e.reserved) .map((e) => e.data) .toList(); - await ref - .read(localNetworkSettingProvider.notifier) - .saveReservations(reservations); + await service.saveReservations(ref, reservedItems); } - void setInitialReservations(List reservedList) { + void setInitialReservations(List reservedList) { final initialSettings = DHCPReservationsSettings( reservations: reservedList .map((e) => ReservedListItem(reserved: true, data: e)) @@ -80,7 +78,7 @@ class DHCPReservationsNotifier extends AutoDisposeNotifier devices: deviceList .map((e) => ReservedListItem( reserved: false, - data: DHCPReservation( + data: DHCPReservationUIModel( macAddress: e.macAddress, ipAddress: e.ipv4Address, description: e.name))) diff --git a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart index 8616eca6f..7814c097a 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; import 'package:privacy_gui/providers/feature_state.dart'; import 'package:privacy_gui/providers/preservable.dart'; @@ -137,6 +137,7 @@ class DHCPReservationState ); } + @override String toJson() => json.encode(toMap()); factory DHCPReservationState.fromJson(String source) => @@ -148,7 +149,7 @@ class DHCPReservationState class ReservedListItem extends Equatable { final bool reserved; - final DHCPReservation data; + final DHCPReservationUIModel data; const ReservedListItem({ required this.reserved, required this.data, @@ -156,7 +157,7 @@ class ReservedListItem extends Equatable { ReservedListItem copyWith({ bool? reserved, - DHCPReservation? data, + DHCPReservationUIModel? data, }) { return ReservedListItem( reserved: reserved ?? this.reserved, @@ -174,7 +175,7 @@ class ReservedListItem extends Equatable { factory ReservedListItem.fromMap(Map map) { return ReservedListItem( reserved: map['reserved'] ?? false, - data: DHCPReservation.fromMap(map['data']), + data: DHCPReservationUIModel.fromMap(map['data']), ); } diff --git a/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart b/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart new file mode 100644 index 000000000..5c231886f --- /dev/null +++ b/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart @@ -0,0 +1,101 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; +import 'package:privacy_gui/utils.dart'; + +final dhcpReservationsServiceProvider = + Provider((ref) { + return DHCPReservationsService( + ref.watch(localNetworkSettingsServiceProvider), + ); +}); + +/// Service for handling DHCP Reservations specific business logic +/// +/// Responsibilities: +/// - Fetch initial reservations from LocalNetworkSettingsProvider +/// - Save reservations via LocalNetworkSettingsService +/// - Provide conflict checking logic +/// +/// This service depends on LocalNetworkSettingsService for actual JNAP operations. +class DHCPReservationsService { + final LocalNetworkSettingsService _localNetworkService; + + DHCPReservationsService(this._localNetworkService); + + /// Fetch initial reservations from LocalNetworkSettingsProvider + /// + /// During Phase 1: Converts from JNAP model to UI model + /// During Phase 2 (after LocalNetworkSettings refactoring): Direct access, no conversion needed + Future> fetchInitialReservations(Ref ref) async { + final localNetworkStatus = + ref.read(localNetworkSettingProvider.select((state) => state.status)); + + // Phase 1: Convert JNAP → UI Model + return _localNetworkService + .convertFromJNAPList(localNetworkStatus.dhcpReservationList); + } + + /// Save reservations by calling LocalNetworkSettingsService + /// + /// Reads current LAN settings from LocalNetworkSettingsProvider + /// and saves with updated reservations. + /// + /// After save, triggers a refresh of LocalNetworkSettings to get the updated state. + Future saveReservations( + Ref ref, + List reservations, + ) async { + final currentSettings = ref.read( + localNetworkSettingProvider.select((state) => state.settings.current)); + + await _localNetworkService.saveReservations( + routerIp: currentSettings.ipAddress, + networkPrefixLength: + NetworkUtils.subnetMaskToPrefixLength(currentSettings.subnetMask), + hostName: currentSettings.hostName, + isDHCPEnabled: currentSettings.isDHCPEnabled, + firstClientIP: currentSettings.firstIPAddress, + lastClientIP: currentSettings.lastIPAddress, + leaseMinutes: currentSettings.clientLeaseTime, + dns1: currentSettings.dns1, + dns2: currentSettings.dns2, + dns3: currentSettings.dns3, + wins: currentSettings.wins, + reservations: reservations, + ); + + // Refresh LocalNetworkSettings after save + await ref + .read(localNetworkSettingProvider.notifier) + .fetch(forceRemote: true); + } + + /// Check if a reservation conflicts with existing ones + /// + /// Conflicts occur when: + /// - MAC address already exists in another reservation + /// - IP address already exists in another reservation + /// + /// The [indexToExclude] parameter is used when editing an existing reservation + /// to avoid self-comparison. + bool isConflict( + DHCPReservationUIModel item, + List existingList, { + int? indexToExclude, + }) { + for (int i = 0; i < existingList.length; i++) { + if (indexToExclude != null && i == indexToExclude) { + continue; + } + + final existing = existingList[i]; + if (existing.macAddress == item.macAddress || + existing.ipAddress == item.ipAddress) { + return true; + } + } + return false; + } +} diff --git a/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart b/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart new file mode 100644 index 000000000..834712dda --- /dev/null +++ b/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart @@ -0,0 +1,130 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; +import 'package:privacy_gui/core/jnap/models/set_lan_settings.dart'; +import 'package:privacy_gui/core/jnap/providers/side_effect_provider.dart'; +import 'package:privacy_gui/core/jnap/router_repository.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; + +final localNetworkSettingsServiceProvider = + Provider((ref) { + return LocalNetworkSettingsService(ref.watch(routerRepositoryProvider)); +}); + +/// Service for handling Local Network Settings JNAP communication +/// +/// Responsibilities: +/// - Fetch LAN settings from router via JNAP +/// - Save LAN settings including DHCP reservations +/// - Transform JNAP DHCPReservation ↔ UI ReservationItemUIModel +/// +/// This service is shared by: +/// - DHCPReservationsProvider (reservation management) +/// - LocalNetworkSettingsProvider (full LAN settings) +class LocalNetworkSettingsService { + final RouterRepository _routerRepository; + + LocalNetworkSettingsService(this._routerRepository); + + /// Fetch LAN settings from router + /// + /// Returns the complete RouterLANSettings from JNAP API + Future fetchLANSettings({bool forceRemote = false}) async { + final response = await _routerRepository.send( + JNAPAction.getLANSettings, + fetchRemote: forceRemote, + auth: true, + ); + return RouterLANSettings.fromMap(response.output); + } + + /// Save reservations to router + /// + /// This method is called by both: + /// - DHCPReservationsProvider (saving reservation changes) + /// - LocalNetworkSettingsProvider (saving full LAN settings) + /// + /// Parameters include all required LAN settings fields since the JNAP API + /// requires the complete settings object. + Future saveReservations({ + required String routerIp, + required int networkPrefixLength, + required String hostName, + required bool isDHCPEnabled, + required String firstClientIP, + required String lastClientIP, + required int leaseMinutes, + String? dns1, + String? dns2, + String? dns3, + String? wins, + required List reservations, + }) async { + final setLANSettings = SetRouterLANSettings( + ipAddress: routerIp, + networkPrefixLength: networkPrefixLength, + hostName: hostName, + isDHCPEnabled: isDHCPEnabled, + dhcpSettings: DHCPSettings( + firstClientIPAddress: firstClientIP, + lastClientIPAddress: lastClientIP, + leaseMinutes: leaseMinutes, + dnsServer1: dns1?.isEmpty == true ? null : dns1, + dnsServer2: dns2?.isEmpty == true ? null : dns2, + dnsServer3: dns3?.isEmpty == true ? null : dns3, + winsServer: wins?.isEmpty == true ? null : wins, + reservations: _toJNAPList(reservations), + ), + ); + + await _routerRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: setLANSettings.toMap()..removeWhere((key, value) => value == null), + sideEffectOverrides: const JNAPSideEffectOverrides(maxRetry: 5), + ); + } + + // ============================================ + // Conversion Helpers (JNAP ↔ UI Model) + // ============================================ + + /// Convert JNAP DHCPReservation to UI Model + DHCPReservationUIModel _fromJNAP(DHCPReservation jnap) { + return DHCPReservationUIModel( + macAddress: jnap.macAddress, + ipAddress: jnap.ipAddress, + description: jnap.description, + ); + } + + /// Convert UI Model to JNAP DHCPReservation + DHCPReservation _toJNAP(DHCPReservationUIModel ui) { + return DHCPReservation( + macAddress: ui.macAddress, + ipAddress: ui.ipAddress, + description: ui.description, + ); + } + + /// Convert list of JNAP models to UI models + List _fromJNAPList(List list) { + return list.map((jnap) => _fromJNAP(jnap)).toList(); + } + + /// Convert list of UI models to JNAP models + List _toJNAPList(List list) { + return list.map((ui) => _toJNAP(ui)).toList(); + } + + /// Public helper for external conversion (used during Phase 1 transition) + /// + /// This is used by DHCPReservationsService to convert from LocalNetworkStatus's + /// JNAP model list to UI model list during the transition phase. + /// + /// In Phase 2 (LocalNetworkSettings refactoring), this will no longer be needed + /// as LocalNetworkStatus will directly use List. + List convertFromJNAPList(List list) { + return _fromJNAPList(list); + } +} diff --git a/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart b/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart index 743b84dfc..151c4064f 100644 --- a/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart +++ b/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacy_gui/core/jnap/providers/device_manager_state.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; import 'package:privacy_gui/page/components/mixin/page_snackbar_mixin.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; import 'package:privacy_gui/core/utils/extension.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; import 'package:privacy_gui/page/components/shortcuts/dialogs.dart'; import 'package:privacy_gui/page/components/ui_kit_page_view.dart'; import 'package:privacy_gui/page/components/views/arguments_view.dart'; @@ -45,8 +46,14 @@ class _DHCPReservationsContentViewState Future.doWhile(() => !mounted).then((value) { ref.read(deviceFilterConfigProvider.notifier).initFilter(); - final reservations = ref.read(localNetworkSettingProvider - .select((state) => state.status.dhcpReservationList)); + + // Fetch initial reservations from LocalNetworkSettings + final localNetworkStatus = + ref.read(localNetworkSettingProvider.select((state) => state.status)); + final service = ref.read(localNetworkSettingsServiceProvider); + final reservations = + service.convertFromJNAPList(localNetworkStatus.dhcpReservationList); + ref .read(dhcpReservationProvider.notifier) .setInitialReservations(reservations); @@ -339,7 +346,7 @@ class _DHCPReservationsContentViewState ? ref.read(dhcpReservationProvider.notifier).updateReservations( ReservedListItem( reserved: true, - data: DHCPReservation( + data: DHCPReservationUIModel( macAddress: mac, ipAddress: ip, description: name), ), true, diff --git a/test/mocks/dhcp_reservations_notifier_mocks.dart b/test/mocks/dhcp_reservations_notifier_mocks.dart index 59aaa7fe1..9bdc91b15 100644 --- a/test/mocks/dhcp_reservations_notifier_mocks.dart +++ b/test/mocks/dhcp_reservations_notifier_mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in privacy_gui/test/mocks/mockito_specs/dhcp_reservations_notifier_spec.dart. // Do not manually edit this file. @@ -7,7 +7,8 @@ import 'dart:async' as _i5; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart' as _i6; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart' + as _i6; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart' as _i4; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart' @@ -27,10 +28,11 @@ import 'package:privacy_gui/page/instant_device/_instant_device.dart' as _i7; // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member -class _FakeNotifierProviderRef_0 extends _i1.SmartFake - implements _i2.NotifierProviderRef { - _FakeNotifierProviderRef_0( +class _FakeAutoDisposeNotifierProviderRef_0 extends _i1.SmartFake + implements _i2.AutoDisposeNotifierProviderRef { + _FakeAutoDisposeNotifierProviderRef_0( Object parent, Invocation parentInvocation, ) : super( @@ -70,12 +72,13 @@ class MockDHCPReservationsNotifier _i2.AutoDisposeNotifierProviderRef<_i3.DHCPReservationState> get ref => (super.noSuchMethod( Invocation.getter(#ref), - returnValue: _FakeNotifierProviderRef_0<_i3.DHCPReservationState>( + returnValue: + _FakeAutoDisposeNotifierProviderRef_0<_i3.DHCPReservationState>( this, Invocation.getter(#ref), ), returnValueForMissingStub: - _FakeNotifierProviderRef_0<_i3.DHCPReservationState>( + _FakeAutoDisposeNotifierProviderRef_0<_i3.DHCPReservationState>( this, Invocation.getter(#ref), ), @@ -172,7 +175,7 @@ class MockDHCPReservationsNotifier ) as _i5.Future); @override - void setInitialReservations(List<_i6.DHCPReservation>? reservedList) => + void setInitialReservations(List<_i6.DHCPReservationUIModel>? reservedList) => super.noSuchMethod( Invocation.method( #setInitialReservations, diff --git a/test/mocks/local_network_settings_service_mocks.dart b/test/mocks/local_network_settings_service_mocks.dart new file mode 100644 index 000000000..f4cac2699 --- /dev/null +++ b/test/mocks/local_network_settings_service_mocks.dart @@ -0,0 +1,536 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in privacy_gui/test/mocks/mockito_specs/local_network_settings_service_spec.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i10; + +import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:privacy_gui/core/jnap/actions/better_action.dart' as _i11; +import 'package:privacy_gui/core/jnap/actions/jnap_transaction.dart' as _i13; +import 'package:privacy_gui/core/jnap/command/base_command.dart' as _i7; +import 'package:privacy_gui/core/jnap/command/http/base_http_command.dart' + as _i5; +import 'package:privacy_gui/core/jnap/jnap_command_executor_mixin.dart' as _i3; +import 'package:privacy_gui/core/jnap/models/lan_settings.dart' as _i8; +import 'package:privacy_gui/core/jnap/providers/side_effect_provider.dart' + as _i12; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart' as _i4; +import 'package:privacy_gui/core/jnap/router_repository.dart' as _i9; +import 'package:privacy_gui/core/jnap/spec/jnap_spec.dart' as _i6; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart' + as _i15; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart' + as _i14; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member + +class _FakeRef_0 extends _i1.SmartFake + implements _i2.Ref { + _FakeRef_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJNAPCommandExecutor_1 extends _i1.SmartFake + implements _i3.JNAPCommandExecutor { + _FakeJNAPCommandExecutor_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJNAPSuccess_2 extends _i1.SmartFake implements _i4.JNAPSuccess { + _FakeJNAPSuccess_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeJNAPTransactionSuccessWrap_3 extends _i1.SmartFake + implements _i4.JNAPTransactionSuccessWrap { + _FakeJNAPTransactionSuccessWrap_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeTransactionHttpCommand_4 extends _i1.SmartFake + implements _i5.TransactionHttpCommand { + _FakeTransactionHttpCommand_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBaseCommand_5> + extends _i1.SmartFake implements _i7.BaseCommand { + _FakeBaseCommand_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeRouterLANSettings_6 extends _i1.SmartFake + implements _i8.RouterLANSettings { + _FakeRouterLANSettings_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [RouterRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRouterRepository extends _i1.Mock implements _i9.RouterRepository { + @override + _i2.Ref get ref => (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _FakeRef_0( + this, + Invocation.getter(#ref), + ), + returnValueForMissingStub: _FakeRef_0( + this, + Invocation.getter(#ref), + ), + ) as _i2.Ref); + + @override + _i3.JNAPCommandExecutor get executor => (super.noSuchMethod( + Invocation.getter(#executor), + returnValue: _FakeJNAPCommandExecutor_1( + this, + Invocation.getter(#executor), + ), + returnValueForMissingStub: _FakeJNAPCommandExecutor_1( + this, + Invocation.getter(#executor), + ), + ) as _i3.JNAPCommandExecutor); + + @override + bool get isEnableBTSetup => (super.noSuchMethod( + Invocation.getter(#isEnableBTSetup), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + set enableBTSetup(bool? isEnable) => super.noSuchMethod( + Invocation.setter( + #enableBTSetup, + isEnable, + ), + returnValueForMissingStub: null, + ); + + @override + _i10.Future<_i4.JNAPSuccess> send( + _i11.JNAPAction? action, { + Map? data = const {}, + Map? extraHeaders = const {}, + bool? auth = false, + _i9.CommandType? type, + bool? fetchRemote = false, + _i7.CacheLevel? cacheLevel, + int? timeoutMs = 10000, + int? retries = 1, + _i12.JNAPSideEffectOverrides? sideEffectOverrides, + }) => + (super.noSuchMethod( + Invocation.method( + #send, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #auth: auth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + returnValue: _i10.Future<_i4.JNAPSuccess>.value(_FakeJNAPSuccess_2( + this, + Invocation.method( + #send, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #auth: auth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + )), + returnValueForMissingStub: + _i10.Future<_i4.JNAPSuccess>.value(_FakeJNAPSuccess_2( + this, + Invocation.method( + #send, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #auth: auth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + )), + ) as _i10.Future<_i4.JNAPSuccess>); + + @override + _i10.Future<_i4.JNAPTransactionSuccessWrap> transaction( + _i13.JNAPTransactionBuilder? builder, { + bool? fetchRemote = false, + _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, + int? timeoutMs = 10000, + int? retries = 1, + _i12.JNAPSideEffectOverrides? sideEffectOverrides, + }) => + (super.noSuchMethod( + Invocation.method( + #transaction, + [builder], + { + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + returnValue: _i10.Future<_i4.JNAPTransactionSuccessWrap>.value( + _FakeJNAPTransactionSuccessWrap_3( + this, + Invocation.method( + #transaction, + [builder], + { + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + )), + returnValueForMissingStub: + _i10.Future<_i4.JNAPTransactionSuccessWrap>.value( + _FakeJNAPTransactionSuccessWrap_3( + this, + Invocation.method( + #transaction, + [builder], + { + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #sideEffectOverrides: sideEffectOverrides, + }, + ), + )), + ) as _i10.Future<_i4.JNAPTransactionSuccessWrap>); + + @override + _i10.Future<_i5.TransactionHttpCommand> createTransaction( + List>? payload, { + bool? needAuth = false, + required List<_i11.JNAPAction>? actions, + bool? fetchRemote = false, + _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, + int? timeoutMs = 10000, + int? retries = 1, + _i9.CommandType? type, + }) => + (super.noSuchMethod( + Invocation.method( + #createTransaction, + [payload], + { + #needAuth: needAuth, + #actions: actions, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #type: type, + }, + ), + returnValue: _i10.Future<_i5.TransactionHttpCommand>.value( + _FakeTransactionHttpCommand_4( + this, + Invocation.method( + #createTransaction, + [payload], + { + #needAuth: needAuth, + #actions: actions, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #type: type, + }, + ), + )), + returnValueForMissingStub: + _i10.Future<_i5.TransactionHttpCommand>.value( + _FakeTransactionHttpCommand_4( + this, + Invocation.method( + #createTransaction, + [payload], + { + #needAuth: needAuth, + #actions: actions, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + #type: type, + }, + ), + )), + ) as _i10.Future<_i5.TransactionHttpCommand>); + + @override + _i10.Future< + _i7 + .BaseCommand<_i4.JNAPResult, _i6.JNAPCommandSpec>> createCommand( + String? action, { + Map? data = const {}, + Map? extraHeaders = const {}, + bool? needAuth = false, + _i9.CommandType? type, + bool? fetchRemote = false, + _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, + int? timeoutMs = 10000, + int? retries = 1, + }) => + (super.noSuchMethod( + Invocation.method( + #createCommand, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #needAuth: needAuth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + }, + ), + returnValue: _i10.Future< + _i7.BaseCommand<_i4.JNAPResult, + _i6.JNAPCommandSpec>>.value( + _FakeBaseCommand_5<_i4.JNAPResult, _i6.JNAPCommandSpec>( + this, + Invocation.method( + #createCommand, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #needAuth: needAuth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + }, + ), + )), + returnValueForMissingStub: _i10.Future< + _i7.BaseCommand<_i4.JNAPResult, + _i6.JNAPCommandSpec>>.value( + _FakeBaseCommand_5<_i4.JNAPResult, _i6.JNAPCommandSpec>( + this, + Invocation.method( + #createCommand, + [action], + { + #data: data, + #extraHeaders: extraHeaders, + #needAuth: needAuth, + #type: type, + #fetchRemote: fetchRemote, + #cacheLevel: cacheLevel, + #timeoutMs: timeoutMs, + #retries: retries, + }, + ), + )), + ) as _i10.Future< + _i7.BaseCommand<_i4.JNAPResult, _i6.JNAPCommandSpec>>); + + @override + _i10.Stream<_i4.JNAPResult> scheduledCommand({ + required _i11.JNAPAction? action, + int? retryDelayInMilliSec = 5000, + int? maxRetry = 10, + int? firstDelayInMilliSec = 3000, + Map? data = const {}, + bool Function(_i4.JNAPResult)? condition, + dynamic Function(bool)? onCompleted, + int? requestTimeoutOverride, + bool? auth = false, + }) => + (super.noSuchMethod( + Invocation.method( + #scheduledCommand, + [], + { + #action: action, + #retryDelayInMilliSec: retryDelayInMilliSec, + #maxRetry: maxRetry, + #firstDelayInMilliSec: firstDelayInMilliSec, + #data: data, + #condition: condition, + #onCompleted: onCompleted, + #requestTimeoutOverride: requestTimeoutOverride, + #auth: auth, + }, + ), + returnValue: _i10.Stream<_i4.JNAPResult>.empty(), + returnValueForMissingStub: _i10.Stream<_i4.JNAPResult>.empty(), + ) as _i10.Stream<_i4.JNAPResult>); +} + +/// A class which mocks [LocalNetworkSettingsService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLocalNetworkSettingsService extends _i1.Mock + implements _i14.LocalNetworkSettingsService { + @override + _i10.Future<_i8.RouterLANSettings> fetchLANSettings( + {bool? forceRemote = false}) => + (super.noSuchMethod( + Invocation.method( + #fetchLANSettings, + [], + {#forceRemote: forceRemote}, + ), + returnValue: + _i10.Future<_i8.RouterLANSettings>.value(_FakeRouterLANSettings_6( + this, + Invocation.method( + #fetchLANSettings, + [], + {#forceRemote: forceRemote}, + ), + )), + returnValueForMissingStub: + _i10.Future<_i8.RouterLANSettings>.value(_FakeRouterLANSettings_6( + this, + Invocation.method( + #fetchLANSettings, + [], + {#forceRemote: forceRemote}, + ), + )), + ) as _i10.Future<_i8.RouterLANSettings>); + + @override + _i10.Future saveReservations({ + required String? routerIp, + required int? networkPrefixLength, + required String? hostName, + required bool? isDHCPEnabled, + required String? firstClientIP, + required String? lastClientIP, + required int? leaseMinutes, + String? dns1, + String? dns2, + String? dns3, + String? wins, + required List<_i15.DHCPReservationUIModel>? reservations, + }) => + (super.noSuchMethod( + Invocation.method( + #saveReservations, + [], + { + #routerIp: routerIp, + #networkPrefixLength: networkPrefixLength, + #hostName: hostName, + #isDHCPEnabled: isDHCPEnabled, + #firstClientIP: firstClientIP, + #lastClientIP: lastClientIP, + #leaseMinutes: leaseMinutes, + #dns1: dns1, + #dns2: dns2, + #dns3: dns3, + #wins: wins, + #reservations: reservations, + }, + ), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + + @override + List<_i15.DHCPReservationUIModel> convertFromJNAPList( + List<_i8.DHCPReservation>? list) => + (super.noSuchMethod( + Invocation.method( + #convertFromJNAPList, + [list], + ), + returnValue: <_i15.DHCPReservationUIModel>[], + returnValueForMissingStub: <_i15.DHCPReservationUIModel>[], + ) as List<_i15.DHCPReservationUIModel>); +} diff --git a/test/mocks/test_data/dhcp_reservations_test_data.dart b/test/mocks/test_data/dhcp_reservations_test_data.dart new file mode 100644 index 000000000..b2c8bbe03 --- /dev/null +++ b/test/mocks/test_data/dhcp_reservations_test_data.dart @@ -0,0 +1,78 @@ +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; + +/// Test data builder for DHCPReservationsService tests +/// +/// Provides factory methods to create ReservationItemUIModel test data +/// with sensible defaults and various test scenarios. +class DHCPReservationsTestData { + /// Create default ReservationItemUIModel + static DHCPReservationUIModel createReservationUIModel({ + String macAddress = '00:11:22:33:44:55', + String ipAddress = '192.168.1.100', + String description = 'Test Device', + }) { + return DHCPReservationUIModel( + macAddress: macAddress, + ipAddress: ipAddress, + description: description, + ); + } + + /// Create a list of test reservations + static List createReservationList() { + return [ + createReservationUIModel( + macAddress: '00:11:22:33:44:55', + ipAddress: '192.168.1.10', + description: 'Device 1', + ), + createReservationUIModel( + macAddress: 'AA:BB:CC:DD:EE:FF', + ipAddress: '192.168.1.20', + description: 'Device 2', + ), + createReservationUIModel( + macAddress: '11:22:33:44:55:66', + ipAddress: '192.168.1.30', + description: 'Device 3', + ), + ]; + } + + /// Create a reservation with conflicting MAC address + static DHCPReservationUIModel createConflictingMACReservation() { + return createReservationUIModel( + macAddress: '00:11:22:33:44:55', // Same as first in list + ipAddress: '192.168.1.99', + description: 'Conflicting Device', + ); + } + + /// Create a reservation with conflicting IP address + static DHCPReservationUIModel createConflictingIPReservation() { + return createReservationUIModel( + macAddress: 'FF:EE:DD:CC:BB:AA', + ipAddress: '192.168.1.10', // Same as first in list + description: 'Conflicting Device', + ); + } + + /// Create a valid new reservation (no conflicts) + static DHCPReservationUIModel createValidNewReservation() { + return createReservationUIModel( + macAddress: 'AB:CD:EF:12:34:56', + ipAddress: '192.168.1.99', + description: 'New Valid Device', + ); + } + + /// Create empty reservation list + static List createEmptyList() { + return []; + } + + /// Create single reservation list + static List createSingleReservation() { + return [createReservationUIModel()]; + } +} diff --git a/test/mocks/test_data/local_network_settings_test_data.dart b/test/mocks/test_data/local_network_settings_test_data.dart new file mode 100644 index 000000000..67ec8930b --- /dev/null +++ b/test/mocks/test_data/local_network_settings_test_data.dart @@ -0,0 +1,97 @@ +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; + +/// Test data builder for LocalNetworkSettingsService tests +/// +/// Provides factory methods to create JNAP mock responses with sensible defaults. +/// This centralizes test data and makes tests more readable. +class LocalNetworkSettingsTestData { + /// Create default RouterLANSettings success response + static JNAPSuccess createGetLANSettingsSuccess({ + String ipAddress = '192.168.1.1', + int networkPrefixLength = 24, + String hostName = 'TestRouter', + bool isDHCPEnabled = true, + String firstClientIP = '192.168.1.100', + String lastClientIP = '192.168.1.150', + int leaseMinutes = 1440, + String? dnsServer1, + String? dnsServer2, + String? dnsServer3, + String? winsServer, + List>? reservations, + int minNetworkPrefixLength = 16, + int maxNetworkPrefixLength = 30, + int minAllowedDHCPLeaseMinutes = 1, + int maxAllowedDHCPLeaseMinutes = 525600, + int maxDHCPReservationDescriptionLength = 63, + }) { + return JNAPSuccess( + result: 'OK', + output: { + 'ipAddress': ipAddress, + 'networkPrefixLength': networkPrefixLength, + 'hostName': hostName, + 'isDHCPEnabled': isDHCPEnabled, + 'minNetworkPrefixLength': minNetworkPrefixLength, + 'maxNetworkPrefixLength': maxNetworkPrefixLength, + 'minAllowedDHCPLeaseMinutes': minAllowedDHCPLeaseMinutes, + 'maxAllowedDHCPLeaseMinutes': maxAllowedDHCPLeaseMinutes, + 'maxDHCPReservationDescriptionLength': + maxDHCPReservationDescriptionLength, + 'dhcpSettings': { + 'firstClientIPAddress': firstClientIP, + 'lastClientIPAddress': lastClientIP, + 'leaseMinutes': leaseMinutes, + if (dnsServer1 != null) 'dnsServer1': dnsServer1, + if (dnsServer2 != null) 'dnsServer2': dnsServer2, + if (dnsServer3 != null) 'dnsServer3': dnsServer3, + if (winsServer != null) 'winsServer': winsServer, + 'reservations': reservations ?? [], + }, + }, + ); + } + + /// Create DHCP reservation map for test data + static Map createReservationMap({ + required String macAddress, + required String ipAddress, + required String description, + }) { + return { + 'macAddress': macAddress, + 'ipAddress': ipAddress, + 'description': description, + }; + } + + /// Create default test reservations + static List> createDefaultReservations() { + return [ + createReservationMap( + macAddress: '00:11:22:33:44:55', + ipAddress: '192.168.1.10', + description: 'Test Device 1', + ), + createReservationMap( + macAddress: 'AA:BB:CC:DD:EE:FF', + ipAddress: '192.168.1.20', + description: 'Test Device 2', + ), + ]; + } + + /// Create empty LAN settings response (no reservations) + static JNAPSuccess createEmptyLANSettingsSuccess() { + return createGetLANSettingsSuccess( + reservations: [], + ); + } + + /// Create LAN settings response with reservations + static JNAPSuccess createLANSettingsWithReservations() { + return createGetLANSettingsSuccess( + reservations: createDefaultReservations(), + ); + } +} diff --git a/test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart b/test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart new file mode 100644 index 000000000..25ab72bac --- /dev/null +++ b/test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart @@ -0,0 +1,164 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; + +void main() { + group('ReservationItemUIModel -', () { + const testMacAddress = '00:11:22:33:44:55'; + const testIpAddress = '192.168.1.100'; + const testDescription = 'Test Device'; + + test('creates instance with required fields', () { + const model = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + expect(model.macAddress, testMacAddress); + expect(model.ipAddress, testIpAddress); + expect(model.description, testDescription); + }); + + test('copyWith creates new instance with updated fields', () { + const original = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + final updated = original.copyWith( + description: 'Updated Device', + ); + + expect(updated.macAddress, testMacAddress); + expect(updated.ipAddress, testIpAddress); + expect(updated.description, 'Updated Device'); + }); + + test('copyWith with no parameters returns same values', () { + const original = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + final updated = original.copyWith(); + + expect(updated.macAddress, original.macAddress); + expect(updated.ipAddress, original.ipAddress); + expect(updated.description, original.description); + }); + + test('toMap returns correct map', () { + const model = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + final map = model.toMap(); + + expect(map['macAddress'], testMacAddress); + expect(map['ipAddress'], testIpAddress); + expect(map['description'], testDescription); + }); + + test('fromMap creates correct instance', () { + final map = { + 'macAddress': testMacAddress, + 'ipAddress': testIpAddress, + 'description': testDescription, + }; + + final model = DHCPReservationUIModel.fromMap(map); + + expect(model.macAddress, testMacAddress); + expect(model.ipAddress, testIpAddress); + expect(model.description, testDescription); + }); + + test('toJson returns valid JSON string', () { + const model = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + final jsonString = model.toJson(); + final decoded = json.decode(jsonString); + + expect(decoded['macAddress'], testMacAddress); + expect(decoded['ipAddress'], testIpAddress); + expect(decoded['description'], testDescription); + }); + + test('fromJson creates correct instance', () { + final jsonString = json.encode({ + 'macAddress': testMacAddress, + 'ipAddress': testIpAddress, + 'description': testDescription, + }); + + final model = DHCPReservationUIModel.fromJson(jsonString); + + expect(model.macAddress, testMacAddress); + expect(model.ipAddress, testIpAddress); + expect(model.description, testDescription); + }); + + test('Equatable props returns correct list', () { + const model = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + expect(model.props, [testMacAddress, testIpAddress, testDescription]); + }); + + test('equality works correctly with same values', () { + const model1 = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + const model2 = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + expect(model1, equals(model2)); + expect(model1.hashCode, equals(model2.hashCode)); + }); + + test('equality works correctly with different values', () { + const model1 = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + const model2 = DHCPReservationUIModel( + macAddress: 'AA:BB:CC:DD:EE:FF', + ipAddress: testIpAddress, + description: testDescription, + ); + + expect(model1, isNot(equals(model2))); + }); + + test('stringify returns true', () { + const model = DHCPReservationUIModel( + macAddress: testMacAddress, + ipAddress: testIpAddress, + description: testDescription, + ); + + expect(model.stringify, isTrue); + }); + }); +} diff --git a/test/page/advanced_settings/local_network_settings/services/dhcp_reservations_service_test.dart b/test/page/advanced_settings/local_network_settings/services/dhcp_reservations_service_test.dart new file mode 100644 index 000000000..aabd4c785 --- /dev/null +++ b/test/page/advanced_settings/local_network_settings/services/dhcp_reservations_service_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; + +import '../../../../mocks/test_data/dhcp_reservations_test_data.dart'; +import '../../../../mocks/router_repository_mocks.dart'; + +void main() { + group('DHCPReservationsService -', () { + late DHCPReservationsService service; + late LocalNetworkSettingsService localNetworkService; + late MockRouterRepository mockRepository; + + setUp(() { + mockRepository = MockRouterRepository(); + localNetworkService = LocalNetworkSettingsService(mockRepository); + service = DHCPReservationsService(localNetworkService); + }); + + group('isConflict -', () { + test('returns true when MAC address conflicts', () { + final existingList = DHCPReservationsTestData.createReservationList(); + final conflictingItem = + DHCPReservationsTestData.createConflictingMACReservation(); + + final result = service.isConflict(conflictingItem, existingList); + + expect(result, isTrue); + }); + + test('returns true when IP address conflicts', () { + final existingList = DHCPReservationsTestData.createReservationList(); + final conflictingItem = + DHCPReservationsTestData.createConflictingIPReservation(); + + final result = service.isConflict(conflictingItem, existingList); + + expect(result, isTrue); + }); + + test('returns false when no conflicts exist', () { + final existingList = DHCPReservationsTestData.createReservationList(); + final validItem = DHCPReservationsTestData.createValidNewReservation(); + + final result = service.isConflict(validItem, existingList); + + expect(result, isFalse); + }); + + test('excludes item at indexToExclude from conflict check', () { + final existingList = DHCPReservationsTestData.createReservationList(); + final itemToEdit = existingList[0]; + + // Same item should not conflict with itself + final result = + service.isConflict(itemToEdit, existingList, indexToExclude: 0); + + expect(result, isFalse); + }); + + test( + 'detects conflict even with indexToExclude if different item matches', + () { + final existingList = DHCPReservationsTestData.createReservationList(); + final conflictingItem = + DHCPReservationsTestData.createReservationUIModel( + macAddress: existingList[1].macAddress, // Matches second item + ipAddress: '192.168.1.99', + description: 'Edited Device', + ); + + // Exclude first item, but should still conflict with second + final result = service.isConflict(conflictingItem, existingList, + indexToExclude: 0); + + expect(result, isTrue); + }); + + test('returns false for empty existing list', () { + final item = DHCPReservationsTestData.createReservationUIModel(); + + final result = service.isConflict(item, []); + + expect(result, isFalse); + }); + }); + }); +} diff --git a/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart b/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart new file mode 100644 index 000000000..bd181d9d7 --- /dev/null +++ b/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart @@ -0,0 +1,321 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; +import 'package:privacy_gui/utils.dart'; + +import '../../../../mocks/test_data/dhcp_reservations_test_data.dart'; +import '../../../../mocks/test_data/local_network_settings_test_data.dart'; +import '../../../../mocks/router_repository_mocks.dart'; + +void main() { + group('LocalNetworkSettingsService -', () { + late MockRouterRepository mockRepository; + late LocalNetworkSettingsService service; + + setUp(() { + mockRepository = MockRouterRepository(); + service = LocalNetworkSettingsService(mockRepository); + }); + + group('fetchLANSettings -', () { + test('fetches LAN settings successfully', () async { + final jnapResponse = + LocalNetworkSettingsTestData.createGetLANSettingsSuccess(); + when(mockRepository.send( + JNAPAction.getLANSettings, + fetchRemote: false, + auth: true, + )).thenAnswer((_) async => jnapResponse); + + final result = await service.fetchLANSettings(); + + expect(result, isA()); + expect(result.ipAddress, '192.168.1.1'); + verify(mockRepository.send( + JNAPAction.getLANSettings, + fetchRemote: false, + auth: true, + )).called(1); + }); + + test('fetches LAN settings with forceRemote=true', () async { + final jnapResponse = + LocalNetworkSettingsTestData.createGetLANSettingsSuccess(); + when(mockRepository.send( + JNAPAction.getLANSettings, + fetchRemote: true, + auth: true, + )).thenAnswer((_) async => jnapResponse); + + await service.fetchLANSettings(forceRemote: true); + + verify(mockRepository.send( + JNAPAction.getLANSettings, + fetchRemote: true, + auth: true, + )).called(1); + }); + + test('throws error when fetch fails', () async { + when(mockRepository.send( + JNAPAction.getLANSettings, + fetchRemote: false, + auth: true, + )).thenThrow(Exception('Network error')); + + expect( + () => service.fetchLANSettings(), + throwsException, + ); + }); + }); + + group('saveReservations -', () { + test('saves reservations successfully with all required fields', + () async { + final reservations = DHCPReservationsTestData.createReservationList(); + final jnapResponse = JNAPSuccess(result: 'OK', output: const {}); + + when(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).thenAnswer((_) async => jnapResponse); + + await service.saveReservations( + routerIp: '192.168.1.1', + networkPrefixLength: + NetworkUtils.subnetMaskToPrefixLength('255.255.255.0'), + hostName: 'MyRouter', + isDHCPEnabled: true, + firstClientIP: '192.168.1.100', + lastClientIP: '192.168.1.200', + leaseMinutes: 1440, + dns1: '8.8.8.8', + dns2: '8.8.4.4', + dns3: null, + wins: null, + reservations: reservations, + ); + + final captured = verify(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: captureAnyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).captured; + + final sentData = captured.first as Map; + expect(sentData['ipAddress'], '192.168.1.1'); + expect(sentData['hostName'], 'MyRouter'); + expect(sentData['isDHCPEnabled'], true); + expect( + sentData['dhcpSettings']['firstClientIPAddress'], '192.168.1.100'); + expect( + sentData['dhcpSettings']['lastClientIPAddress'], '192.168.1.200'); + expect(sentData['dhcpSettings']['leaseMinutes'], 1440); + expect(sentData['dhcpSettings']['dnsServer1'], '8.8.8.8'); + expect(sentData['dhcpSettings']['dnsServer2'], '8.8.4.4'); + expect(sentData['dhcpSettings']['reservations'], isA()); + expect(sentData['dhcpSettings']['reservations'].length, 3); + }); + + test('removes null optional fields before sending', () async { + final reservations = [ + DHCPReservationsTestData.createReservationUIModel() + ]; + final jnapResponse = JNAPSuccess(result: 'OK', output: const {}); + + when(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).thenAnswer((_) async => jnapResponse); + + await service.saveReservations( + routerIp: '192.168.1.1', + networkPrefixLength: 24, + hostName: 'MyRouter', + isDHCPEnabled: true, + firstClientIP: '192.168.1.100', + lastClientIP: '192.168.1.200', + leaseMinutes: 1440, + dns1: null, + dns2: null, + dns3: null, + wins: null, + reservations: reservations, + ); + + final captured = verify(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: captureAnyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).captured; + + final sentData = captured.first as Map; + expect(sentData['dhcpSettings']['dnsServer1'], isNull); + expect(sentData['dhcpSettings']['dnsServer2'], isNull); + expect(sentData['dhcpSettings']['dnsServer3'], isNull); + expect(sentData['dhcpSettings']['winsServer'], isNull); + }); + + test('removes empty string DNS values', () async { + final reservations = [ + DHCPReservationsTestData.createReservationUIModel() + ]; + final jnapResponse = JNAPSuccess(result: 'OK', output: const {}); + + when(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).thenAnswer((_) async => jnapResponse); + + await service.saveReservations( + routerIp: '192.168.1.1', + networkPrefixLength: 24, + hostName: 'MyRouter', + isDHCPEnabled: true, + firstClientIP: '192.168.1.100', + lastClientIP: '192.168.1.200', + leaseMinutes: 1440, + dns1: '', + dns2: '', + dns3: '', + wins: '', + reservations: reservations, + ); + + final captured = verify(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: captureAnyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).captured; + + final sentData = captured.first as Map; + expect(sentData['dhcpSettings']['dnsServer1'], isNull); + expect(sentData['dhcpSettings']['dnsServer2'], isNull); + expect(sentData['dhcpSettings']['dnsServer3'], isNull); + expect(sentData['dhcpSettings']['winsServer'], isNull); + }); + + test('uses maxRetry=5 for side effects', () async { + final reservations = [ + DHCPReservationsTestData.createReservationUIModel() + ]; + final jnapResponse = JNAPSuccess(result: 'OK', output: const {}); + + when(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).thenAnswer((_) async => jnapResponse); + + await service.saveReservations( + routerIp: '192.168.1.1', + networkPrefixLength: 24, + hostName: 'MyRouter', + isDHCPEnabled: true, + firstClientIP: '192.168.1.100', + lastClientIP: '192.168.1.200', + leaseMinutes: 1440, + reservations: reservations, + ); + + verify(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).called(1); + }); + + test('throws error when save fails', () async { + final reservations = [ + DHCPReservationsTestData.createReservationUIModel() + ]; + + when(mockRepository.send( + JNAPAction.setLANSettings, + auth: true, + data: anyNamed('data'), + sideEffectOverrides: anyNamed('sideEffectOverrides'), + )).thenThrow(Exception('Save failed')); + + expect( + () => service.saveReservations( + routerIp: '192.168.1.1', + networkPrefixLength: 24, + hostName: 'MyRouter', + isDHCPEnabled: true, + firstClientIP: '192.168.1.100', + lastClientIP: '192.168.1.200', + leaseMinutes: 1440, + reservations: reservations, + ), + throwsException, + ); + }); + }); + + group('convertFromJNAPList -', () { + test('converts JNAP list to UI model list', () { + final jnapList = [ + DHCPReservation( + macAddress: '00:11:22:33:44:55', + ipAddress: '192.168.1.100', + description: 'Device 1', + ), + DHCPReservation( + macAddress: 'AA:BB:CC:DD:EE:FF', + ipAddress: '192.168.1.101', + description: 'Device 2', + ), + ]; + + final result = service.convertFromJNAPList(jnapList); + + expect(result, isA>()); + expect(result.length, 2); + expect(result[0].macAddress, '00:11:22:33:44:55'); + expect(result[0].ipAddress, '192.168.1.100'); + expect(result[0].description, 'Device 1'); + expect(result[1].macAddress, 'AA:BB:CC:DD:EE:FF'); + expect(result[1].ipAddress, '192.168.1.101'); + expect(result[1].description, 'Device 2'); + }); + + test('converts empty JNAP list', () { + final result = service.convertFromJNAPList([]); + + expect(result, isEmpty); + }); + + test('preserves all fields during conversion', () { + final jnapReservation = DHCPReservation( + macAddress: '00:11:22:33:44:55', + ipAddress: '192.168.1.100', + description: 'Test Device with Special Chars !@#\$%', + ); + + final result = service.convertFromJNAPList([jnapReservation]); + + expect(result.length, 1); + expect(result[0].macAddress, jnapReservation.macAddress); + expect(result[0].ipAddress, jnapReservation.ipAddress); + expect(result[0].description, jnapReservation.description); + }); + }); + }); +} From 74dbb7c43190b69a1e3976332a2fa20454d2d517 Mon Sep 17 00:00:00 2001 From: Peter Jhong Date: Fri, 26 Dec 2025 16:45:22 +0800 Subject: [PATCH 2/2] refactor: migrate LocalNetworkSettings to use UI models and service layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed LocalNetworkStatus.dhcpReservationList from JNAP DHCPReservation to DHCPReservationUIModel - Service layer now handles all JNAP ↔ UI model conversions through private methods - Removed JNAP dependencies from Provider and State layers - Updated DHCPReservationsProvider to work with UI models - Updated device_detail_view to use DHCPReservationUIModel - Fixed use_build_context_synchronously warning in local_network_settings_view - Renamed reservation_item_ui_model.dart to dhcp_reservation_ui_model.dart for clarity - Updated all tests to reflect new model structure - Removed obsolete test mocks (535 lines deleted) This refactoring achieves complete separation of concerns per Constitution Article V Section 5.3: - Provider/State layers: UI models only, no JNAP knowledge - Service layer: Handles JNAP communication and model transformation - View layer: Presentation logic with UI models --- ...el.dart => dhcp_reservation_ui_model.dart} | 0 .../providers/dhcp_reservations_provider.dart | 2 +- .../providers/dhcp_reservations_state.dart | 2 +- .../local_network_settings_provider.dart | 145 ++--- .../local_network_settings_state.dart | 11 +- .../services/dhcp_reservations_service.dart | 11 +- .../local_network_settings_service.dart | 89 ++- .../views/dhcp_reservations_view.dart | 9 +- .../views/local_network_settings_view.dart | 2 + .../views/device_detail_view.dart | 4 +- .../dhcp_reservations_notifier_mocks.dart | 2 +- ...local_network_settings_notifier_mocks.dart | 14 +- .../local_network_settings_service_mocks.dart | 536 ------------------ .../dhcp_reservations_test_data.dart | 6 +- ...rt => dhcp_reservation_ui_model_test.dart} | 4 +- .../local_network_settings_service_test.dart | 51 +- .../device_detail_view_test.dart | 4 +- 17 files changed, 157 insertions(+), 735 deletions(-) rename lib/page/advanced_settings/local_network_settings/models/{reservation_item_ui_model.dart => dhcp_reservation_ui_model.dart} (100%) delete mode 100644 test/mocks/local_network_settings_service_mocks.dart rename test/page/advanced_settings/local_network_settings/models/{reservation_item_ui_model_test.dart => dhcp_reservation_ui_model_test.dart} (98%) diff --git a/lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart b/lib/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart similarity index 100% rename from lib/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart rename to lib/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart diff --git a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart index 18932323c..c6a802012 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart'; import 'package:privacy_gui/page/instant_device/_instant_device.dart'; diff --git a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart index 7814c097a..70c8a5f78 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/providers/feature_state.dart'; import 'package:privacy_gui/providers/preservable.dart'; diff --git a/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart b/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart index be0327cb2..7582e252b 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart @@ -1,12 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/core/jnap/actions/better_action.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; -import 'package:privacy_gui/core/jnap/models/set_lan_settings.dart'; -import 'package:privacy_gui/core/jnap/providers/side_effect_provider.dart'; -import 'package:privacy_gui/core/jnap/router_repository.dart'; import 'package:privacy_gui/core/utils/logger.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; import 'package:privacy_gui/page/instant_safety/providers/instant_safety_provider.dart'; import 'package:privacy_gui/providers/preservable_contract.dart'; import 'package:privacy_gui/providers/preservable_notifier_mixin.dart'; @@ -55,52 +52,10 @@ class LocalNetworkSettingsNotifier extends Notifier @override Future<(LocalNetworkSettings?, LocalNetworkStatus?)> performFetch( {bool forceRemote = false, bool updateStatusOnly = false}) async { - final repo = ref.read(routerRepositoryProvider); - final lanSettings = await repo - .send( - JNAPAction.getLANSettings, - fetchRemote: forceRemote, - auth: true, - ) - .then((value) => RouterLANSettings.fromMap(value.output)); - - final subnetMaskString = NetworkUtils.prefixLengthToSubnetMask( - lanSettings.networkPrefixLength, - ); - final maxUserAllowed = NetworkUtils.getMaxUserAllowedInDHCPRange( - lanSettings.ipAddress, - lanSettings.dhcpSettings.firstClientIPAddress, - lanSettings.dhcpSettings.lastClientIPAddress, - ); - final maxUserLimit = NetworkUtils.getMaxUserLimit( - lanSettings.ipAddress, - lanSettings.dhcpSettings.firstClientIPAddress, - subnetMaskString, - maxUserAllowed, - ); - - final newSettings = LocalNetworkSettings( - hostName: lanSettings.hostName, - ipAddress: lanSettings.ipAddress, - subnetMask: subnetMaskString, - isDHCPEnabled: lanSettings.isDHCPEnabled, - firstIPAddress: lanSettings.dhcpSettings.firstClientIPAddress, - lastIPAddress: lanSettings.dhcpSettings.lastClientIPAddress, - maxUserAllowed: maxUserAllowed, - clientLeaseTime: lanSettings.dhcpSettings.leaseMinutes, - dns1: lanSettings.dhcpSettings.dnsServer1, - dns2: lanSettings.dhcpSettings.dnsServer2, - dns3: lanSettings.dhcpSettings.dnsServer3, - wins: lanSettings.dhcpSettings.winsServer, - ); - - final newStatus = state.status.copyWith( - maxUserLimit: maxUserLimit, - minNetworkPrefixLength: lanSettings.minNetworkPrefixLength, - maxNetworkPrefixLength: lanSettings.maxNetworkPrefixLength, - minAllowDHCPLeaseMinutes: lanSettings.minAllowedDHCPLeaseMinutes, - maxAllowDHCPLeaseMinutes: lanSettings.maxAllowedDHCPLeaseMinutes, - dhcpReservationList: lanSettings.dhcpSettings.reservations, + final service = ref.read(localNetworkSettingsServiceProvider); + final (newSettings, newStatus) = await service.fetchLANSettingsWithUIModels( + forceRemote: forceRemote, + currentStatus: state.status, ); _updateValidators(state.copyWith( @@ -112,65 +67,51 @@ class LocalNetworkSettingsNotifier extends Notifier @override Future performSave() async { + final service = ref.read(localNetworkSettingsServiceProvider); final settings = state.settings.current; - final newSettings = SetRouterLANSettings( - ipAddress: settings.ipAddress, + + // Clear reservations if router IP changed + final reservations = + (state.settings.original.ipAddress != settings.ipAddress) + ? [] + : state.status.dhcpReservationList; + + await service.saveReservations( + routerIp: settings.ipAddress, networkPrefixLength: NetworkUtils.subnetMaskToPrefixLength(settings.subnetMask), hostName: settings.hostName, isDHCPEnabled: settings.isDHCPEnabled, - dhcpSettings: DHCPSettings( - firstClientIPAddress: settings.firstIPAddress, - lastClientIPAddress: settings.lastIPAddress, - leaseMinutes: settings.clientLeaseTime, - dnsServer1: settings.dns1?.isEmpty == true ? null : settings.dns1, - dnsServer2: settings.dns2?.isEmpty == true ? null : settings.dns2, - dnsServer3: settings.dns3?.isEmpty == true ? null : settings.dns3, - winsServer: settings.wins?.isEmpty == true ? null : settings.wins, - reservations: (state.settings.original.ipAddress != settings.ipAddress) - ? [] - : state.status.dhcpReservationList, - ), - ); - final routerRepository = ref.read(routerRepositoryProvider); - await routerRepository.send( - JNAPAction.setLANSettings, - auth: true, - data: newSettings.toMap()..removeWhere((key, value) => value == null), - sideEffectOverrides: const JNAPSideEffectOverrides(maxRetry: 5), + firstClientIP: settings.firstIPAddress, + lastClientIP: settings.lastIPAddress, + leaseMinutes: settings.clientLeaseTime, + dns1: settings.dns1, + dns2: settings.dns2, + dns3: settings.dns3, + wins: settings.wins, + reservations: reservations, ); await ref.read(instantSafetyProvider.notifier).fetch(); } - Future saveReservations(List list) async { + Future saveReservations(List list) async { + final service = ref.read(localNetworkSettingsServiceProvider); final currentSettings = state.settings.current; - final newSettings = SetRouterLANSettings( - ipAddress: currentSettings.ipAddress, + + await service.saveReservations( + routerIp: currentSettings.ipAddress, networkPrefixLength: NetworkUtils.subnetMaskToPrefixLength(currentSettings.subnetMask), hostName: currentSettings.hostName, isDHCPEnabled: currentSettings.isDHCPEnabled, - dhcpSettings: DHCPSettings( - firstClientIPAddress: currentSettings.firstIPAddress, - lastClientIPAddress: currentSettings.lastIPAddress, - leaseMinutes: currentSettings.clientLeaseTime, - dnsServer1: - currentSettings.dns1?.isEmpty == true ? null : currentSettings.dns1, - dnsServer2: - currentSettings.dns2?.isEmpty == true ? null : currentSettings.dns2, - dnsServer3: - currentSettings.dns3?.isEmpty == true ? null : currentSettings.dns3, - winsServer: - currentSettings.wins?.isEmpty == true ? null : currentSettings.wins, - reservations: list, - ), - ); - final routerRepository = ref.read(routerRepositoryProvider); - await routerRepository.send( - JNAPAction.setLANSettings, - auth: true, - data: newSettings.toMap()..removeWhere((key, value) => value == null), - sideEffectOverrides: const JNAPSideEffectOverrides(maxRetry: 5), + firstClientIP: currentSettings.firstIPAddress, + lastClientIP: currentSettings.lastIPAddress, + leaseMinutes: currentSettings.clientLeaseTime, + dns1: currentSettings.dns1, + dns2: currentSettings.dns2, + dns3: currentSettings.dns3, + wins: currentSettings.wins, + reservations: list, ); // After saving, we need to refetch the local network settings to get the updated state. await fetch(forceRemote: true); @@ -246,10 +187,10 @@ class LocalNetworkSettingsNotifier extends Notifier } void updateDHCPReservationList( - List addedDHCPReservationList) { + List addedDHCPReservationList) { final filteredList = addedDHCPReservationList .where((element) => !isReservationOverlap(item: element)); - final List newList = [ + final List newList = [ ...state.status.dhcpReservationList, ...filteredList ]; @@ -257,8 +198,9 @@ class LocalNetworkSettingsNotifier extends Notifier status: state.status.copyWith(dhcpReservationList: newList)); } - bool updateDHCPReservationOfIndex(DHCPReservation item, int index) { - List newList = List.from(state.status.dhcpReservationList); + bool updateDHCPReservationOfIndex(DHCPReservationUIModel item, int index) { + List newList = + List.from(state.status.dhcpReservationList); bool succeed = false; if (item.ipAddress == 'DELETE') { newList.removeAt(index); @@ -274,7 +216,8 @@ class LocalNetworkSettingsNotifier extends Notifier return succeed; } - bool isReservationOverlap({required DHCPReservation item, int? index}) { + bool isReservationOverlap( + {required DHCPReservationUIModel item, int? index}) { final overlap = state.status.dhcpReservationList.where((element) { // Not compare with self if on editing if (index != null && diff --git a/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart b/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart index 820933502..52882896c 100644 --- a/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart +++ b/lib/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart @@ -4,8 +4,8 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/providers/feature_state.dart'; import 'package:privacy_gui/providers/preservable.dart'; import 'package:privacy_gui/utils.dart'; @@ -201,7 +201,7 @@ class LocalNetworkStatus extends Equatable { final int maxAllowDHCPLeaseMinutes; final int minNetworkPrefixLength; final int maxNetworkPrefixLength; - final List dhcpReservationList; + final List dhcpReservationList; final Map errorTextMap; final bool hasErrorOnHostNameTab; final bool hasErrorOnIPAddressTab; @@ -248,7 +248,7 @@ class LocalNetworkStatus extends Equatable { int? maxAllowDHCPLeaseMinutes, int? minNetworkPrefixLength, int? maxNetworkPrefixLength, - List? dhcpReservationList, + List? dhcpReservationList, Map? errorTextMap, bool? hasErrorOnHostNameTab, bool? hasErrorOnIPAddressTab, @@ -297,8 +297,9 @@ class LocalNetworkStatus extends Equatable { maxAllowDHCPLeaseMinutes: map['maxAllowDHCPLeaseMinutes']?.toInt() ?? 0, minNetworkPrefixLength: map['minNetworkPrefixLength']?.toInt() ?? 0, maxNetworkPrefixLength: map['maxNetworkPrefixLength']?.toInt() ?? 0, - dhcpReservationList: List.from( - map['dhcpReservationList']?.map((x) => DHCPReservation.fromMap(x))), + dhcpReservationList: List.from( + map['dhcpReservationList'] + ?.map((x) => DHCPReservationUIModel.fromMap(x))), errorTextMap: Map.from(map['errorTextMap'] ?? {}), hasErrorOnHostNameTab: map['hasErrorOnHostNameTab'] ?? false, hasErrorOnIPAddressTab: map['hasErrorOnIPAddressTab'] ?? false, diff --git a/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart b/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart index 5c231886f..6908047a0 100644 --- a/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart +++ b/lib/page/advanced_settings/local_network_settings/services/dhcp_reservations_service.dart @@ -1,5 +1,5 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; import 'package:privacy_gui/utils.dart'; @@ -26,15 +26,14 @@ class DHCPReservationsService { /// Fetch initial reservations from LocalNetworkSettingsProvider /// - /// During Phase 1: Converts from JNAP model to UI model - /// During Phase 2 (after LocalNetworkSettings refactoring): Direct access, no conversion needed + /// Returns the current DHCP reservation list as UI models. + /// No conversion needed as LocalNetworkStatus now uses UI models directly. Future> fetchInitialReservations(Ref ref) async { final localNetworkStatus = ref.read(localNetworkSettingProvider.select((state) => state.status)); - // Phase 1: Convert JNAP → UI Model - return _localNetworkService - .convertFromJNAPList(localNetworkStatus.dhcpReservationList); + // Direct access - LocalNetworkStatus.dhcpReservationList is already UI Model + return localNetworkStatus.dhcpReservationList; } /// Save reservations by calling LocalNetworkSettingsService diff --git a/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart b/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart index 834712dda..1d1db5933 100644 --- a/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart +++ b/lib/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart @@ -4,7 +4,9 @@ import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; import 'package:privacy_gui/core/jnap/models/set_lan_settings.dart'; import 'package:privacy_gui/core/jnap/providers/side_effect_provider.dart'; import 'package:privacy_gui/core/jnap/router_repository.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart'; +import 'package:privacy_gui/utils.dart'; final localNetworkSettingsServiceProvider = Provider((ref) { @@ -16,7 +18,8 @@ final localNetworkSettingsServiceProvider = /// Responsibilities: /// - Fetch LAN settings from router via JNAP /// - Save LAN settings including DHCP reservations -/// - Transform JNAP DHCPReservation ↔ UI ReservationItemUIModel +/// - Transform JNAP DHCPReservation ↔ UI DHCPReservationUIModel +/// - Perform network calculations (IP ranges, subnet masks, etc.) /// /// This service is shared by: /// - DHCPReservationsProvider (reservation management) @@ -26,9 +29,10 @@ class LocalNetworkSettingsService { LocalNetworkSettingsService(this._routerRepository); - /// Fetch LAN settings from router + /// Fetch LAN settings from router (JNAP Data Model) /// - /// Returns the complete RouterLANSettings from JNAP API + /// Returns the complete RouterLANSettings from JNAP API. + /// This is a low-level method used internally for JNAP communication. Future fetchLANSettings({bool forceRemote = false}) async { final response = await _routerRepository.send( JNAPAction.getLANSettings, @@ -38,6 +42,72 @@ class LocalNetworkSettingsService { return RouterLANSettings.fromMap(response.output); } + /// Fetch LAN settings and convert to UI models with network calculations + /// + /// This is the main method for LocalNetworkSettingsProvider to use. + /// Returns a tuple of (LocalNetworkSettings, LocalNetworkStatus) with: + /// - All JNAP data converted to UI models + /// - Network calculations performed (subnet mask, max users, etc.) + /// - DHCP reservations converted to DHCPReservationUIModel + Future<(LocalNetworkSettings, LocalNetworkStatus)> + fetchLANSettingsWithUIModels({ + bool forceRemote = false, + required LocalNetworkStatus currentStatus, + }) async { + final lanSettings = await fetchLANSettings(forceRemote: forceRemote); + + // Convert prefix length to subnet mask string + final subnetMaskString = NetworkUtils.prefixLengthToSubnetMask( + lanSettings.networkPrefixLength, + ); + + // Calculate max user allowed in DHCP range + final maxUserAllowed = NetworkUtils.getMaxUserAllowedInDHCPRange( + lanSettings.ipAddress, + lanSettings.dhcpSettings.firstClientIPAddress, + lanSettings.dhcpSettings.lastClientIPAddress, + ); + + // Calculate max user limit + final maxUserLimit = NetworkUtils.getMaxUserLimit( + lanSettings.ipAddress, + lanSettings.dhcpSettings.firstClientIPAddress, + subnetMaskString, + maxUserAllowed, + ); + + // Build LocalNetworkSettings (UI layer settings) + final newSettings = LocalNetworkSettings( + hostName: lanSettings.hostName, + ipAddress: lanSettings.ipAddress, + subnetMask: subnetMaskString, + isDHCPEnabled: lanSettings.isDHCPEnabled, + firstIPAddress: lanSettings.dhcpSettings.firstClientIPAddress, + lastIPAddress: lanSettings.dhcpSettings.lastClientIPAddress, + maxUserAllowed: maxUserAllowed, + clientLeaseTime: lanSettings.dhcpSettings.leaseMinutes, + dns1: lanSettings.dhcpSettings.dnsServer1, + dns2: lanSettings.dhcpSettings.dnsServer2, + dns3: lanSettings.dhcpSettings.dnsServer3, + wins: lanSettings.dhcpSettings.winsServer, + ); + + // Convert JNAP DHCPReservation list to UI models + final reservationsUI = _fromJNAPList(lanSettings.dhcpSettings.reservations); + + // Build LocalNetworkStatus (UI layer status) + final newStatus = currentStatus.copyWith( + maxUserLimit: maxUserLimit, + minNetworkPrefixLength: lanSettings.minNetworkPrefixLength, + maxNetworkPrefixLength: lanSettings.maxNetworkPrefixLength, + minAllowDHCPLeaseMinutes: lanSettings.minAllowedDHCPLeaseMinutes, + maxAllowDHCPLeaseMinutes: lanSettings.maxAllowedDHCPLeaseMinutes, + dhcpReservationList: reservationsUI, + ); + + return (newSettings, newStatus); + } + /// Save reservations to router /// /// This method is called by both: @@ -116,15 +186,4 @@ class LocalNetworkSettingsService { List _toJNAPList(List list) { return list.map((ui) => _toJNAP(ui)).toList(); } - - /// Public helper for external conversion (used during Phase 1 transition) - /// - /// This is used by DHCPReservationsService to convert from LocalNetworkStatus's - /// JNAP model list to UI model list during the transition phase. - /// - /// In Phase 2 (LocalNetworkSettings refactoring), this will no longer be needed - /// as LocalNetworkStatus will directly use List. - List convertFromJNAPList(List list) { - return _fromJNAPList(list); - } } diff --git a/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart b/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart index 151c4064f..de8abfcc6 100644 --- a/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart +++ b/lib/page/advanced_settings/local_network_settings/views/dhcp_reservations_view.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacy_gui/core/jnap/providers/device_manager_state.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_state.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; import 'package:privacy_gui/page/components/mixin/page_snackbar_mixin.dart'; import 'package:privacy_gui/core/utils/extension.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; @@ -50,9 +49,9 @@ class _DHCPReservationsContentViewState // Fetch initial reservations from LocalNetworkSettings final localNetworkStatus = ref.read(localNetworkSettingProvider.select((state) => state.status)); - final service = ref.read(localNetworkSettingsServiceProvider); - final reservations = - service.convertFromJNAPList(localNetworkStatus.dhcpReservationList); + + // Direct access - LocalNetworkStatus.dhcpReservationList is already UI Model + final reservations = localNetworkStatus.dhcpReservationList; ref .read(dhcpReservationProvider.notifier) diff --git a/lib/page/advanced_settings/local_network_settings/views/local_network_settings_view.dart b/lib/page/advanced_settings/local_network_settings/views/local_network_settings_view.dart index a12e8da0e..caa64f66e 100644 --- a/lib/page/advanced_settings/local_network_settings/views/local_network_settings_view.dart +++ b/lib/page/advanced_settings/local_network_settings/views/local_network_settings_view.dart @@ -265,6 +265,7 @@ class _LocalNetworkSettingsViewState _finishSaveSettings(); }, ).catchError((error) { + if (!mounted) return; final state = ref.read(localNetworkSettingProvider); final currentUrl = ref.read(routerRepositoryProvider).getLocalIP(); final regex = RegExp(r'(www\.)?myrouter\.info'); @@ -282,6 +283,7 @@ class _LocalNetworkSettingsViewState } }, test: (error) => error is JNAPSideEffectError).onError( (error, stackTrace) { + if (!mounted) return; showErrorMessageSnackBar(error); }); } diff --git a/lib/page/instant_device/views/device_detail_view.dart b/lib/page/instant_device/views/device_detail_view.dart index 310962a88..af94cb4a0 100644 --- a/lib/page/instant_device/views/device_detail_view.dart +++ b/lib/page/instant_device/views/device_detail_view.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; import 'package:privacy_gui/core/jnap/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/jnap/providers/device_manager_state.dart'; import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; @@ -12,6 +11,7 @@ import 'package:privacy_gui/core/utils/extension.dart'; import 'package:privacy_gui/core/utils/icon_device_category.dart'; import 'package:privacy_gui/core/utils/logger.dart'; import 'package:privacy_gui/core/utils/wifi.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart'; import 'package:privacy_gui/page/components/shared_widgets.dart'; import 'package:privacy_gui/page/components/shortcuts/dialogs.dart'; @@ -479,7 +479,7 @@ class _DeviceDetailViewState extends ConsumerState { Future handleReserveDhcp( DeviceListItem item, bool isReservedIp) async { final notifier = ref.read(localNetworkSettingProvider.notifier); - final dhcpReservationItem = DHCPReservation( + final dhcpReservationItem = DHCPReservationUIModel( description: item.name.replaceAll(HostNameRule().rule, ''), ipAddress: item.ipv4Address, macAddress: item.macAddress, diff --git a/test/mocks/dhcp_reservations_notifier_mocks.dart b/test/mocks/dhcp_reservations_notifier_mocks.dart index 9bdc91b15..fc48e123c 100644 --- a/test/mocks/dhcp_reservations_notifier_mocks.dart +++ b/test/mocks/dhcp_reservations_notifier_mocks.dart @@ -7,7 +7,7 @@ import 'dart:async' as _i5; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart' +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart' as _i6; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/dhcp_reservations_provider.dart' as _i4; diff --git a/test/mocks/local_network_settings_notifier_mocks.dart b/test/mocks/local_network_settings_notifier_mocks.dart index 4d770f9bb..c33c7f66e 100644 --- a/test/mocks/local_network_settings_notifier_mocks.dart +++ b/test/mocks/local_network_settings_notifier_mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in privacy_gui/test/mocks/mockito_specs/local_network_settings_notifier_spec.dart. // Do not manually edit this file. @@ -8,7 +8,8 @@ import 'dart:async' as _i5; import 'package:flutter/widgets.dart' as _i7; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart' as _i6; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart' + as _i6; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_provider.dart' as _i4; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart' @@ -27,6 +28,7 @@ import 'package:privacy_gui/page/advanced_settings/local_network_settings/provid // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class +// ignore_for_file: invalid_use_of_internal_member class _FakeNotifierProviderRef_0 extends _i1.SmartFake implements _i2.NotifierProviderRef { @@ -178,7 +180,7 @@ class MockLocalNetworkSettingsNotifier ) as _i5.Future); @override - _i5.Future saveReservations(List<_i6.DHCPReservation>? list) => + _i5.Future saveReservations(List<_i6.DHCPReservationUIModel>? list) => (super.noSuchMethod( Invocation.method( #saveReservations, @@ -234,7 +236,7 @@ class MockLocalNetworkSettingsNotifier @override void updateDHCPReservationList( - List<_i6.DHCPReservation>? addedDHCPReservationList) => + List<_i6.DHCPReservationUIModel>? addedDHCPReservationList) => super.noSuchMethod( Invocation.method( #updateDHCPReservationList, @@ -245,7 +247,7 @@ class MockLocalNetworkSettingsNotifier @override bool updateDHCPReservationOfIndex( - _i6.DHCPReservation? item, + _i6.DHCPReservationUIModel? item, int? index, ) => (super.noSuchMethod( @@ -262,7 +264,7 @@ class MockLocalNetworkSettingsNotifier @override bool isReservationOverlap({ - required _i6.DHCPReservation? item, + required _i6.DHCPReservationUIModel? item, int? index, }) => (super.noSuchMethod( diff --git a/test/mocks/local_network_settings_service_mocks.dart b/test/mocks/local_network_settings_service_mocks.dart deleted file mode 100644 index f4cac2699..000000000 --- a/test/mocks/local_network_settings_service_mocks.dart +++ /dev/null @@ -1,536 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in privacy_gui/test/mocks/mockito_specs/local_network_settings_service_spec.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i10; - -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; -import 'package:mockito/mockito.dart' as _i1; -import 'package:privacy_gui/core/jnap/actions/better_action.dart' as _i11; -import 'package:privacy_gui/core/jnap/actions/jnap_transaction.dart' as _i13; -import 'package:privacy_gui/core/jnap/command/base_command.dart' as _i7; -import 'package:privacy_gui/core/jnap/command/http/base_http_command.dart' - as _i5; -import 'package:privacy_gui/core/jnap/jnap_command_executor_mixin.dart' as _i3; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart' as _i8; -import 'package:privacy_gui/core/jnap/providers/side_effect_provider.dart' - as _i12; -import 'package:privacy_gui/core/jnap/result/jnap_result.dart' as _i4; -import 'package:privacy_gui/core/jnap/router_repository.dart' as _i9; -import 'package:privacy_gui/core/jnap/spec/jnap_spec.dart' as _i6; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart' - as _i15; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart' - as _i14; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -class _FakeRef_0 extends _i1.SmartFake - implements _i2.Ref { - _FakeRef_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJNAPCommandExecutor_1 extends _i1.SmartFake - implements _i3.JNAPCommandExecutor { - _FakeJNAPCommandExecutor_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJNAPSuccess_2 extends _i1.SmartFake implements _i4.JNAPSuccess { - _FakeJNAPSuccess_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeJNAPTransactionSuccessWrap_3 extends _i1.SmartFake - implements _i4.JNAPTransactionSuccessWrap { - _FakeJNAPTransactionSuccessWrap_3( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeTransactionHttpCommand_4 extends _i1.SmartFake - implements _i5.TransactionHttpCommand { - _FakeTransactionHttpCommand_4( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeBaseCommand_5> - extends _i1.SmartFake implements _i7.BaseCommand { - _FakeBaseCommand_5( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeRouterLANSettings_6 extends _i1.SmartFake - implements _i8.RouterLANSettings { - _FakeRouterLANSettings_6( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [RouterRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockRouterRepository extends _i1.Mock implements _i9.RouterRepository { - @override - _i2.Ref get ref => (super.noSuchMethod( - Invocation.getter(#ref), - returnValue: _FakeRef_0( - this, - Invocation.getter(#ref), - ), - returnValueForMissingStub: _FakeRef_0( - this, - Invocation.getter(#ref), - ), - ) as _i2.Ref); - - @override - _i3.JNAPCommandExecutor get executor => (super.noSuchMethod( - Invocation.getter(#executor), - returnValue: _FakeJNAPCommandExecutor_1( - this, - Invocation.getter(#executor), - ), - returnValueForMissingStub: _FakeJNAPCommandExecutor_1( - this, - Invocation.getter(#executor), - ), - ) as _i3.JNAPCommandExecutor); - - @override - bool get isEnableBTSetup => (super.noSuchMethod( - Invocation.getter(#isEnableBTSetup), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); - - @override - set enableBTSetup(bool? isEnable) => super.noSuchMethod( - Invocation.setter( - #enableBTSetup, - isEnable, - ), - returnValueForMissingStub: null, - ); - - @override - _i10.Future<_i4.JNAPSuccess> send( - _i11.JNAPAction? action, { - Map? data = const {}, - Map? extraHeaders = const {}, - bool? auth = false, - _i9.CommandType? type, - bool? fetchRemote = false, - _i7.CacheLevel? cacheLevel, - int? timeoutMs = 10000, - int? retries = 1, - _i12.JNAPSideEffectOverrides? sideEffectOverrides, - }) => - (super.noSuchMethod( - Invocation.method( - #send, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #auth: auth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - returnValue: _i10.Future<_i4.JNAPSuccess>.value(_FakeJNAPSuccess_2( - this, - Invocation.method( - #send, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #auth: auth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - )), - returnValueForMissingStub: - _i10.Future<_i4.JNAPSuccess>.value(_FakeJNAPSuccess_2( - this, - Invocation.method( - #send, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #auth: auth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - )), - ) as _i10.Future<_i4.JNAPSuccess>); - - @override - _i10.Future<_i4.JNAPTransactionSuccessWrap> transaction( - _i13.JNAPTransactionBuilder? builder, { - bool? fetchRemote = false, - _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, - int? timeoutMs = 10000, - int? retries = 1, - _i12.JNAPSideEffectOverrides? sideEffectOverrides, - }) => - (super.noSuchMethod( - Invocation.method( - #transaction, - [builder], - { - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - returnValue: _i10.Future<_i4.JNAPTransactionSuccessWrap>.value( - _FakeJNAPTransactionSuccessWrap_3( - this, - Invocation.method( - #transaction, - [builder], - { - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - )), - returnValueForMissingStub: - _i10.Future<_i4.JNAPTransactionSuccessWrap>.value( - _FakeJNAPTransactionSuccessWrap_3( - this, - Invocation.method( - #transaction, - [builder], - { - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #sideEffectOverrides: sideEffectOverrides, - }, - ), - )), - ) as _i10.Future<_i4.JNAPTransactionSuccessWrap>); - - @override - _i10.Future<_i5.TransactionHttpCommand> createTransaction( - List>? payload, { - bool? needAuth = false, - required List<_i11.JNAPAction>? actions, - bool? fetchRemote = false, - _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, - int? timeoutMs = 10000, - int? retries = 1, - _i9.CommandType? type, - }) => - (super.noSuchMethod( - Invocation.method( - #createTransaction, - [payload], - { - #needAuth: needAuth, - #actions: actions, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #type: type, - }, - ), - returnValue: _i10.Future<_i5.TransactionHttpCommand>.value( - _FakeTransactionHttpCommand_4( - this, - Invocation.method( - #createTransaction, - [payload], - { - #needAuth: needAuth, - #actions: actions, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #type: type, - }, - ), - )), - returnValueForMissingStub: - _i10.Future<_i5.TransactionHttpCommand>.value( - _FakeTransactionHttpCommand_4( - this, - Invocation.method( - #createTransaction, - [payload], - { - #needAuth: needAuth, - #actions: actions, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - #type: type, - }, - ), - )), - ) as _i10.Future<_i5.TransactionHttpCommand>); - - @override - _i10.Future< - _i7 - .BaseCommand<_i4.JNAPResult, _i6.JNAPCommandSpec>> createCommand( - String? action, { - Map? data = const {}, - Map? extraHeaders = const {}, - bool? needAuth = false, - _i9.CommandType? type, - bool? fetchRemote = false, - _i7.CacheLevel? cacheLevel = _i7.CacheLevel.localCached, - int? timeoutMs = 10000, - int? retries = 1, - }) => - (super.noSuchMethod( - Invocation.method( - #createCommand, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #needAuth: needAuth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - }, - ), - returnValue: _i10.Future< - _i7.BaseCommand<_i4.JNAPResult, - _i6.JNAPCommandSpec>>.value( - _FakeBaseCommand_5<_i4.JNAPResult, _i6.JNAPCommandSpec>( - this, - Invocation.method( - #createCommand, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #needAuth: needAuth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - }, - ), - )), - returnValueForMissingStub: _i10.Future< - _i7.BaseCommand<_i4.JNAPResult, - _i6.JNAPCommandSpec>>.value( - _FakeBaseCommand_5<_i4.JNAPResult, _i6.JNAPCommandSpec>( - this, - Invocation.method( - #createCommand, - [action], - { - #data: data, - #extraHeaders: extraHeaders, - #needAuth: needAuth, - #type: type, - #fetchRemote: fetchRemote, - #cacheLevel: cacheLevel, - #timeoutMs: timeoutMs, - #retries: retries, - }, - ), - )), - ) as _i10.Future< - _i7.BaseCommand<_i4.JNAPResult, _i6.JNAPCommandSpec>>); - - @override - _i10.Stream<_i4.JNAPResult> scheduledCommand({ - required _i11.JNAPAction? action, - int? retryDelayInMilliSec = 5000, - int? maxRetry = 10, - int? firstDelayInMilliSec = 3000, - Map? data = const {}, - bool Function(_i4.JNAPResult)? condition, - dynamic Function(bool)? onCompleted, - int? requestTimeoutOverride, - bool? auth = false, - }) => - (super.noSuchMethod( - Invocation.method( - #scheduledCommand, - [], - { - #action: action, - #retryDelayInMilliSec: retryDelayInMilliSec, - #maxRetry: maxRetry, - #firstDelayInMilliSec: firstDelayInMilliSec, - #data: data, - #condition: condition, - #onCompleted: onCompleted, - #requestTimeoutOverride: requestTimeoutOverride, - #auth: auth, - }, - ), - returnValue: _i10.Stream<_i4.JNAPResult>.empty(), - returnValueForMissingStub: _i10.Stream<_i4.JNAPResult>.empty(), - ) as _i10.Stream<_i4.JNAPResult>); -} - -/// A class which mocks [LocalNetworkSettingsService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockLocalNetworkSettingsService extends _i1.Mock - implements _i14.LocalNetworkSettingsService { - @override - _i10.Future<_i8.RouterLANSettings> fetchLANSettings( - {bool? forceRemote = false}) => - (super.noSuchMethod( - Invocation.method( - #fetchLANSettings, - [], - {#forceRemote: forceRemote}, - ), - returnValue: - _i10.Future<_i8.RouterLANSettings>.value(_FakeRouterLANSettings_6( - this, - Invocation.method( - #fetchLANSettings, - [], - {#forceRemote: forceRemote}, - ), - )), - returnValueForMissingStub: - _i10.Future<_i8.RouterLANSettings>.value(_FakeRouterLANSettings_6( - this, - Invocation.method( - #fetchLANSettings, - [], - {#forceRemote: forceRemote}, - ), - )), - ) as _i10.Future<_i8.RouterLANSettings>); - - @override - _i10.Future saveReservations({ - required String? routerIp, - required int? networkPrefixLength, - required String? hostName, - required bool? isDHCPEnabled, - required String? firstClientIP, - required String? lastClientIP, - required int? leaseMinutes, - String? dns1, - String? dns2, - String? dns3, - String? wins, - required List<_i15.DHCPReservationUIModel>? reservations, - }) => - (super.noSuchMethod( - Invocation.method( - #saveReservations, - [], - { - #routerIp: routerIp, - #networkPrefixLength: networkPrefixLength, - #hostName: hostName, - #isDHCPEnabled: isDHCPEnabled, - #firstClientIP: firstClientIP, - #lastClientIP: lastClientIP, - #leaseMinutes: leaseMinutes, - #dns1: dns1, - #dns2: dns2, - #dns3: dns3, - #wins: wins, - #reservations: reservations, - }, - ), - returnValue: _i10.Future.value(), - returnValueForMissingStub: _i10.Future.value(), - ) as _i10.Future); - - @override - List<_i15.DHCPReservationUIModel> convertFromJNAPList( - List<_i8.DHCPReservation>? list) => - (super.noSuchMethod( - Invocation.method( - #convertFromJNAPList, - [list], - ), - returnValue: <_i15.DHCPReservationUIModel>[], - returnValueForMissingStub: <_i15.DHCPReservationUIModel>[], - ) as List<_i15.DHCPReservationUIModel>); -} diff --git a/test/mocks/test_data/dhcp_reservations_test_data.dart b/test/mocks/test_data/dhcp_reservations_test_data.dart index b2c8bbe03..c127b2dea 100644 --- a/test/mocks/test_data/dhcp_reservations_test_data.dart +++ b/test/mocks/test_data/dhcp_reservations_test_data.dart @@ -1,11 +1,11 @@ -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; /// Test data builder for DHCPReservationsService tests /// -/// Provides factory methods to create ReservationItemUIModel test data +/// Provides factory methods to create DHCPReservationUIModel test data /// with sensible defaults and various test scenarios. class DHCPReservationsTestData { - /// Create default ReservationItemUIModel + /// Create default DHCPReservationUIModel static DHCPReservationUIModel createReservationUIModel({ String macAddress = '00:11:22:33:44:55', String ipAddress = '192.168.1.100', diff --git a/test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart b/test/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model_test.dart similarity index 98% rename from test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart rename to test/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model_test.dart index 25ab72bac..c9d54085d 100644 --- a/test/page/advanced_settings/local_network_settings/models/reservation_item_ui_model_test.dart +++ b/test/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model_test.dart @@ -1,10 +1,10 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; void main() { - group('ReservationItemUIModel -', () { + group('DHCPReservationUIModel -', () { const testMacAddress = '00:11:22:33:44:55'; const testIpAddress = '192.168.1.100'; const testDescription = 'Test Device'; diff --git a/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart b/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart index bd181d9d7..af3b1eda6 100644 --- a/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart +++ b/test/page/advanced_settings/local_network_settings/services/local_network_settings_service_test.dart @@ -3,7 +3,6 @@ import 'package:mockito/mockito.dart'; import 'package:privacy_gui/core/jnap/actions/better_action.dart'; import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; -import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/reservation_item_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/services/local_network_settings_service.dart'; import 'package:privacy_gui/utils.dart'; @@ -269,53 +268,7 @@ void main() { }); }); - group('convertFromJNAPList -', () { - test('converts JNAP list to UI model list', () { - final jnapList = [ - DHCPReservation( - macAddress: '00:11:22:33:44:55', - ipAddress: '192.168.1.100', - description: 'Device 1', - ), - DHCPReservation( - macAddress: 'AA:BB:CC:DD:EE:FF', - ipAddress: '192.168.1.101', - description: 'Device 2', - ), - ]; - - final result = service.convertFromJNAPList(jnapList); - - expect(result, isA>()); - expect(result.length, 2); - expect(result[0].macAddress, '00:11:22:33:44:55'); - expect(result[0].ipAddress, '192.168.1.100'); - expect(result[0].description, 'Device 1'); - expect(result[1].macAddress, 'AA:BB:CC:DD:EE:FF'); - expect(result[1].ipAddress, '192.168.1.101'); - expect(result[1].description, 'Device 2'); - }); - - test('converts empty JNAP list', () { - final result = service.convertFromJNAPList([]); - - expect(result, isEmpty); - }); - - test('preserves all fields during conversion', () { - final jnapReservation = DHCPReservation( - macAddress: '00:11:22:33:44:55', - ipAddress: '192.168.1.100', - description: 'Test Device with Special Chars !@#\$%', - ); - - final result = service.convertFromJNAPList([jnapReservation]); - - expect(result.length, 1); - expect(result[0].macAddress, jnapReservation.macAddress); - expect(result[0].ipAddress, jnapReservation.ipAddress); - expect(result[0].description, jnapReservation.description); - }); - }); + // Note: convertFromJNAPList is now a private method (_fromJNAPList) + // The conversion logic is tested through fetchLANSettingsWithUIModels }); } diff --git a/test/page/instant_device/views/localizations/device_detail_view_test.dart b/test/page/instant_device/views/localizations/device_detail_view_test.dart index 65988673b..92c447be1 100644 --- a/test/page/instant_device/views/localizations/device_detail_view_test.dart +++ b/test/page/instant_device/views/localizations/device_detail_view_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:privacy_gui/core/jnap/models/lan_settings.dart'; +import 'package:privacy_gui/page/advanced_settings/local_network_settings/models/dhcp_reservation_ui_model.dart'; import 'package:privacy_gui/page/advanced_settings/local_network_settings/providers/local_network_settings_state.dart'; import 'package:privacy_gui/page/instant_device/_instant_device.dart'; import 'package:ui_kit_library/ui_kit.dart'; @@ -34,7 +34,7 @@ final _defaultNetworkState = LocalNetworkSettingsState.fromMap(mockLocalNetworkSettingsState); LocalNetworkSettingsState _networkStateWithReservation() { - final reservation = DHCPReservation( + final reservation = DHCPReservationUIModel( macAddress: _defaultExternalState.item.macAddress, ipAddress: _defaultExternalState.item.ipv4Address, description: 'Reserved device',