diff --git a/lib/app.dart b/lib/app.dart index ac1cafefc..bc9e5d025 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -175,7 +175,7 @@ class _LinksysAppState extends ConsumerState // .then((prefs) { // final currentSN = prefs.getString(pCurrentSN); // if (currentSN != null && - // ref.read(dashboardManagerProvider).deviceInfo?.serialNumber != + // ref.read(sessionProvider).deviceInfo?.serialNumber != // currentSN) { // // if (mounted) { // // showRouterNotFoundAlert(context, ref); diff --git a/lib/core/data/providers/dashboard_manager_provider.dart b/lib/core/data/providers/dashboard_manager_provider.dart deleted file mode 100644 index 886a67f8a..000000000 --- a/lib/core/data/providers/dashboard_manager_provider.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/constants/_constants.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; -import 'package:privacy_gui/core/data/providers/polling_provider.dart'; -import 'package:privacy_gui/core/data/services/dashboard_manager_service.dart'; -import 'package:privacy_gui/core/utils/bench_mark.dart'; -import 'package:privacy_gui/core/utils/logger.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -final dashboardManagerProvider = - NotifierProvider( - () => DashboardManagerNotifier(), -); - -class DashboardManagerNotifier extends Notifier { - @override - DashboardManagerState build() { - final coreTransactionData = ref.watch(pollingProvider).value; - final service = ref.read(dashboardManagerServiceProvider); - return service.transformPollingData(coreTransactionData); - } - - Future saveSelectedNetwork( - String serialNumber, String networkId) async { - logger.i('[Prepare]: saveSelectedNetwork - $networkId, $serialNumber'); - final pref = await SharedPreferences.getInstance(); - logger.d('[Prepare]: save selected network - $serialNumber, $networkId'); - await pref.setString(pCurrentSN, serialNumber); - await pref.setString(pSelectedNetworkId, networkId); - ref.read(selectedNetworkIdProvider.notifier).state = networkId; - state = const DashboardManagerState(); - } - - Future checkRouterIsBack() async { - final service = ref.read(dashboardManagerServiceProvider); - final prefs = await SharedPreferences.getInstance(); - final currentSN = - prefs.getString(pCurrentSN) ?? prefs.getString(pPnpConfiguredSN); - return service.checkRouterIsBack(currentSN ?? ''); - } - - Future checkDeviceInfo(String? serialNumber) async { - final benchMark = BenchMarkLogger(name: 'checkDeviceInfo'); - benchMark.start(); - final service = ref.read(dashboardManagerServiceProvider); - final nodeDeviceInfo = await service.checkDeviceInfo(state.deviceInfo); - benchMark.end(); - return nodeDeviceInfo; - } -} - -final selectedNetworkIdProvider = StateProvider((ref) { - return null; -}); diff --git a/lib/core/data/providers/dashboard_manager_state.dart b/lib/core/data/providers/dashboard_manager_state.dart deleted file mode 100644 index b261c570c..000000000 --- a/lib/core/data/providers/dashboard_manager_state.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'dart:convert'; - -import 'package:equatable/equatable.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; -import 'package:privacy_gui/core/jnap/models/radio_info.dart'; - -class DashboardManagerState extends Equatable { - final NodeDeviceInfo? deviceInfo; - final List mainRadios; - final List guestRadios; - final bool isGuestNetworkEnabled; - final int uptimes; - final String? wanConnection; - final List lanConnections; - final String? skuModelNumber; - final int localTime; - final String? cpuLoad; - final String? memoryLoad; - - const DashboardManagerState({ - this.deviceInfo, - this.mainRadios = const [], - this.guestRadios = const [], - this.isGuestNetworkEnabled = false, - this.uptimes = 0, - this.wanConnection, - this.lanConnections = const [], - this.skuModelNumber, - this.localTime = 0, - this.cpuLoad, - this.memoryLoad, - }); - - @override - List get props { - return [ - deviceInfo, - mainRadios, - guestRadios, - isGuestNetworkEnabled, - uptimes, - wanConnection, - lanConnections, - skuModelNumber, - localTime, - cpuLoad, - memoryLoad, - ]; - } - - DashboardManagerState copyWith({ - NodeDeviceInfo? deviceInfo, - List? mainRadios, - List? guestRadios, - bool? isGuestNetworkEnabled, - int? uptimes, - String? wanConnection, - List? lanConnections, - String? skuModelNumber, - int? localTime, - String? cpuLoad, - String? memoryLoad, - }) { - return DashboardManagerState( - deviceInfo: deviceInfo ?? this.deviceInfo, - mainRadios: mainRadios ?? this.mainRadios, - guestRadios: guestRadios ?? this.guestRadios, - isGuestNetworkEnabled: - isGuestNetworkEnabled ?? this.isGuestNetworkEnabled, - uptimes: uptimes ?? this.uptimes, - wanConnection: wanConnection ?? this.wanConnection, - lanConnections: lanConnections ?? this.lanConnections, - skuModelNumber: skuModelNumber ?? this.skuModelNumber, - localTime: localTime ?? this.localTime, - cpuLoad: cpuLoad ?? this.cpuLoad, - memoryLoad: memoryLoad ?? this.memoryLoad, - ); - } - - Map toMap() { - return { - 'deviceInfo': deviceInfo?.toJson(), - 'mainRadios': mainRadios.map((x) => x.toMap()).toList(), - 'guestRadios': guestRadios.map((x) => x.toMap()).toList(), - 'isGuestNetworkEnabled': isGuestNetworkEnabled, - 'uptimes': uptimes, - 'skuModelNumber': skuModelNumber, - 'localTime': localTime, - 'cpuLoad': cpuLoad, - 'memoryLoad': memoryLoad, - }..removeWhere((key, value) => value == null); - } - - factory DashboardManagerState.fromMap(Map map) { - return DashboardManagerState( - deviceInfo: map['deviceInfo'] != null - ? NodeDeviceInfo.fromJson(map['deviceInfo']) - : null, - mainRadios: map['mainRadios'] != null - ? List.from( - map['mainRadios'].map( - (x) => RouterRadio.fromMap(x), - ), - ) - : [], - guestRadios: map['guestRadios'] != null - ? List.from( - map['guestRadios'].map( - (x) => GuestRadioInfo.fromMap(x), - ), - ) - : [], - isGuestNetworkEnabled: map['isGuestNetworkEnabled'] as bool, - uptimes: map['uptimes'] as int, - skuModelNumber: map['skuModelNumber'], - localTime: map['localTime'], - cpuLoad: map['cpuLoad'], - memoryLoad: map['memoryLoad'], - ); - } - - String toJson() => json.encode(toMap()); - - factory DashboardManagerState.fromJson(String source) => - DashboardManagerState.fromMap( - json.decode(source) as Map); - - @override - bool get stringify => true; -} diff --git a/lib/core/data/providers/device_info_provider.dart b/lib/core/data/providers/device_info_provider.dart new file mode 100644 index 000000000..481a50ab9 --- /dev/null +++ b/lib/core/data/providers/device_info_provider.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.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/device_info.dart'; +import 'package:privacy_gui/core/jnap/models/soft_sku_settings.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/polling_helpers.dart'; + +final deviceInfoProvider = Provider((ref) { + final pollingData = ref.watch(pollingProvider).value; + + NodeDeviceInfo? deviceInfo; + String? skuModelNumber; + + final deviceInfoOutput = + getPollingOutput(pollingData, JNAPAction.getDeviceInfo); + if (deviceInfoOutput != null) { + deviceInfo = NodeDeviceInfo.fromJson(deviceInfoOutput); + } + + final skuOutput = + getPollingOutput(pollingData, JNAPAction.getSoftSKUSettings); + if (skuOutput != null) { + final settings = SoftSKUSettings.fromMap(skuOutput); + skuModelNumber = settings.modelNumber; + } + + return DeviceInfoState( + deviceInfo: deviceInfo, + skuModelNumber: skuModelNumber, + ); +}); + +class DeviceInfoState extends Equatable { + final NodeDeviceInfo? deviceInfo; + final String? skuModelNumber; + + const DeviceInfoState({ + this.deviceInfo, + this.skuModelNumber, + }); + + @override + List get props => [deviceInfo, skuModelNumber]; +} diff --git a/lib/core/data/providers/ethernet_ports_provider.dart b/lib/core/data/providers/ethernet_ports_provider.dart new file mode 100644 index 000000000..25d4ff283 --- /dev/null +++ b/lib/core/data/providers/ethernet_ports_provider.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/polling_helpers.dart'; + +final ethernetPortsProvider = Provider((ref) { + final pollingData = ref.watch(pollingProvider).value; + + String? wanConnection; + List lanConnections = []; + + final portsOutput = + getPollingOutput(pollingData, JNAPAction.getEthernetPortConnections); + if (portsOutput != null) { + wanConnection = portsOutput['wanPortConnection'] as String?; + lanConnections = List.from(portsOutput['lanPortConnections'] ?? []); + } + + return EthernetPortsState( + wanConnection: wanConnection, + lanConnections: lanConnections, + ); +}); + +class EthernetPortsState extends Equatable { + final String? wanConnection; + final List lanConnections; + + const EthernetPortsState({ + this.wanConnection, + this.lanConnections = const [], + }); + + @override + List get props => [wanConnection, lanConnections]; +} diff --git a/lib/core/data/providers/polling_helpers.dart b/lib/core/data/providers/polling_helpers.dart new file mode 100644 index 000000000..a641919d7 --- /dev/null +++ b/lib/core/data/providers/polling_helpers.dart @@ -0,0 +1,24 @@ +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; + +/// Extracts the output Map from polling data for a specific action. +Map? getPollingOutput( + CoreTransactionData? data, + JNAPAction action, +) { + return _getPollingSuccess(data, action)?.output; +} + +/// Safely extracts a successful result for a specific action from polling data. +/// +/// Returns null if the data is null, the action is not found, or the result +/// is not a JNAPSuccess (e.g., it's a JNAPError). +JNAPSuccess? _getPollingSuccess( + CoreTransactionData? data, + JNAPAction action, +) { + if (data == null) return null; + final result = data.data[action]; + return result is JNAPSuccess ? result : null; +} diff --git a/lib/core/data/providers/router_time_provider.dart b/lib/core/data/providers/router_time_provider.dart new file mode 100644 index 000000000..5a7a71275 --- /dev/null +++ b/lib/core/data/providers/router_time_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/polling_helpers.dart'; + +/// Provides the router's local time as milliseconds since epoch. +/// +/// Extracts the current time from polling data for getLocalTime action. +/// Falls back to the current system time if unable to retrieve router time. +final routerTimeProvider = Provider((ref) { + final pollingData = ref.watch(pollingProvider).value; + + final timeOutput = getPollingOutput(pollingData, JNAPAction.getLocalTime); + if (timeOutput != null) { + final timeString = timeOutput['currentTime'] as String?; + if (timeString != null) { + final parsedTime = + DateFormat("yyyy-MM-ddThh:mm:ssZ").tryParse(timeString); + if (parsedTime != null) { + return parsedTime.millisecondsSinceEpoch; + } + } + } + + return DateTime.now().millisecondsSinceEpoch; +}); diff --git a/lib/core/data/providers/session_provider.dart b/lib/core/data/providers/session_provider.dart new file mode 100644 index 000000000..fe4554f3d --- /dev/null +++ b/lib/core/data/providers/session_provider.dart @@ -0,0 +1,102 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/constants/_constants.dart'; +import 'package:privacy_gui/core/jnap/models/device_info.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/services/session_service.dart'; +import 'package:privacy_gui/core/utils/bench_mark.dart'; +import 'package:privacy_gui/core/utils/logger.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Session Provider +/// +/// Provides session management operations without managing state. +/// Uses Notifier for convenient access to ref and other providers. +/// +/// This is a stateless notifier that serves as a collection of session-related +/// operations rather than managing reactive state. It handles: +/// - Selected network (serialNumber, networkId) persistence to SharedPreferences +/// - Router connectivity validation and serial number verification +/// - Device info retrieval with caching support +/// +/// This provider serves as the central point for managing which router/network +/// the user is currently connected to, and provides methods to verify connectivity +/// and retrieve router information during session initialization. +/// +/// Note: While this uses NotifierProvider, it doesn't manage any reactive state. +/// The Notifier pattern is used purely for accessing ref and organizing related +/// operations in a single class. + +final sessionProvider = NotifierProvider( + () => SessionNotifier(), +); + +class SessionNotifier extends Notifier { + @override + void build() { + // No state management needed - this is a utility notifier + } + + /// Checks if the router is accessible and matches the expected serial number. + /// + /// This method retrieves the expected serial number from SharedPreferences + /// (prioritizing [pCurrentSN] over [pPnpConfiguredSN]) and validates that + /// the router is reachable with matching serial number. + /// + /// Returns: [NodeDeviceInfo] if router is accessible and SN matches + /// + /// Throws: + /// - [SerialNumberMismatchError] if connected router has different SN + /// - [ConnectivityError] if router is unreachable + Future checkRouterIsBack() async { + final service = ref.read(sessionServiceProvider); + final prefs = await SharedPreferences.getInstance(); + final currentSN = + prefs.getString(pCurrentSN) ?? prefs.getString(pPnpConfiguredSN); + return service.checkRouterIsBack(currentSN ?? ''); + } + + /// Retrieves device info, using cached value if available. + /// + /// This method first checks [deviceInfoProvider] for cached data. + /// If cache is unavailable, it fetches fresh data from the router. + /// + /// [serialNumber] - Currently unused, kept for API compatibility + /// + /// Returns: [NodeDeviceInfo] from cache or fresh API call + /// + /// Throws: [ServiceError] on API failure when cache is unavailable + Future checkDeviceInfo(String? serialNumber) async { + final benchMark = BenchMarkLogger(name: 'checkDeviceInfo'); + benchMark.start(); + final service = ref.read(sessionServiceProvider); + final cachedDeviceInfo = ref.read(deviceInfoProvider).deviceInfo; + final nodeDeviceInfo = await service.checkDeviceInfo(cachedDeviceInfo); + benchMark.end(); + return nodeDeviceInfo; + } + + /// Saves the selected network and serial number to SharedPreferences. + /// + /// This method persists the current session information and updates the + /// [selectedNetworkIdProvider] state. + /// + /// [serialNumber] - The router's serial number + /// [networkId] - The network ID (cloud-based) or empty string for local sessions + Future saveSelectedNetwork( + String serialNumber, String networkId) async { + logger.i('[Session]: saveSelectedNetwork - $networkId, $serialNumber'); + final pref = await SharedPreferences.getInstance(); + logger.d('[Session]: save selected network - $serialNumber, $networkId'); + await pref.setString(pCurrentSN, serialNumber); + await pref.setString(pSelectedNetworkId, networkId); + ref.read(selectedNetworkIdProvider.notifier).state = networkId; + } +} + +/// State provider for the currently selected network ID. +/// +/// This is updated by [SessionNotifier.saveSelectedNetwork] and used +/// throughout the app to track which cloud network is active. +final selectedNetworkIdProvider = StateProvider((ref) { + return null; +}); diff --git a/lib/core/data/providers/system_stats_provider.dart b/lib/core/data/providers/system_stats_provider.dart new file mode 100644 index 000000000..a1e30b862 --- /dev/null +++ b/lib/core/data/providers/system_stats_provider.dart @@ -0,0 +1,41 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/polling_helpers.dart'; + +final systemStatsProvider = Provider((ref) { + final pollingData = ref.watch(pollingProvider).value; + + int uptimes = 0; + String? cpuLoad; + String? memoryLoad; + + final statsOutput = getPollingOutput(pollingData, JNAPAction.getSystemStats); + if (statsOutput != null) { + uptimes = statsOutput['uptimeSeconds'] as int? ?? 0; + cpuLoad = statsOutput['CPULoad'] as String?; + memoryLoad = statsOutput['MemoryLoad'] as String?; + } + + return SystemStatsState( + uptimes: uptimes, + cpuLoad: cpuLoad, + memoryLoad: memoryLoad, + ); +}); + +class SystemStatsState extends Equatable { + final int uptimes; + final String? cpuLoad; + final String? memoryLoad; + + const SystemStatsState({ + this.uptimes = 0, + this.cpuLoad, + this.memoryLoad, + }); + + @override + List get props => [uptimes, cpuLoad, memoryLoad]; +} diff --git a/lib/core/data/providers/wifi_radios_provider.dart b/lib/core/data/providers/wifi_radios_provider.dart new file mode 100644 index 000000000..88267656b --- /dev/null +++ b/lib/core/data/providers/wifi_radios_provider.dart @@ -0,0 +1,51 @@ +import 'package:equatable/equatable.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/radio_info.dart'; +import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/polling_helpers.dart'; + +final wifiRadiosProvider = Provider((ref) { + final pollingData = ref.watch(pollingProvider).value; + + List mainRadios = []; + List guestRadios = []; + bool isGuestNetworkEnabled = false; + + final radioInfoOutput = + getPollingOutput(pollingData, JNAPAction.getRadioInfo); + if (radioInfoOutput != null) { + final getRadioInfo = GetRadioInfo.fromMap(radioInfoOutput); + mainRadios = getRadioInfo.radios; + } + + final guestOutput = + getPollingOutput(pollingData, JNAPAction.getGuestRadioSettings); + if (guestOutput != null) { + final guestSettings = GuestRadioSettings.fromMap(guestOutput); + guestRadios = guestSettings.radios; + isGuestNetworkEnabled = guestSettings.isGuestNetworkEnabled; + } + + return WifiRadiosState( + mainRadios: mainRadios, + guestRadios: guestRadios, + isGuestNetworkEnabled: isGuestNetworkEnabled, + ); +}); + +class WifiRadiosState extends Equatable { + final List mainRadios; + final List guestRadios; + final bool isGuestNetworkEnabled; + + const WifiRadiosState({ + this.mainRadios = const [], + this.guestRadios = const [], + this.isGuestNetworkEnabled = false, + }); + + @override + List get props => [mainRadios, guestRadios, isGuestNetworkEnabled]; +} diff --git a/lib/core/data/services/dashboard_manager_service.dart b/lib/core/data/services/dashboard_manager_service.dart deleted file mode 100644 index 85b504ef6..000000000 --- a/lib/core/data/services/dashboard_manager_service.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:intl/intl.dart'; -import 'package:privacy_gui/core/errors/service_error.dart'; -import 'package:privacy_gui/core/jnap/actions/better_action.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; -import 'package:privacy_gui/core/jnap/models/radio_info.dart'; -import 'package:privacy_gui/core/jnap/models/soft_sku_settings.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; -import 'package:privacy_gui/core/data/providers/polling_provider.dart'; -import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; -import 'package:privacy_gui/core/jnap/router_repository.dart'; - -final dashboardManagerServiceProvider = - Provider((ref) { - return DashboardManagerService(ref.watch(routerRepositoryProvider)); -}); - -/// Service for dashboard management operations. -/// -/// Handles JNAP communication and transforms raw API responses -/// into DashboardManagerState. This isolates JNAP protocol details -/// from the DashboardManagerNotifier. -class DashboardManagerService { - final RouterRepository _routerRepository; - - DashboardManagerService(this._routerRepository); - - // === Data Transformation === - - /// Transforms polling data into DashboardManagerState. - /// - /// [pollingResult] - Raw JNAP transaction data from pollingProvider. - /// Can be null during initial load. - /// - /// Returns: Complete DashboardManagerState with all dashboard information. - /// - /// Behavior: - /// - If [pollingResult] is null, returns empty default state - /// - Processes all available JNAP action results - /// - Skips failed actions gracefully (partial state) - /// - Never throws - always returns valid state - DashboardManagerState transformPollingData( - CoreTransactionData? pollingResult) { - Map? getDeviceInfoData; - Map? getRadioInfoData; - Map? getGuestRadioSettingsData; - Map? getSystemStats; - Map? getEthernetPortConnections; - Map? getLocalTime; - - final result = pollingResult?.data; - if (result != null) { - // Safely extract output only from successful results - JNAPSuccess? getSuccess(JNAPAction action) { - final r = result[action]; - return r is JNAPSuccess ? r : null; - } - - getDeviceInfoData = getSuccess(JNAPAction.getDeviceInfo)?.output; - getRadioInfoData = getSuccess(JNAPAction.getRadioInfo)?.output; - getGuestRadioSettingsData = - getSuccess(JNAPAction.getGuestRadioSettings)?.output; - getSystemStats = getSuccess(JNAPAction.getSystemStats)?.output; - getEthernetPortConnections = - getSuccess(JNAPAction.getEthernetPortConnections)?.output; - getLocalTime = getSuccess(JNAPAction.getLocalTime)?.output; - } - - var newState = const DashboardManagerState(); - if (getDeviceInfoData != null) { - newState = newState.copyWith( - deviceInfo: NodeDeviceInfo.fromJson(getDeviceInfoData)); - } - if (getRadioInfoData != null) { - newState = _getMainRadioList(newState, getRadioInfoData); - } - if (getGuestRadioSettingsData != null) { - newState = _getGuestRadioList(newState, getGuestRadioSettingsData); - } - - if (getSystemStats != null) { - final uptimeSeconds = getSystemStats['uptimeSeconds']; - final cpuLoad = getSystemStats['CPULoad']; - final memoryLoad = getSystemStats['MemoryLoad']; - newState = newState.copyWith( - uptimes: uptimeSeconds, cpuLoad: cpuLoad, memoryLoad: memoryLoad); - } - - if (getEthernetPortConnections != null) { - final lanPortConnections = - List.from(getEthernetPortConnections['lanPortConnections']); - final wanPortConnection = getEthernetPortConnections['wanPortConnection']; - newState = newState.copyWith( - lanConnections: lanPortConnections, wanConnection: wanPortConnection); - } - - String? timeString; - if (getLocalTime != null) { - timeString = getLocalTime['currentTime']; - } - - // Try to parse the time string, fallback to current time if parsing fails - DateTime? parsedTime; - if (timeString != null) { - parsedTime = DateFormat("yyyy-MM-ddThh:mm:ssZ").tryParse(timeString); - } - final localTime = (parsedTime ?? DateTime.now()).millisecondsSinceEpoch; - newState = newState.copyWith(localTime: localTime); - - final softSKUSettings = JNAPTransactionSuccessWrap.getResult( - JNAPAction.getSoftSKUSettings, result ?? {}); - if (softSKUSettings != null) { - final settings = SoftSKUSettings.fromMap(softSKUSettings.output); - newState = newState.copyWith(skuModelNumber: settings.modelNumber); - } - - return newState; - } - - /// Extract main radio list from radio info data. - DashboardManagerState _getMainRadioList( - DashboardManagerState state, Map data) { - final getRadioInfoData = GetRadioInfo.fromMap(data); - return state.copyWith(mainRadios: getRadioInfoData.radios); - } - - /// Extract guest radio list from guest radio settings data. - DashboardManagerState _getGuestRadioList( - DashboardManagerState state, Map data) { - final guestRadioSettings = GuestRadioSettings.fromMap(data); - return state.copyWith( - guestRadios: guestRadioSettings.radios, - isGuestNetworkEnabled: guestRadioSettings.isGuestNetworkEnabled); - } - - // === Router Connectivity === - - /// Checks if the router is accessible and matches expected serial number. - /// - /// [expectedSerialNumber] - The serial number to verify against - /// - /// Returns: NodeDeviceInfo if router is reachable and SN matches - /// - /// Throws: - /// - [SerialNumberMismatchError] if connected router has different SN - /// - [ConnectivityError] if router is unreachable - Future checkRouterIsBack(String expectedSerialNumber) async { - try { - final result = await _routerRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - ); - final nodeDeviceInfo = NodeDeviceInfo.fromJson(result.output); - - if (expectedSerialNumber.isNotEmpty && - expectedSerialNumber != nodeDeviceInfo.serialNumber) { - throw SerialNumberMismatchError( - expected: expectedSerialNumber, - actual: nodeDeviceInfo.serialNumber, - ); - } - - return nodeDeviceInfo; - } on JNAPError catch (e) { - throw _mapJnapError(e); - } on SerialNumberMismatchError { - rethrow; - } catch (e) { - throw ConnectivityError(message: e.toString()); - } - } - - // === Device Info === - - /// Retrieves device info, using cached value if available. - /// - /// [cachedDeviceInfo] - Previously cached device info (from state) - /// - /// Returns: NodeDeviceInfo from cache or fresh API call - /// - /// Throws: [ServiceError] on API failure when cache is unavailable - Future checkDeviceInfo( - NodeDeviceInfo? cachedDeviceInfo) async { - if (cachedDeviceInfo != null) { - return cachedDeviceInfo; - } - - try { - final result = await _routerRepository.send( - JNAPAction.getDeviceInfo, - retries: 0, - timeoutMs: 3000, - ); - return NodeDeviceInfo.fromJson(result.output); - } on JNAPError catch (e) { - throw _mapJnapError(e); - } - } - - /// Maps JNAP errors to ServiceError types - ServiceError _mapJnapError(JNAPError error) { - return switch (error.result) { - '_ErrorUnauthorized' => const UnauthorizedError(), - 'ErrorDeviceNotFound' => const ResourceNotFoundError(), - 'ErrorInvalidInput' => InvalidInputError(message: error.error), - _ => UnexpectedError(originalError: error, message: error.result), - }; - } -} diff --git a/lib/core/data/services/session_service.dart b/lib/core/data/services/session_service.dart new file mode 100644 index 000000000..7840ffd3f --- /dev/null +++ b/lib/core/data/services/session_service.dart @@ -0,0 +1,98 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:privacy_gui/core/errors/service_error.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/models/device_info.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/jnap/router_repository.dart'; + +final sessionServiceProvider = Provider((ref) { + return SessionService(ref.watch(routerRepositoryProvider)); +}); + +/// Service for session management operations. +/// +/// Handles JNAP communication for: +/// - Router connectivity validation and serial number verification +/// - Device info retrieval with caching support +/// +/// This service is used by SessionProvider to manage the current router session. +class SessionService { + final RouterRepository _routerRepository; + + SessionService(this._routerRepository); + + // === Router Connectivity === + + /// Checks if the router is accessible and matches expected serial number. + /// + /// [expectedSerialNumber] - The serial number to verify against + /// + /// Returns: NodeDeviceInfo if router is reachable and SN matches + /// + /// Throws: + /// - [SerialNumberMismatchError] if connected router has different SN + /// - [ConnectivityError] if router is unreachable + Future checkRouterIsBack(String expectedSerialNumber) async { + try { + final result = await _routerRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + ); + final nodeDeviceInfo = NodeDeviceInfo.fromJson(result.output); + + if (expectedSerialNumber.isNotEmpty && + expectedSerialNumber != nodeDeviceInfo.serialNumber) { + throw SerialNumberMismatchError( + expected: expectedSerialNumber, + actual: nodeDeviceInfo.serialNumber, + ); + } + + return nodeDeviceInfo; + } on JNAPError catch (e) { + throw _mapJnapError(e); + } on SerialNumberMismatchError { + rethrow; + } catch (e) { + throw ConnectivityError(message: e.toString()); + } + } + + // === Device Info === + + /// Retrieves device info, using cached value if available. + /// + /// [cachedDeviceInfo] - Previously cached device info (from state) + /// + /// Returns: NodeDeviceInfo from cache or fresh API call + /// + /// Throws: [ServiceError] on API failure when cache is unavailable + Future checkDeviceInfo( + NodeDeviceInfo? cachedDeviceInfo) async { + if (cachedDeviceInfo != null) { + return cachedDeviceInfo; + } + + try { + final result = await _routerRepository.send( + JNAPAction.getDeviceInfo, + retries: 0, + timeoutMs: 3000, + ); + return NodeDeviceInfo.fromJson(result.output); + } on JNAPError catch (e) { + throw _mapJnapError(e); + } + } + + /// Maps JNAP errors to ServiceError types + ServiceError _mapJnapError(JNAPError error) { + return switch (error.result) { + '_ErrorUnauthorized' => const UnauthorizedError(), + 'ErrorDeviceNotFound' => const ResourceNotFoundError(), + 'ErrorInvalidInput' => InvalidInputError(message: error.error), + _ => UnexpectedError(originalError: error, message: error.result), + }; + } +} diff --git a/lib/core/jnap/router_repository.dart b/lib/core/jnap/router_repository.dart index 266dff7f0..2e21af42d 100644 --- a/lib/core/jnap/router_repository.dart +++ b/lib/core/jnap/router_repository.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:privacy_gui/core/cache/utility.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/providers/auth/_auth.dart'; import 'package:privacy_gui/providers/auth/auth_provider.dart'; import 'package:privacy_gui/providers/connectivity/_connectivity.dart'; diff --git a/lib/page/components/shortcuts/dialogs.dart b/lib/page/components/shortcuts/dialogs.dart index 09840e818..97e425852 100644 --- a/lib/page/components/shortcuts/dialogs.dart +++ b/lib/page/components/shortcuts/dialogs.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/core/utils/logger.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; @@ -364,7 +364,7 @@ Future showRouterNotFoundAlert(BuildContext context, WidgetRef ref, label: loc(context).tryAgain, onTap: () async { await ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .checkRouterIsBack() .then((_) { logger.d('[RouterNotFound] Found!'); diff --git a/lib/page/dashboard/providers/dashboard_home_provider.dart b/lib/page/dashboard/providers/dashboard_home_provider.dart index 91f313a76..93f74a69d 100644 --- a/lib/page/dashboard/providers/dashboard_home_provider.dart +++ b/lib/page/dashboard/providers/dashboard_home_provider.dart @@ -1,5 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_state.dart'; import 'package:privacy_gui/page/dashboard/services/dashboard_home_service.dart'; @@ -12,12 +15,18 @@ final dashboardHomeProvider = class DashboardHomeNotifier extends Notifier { @override DashboardHomeState build() { - final dashboardManagerState = ref.watch(dashboardManagerProvider); + final deviceInfoState = ref.watch(deviceInfoProvider); + final wifiRadiosState = ref.watch(wifiRadiosProvider); + final ethernetPortsState = ref.watch(ethernetPortsProvider); + final systemStatsState = ref.watch(systemStatsProvider); final deviceManagerState = ref.watch(deviceManagerProvider); final service = ref.read(dashboardHomeServiceProvider); return service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: (device) => ref.read(deviceManagerProvider.notifier).getBandConnectedBy(device), diff --git a/lib/page/dashboard/services/dashboard_home_service.dart b/lib/page/dashboard/services/dashboard_home_service.dart index 6552c62c6..cd41800b6 100644 --- a/lib/page/dashboard/services/dashboard_home_service.dart +++ b/lib/page/dashboard/services/dashboard_home_service.dart @@ -2,7 +2,10 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; import 'package:privacy_gui/core/jnap/models/radio_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_state.dart'; import 'package:privacy_gui/core/utils/devices.dart'; import 'package:privacy_gui/core/utils/icon_rules.dart'; @@ -26,23 +29,26 @@ class DashboardHomeService { /// This method orchestrates all the data transformation logic required /// to build the UI state for the dashboard home view. DashboardHomeState buildDashboardHomeState({ - required DashboardManagerState dashboardManagerState, + required DeviceInfoState deviceInfoState, + required WifiRadiosState wifiRadiosState, + required EthernetPortsState ethernetPortsState, + required SystemStatsState systemStatsState, required DeviceManagerState deviceManagerState, required String Function(LinksysDevice device) getBandForDevice, required List deviceList, }) { // Build WiFi list final wifiList = _buildMainWiFiItems( - mainRadios: dashboardManagerState.mainRadios, + mainRadios: wifiRadiosState.mainRadios, mainWifiDevices: deviceManagerState.mainWifiDevices, getBandForDevice: getBandForDevice, ); // Add guest WiFi if exists final guestWifi = _buildGuestWiFiItem( - guestRadios: dashboardManagerState.guestRadios, + guestRadios: wifiRadiosState.guestRadios, guestWifiDevices: deviceManagerState.guestWifiDevices, - isGuestNetworkEnabled: dashboardManagerState.isGuestNetworkEnabled, + isGuestNetworkEnabled: wifiRadiosState.isGuestNetworkEnabled, ); if (guestWifi != null) { wifiList.add(guestWifi); @@ -66,7 +72,7 @@ class DashboardHomeService { ); // Determine port layout - final deviceInfo = dashboardManagerState.deviceInfo; + final deviceInfo = deviceInfoState.deviceInfo; final horizontalPortLayout = isHorizontalPorts( modelNumber: deviceInfo?.modelNumber ?? '', hardwareVersion: deviceInfo?.hardwareVersion ?? '1', @@ -74,9 +80,9 @@ class DashboardHomeService { return DashboardHomeState( wifis: wifiList, - uptime: dashboardManagerState.uptimes, - wanPortConnection: dashboardManagerState.wanConnection, - lanPortConnections: dashboardManagerState.lanConnections, + uptime: systemStatsState.uptimes, + wanPortConnection: ethernetPortsState.wanConnection, + lanPortConnections: ethernetPortsState.lanConnections, isFirstPolling: isFirstPolling, masterIcon: masterIcon, isAnyNodesOffline: isAnyNodesOffline, diff --git a/lib/page/dashboard/views/components/widgets/home_title.dart b/lib/page/dashboard/views/components/widgets/home_title.dart index 409978584..cdb1393a6 100644 --- a/lib/page/dashboard/views/components/widgets/home_title.dart +++ b/lib/page/dashboard/views/components/widgets/home_title.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/router_time_provider.dart'; import 'package:privacy_gui/core/data/providers/node_internet_status_provider.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; import 'package:privacy_gui/page/components/shortcuts/dialogs.dart'; @@ -26,9 +26,9 @@ class DashboardHomeTitle extends ConsumerWidget { Widget _buildContent(BuildContext context, WidgetRef ref) { final wanStatus = ref.watch(internetStatusProvider); - final state = ref.watch(dashboardManagerProvider); + final routerTime = ref.watch(routerTimeProvider); final isOnline = wanStatus == InternetStatus.online; - final localTime = DateTime.fromMillisecondsSinceEpoch(state.localTime); + final localTime = DateTime.fromMillisecondsSinceEpoch(routerTime); return Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/page/dashboard/views/prepare_dashboard_view.dart b/lib/page/dashboard/views/prepare_dashboard_view.dart index 0e8cd6413..62c146bd8 100644 --- a/lib/page/dashboard/views/prepare_dashboard_view.dart +++ b/lib/page/dashboard/views/prepare_dashboard_view.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacy_gui/core/cache/linksys_cache_manager.dart'; import 'package:go_router/go_router.dart'; import 'package:privacy_gui/core/jnap/actions/better_action.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/jnap/router_repository.dart'; import 'package:privacy_gui/core/utils/logger.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; @@ -69,7 +69,7 @@ class _PrepareDashboardViewState extends ConsumerState { return; } else { await ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(serialNumber, networkId); } } @@ -85,7 +85,7 @@ class _PrepareDashboardViewState extends ConsumerState { .then( (value) => NodeDeviceInfo.fromJson(value.output).serialNumber); await ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(newSerialNumber, ''); } logger.d('Go to dashboard'); @@ -93,7 +93,7 @@ class _PrepareDashboardViewState extends ConsumerState { .read(linksysCacheManagerProvider) .loadCache(serialNumber: serialNumber ?? ''); final nodeDeviceInfo = await ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .checkDeviceInfo(null) .then((nodeDeviceInfo) { // Build/Update better actions diff --git a/lib/page/instant_admin/views/instant_admin_view.dart b/lib/page/instant_admin/views/instant_admin_view.dart index 91b7d9c99..65a115b7b 100644 --- a/lib/page/instant_admin/views/instant_admin_view.dart +++ b/lib/page/instant_admin/views/instant_admin_view.dart @@ -5,8 +5,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:go_router/go_router.dart'; import 'package:privacy_gui/constants/build_config.dart'; import 'package:privacy_gui/core/jnap/models/firmware_update_settings.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; import 'package:privacy_gui/core/data/providers/firmware_update_provider.dart'; import 'package:privacy_gui/core/errors/service_error.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; @@ -71,15 +70,15 @@ class _InstantAdminViewState extends ConsumerState { final isFwAutoUpdate = ref.watch(firmwareUpdateProvider .select((value) => value.settings.updatePolicy)) == FirmwareUpdateSettings.firmwareUpdatePolicyAuto; - final dashboardManagerState = ref.watch(dashboardManagerProvider); + final deviceInfoState = ref.watch(deviceInfoProvider); final timezoneState = ref.watch(timezoneProvider); final powerTableState = ref.watch(powerTableProvider); final widgets = [ _buildPasswordWidget(context, routerPasswordState), _buildAutoFirmwareWidget(context, isFwAutoUpdate), - if (dashboardManagerState.skuModelNumber?.endsWith('AH') != true) - _buildManualFirmwareWidget(context, dashboardManagerState), + if (deviceInfoState.skuModelNumber?.endsWith('AH') != true) + _buildManualFirmwareWidget(context, deviceInfoState), _buildTimezoneWidget(context, timezoneState), if (powerTableState.isPowerTableSelectable) _buildTransmitRegionWidget(context, powerTableState), @@ -162,9 +161,9 @@ class _InstantAdminViewState extends ConsumerState { Widget _buildManualFirmwareWidget( BuildContext context, - DashboardManagerState dashboardManagerState, + DeviceInfoState deviceInfoState, ) { - final firmwareVersion = dashboardManagerState.deviceInfo?.firmwareVersion; + final firmwareVersion = deviceInfoState.deviceInfo?.firmwareVersion; return _buildListCard( title: loc(context).manualFirmwareUpdate, description: firmwareVersion ?? '--', diff --git a/lib/page/instant_device/providers/device_filtered_list_provider.dart b/lib/page/instant_device/providers/device_filtered_list_provider.dart index 8a6ae0027..e8c421ef9 100644 --- a/lib/page/instant_device/providers/device_filtered_list_provider.dart +++ b/lib/page/instant_device/providers/device_filtered_list_provider.dart @@ -1,6 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/utils/devices.dart'; import 'package:privacy_gui/core/utils/logger.dart'; @@ -109,7 +109,7 @@ class DeviceFilterConfigNotifier extends Notifier { logger.i( 'Filter<$deviceUUID>:: Collect additional radios from connected devices: $radios'); return (ref - .read(dashboardManagerProvider) + .read(wifiRadiosProvider) .mainRadios .unique((x) => x.band) .map((e) => e.band) diff --git a/lib/page/instant_device/views/devices_filter_widget.dart b/lib/page/instant_device/views/devices_filter_widget.dart index c00ec5d02..032f1de9d 100644 --- a/lib/page/instant_device/views/devices_filter_widget.dart +++ b/lib/page/instant_device/views/devices_filter_widget.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/utils/extension.dart'; import 'package:privacy_gui/localization/localization_hook.dart'; @@ -68,7 +68,7 @@ class _DevicesFilterWidgetState extends ConsumerState { wifiState.settings.current.wifiList.guestWiFi.ssid, ]; final radios = (ref - .watch(dashboardManagerProvider.select((value) => value.mainRadios)) + .watch(wifiRadiosProvider.select((value) => value.mainRadios)) .unique((x) => x.band) .map((e) => e.band) .toList() diff --git a/lib/page/instant_verify/services/instant_verify_pdf_service.dart b/lib/page/instant_verify/services/instant_verify_pdf_service.dart index 0a5b14dc9..d811cb999 100644 --- a/lib/page/instant_verify/services/instant_verify_pdf_service.dart +++ b/lib/page/instant_verify/services/instant_verify_pdf_service.dart @@ -6,7 +6,8 @@ import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; import 'package:privacy_gui/core/jnap/models/back_haul_info.dart'; import 'package:privacy_gui/core/jnap/models/wan_status.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/utils/devices.dart'; import 'package:privacy_gui/core/utils/logger.dart'; @@ -62,21 +63,22 @@ class InstantVerifyPdfService { } static List _buildInfo(BuildContext context, WidgetRef ref) { - final dashboardState = ref.read(dashboardManagerProvider); + final deviceInfoState = ref.read(deviceInfoProvider); + final systemStatsState = ref.read(systemStatsProvider); final deviceManagerState = ref.read(deviceManagerProvider); final devicesState = ref.read(deviceManagerProvider); final healthCheckState = ref.read(healthCheckProvider); final systemConnectivityState = ref.read(instantVerifyProvider); final uptime = DateFormatUtils.formatDuration( - Duration(seconds: dashboardState.uptimes), context); + Duration(seconds: systemStatsState.uptimes), context); final master = devicesState.masterDevice; final guestWiFi = systemConnectivityState.guestRadioSettings.radios.firstOrNull; final isSupportHealthCheck = ref.watch(healthCheckProvider).isSpeedTestModuleSupported; - final cpuLoad = dashboardState.cpuLoad; - final memoryLoad = dashboardState.memoryLoad; + final cpuLoad = systemStatsState.cpuLoad; + final memoryLoad = systemStatsState.memoryLoad; return [ // pw.Text(loc(context).deviceInfo), @@ -84,7 +86,7 @@ class InstantVerifyPdfService { '${loc(context).systemTestDateFormat(DateTime.now())} ${loc(context).systemTestDateTime(DateTime.now())}'), pw.Text('${loc(context).uptime}: $uptime'), pw.Text('${loc(context).model}: ${master.modelNumber ?? '--'}'), - pw.Text('${loc(context).sku}: ${dashboardState.skuModelNumber ?? '--'}'), + pw.Text('${loc(context).sku}: ${deviceInfoState.skuModelNumber ?? '--'}'), pw.Text( '${loc(context).serialNumber}: ${master.unit.serialNumber ?? '--'}'), pw.Text('${loc(context).macAddress}: ${master.getMacAddress()}'), diff --git a/lib/page/instant_verify/views/instant_verify_view.dart b/lib/page/instant_verify/views/instant_verify_view.dart index 3aaef7a30..70f8e29f7 100644 --- a/lib/page/instant_verify/views/instant_verify_view.dart +++ b/lib/page/instant_verify/views/instant_verify_view.dart @@ -3,8 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:privacy_gui/constants/build_config.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/router_time_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; import 'package:privacy_gui/core/data/providers/firmware_update_provider.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/page/instant_verify/providers/wan_external_provider.dart'; @@ -538,15 +541,16 @@ class _InstantVerifyViewState extends ConsumerState } Widget _deviceInfoCard(BuildContext context, WidgetRef ref) { - final dashboardState = ref.watch(dashboardManagerProvider); + final deviceInfoState = ref.watch(deviceInfoProvider); + final systemStatsState = ref.watch(systemStatsProvider); + final routerTime = ref.watch(routerTimeProvider); final devicesState = ref.watch(deviceManagerProvider); final uptime = DateFormatUtils.formatDuration( - Duration(seconds: dashboardState.uptimes), context, true); - final localTime = - DateTime.fromMillisecondsSinceEpoch(dashboardState.localTime); + Duration(seconds: systemStatsState.uptimes), context, true); + final localTime = DateTime.fromMillisecondsSinceEpoch(routerTime); final master = devicesState.masterDevice; - final cpuLoad = dashboardState.cpuLoad; - final memoryLoad = dashboardState.memoryLoad; + final cpuLoad = systemStatsState.cpuLoad; + final memoryLoad = systemStatsState.memoryLoad; return AppCard( key: const ValueKey('deviceInfoCard'), @@ -631,7 +635,7 @@ class _InstantVerifyViewState extends ConsumerState children: [ AppText.bodySmall(loc(context).sku), AppText.labelMedium( - dashboardState.skuModelNumber ?? '--', + deviceInfoState.skuModelNumber ?? '--', selectable: true, ), ], @@ -720,7 +724,7 @@ class _InstantVerifyViewState extends ConsumerState } Widget _portsCard(BuildContext context, WidgetRef ref) { - final state = ref.watch(dashboardManagerProvider); + final state = ref.watch(ethernetPortsProvider); return AppCard( key: const ValueKey('portCard'), padding: EdgeInsets.zero, diff --git a/lib/page/login/views/login_local_view.dart b/lib/page/login/views/login_local_view.dart index 55aa5c351..6b27a3bdd 100644 --- a/lib/page/login/views/login_local_view.dart +++ b/lib/page/login/views/login_local_view.dart @@ -6,7 +6,7 @@ import 'package:go_router/go_router.dart'; import 'package:privacy_gui/constants/error_code.dart'; import 'package:privacy_gui/core/jnap/actions/better_action.dart'; import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/page/components/shortcuts/dialogs.dart'; import 'package:privacy_gui/page/components/styled/bottom_bar.dart'; import 'package:privacy_gui/page/components/ui_kit_page_view.dart'; @@ -51,10 +51,7 @@ class _LoginViewState extends ConsumerState { doSomethingWithSpinner(context, Future.doWhile(() => !mounted)) .then((value) { _getAdminPasswordHint(); - ref - .read(dashboardManagerProvider.notifier) - .checkDeviceInfo(null) - .then((value) { + ref.read(sessionProvider.notifier).checkDeviceInfo(null).then((value) { _deviceInfo = value; buildBetterActions(value.services); if (_p != null) { diff --git a/lib/page/select_network/views/select_network_view.dart b/lib/page/select_network/views/select_network_view.dart index 9d2a706c3..9a5ed6655 100644 --- a/lib/page/select_network/views/select_network_view.dart +++ b/lib/page/select_network/views/select_network_view.dart @@ -3,7 +3,7 @@ import 'package:animated_list_plus/transitions.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/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/utils/device_image_helper.dart'; import 'package:privacy_gui/core/utils/logger.dart'; import 'package:privacy_gui/page/components/ui_kit_page_view.dart'; @@ -124,10 +124,9 @@ class _SelectNetworkViewState extends ConsumerState { context.pop(); } else { // Select another network - await ref - .read(dashboardManagerProvider.notifier) - .saveSelectedNetwork(network.network.routerSerialNumber, - network.network.networkId); + await ref.read(sessionProvider.notifier).saveSelectedNetwork( + network.network.routerSerialNumber, + network.network.networkId); goRouter.goNamed('prepareDashboard'); } } diff --git a/lib/page/wifi_settings/providers/wifi_bundle_provider.dart b/lib/page/wifi_settings/providers/wifi_bundle_provider.dart index cda7eca83..3d8b7c65c 100644 --- a/lib/page/wifi_settings/providers/wifi_bundle_provider.dart +++ b/lib/page/wifi_settings/providers/wifi_bundle_provider.dart @@ -3,8 +3,9 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:privacy_gui/core/jnap/actions/jnap_service_supported.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; import 'package:privacy_gui/page/instant_privacy/providers/instant_privacy_state.dart'; import 'package:privacy_gui/page/wifi_settings/providers/wifi_advanced_state.dart'; import 'package:privacy_gui/page/wifi_settings/providers/wifi_bundle_state.dart'; @@ -32,28 +33,28 @@ class WifiBundleNotifier extends Notifier WifiBundleState> { @override WifiBundleState build() { - final dashboardManagerState = ref.read(dashboardManagerProvider); + final wifiRadiosState = ref.read(wifiRadiosProvider); final deviceManagerState = ref.read(deviceManagerProvider); + final ethernetPortsState = ref.read(ethernetPortsProvider); // Use service layer to create initial WiFi list settings // This avoids importing JNAP models directly in the provider - final initialWifiListSettings = ref - .read(wifiSettingsServiceProvider) - .createInitialWifiListSettings( - mainRadios: dashboardManagerState.mainRadios, - isGuestNetworkEnabled: dashboardManagerState.isGuestNetworkEnabled, - guestSSID: dashboardManagerState.guestRadios.firstOrNull?.guestSSID, - guestPassword: - dashboardManagerState.guestRadios.firstOrNull?.guestWPAPassphrase, - mainWifiDevices: deviceManagerState.mainWifiDevices, - guestWifiDevicesCount: deviceManagerState.guestWifiDevices.length, - getBandConnectedBy: (device) => ref - .read(deviceManagerProvider.notifier) - .getBandConnectedBy(device), - ); + final initialWifiListSettings = + ref.read(wifiSettingsServiceProvider).createInitialWifiListSettings( + mainRadios: wifiRadiosState.mainRadios, + isGuestNetworkEnabled: wifiRadiosState.isGuestNetworkEnabled, + guestSSID: wifiRadiosState.guestRadios.firstOrNull?.guestSSID, + guestPassword: + wifiRadiosState.guestRadios.firstOrNull?.guestWPAPassphrase, + mainWifiDevices: deviceManagerState.mainWifiDevices, + guestWifiDevicesCount: deviceManagerState.guestWifiDevices.length, + getBandConnectedBy: (device) => ref + .read(deviceManagerProvider.notifier) + .getBandConnectedBy(device), + ); final initialWifiListStatus = WiFiListStatus( - canDisableMainWiFi: dashboardManagerState.lanConnections.isNotEmpty); + canDisableMainWiFi: ethernetPortsState.lanConnections.isNotEmpty); const initialAdvancedSettings = WifiAdvancedSettingsState(); final initialPrivacySettings = InstantPrivacySettings.init(); @@ -82,7 +83,7 @@ class WifiBundleNotifier extends Notifier @override Future<(WifiBundleSettings?, WifiBundleStatus?)> performFetch( {bool forceRemote = false, bool updateStatusOnly = false}) async { - final dashboardManagerState = ref.read(dashboardManagerProvider); + final ethernetPortsState = ref.read(ethernetPortsProvider); final deviceManagerState = ref.read(deviceManagerProvider); final (newSettings, newStatus) = await ref.read(wifiSettingsServiceProvider).fetchBundleSettings( @@ -91,7 +92,7 @@ class WifiBundleNotifier extends Notifier mainWifiDevices: deviceManagerState.mainWifiDevices, guestWifiDevices: deviceManagerState.guestWifiDevices, allDevices: deviceManagerState.deviceList, - isLanConnected: dashboardManagerState.lanConnections.isNotEmpty, + isLanConnected: ethernetPortsState.lanConnections.isNotEmpty, getBandConnectedBy: (device) => ref .read(deviceManagerProvider.notifier) .getBandConnectedBy(device), diff --git a/lib/providers/auth/auth_provider.dart b/lib/providers/auth/auth_provider.dart index 63fa48a63..ed11716be 100644 --- a/lib/providers/auth/auth_provider.dart +++ b/lib/providers/auth/auth_provider.dart @@ -9,7 +9,7 @@ import 'package:privacy_gui/core/cloud/model/guardians_remote_assistance.dart'; import 'package:privacy_gui/core/cloud/model/region_code.dart'; import 'package:privacy_gui/core/http/linksys_http_client.dart'; import 'package:privacy_gui/core/jnap/actions/better_action.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/core/errors/service_error.dart'; @@ -378,9 +378,9 @@ class AuthNotifier extends AsyncNotifier { String networkId, String serialNumber, ) async { - // Update selected network via dashboard manager + // Update selected network via session provider await ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(serialNumber, networkId); // Delegate to AuthService diff --git a/lib/route/router_provider.dart b/lib/route/router_provider.dart index a66697416..c2e50b5f4 100644 --- a/lib/route/router_provider.dart +++ b/lib/route/router_provider.dart @@ -9,7 +9,8 @@ import 'package:privacy_gui/core/cache/linksys_cache_manager.dart'; import 'package:privacy_gui/core/jnap/actions/better_action.dart'; import 'package:privacy_gui/page/instant_setup/models/pnp_ui_models.dart'; import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/core/jnap/router_repository.dart'; import 'package:privacy_gui/core/utils/logger.dart'; @@ -368,7 +369,7 @@ class RouterNotifier extends ChangeNotifier { .loadCache(serialNumber: serialNumber ?? ''); logger.d('[Prepare]: device info check - $serialNumber'); final nodeDeviceInfo = await _ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .checkDeviceInfo(serialNumber) .then((nodeDeviceInfo) { // Build/Update better actions @@ -409,7 +410,7 @@ class RouterNotifier extends ChangeNotifier { return null; } await _ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(serialNumber, ''); } else if (_ref.read(selectedNetworkIdProvider) == null) { _ref.read(selectNetworkProvider.notifier).refreshCloudNetworks(); @@ -417,7 +418,7 @@ class RouterNotifier extends ChangeNotifier { return RoutePath.selectNetwork; } await _ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(serialNumber, networkId); } return null; @@ -451,7 +452,7 @@ class RouterNotifier extends ChangeNotifier { // Save serial number if serial number changed await _ref - .read(dashboardManagerProvider.notifier) + .read(sessionProvider.notifier) .saveSelectedNetwork(newSerialNumber, ''); return null; @@ -461,7 +462,7 @@ class RouterNotifier extends ChangeNotifier { serialNumber != null && serialNumber == _getStateDeviceInfo()?.serialNumber; NodeDeviceInfo? _getStateDeviceInfo() => - _ref.read(dashboardManagerProvider).deviceInfo; + _ref.read(deviceInfoProvider).deviceInfo; } final autoParentFirstLoginStateProvider = StateProvider((ref) { diff --git a/test/common/test_helper.dart b/test/common/test_helper.dart index d7b192ffa..c54f0c95f 100644 --- a/test/common/test_helper.dart +++ b/test/common/test_helper.dart @@ -78,7 +78,7 @@ import 'screen.dart'; import 'testable_router.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_provider.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/data/providers/firmware_update_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/page/instant_privacy/providers/instant_privacy_provider.dart'; @@ -92,7 +92,6 @@ import '../test_data/_index.dart'; import 'package:privacy_gui/page/advanced_settings/apps_and_gaming/providers/apps_and_gaming_state.dart'; import 'package:privacy_gui/page/advanced_settings/apps_and_gaming/ddns/providers/ddns_state.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_state.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; import 'package:privacy_gui/core/data/providers/firmware_update_state.dart'; import 'package:privacy_gui/core/data/providers/device_manager_state.dart'; import 'package:privacy_gui/page/instant_privacy/providers/instant_privacy_state.dart'; @@ -111,7 +110,7 @@ class TestHelper { late MockPortRangeTriggeringRuleNotifier mockPortRangeTriggeringRuleNotifier; late MockDMZSettingsNotifier mockDMZSettingsNotifier; late MockDashboardHomeNotifier mockDashboardHomeNotifier; - late MockDashboardManagerNotifier mockDashboardManagerNotifier; + late MockSessionNotifier mockSessionNotifier; late MockFirmwareUpdateNotifier mockFirmwareUpdateNotifier; late MockDeviceManagerNotifier mockDeviceManagerNotifier; late MockInstantPrivacyNotifier mockInstantPrivacyNotifier; @@ -176,7 +175,7 @@ class TestHelper { mockPortRangeTriggeringRuleNotifier = MockPortRangeTriggeringRuleNotifier(); mockDMZSettingsNotifier = MockDMZSettingsNotifier(); mockDashboardHomeNotifier = MockDashboardHomeNotifier(); - mockDashboardManagerNotifier = MockDashboardManagerNotifier(); + mockSessionNotifier = MockSessionNotifier(); mockFirmwareUpdateNotifier = MockFirmwareUpdateNotifier(); mockDeviceManagerNotifier = MockDeviceManagerNotifier(); mockInstantPrivacyNotifier = MockInstantPrivacyNotifier(); @@ -284,8 +283,7 @@ class TestHelper { .thenReturn(DMZSettingsState.fromMap(dmzSettingsTestState)); when(mockDashboardHomeNotifier.build()) .thenReturn(DashboardHomeState.fromMap(dashboardHomePinnacleTestState)); - when(mockDashboardManagerNotifier.build()).thenReturn( - DashboardManagerState.fromMap(dashboardManagerPinnacleTestState)); + // SessionNotifier now uses void state - no build() mock needed when(mockFirmwareUpdateNotifier.build()) .thenReturn(FirmwareUpdateState.fromMap(firmwareUpdateTestData)); when(mockDeviceManagerNotifier.build()) @@ -400,8 +398,7 @@ class TestHelper { .overrideWith(() => mockPortRangeTriggeringRuleNotifier), dmzSettingsProvider.overrideWith(() => mockDMZSettingsNotifier), dashboardHomeProvider.overrideWith(() => mockDashboardHomeNotifier), - dashboardManagerProvider - .overrideWith(() => mockDashboardManagerNotifier), + sessionProvider.overrideWith(() => mockSessionNotifier), firmwareUpdateProvider.overrideWith(() => mockFirmwareUpdateNotifier), deviceManagerProvider.overrideWith(() => mockDeviceManagerNotifier), instantPrivacyProvider.overrideWith(() => mockInstantPrivacyNotifier), diff --git a/test/core/data/providers/device_info_provider_test.dart b/test/core/data/providers/device_info_provider_test.dart new file mode 100644 index 000000000..658d3cfb9 --- /dev/null +++ b/test/core/data/providers/device_info_provider_test.dart @@ -0,0 +1,206 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; + +void main() { + group('deviceInfoProvider', () { + late ProviderContainer container; + + tearDown(() { + container.dispose(); + }); + + CoreTransactionData createPollingData({ + Map? deviceInfoOutput, + Map? softSkuOutput, + }) { + final data = {}; + if (deviceInfoOutput != null) { + data[JNAPAction.getDeviceInfo] = + JNAPSuccess(result: 'OK', output: deviceInfoOutput); + } + if (softSkuOutput != null) { + data[JNAPAction.getSoftSKUSettings] = + JNAPSuccess(result: 'OK', output: softSkuOutput); + } + return CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + } + + test('returns empty DeviceInfoState when polling data is null', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(null)), + ], + ); + + // Act + final state = container.read(deviceInfoProvider); + + // Assert + expect(state.deviceInfo, isNull); + expect(state.skuModelNumber, isNull); + }); + + test('extracts deviceInfo from getDeviceInfo action', () { + // Arrange + final deviceInfoOutput = { + 'serialNumber': 'TEST123456', + 'modelNumber': 'MX5300', + 'hardwareVersion': '1', + 'manufacturer': 'Linksys', + 'description': 'Test Router', + 'firmwareVersion': '1.0.0', + 'firmwareDate': '2024-01-01T00:00:00Z', + 'services': const ['http://linksys.com/jnap/core/Core'], + }; + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData(deviceInfoOutput: deviceInfoOutput), + )), + ], + ); + + // Act + final state = container.read(deviceInfoProvider); + + // Assert + expect(state.deviceInfo, isNotNull); + expect(state.deviceInfo!.serialNumber, 'TEST123456'); + expect(state.deviceInfo!.modelNumber, 'MX5300'); + expect(state.deviceInfo!.hardwareVersion, '1'); + }); + + test('extracts skuModelNumber from getSoftSKUSettings action', () { + // Arrange + final deviceInfoOutput = { + 'serialNumber': 'TEST123456', + 'modelNumber': 'MX5300', + 'hardwareVersion': '1', + 'manufacturer': 'Linksys', + 'description': 'Test Router', + 'firmwareVersion': '1.0.0', + 'firmwareDate': '2024-01-01T00:00:00Z', + 'services': const [], + }; + + final softSkuOutput = { + 'modelNumber': 'MX5300-SKU', + 'isChangeable': false, + }; + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + deviceInfoOutput: deviceInfoOutput, + softSkuOutput: softSkuOutput, + ), + )), + ], + ); + + // Act + final state = container.read(deviceInfoProvider); + + // Assert + expect(state.skuModelNumber, 'MX5300-SKU'); + }); + + test('returns null skuModelNumber when getSoftSKUSettings is missing', () { + // Arrange + final deviceInfoOutput = { + 'serialNumber': 'TEST123456', + 'modelNumber': 'MX5300', + 'hardwareVersion': '1', + 'manufacturer': 'Linksys', + 'description': 'Test Router', + 'firmwareVersion': '1.0.0', + 'firmwareDate': '2024-01-01T00:00:00Z', + 'services': const [], + }; + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData(deviceInfoOutput: deviceInfoOutput), + )), + ], + ); + + // Act + final state = container.read(deviceInfoProvider); + + // Assert + expect(state.deviceInfo, isNotNull); + expect(state.skuModelNumber, isNull); + }); + + test('returns null deviceInfo when getDeviceInfo returns JNAPError', () { + // Arrange + final data = { + JNAPAction.getDeviceInfo: const JNAPError( + result: 'ErrorDeviceNotFound', + error: 'Device not found', + ), + }; + final pollingData = + CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(deviceInfoProvider); + + // Assert + expect(state.deviceInfo, isNull); + }); + + group('DeviceInfoState', () { + test('props returns correct list for equality comparison', () { + // Arrange & Act + const state1 = DeviceInfoState(skuModelNumber: 'SKU1'); + const state2 = DeviceInfoState(skuModelNumber: 'SKU1'); + const state3 = DeviceInfoState(skuModelNumber: 'SKU2'); + + // Assert + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + test('default constructor creates empty state', () { + // Arrange & Act + const state = DeviceInfoState(); + + // Assert + expect(state.deviceInfo, isNull); + expect(state.skuModelNumber, isNull); + }); + }); + }); +} + +class _MockPollingNotifier extends AsyncNotifier + implements PollingNotifier { + final CoreTransactionData? _data; + + _MockPollingNotifier(this._data); + + @override + CoreTransactionData build() { + return _data ?? + const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/core/data/providers/ethernet_ports_provider_test.dart b/test/core/data/providers/ethernet_ports_provider_test.dart new file mode 100644 index 000000000..13cc721ed --- /dev/null +++ b/test/core/data/providers/ethernet_ports_provider_test.dart @@ -0,0 +1,251 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; + +void main() { + group('ethernetPortsProvider', () { + late ProviderContainer container; + + tearDown(() { + container.dispose(); + }); + + CoreTransactionData createPollingData({ + Map? ethernetPortsOutput, + }) { + final data = {}; + if (ethernetPortsOutput != null) { + data[JNAPAction.getEthernetPortConnections] = + JNAPSuccess(result: 'OK', output: ethernetPortsOutput); + } + return CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + } + + Map createEthernetPortsOutput({ + String wanPortConnection = 'Linked-1000Mbps', + List lanPortConnections = const [ + 'Linked-100Mbps', + 'None', + 'None', + 'None' + ], + }) { + return { + 'wanPortConnection': wanPortConnection, + 'lanPortConnections': lanPortConnections, + }; + } + + test('returns empty EthernetPortsState when polling data is null', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(null)), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.wanConnection, isNull); + expect(state.lanConnections, isEmpty); + }); + + test('extracts wanConnection from getEthernetPortConnections action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + ethernetPortsOutput: createEthernetPortsOutput( + wanPortConnection: 'Linked-1000Mbps', + ), + ), + )), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.wanConnection, 'Linked-1000Mbps'); + }); + + test('extracts lanConnections from getEthernetPortConnections action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + ethernetPortsOutput: createEthernetPortsOutput( + lanPortConnections: [ + 'Linked-100Mbps', + 'Linked-1000Mbps', + 'None', + 'None' + ], + ), + ), + )), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.lanConnections.length, 4); + expect(state.lanConnections[0], 'Linked-100Mbps'); + expect(state.lanConnections[1], 'Linked-1000Mbps'); + expect(state.lanConnections[2], 'None'); + expect(state.lanConnections[3], 'None'); + }); + + test('returns empty lanConnections when lanPortConnections is null', () { + // Arrange + final pollingData = createPollingData( + ethernetPortsOutput: { + 'wanPortConnection': 'Linked-1000Mbps', + // lanPortConnections is missing + }, + ); + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.wanConnection, 'Linked-1000Mbps'); + expect(state.lanConnections, isEmpty); + }); + + test( + 'returns empty state when getEthernetPortConnections returns JNAPError', + () { + // Arrange + final data = { + JNAPAction.getEthernetPortConnections: const JNAPError( + result: 'ErrorUnsupportedAction', + error: 'Unsupported action', + ), + }; + final pollingData = + CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.wanConnection, isNull); + expect(state.lanConnections, isEmpty); + }); + + test('handles different WAN connection states', () { + // Test various connection states: None, Linked-100Mbps, Linked-1000Mbps + final connectionStates = [ + 'None', + 'Linked-100Mbps', + 'Linked-1000Mbps', + 'Linked-2500Mbps' + ]; + + for (final connectionState in connectionStates) { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + ethernetPortsOutput: createEthernetPortsOutput( + wanPortConnection: connectionState, + ), + ), + )), + ], + ); + + // Act + final state = container.read(ethernetPortsProvider); + + // Assert + expect(state.wanConnection, connectionState); + + container.dispose(); + } + }); + + group('EthernetPortsState', () { + test('props returns correct list for equality comparison', () { + // Arrange & Act + const state1 = EthernetPortsState( + wanConnection: 'Linked-1000Mbps', + lanConnections: ['Linked-100Mbps', 'None'], + ); + const state2 = EthernetPortsState( + wanConnection: 'Linked-1000Mbps', + lanConnections: ['Linked-100Mbps', 'None'], + ); + const state3 = EthernetPortsState( + wanConnection: 'None', + lanConnections: ['Linked-100Mbps', 'None'], + ); + + // Assert + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + test('default constructor creates empty state', () { + // Arrange & Act + const state = EthernetPortsState(); + + // Assert + expect(state.wanConnection, isNull); + expect(state.lanConnections, isEmpty); + }); + + test('lanConnections with different content are not equal', () { + // Arrange & Act + const state1 = EthernetPortsState( + lanConnections: ['Linked-100Mbps', 'None'], + ); + const state2 = EthernetPortsState( + lanConnections: ['Linked-1000Mbps', 'None'], + ); + + // Assert + expect(state1, isNot(equals(state2))); + }); + }); + }); +} + +class _MockPollingNotifier extends AsyncNotifier + implements PollingNotifier { + final CoreTransactionData? _data; + + _MockPollingNotifier(this._data); + + @override + CoreTransactionData build() { + return _data ?? + const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/core/data/providers/router_time_provider_test.dart b/test/core/data/providers/router_time_provider_test.dart new file mode 100644 index 000000000..332ee04b3 --- /dev/null +++ b/test/core/data/providers/router_time_provider_test.dart @@ -0,0 +1,218 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/router_time_provider.dart'; + +void main() { + group('routerTimeProvider', () { + late ProviderContainer container; + + tearDown(() { + container.dispose(); + }); + + CoreTransactionData createPollingData({ + Map? localTimeOutput, + }) { + final data = {}; + if (localTimeOutput != null) { + data[JNAPAction.getLocalTime] = + JNAPSuccess(result: 'OK', output: localTimeOutput); + } + return CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + } + + test('returns current system time when polling data is null', () { + // Arrange + final beforeTest = DateTime.now().millisecondsSinceEpoch; + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(null)), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + final afterTest = DateTime.now().millisecondsSinceEpoch; + + // Assert - result should be between before and after test timestamps + expect(result, greaterThanOrEqualTo(beforeTest)); + expect(result, lessThanOrEqualTo(afterTest)); + }); + + test('parses valid time string and returns a past timestamp', () { + // Arrange + // Note: DateFormat("yyyy-MM-ddThh:mm:ssZ") uses 12-hour format (hh) + // The exact timestamp depends on how hh parses the time, + // but we verify it returns a past timestamp (not current time) + final beforeTest = DateTime.now().millisecondsSinceEpoch; + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + localTimeOutput: { + 'currentTime': '2024-01-15T10:30:00Z', + }, + ), + )), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + + // Assert - result should be a past timestamp (year 2024), not current time + expect(result, lessThan(beforeTest)); + expect(result, greaterThan(0)); // Sanity check: should be positive + }); + + test('returns current system time when currentTime is null', () { + // Arrange + final beforeTest = DateTime.now().millisecondsSinceEpoch; + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + localTimeOutput: { + // currentTime is missing + }, + ), + )), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + final afterTest = DateTime.now().millisecondsSinceEpoch; + + // Assert + expect(result, greaterThanOrEqualTo(beforeTest)); + expect(result, lessThanOrEqualTo(afterTest)); + }); + + test('returns current system time when time string is invalid format', () { + // Arrange + final beforeTest = DateTime.now().millisecondsSinceEpoch; + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + localTimeOutput: { + 'currentTime': 'invalid-time-format', + }, + ), + )), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + final afterTest = DateTime.now().millisecondsSinceEpoch; + + // Assert + expect(result, greaterThanOrEqualTo(beforeTest)); + expect(result, lessThanOrEqualTo(afterTest)); + }); + + test('returns current system time when getLocalTime returns JNAPError', () { + // Arrange + final beforeTest = DateTime.now().millisecondsSinceEpoch; + final data = { + JNAPAction.getLocalTime: const JNAPError( + result: 'ErrorUnsupportedAction', + error: 'Unsupported action', + ), + }; + final pollingData = + CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + final afterTest = DateTime.now().millisecondsSinceEpoch; + + // Assert + expect(result, greaterThanOrEqualTo(beforeTest)); + expect(result, lessThanOrEqualTo(afterTest)); + }); + + test('parses different valid ISO 8601 time strings', () { + // Test that valid time strings are parsed (not falling back to DateTime.now()) + final testTimeStrings = [ + '2024-06-20T02:45:30Z', // Use 12-hour compatible times + '2023-12-31T11:59:59Z', + '2024-01-01T08:00:00Z', + ]; + + for (final timeString in testTimeStrings) { + // Arrange + final beforeTest = DateTime.now().millisecondsSinceEpoch; + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + localTimeOutput: { + 'currentTime': timeString, + }, + ), + )), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + + // Assert - parsed time should be in the past (2023-2024), not current time + expect(result, lessThan(beforeTest), + reason: + 'Should parse $timeString to a past timestamp, not current time'); + + container.dispose(); + } + }); + + test('returns int type', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + localTimeOutput: { + 'currentTime': '2024-01-15T10:30:00Z', + }, + ), + )), + ], + ); + + // Act + final result = container.read(routerTimeProvider); + + // Assert + expect(result, isA()); + }); + }); +} + +class _MockPollingNotifier extends AsyncNotifier + implements PollingNotifier { + final CoreTransactionData? _data; + + _MockPollingNotifier(this._data); + + @override + CoreTransactionData build() { + return _data ?? + const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/core/data/providers/session_provider_test.dart b/test/core/data/providers/session_provider_test.dart new file mode 100644 index 000000000..f5882f1ff --- /dev/null +++ b/test/core/data/providers/session_provider_test.dart @@ -0,0 +1,272 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:privacy_gui/constants/pref_key.dart'; +import 'package:privacy_gui/core/errors/service_error.dart'; +import 'package:privacy_gui/core/jnap/models/device_info.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/services/session_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../mocks/test_data/dashboard_manager_test_data.dart'; + +class MockSessionService extends Mock implements SessionService {} + +void main() { + // Initialize Flutter binding for SharedPreferences + TestWidgetsFlutterBinding.ensureInitialized(); + late MockSessionService mockService; + late ProviderContainer container; + + setUp(() { + mockService = MockSessionService(); + }); + + tearDown(() { + container.dispose(); + }); + + group('SessionNotifier - build', () { + test('build returns void (no state management)', () { + // Arrange + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act & Assert - Just verify it doesn't throw + expect(() => container.read(sessionProvider), returnsNormally); + }); + }); + + group('SessionNotifier - saveSelectedNetwork', () { + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('saves serialNumber and networkId to SharedPreferences', () async { + // Arrange + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act + await container + .read(sessionProvider.notifier) + .saveSelectedNetwork('TEST_SN', 'TEST_NETWORK_ID'); + + // Assert + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString(pCurrentSN), equals('TEST_SN')); + expect(prefs.getString(pSelectedNetworkId), equals('TEST_NETWORK_ID')); + expect( + container.read(selectedNetworkIdProvider), equals('TEST_NETWORK_ID')); + }); + + test('saves empty networkId for local sessions', () async { + // Arrange + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act + await container + .read(sessionProvider.notifier) + .saveSelectedNetwork('LOCAL_SN', ''); + + // Assert + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString(pCurrentSN), equals('LOCAL_SN')); + expect(prefs.getString(pSelectedNetworkId), equals('')); + expect(container.read(selectedNetworkIdProvider), equals('')); + }); + }); + + group('SessionNotifier - checkRouterIsBack', () { + setUp(() { + // Mock SharedPreferences for checkRouterIsBack tests + SharedPreferences.setMockInitialValues({ + pCurrentSN: 'MOCK_SN', + }); + }); + + test('delegates to service.checkRouterIsBack with currentSN', () async { + // Arrange + final expectedDeviceInfo = NodeDeviceInfo.fromJson( + SessionTestData.createDeviceInfoSuccess(serialNumber: 'MOCK_SN').output, + ); + + when(() => mockService.checkRouterIsBack('MOCK_SN')) + .thenAnswer((_) async => expectedDeviceInfo); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act + final result = + await container.read(sessionProvider.notifier).checkRouterIsBack(); + + // Assert + verify(() => mockService.checkRouterIsBack('MOCK_SN')).called(1); + expect(result.serialNumber, equals('MOCK_SN')); + }); + + test('falls back to pnpConfiguredSN if currentSN is null', () async { + // Arrange + SharedPreferences.setMockInitialValues({ + pPnpConfiguredSN: 'PNP_SN', + }); + + final expectedDeviceInfo = NodeDeviceInfo.fromJson( + SessionTestData.createDeviceInfoSuccess(serialNumber: 'PNP_SN').output, + ); + + when(() => mockService.checkRouterIsBack('PNP_SN')) + .thenAnswer((_) async => expectedDeviceInfo); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act + final result = + await container.read(sessionProvider.notifier).checkRouterIsBack(); + + // Assert + verify(() => mockService.checkRouterIsBack('PNP_SN')).called(1); + expect(result.serialNumber, equals('PNP_SN')); + }); + + test('propagates SerialNumberMismatchError from service', () async { + // Arrange + when(() => mockService.checkRouterIsBack(any())).thenThrow( + const SerialNumberMismatchError(expected: 'EXPECTED', actual: 'ACTUAL'), + ); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act & Assert + await expectLater( + () => container.read(sessionProvider.notifier).checkRouterIsBack(), + throwsA(isA()), + ); + }); + + test('propagates ConnectivityError from service', () async { + // Arrange + when(() => mockService.checkRouterIsBack(any())).thenThrow( + const ConnectivityError(message: 'Router unreachable'), + ); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act & Assert + await expectLater( + () => container.read(sessionProvider.notifier).checkRouterIsBack(), + throwsA(isA()), + ); + }); + }); + + group('SessionNotifier - checkDeviceInfo', () { + test('delegates to service.checkDeviceInfo with cached state', () async { + // Arrange + final cachedDeviceInfo = NodeDeviceInfo.fromJson( + SessionTestData.createDeviceInfoSuccess(serialNumber: 'CACHED_SN') + .output, + ); + final deviceInfoState = DeviceInfoState(deviceInfo: cachedDeviceInfo); + + when(() => mockService.checkDeviceInfo(cachedDeviceInfo)) + .thenAnswer((_) async => cachedDeviceInfo); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(deviceInfoState), + ], + ); + + // Act + final result = + await container.read(sessionProvider.notifier).checkDeviceInfo(null); + + // Assert + verify(() => mockService.checkDeviceInfo(cachedDeviceInfo)).called(1); + expect(result.serialNumber, equals('CACHED_SN')); + }); + + test('delegates to service.checkDeviceInfo with null when no cache', + () async { + // Arrange + final freshDeviceInfo = NodeDeviceInfo.fromJson( + SessionTestData.createDeviceInfoSuccess(serialNumber: 'FRESH_SN') + .output, + ); + + when(() => mockService.checkDeviceInfo(null)) + .thenAnswer((_) async => freshDeviceInfo); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act + final result = + await container.read(sessionProvider.notifier).checkDeviceInfo(null); + + // Assert + verify(() => mockService.checkDeviceInfo(null)).called(1); + expect(result.serialNumber, equals('FRESH_SN')); + }); + + test('propagates ServiceError from service', () async { + // Arrange + when(() => mockService.checkDeviceInfo(any())).thenThrow( + const ResourceNotFoundError(), + ); + + container = ProviderContainer( + overrides: [ + sessionServiceProvider.overrideWithValue(mockService), + deviceInfoProvider.overrideWithValue(const DeviceInfoState()), + ], + ); + + // Act & Assert + expect( + () => container.read(sessionProvider.notifier).checkDeviceInfo(null), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/core/data/providers/system_stats_provider_test.dart b/test/core/data/providers/system_stats_provider_test.dart new file mode 100644 index 000000000..77d3f091e --- /dev/null +++ b/test/core/data/providers/system_stats_provider_test.dart @@ -0,0 +1,298 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; + +void main() { + group('systemStatsProvider', () { + late ProviderContainer container; + + tearDown(() { + container.dispose(); + }); + + CoreTransactionData createPollingData({ + Map? systemStatsOutput, + }) { + final data = {}; + if (systemStatsOutput != null) { + data[JNAPAction.getSystemStats] = + JNAPSuccess(result: 'OK', output: systemStatsOutput); + } + return CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + } + + Map createSystemStatsOutput({ + int uptimeSeconds = 86400, + String cpuLoad = '15%', + String memoryLoad = '45%', + }) { + return { + 'uptimeSeconds': uptimeSeconds, + 'CPULoad': cpuLoad, + 'MemoryLoad': memoryLoad, + }; + } + + test('returns default SystemStatsState when polling data is null', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(null)), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 0); + expect(state.cpuLoad, isNull); + expect(state.memoryLoad, isNull); + }); + + test('extracts uptimes from getSystemStats action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + systemStatsOutput: createSystemStatsOutput( + uptimeSeconds: 172800, + ), + ), + )), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 172800); + }); + + test('extracts cpuLoad from getSystemStats action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + systemStatsOutput: createSystemStatsOutput( + cpuLoad: '25%', + ), + ), + )), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.cpuLoad, '25%'); + }); + + test('extracts memoryLoad from getSystemStats action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + systemStatsOutput: createSystemStatsOutput( + memoryLoad: '60%', + ), + ), + )), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.memoryLoad, '60%'); + }); + + test('extracts all fields from getSystemStats action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + systemStatsOutput: createSystemStatsOutput( + uptimeSeconds: 259200, + cpuLoad: '35%', + memoryLoad: '70%', + ), + ), + )), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 259200); + expect(state.cpuLoad, '35%'); + expect(state.memoryLoad, '70%'); + }); + + test('returns default uptimes (0) when uptimeSeconds is null', () { + // Arrange + final pollingData = createPollingData( + systemStatsOutput: { + 'CPULoad': '15%', + 'MemoryLoad': '45%', + // uptimeSeconds is missing + }, + ); + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 0); + expect(state.cpuLoad, '15%'); + expect(state.memoryLoad, '45%'); + }); + + test('returns default state when getSystemStats returns JNAPError', () { + // Arrange + final data = { + JNAPAction.getSystemStats: const JNAPError( + result: 'ErrorUnsupportedAction', + error: 'Unsupported action', + ), + }; + final pollingData = + CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 0); + expect(state.cpuLoad, isNull); + expect(state.memoryLoad, isNull); + }); + + test('handles partial data with only uptimeSeconds', () { + // Arrange + final pollingData = createPollingData( + systemStatsOutput: { + 'uptimeSeconds': 43200, + // CPULoad and MemoryLoad are missing + }, + ); + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(systemStatsProvider); + + // Assert + expect(state.uptimes, 43200); + expect(state.cpuLoad, isNull); + expect(state.memoryLoad, isNull); + }); + + group('SystemStatsState', () { + test('props returns correct list for equality comparison', () { + // Arrange & Act + const state1 = SystemStatsState( + uptimes: 86400, + cpuLoad: '15%', + memoryLoad: '45%', + ); + const state2 = SystemStatsState( + uptimes: 86400, + cpuLoad: '15%', + memoryLoad: '45%', + ); + const state3 = SystemStatsState( + uptimes: 172800, + cpuLoad: '15%', + memoryLoad: '45%', + ); + + // Assert + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + test('default constructor creates state with default values', () { + // Arrange & Act + const state = SystemStatsState(); + + // Assert + expect(state.uptimes, 0); + expect(state.cpuLoad, isNull); + expect(state.memoryLoad, isNull); + }); + + test('states with different cpuLoad are not equal', () { + // Arrange & Act + const state1 = SystemStatsState( + uptimes: 86400, + cpuLoad: '15%', + ); + const state2 = SystemStatsState( + uptimes: 86400, + cpuLoad: '25%', + ); + + // Assert + expect(state1, isNot(equals(state2))); + }); + + test('states with different memoryLoad are not equal', () { + // Arrange & Act + const state1 = SystemStatsState( + uptimes: 86400, + memoryLoad: '45%', + ); + const state2 = SystemStatsState( + uptimes: 86400, + memoryLoad: '60%', + ); + + // Assert + expect(state1, isNot(equals(state2))); + }); + }); + }); +} + +class _MockPollingNotifier extends AsyncNotifier + implements PollingNotifier { + final CoreTransactionData? _data; + + _MockPollingNotifier(this._data); + + @override + CoreTransactionData build() { + return _data ?? + const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/core/data/providers/wifi_radios_provider_test.dart b/test/core/data/providers/wifi_radios_provider_test.dart new file mode 100644 index 000000000..9272ab9ec --- /dev/null +++ b/test/core/data/providers/wifi_radios_provider_test.dart @@ -0,0 +1,297 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/data/providers/polling_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; + +void main() { + group('wifiRadiosProvider', () { + late ProviderContainer container; + + tearDown(() { + container.dispose(); + }); + + CoreTransactionData createPollingData({ + Map? radioInfoOutput, + Map? guestRadioOutput, + }) { + final data = {}; + if (radioInfoOutput != null) { + data[JNAPAction.getRadioInfo] = + JNAPSuccess(result: 'OK', output: radioInfoOutput); + } + if (guestRadioOutput != null) { + data[JNAPAction.getGuestRadioSettings] = + JNAPSuccess(result: 'OK', output: guestRadioOutput); + } + return CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + } + + Map createRadioInfoOutput({ + int radioCount = 2, + }) { + final radios = >[]; + if (radioCount >= 1) { + radios.add({ + 'radioID': 'RADIO_2.4GHz', + 'physicalRadioID': 'wl0', + 'bssid': 'AA:BB:CC:DD:EE:01', + 'band': '2.4GHz', + 'supportedModes': const ['802.11b/g/n'], + 'supportedChannelsForChannelWidths': const [ + { + 'channelWidth': 'Auto', + 'channels': [1, 6, 11], + } + ], + 'supportedSecurityTypes': const ['None', 'WPA2-Personal'], + 'maxRADIUSSharedKeyLength': 64, + 'settings': { + 'isEnabled': true, + 'mode': '802.11b/g/n', + 'ssid': 'TestNetwork-2.4', + 'broadcastSSID': true, + 'channelWidth': 'Auto', + 'channel': 6, + 'security': 'WPA2-Personal', + 'wpaPersonalSettings': { + 'passphrase': 'password123', + }, + }, + }); + } + if (radioCount >= 2) { + radios.add({ + 'radioID': 'RADIO_5GHz', + 'physicalRadioID': 'wl1', + 'bssid': 'AA:BB:CC:DD:EE:02', + 'band': '5GHz', + 'supportedModes': const ['802.11a/n/ac'], + 'supportedChannelsForChannelWidths': const [ + { + 'channelWidth': 'Auto', + 'channels': [36, 40, 44, 48], + } + ], + 'supportedSecurityTypes': const ['None', 'WPA2-Personal'], + 'maxRADIUSSharedKeyLength': 64, + 'settings': { + 'isEnabled': true, + 'mode': '802.11a/n/ac', + 'ssid': 'TestNetwork-5', + 'broadcastSSID': true, + 'channelWidth': 'Auto', + 'channel': 36, + 'security': 'WPA2-Personal', + 'wpaPersonalSettings': { + 'passphrase': 'password123', + }, + }, + }); + } + return { + 'isBandSteeringSupported': true, + 'radios': radios, + }; + } + + Map createGuestRadioOutput({ + bool isGuestNetworkEnabled = true, + }) { + return { + 'isGuestNetworkACaptivePortal': false, + 'isGuestNetworkEnabled': isGuestNetworkEnabled, + 'radios': [ + { + 'radioID': 'RADIO_2.4GHz', + 'isEnabled': true, + 'broadcastGuestSSID': true, + 'guestSSID': 'Guest-Network', + 'guestWPAPassphrase': 'guestpass123', + 'canEnableRadio': true, + }, + { + 'radioID': 'RADIO_5GHz', + 'isEnabled': true, + 'broadcastGuestSSID': true, + 'guestSSID': 'Guest-Network', + 'guestWPAPassphrase': 'guestpass123', + 'canEnableRadio': true, + }, + ], + }; + } + + test('returns empty WifiRadiosState when polling data is null', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(null)), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.mainRadios, isEmpty); + expect(state.guestRadios, isEmpty); + expect(state.isGuestNetworkEnabled, isFalse); + }); + + test('extracts mainRadios from getRadioInfo action', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData(radioInfoOutput: createRadioInfoOutput()), + )), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.mainRadios.length, 2); + expect(state.mainRadios[0].radioID, 'RADIO_2.4GHz'); + expect(state.mainRadios[0].band, '2.4GHz'); + expect(state.mainRadios[1].radioID, 'RADIO_5GHz'); + expect(state.mainRadios[1].band, '5GHz'); + }); + + test( + 'extracts guestRadios and isGuestNetworkEnabled from getGuestRadioSettings', + () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + radioInfoOutput: createRadioInfoOutput(), + guestRadioOutput: createGuestRadioOutput( + isGuestNetworkEnabled: true, + ), + ), + )), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.guestRadios.length, 2); + expect(state.guestRadios[0].radioID, 'RADIO_2.4GHz'); + expect(state.guestRadios[0].guestSSID, 'Guest-Network'); + expect(state.isGuestNetworkEnabled, isTrue); + }); + + test('returns isGuestNetworkEnabled false when guest network is disabled', + () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData( + radioInfoOutput: createRadioInfoOutput(), + guestRadioOutput: createGuestRadioOutput( + isGuestNetworkEnabled: false, + ), + ), + )), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.isGuestNetworkEnabled, isFalse); + }); + + test('returns empty guestRadios when getGuestRadioSettings is missing', () { + // Arrange + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier( + createPollingData(radioInfoOutput: createRadioInfoOutput()), + )), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.mainRadios.length, 2); + expect(state.guestRadios, isEmpty); + expect(state.isGuestNetworkEnabled, isFalse); + }); + + test('returns empty mainRadios when getRadioInfo returns JNAPError', () { + // Arrange + final data = { + JNAPAction.getRadioInfo: const JNAPError( + result: 'ErrorRadioNotFound', + error: 'Radio not found', + ), + }; + final pollingData = + CoreTransactionData(lastUpdate: 1, isReady: true, data: data); + + container = ProviderContainer( + overrides: [ + pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), + ], + ); + + // Act + final state = container.read(wifiRadiosProvider); + + // Assert + expect(state.mainRadios, isEmpty); + }); + + group('WifiRadiosState', () { + test('props returns correct list for equality comparison', () { + // Arrange & Act + const state1 = WifiRadiosState(isGuestNetworkEnabled: true); + const state2 = WifiRadiosState(isGuestNetworkEnabled: true); + const state3 = WifiRadiosState(isGuestNetworkEnabled: false); + + // Assert + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + test('default constructor creates empty state', () { + // Arrange & Act + const state = WifiRadiosState(); + + // Assert + expect(state.mainRadios, isEmpty); + expect(state.guestRadios, isEmpty); + expect(state.isGuestNetworkEnabled, isFalse); + }); + }); + }); +} + +class _MockPollingNotifier extends AsyncNotifier + implements PollingNotifier { + final CoreTransactionData? _data; + + _MockPollingNotifier(this._data); + + @override + CoreTransactionData build() { + return _data ?? + const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/test/core/data/services/session_service_test.dart b/test/core/data/services/session_service_test.dart new file mode 100644 index 000000000..7a007af07 --- /dev/null +++ b/test/core/data/services/session_service_test.dart @@ -0,0 +1,200 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:privacy_gui/core/errors/service_error.dart'; +import 'package:privacy_gui/core/jnap/actions/better_action.dart'; +import 'package:privacy_gui/core/jnap/models/device_info.dart'; +import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; +import 'package:privacy_gui/core/jnap/router_repository.dart'; +import 'package:privacy_gui/core/data/services/session_service.dart'; + +import '../../../mocks/test_data/dashboard_manager_test_data.dart'; + +class MockRouterRepository extends Mock implements RouterRepository {} + +void main() { + late SessionService service; + late MockRouterRepository mockRouterRepository; + + setUpAll(() { + registerFallbackValue(JNAPAction.getDeviceInfo); + }); + + setUp(() { + mockRouterRepository = MockRouterRepository(); + service = SessionService(mockRouterRepository); + }); + + group('SessionService - checkRouterIsBack', () { + // T034: checkRouterIsBack returns NodeDeviceInfo when SN matches + test('returns NodeDeviceInfo when serial number matches', () async { + // Arrange + const expectedSN = 'TEST123456'; + final deviceInfoOutput = + SessionTestData.createDeviceInfoSuccess(serialNumber: expectedSN) + .output; + + when(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + )) + .thenAnswer( + (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); + + // Act + final result = await service.checkRouterIsBack(expectedSN); + + // Assert + expect(result, isA()); + expect(result.serialNumber, equals(expectedSN)); + }); + + // T035: checkRouterIsBack throws SerialNumberMismatchError when SN doesn't match + test('throws SerialNumberMismatchError when serial number does not match', + () async { + // Arrange + const expectedSN = 'EXPECTED_SN'; + const actualSN = 'ACTUAL_SN'; + final deviceInfoOutput = + SessionTestData.createDeviceInfoSuccess(serialNumber: actualSN) + .output; + + when(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + )) + .thenAnswer( + (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); + + // Act & Assert + expect( + () => service.checkRouterIsBack(expectedSN), + throwsA(isA()), + ); + }); + + // T036: checkRouterIsBack throws ConnectivityError when router unreachable + test('throws ConnectivityError when router is unreachable', () async { + // Arrange + when(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + )).thenThrow(Exception('Network error')); + + // Act & Assert + expect( + () => service.checkRouterIsBack('TEST123456'), + throwsA(isA()), + ); + }); + + // T037: checkRouterIsBack maps JNAPError to ServiceError correctly + test('maps JNAPError to ServiceError correctly', () async { + // Arrange + when(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + )) + .thenThrow(const JNAPError( + result: '_ErrorUnauthorized', error: 'Unauthorized')); + + // Act & Assert + expect( + () => service.checkRouterIsBack('TEST123456'), + throwsA(isA()), + ); + }); + + test('returns NodeDeviceInfo when expected serial number is empty', + () async { + // Arrange - empty SN should skip validation + final deviceInfoOutput = + SessionTestData.createDeviceInfoSuccess(serialNumber: 'ANY_SN') + .output; + + when(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + fetchRemote: true, + retries: 0, + )) + .thenAnswer( + (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); + + // Act + final result = await service.checkRouterIsBack(''); + + // Assert + expect(result, isA()); + expect(result.serialNumber, equals('ANY_SN')); + }); + }); + + group('SessionService - checkDeviceInfo', () { + // T044: checkDeviceInfo returns cached value immediately when available + test('returns cached value immediately when available', () async { + // Arrange + final cachedDeviceInfo = NodeDeviceInfo.fromJson( + SessionTestData.createDeviceInfoSuccess(serialNumber: 'CACHED_SN') + .output, + ); + + // Act + final result = await service.checkDeviceInfo(cachedDeviceInfo); + + // Assert + expect(result, equals(cachedDeviceInfo)); + expect(result.serialNumber, equals('CACHED_SN')); + // No API call should be made when cached value exists + verifyZeroInteractions(mockRouterRepository); + }); + + // T045: checkDeviceInfo makes API call when cached value is null + test('makes API call when cached value is null', () async { + // Arrange + final deviceInfoOutput = + SessionTestData.createDeviceInfoSuccess(serialNumber: 'FRESH_SN') + .output; + + when(() => mockRouterRepository.send( + any(), + retries: any(named: 'retries'), + timeoutMs: any(named: 'timeoutMs'), + )) + .thenAnswer( + (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); + + // Act + final result = await service.checkDeviceInfo(null); + + // Assert + expect(result, isA()); + expect(result.serialNumber, equals('FRESH_SN')); + verify(() => mockRouterRepository.send( + JNAPAction.getDeviceInfo, + retries: 0, + timeoutMs: 3000, + )).called(1); + }); + + // T046: checkDeviceInfo throws ServiceError on API failure + test('throws ServiceError on API failure', () async { + // Arrange + when(() => mockRouterRepository.send( + any(), + retries: any(named: 'retries'), + timeoutMs: any(named: 'timeoutMs'), + )) + .thenThrow(const JNAPError( + result: 'ErrorDeviceNotFound', error: 'Device not found')); + + // Act & Assert + expect( + () => service.checkDeviceInfo(null), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/core/jnap/providers/dashboard_manager_provider_test.dart b/test/core/jnap/providers/dashboard_manager_provider_test.dart deleted file mode 100644 index 722318568..000000000 --- a/test/core/jnap/providers/dashboard_manager_provider_test.dart +++ /dev/null @@ -1,323 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:privacy_gui/core/errors/service_error.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; -import 'package:privacy_gui/core/data/providers/polling_provider.dart'; -import 'package:privacy_gui/core/data/services/dashboard_manager_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../../mocks/test_data/dashboard_manager_test_data.dart'; - -class MockDashboardManagerService extends Mock - implements DashboardManagerService {} - -void main() { - // Initialize Flutter binding for SharedPreferences - TestWidgetsFlutterBinding.ensureInitialized(); - late MockDashboardManagerService mockService; - late ProviderContainer container; - - setUp(() { - mockService = MockDashboardManagerService(); - }); - - tearDown(() { - container.dispose(); - }); - - group('DashboardManagerNotifier - build', () { - test('delegates to service.transformPollingData with polling data', () { - // Arrange - final pollingData = - DashboardManagerTestData.createSuccessfulPollingData(); - final expectedState = DashboardManagerState( - deviceInfo: NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ), - uptimes: 86400, - wanConnection: 'Linked-1000Mbps', - ); - - when(() => mockService.transformPollingData(pollingData)) - .thenReturn(expectedState); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(pollingData)), - ], - ); - - // Act - final state = container.read(dashboardManagerProvider); - - // Assert - verify(() => mockService.transformPollingData(pollingData)).called(1); - expect(state, equals(expectedState)); - }); - - test('handles null polling data by returning default state', () { - // Arrange - const expectedState = DashboardManagerState(); - - when(() => mockService.transformPollingData(any())) - .thenReturn(expectedState); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act - final state = container.read(dashboardManagerProvider); - - // Assert - verify(() => mockService.transformPollingData(any())).called(1); - expect(state.deviceInfo, isNull); - expect(state.mainRadios, isEmpty); - expect(state.uptimes, equals(0)); - }); - - test('rebuilds when polling data changes', () async { - // Arrange - final pollingData1 = DashboardManagerTestData.createSuccessfulPollingData( - systemStats: DashboardManagerTestData.createSystemStatsSuccess( - uptimeSeconds: 1000, - ), - ); - final pollingData2 = DashboardManagerTestData.createSuccessfulPollingData( - systemStats: DashboardManagerTestData.createSystemStatsSuccess( - uptimeSeconds: 2000, - ), - ); - - const state1 = DashboardManagerState(uptimes: 1000); - const state2 = DashboardManagerState(uptimes: 2000); - - when(() => mockService.transformPollingData(pollingData1)) - .thenReturn(state1); - when(() => mockService.transformPollingData(pollingData2)) - .thenReturn(state2); - - // First container with pollingData1 - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider - .overrideWith(() => _MockPollingNotifier(pollingData1)), - ], - ); - - // Act - First read - final firstState = container.read(dashboardManagerProvider); - expect(firstState.uptimes, equals(1000)); - - // Dispose and create new container with pollingData2 - container.dispose(); - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider - .overrideWith(() => _MockPollingNotifier(pollingData2)), - ], - ); - - final secondState = container.read(dashboardManagerProvider); - - // Assert - expect(secondState.uptimes, equals(2000)); - }); - }); - - group('DashboardManagerNotifier - checkRouterIsBack', () { - setUp(() { - // Mock SharedPreferences for checkRouterIsBack tests - SharedPreferences.setMockInitialValues({ - 'currentSN': 'MOCK_SN', - }); - }); - - test('delegates to service.checkRouterIsBack', () async { - // Arrange - final expectedDeviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'TEST_SN') - .output, - ); - - when(() => mockService.transformPollingData(any())) - .thenReturn(const DashboardManagerState()); - when(() => mockService.checkRouterIsBack(any())) - .thenAnswer((_) async => expectedDeviceInfo); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act - final result = await container - .read(dashboardManagerProvider.notifier) - .checkRouterIsBack(); - - // Assert - verify(() => mockService.checkRouterIsBack(any())).called(1); - expect(result.serialNumber, equals('TEST_SN')); - }); - - test('propagates SerialNumberMismatchError from service', () async { - // Arrange - when(() => mockService.transformPollingData(any())) - .thenReturn(const DashboardManagerState()); - when(() => mockService.checkRouterIsBack(any())).thenThrow( - const SerialNumberMismatchError(expected: 'EXPECTED', actual: 'ACTUAL'), - ); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act & Assert - await expectLater( - () => container - .read(dashboardManagerProvider.notifier) - .checkRouterIsBack(), - throwsA(isA()), - ); - }); - - test('propagates ConnectivityError from service', () async { - // Arrange - when(() => mockService.transformPollingData(any())) - .thenReturn(const DashboardManagerState()); - when(() => mockService.checkRouterIsBack(any())).thenThrow( - const ConnectivityError(message: 'Router unreachable'), - ); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act & Assert - await expectLater( - () => container - .read(dashboardManagerProvider.notifier) - .checkRouterIsBack(), - throwsA(isA()), - ); - }); - }); - - group('DashboardManagerNotifier - checkDeviceInfo', () { - test('delegates to service.checkDeviceInfo with cached state', () async { - // Arrange - final cachedDeviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'CACHED_SN') - .output, - ); - final initialState = DashboardManagerState(deviceInfo: cachedDeviceInfo); - - when(() => mockService.transformPollingData(any())) - .thenReturn(initialState); - when(() => mockService.checkDeviceInfo(cachedDeviceInfo)) - .thenAnswer((_) async => cachedDeviceInfo); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act - final result = await container - .read(dashboardManagerProvider.notifier) - .checkDeviceInfo(null); - - // Assert - verify(() => mockService.checkDeviceInfo(cachedDeviceInfo)).called(1); - expect(result.serialNumber, equals('CACHED_SN')); - }); - - test('delegates to service.checkDeviceInfo with null when no cache', - () async { - // Arrange - final freshDeviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'FRESH_SN') - .output, - ); - - when(() => mockService.transformPollingData(any())) - .thenReturn(const DashboardManagerState()); - when(() => mockService.checkDeviceInfo(null)) - .thenAnswer((_) async => freshDeviceInfo); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act - final result = await container - .read(dashboardManagerProvider.notifier) - .checkDeviceInfo(null); - - // Assert - verify(() => mockService.checkDeviceInfo(null)).called(1); - expect(result.serialNumber, equals('FRESH_SN')); - }); - - test('propagates ServiceError from service', () async { - // Arrange - when(() => mockService.transformPollingData(any())) - .thenReturn(const DashboardManagerState()); - when(() => mockService.checkDeviceInfo(any())).thenThrow( - const ResourceNotFoundError(), - ); - - container = ProviderContainer( - overrides: [ - dashboardManagerServiceProvider.overrideWithValue(mockService), - pollingProvider.overrideWith(() => _MockPollingNotifier(null)), - ], - ); - - // Act & Assert - expect( - () => container - .read(dashboardManagerProvider.notifier) - .checkDeviceInfo(null), - throwsA(isA()), - ); - }); - }); -} - -/// Mock polling notifier that returns the given data -class _MockPollingNotifier extends PollingNotifier { - final CoreTransactionData? _data; - - _MockPollingNotifier(this._data); - - @override - CoreTransactionData build() => - _data ?? - const CoreTransactionData(lastUpdate: 0, isReady: false, data: {}); -} diff --git a/test/core/jnap/providers/dashboard_manager_state_test.dart b/test/core/jnap/providers/dashboard_manager_state_test.dart deleted file mode 100644 index f986345f9..000000000 --- a/test/core/jnap/providers/dashboard_manager_state_test.dart +++ /dev/null @@ -1,367 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; -import 'package:privacy_gui/core/jnap/models/radio_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; - -import '../../../mocks/test_data/dashboard_manager_test_data.dart'; - -void main() { - group('DashboardManagerState - default values', () { - test('has correct default values', () { - // Arrange & Act - const state = DashboardManagerState(); - - // Assert - expect(state.deviceInfo, isNull); - expect(state.mainRadios, isEmpty); - expect(state.guestRadios, isEmpty); - expect(state.isGuestNetworkEnabled, isFalse); - expect(state.uptimes, equals(0)); - expect(state.wanConnection, isNull); - expect(state.lanConnections, isEmpty); - expect(state.skuModelNumber, isNull); - expect(state.localTime, equals(0)); - expect(state.cpuLoad, isNull); - expect(state.memoryLoad, isNull); - }); - - test('stringify is enabled', () { - // Arrange - const state = DashboardManagerState(); - - // Assert - expect(state.stringify, isTrue); - }); - }); - - group('DashboardManagerState - equality and copyWith', () { - test('two states with same values are equal', () { - // Arrange - final deviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ); - final state1 = DashboardManagerState( - deviceInfo: deviceInfo, - uptimes: 1000, - wanConnection: 'Linked-1000Mbps', - ); - final state2 = DashboardManagerState( - deviceInfo: deviceInfo, - uptimes: 1000, - wanConnection: 'Linked-1000Mbps', - ); - - // Assert - expect(state1, equals(state2)); - }); - - test('two states with different values are not equal', () { - // Arrange - const state1 = DashboardManagerState(uptimes: 1000); - const state2 = DashboardManagerState(uptimes: 2000); - - // Assert - expect(state1, isNot(equals(state2))); - }); - - test('states with different mainRadios are not equal', () { - // Arrange - final radios = DashboardManagerTestData.createRadioInfoSuccess(); - final radioList = (radios.output['radios'] as List) - .map((e) => RouterRadio.fromMap(e as Map)) - .toList(); - - final state1 = DashboardManagerState(mainRadios: radioList); - const state2 = DashboardManagerState(mainRadios: []); - - // Assert - expect(state1, isNot(equals(state2))); - }); - - test('states with different guestRadios are not equal', () { - // Arrange - final guestSettings = - DashboardManagerTestData.createGuestRadioSettingsSuccess(); - final guestRadioList = (guestSettings.output['radios'] as List) - .map((e) => GuestRadioInfo.fromMap(e as Map)) - .toList(); - - final state1 = DashboardManagerState(guestRadios: guestRadioList); - const state2 = DashboardManagerState(guestRadios: []); - - // Assert - expect(state1, isNot(equals(state2))); - }); - - test('copyWith preserves unmodified values', () { - // Arrange - final deviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ); - final original = DashboardManagerState( - deviceInfo: deviceInfo, - uptimes: 1000, - wanConnection: 'Linked-1000Mbps', - localTime: 1234567890, - ); - - // Act - final copied = original.copyWith(uptimes: 2000); - - // Assert - expect(copied.deviceInfo, equals(deviceInfo)); - expect(copied.uptimes, equals(2000)); - expect(copied.wanConnection, equals('Linked-1000Mbps')); - expect(copied.localTime, equals(1234567890)); - }); - - test('copyWith can update all fields', () { - // Arrange - const original = DashboardManagerState(); - final newDeviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ); - - // Act - final copied = original.copyWith( - deviceInfo: newDeviceInfo, - uptimes: 5000, - wanConnection: 'Linked-100Mbps', - lanConnections: ['Port1', 'Port2'], - skuModelNumber: 'MX5300', - localTime: 9999999999, - cpuLoad: '50%', - memoryLoad: '70%', - isGuestNetworkEnabled: true, - ); - - // Assert - expect(copied.deviceInfo, equals(newDeviceInfo)); - expect(copied.uptimes, equals(5000)); - expect(copied.wanConnection, equals('Linked-100Mbps')); - expect(copied.lanConnections, equals(['Port1', 'Port2'])); - expect(copied.skuModelNumber, equals('MX5300')); - expect(copied.localTime, equals(9999999999)); - expect(copied.cpuLoad, equals('50%')); - expect(copied.memoryLoad, equals('70%')); - expect(copied.isGuestNetworkEnabled, isTrue); - }); - - test('copyWith can update mainRadios and guestRadios', () { - // Arrange - const original = DashboardManagerState(); - final radios = DashboardManagerTestData.createRadioInfoSuccess(); - final radioList = (radios.output['radios'] as List) - .map((e) => RouterRadio.fromMap(e as Map)) - .toList(); - final guestSettings = - DashboardManagerTestData.createGuestRadioSettingsSuccess(); - final guestRadioList = (guestSettings.output['radios'] as List) - .map((e) => GuestRadioInfo.fromMap(e as Map)) - .toList(); - - // Act - final copied = original.copyWith( - mainRadios: radioList, - guestRadios: guestRadioList, - ); - - // Assert - expect(copied.mainRadios.length, equals(2)); - expect(copied.guestRadios.length, equals(2)); - expect(copied.mainRadios[0].band, equals('2.4GHz')); - expect(copied.mainRadios[1].band, equals('5GHz')); - }); - }); - - group('DashboardManagerState - serialization', () { - test('toMap produces correct map structure', () { - // Arrange - final deviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ); - final state = DashboardManagerState( - deviceInfo: deviceInfo, - uptimes: 1000, - skuModelNumber: 'MX5300', - localTime: 1234567890, - isGuestNetworkEnabled: false, - ); - - // Act - final map = state.toMap(); - - // Assert - expect(map['deviceInfo'], isNotNull); - expect(map['uptimes'], equals(1000)); - expect(map['skuModelNumber'], equals('MX5300')); - expect(map['localTime'], equals(1234567890)); - expect(map['isGuestNetworkEnabled'], equals(false)); - }); - - test('toMap removes null values', () { - // Arrange - const state = DashboardManagerState(uptimes: 100); - - // Act - final map = state.toMap(); - - // Assert - expect(map.containsKey('deviceInfo'), isFalse); - expect(map.containsKey('wanConnection'), isFalse); - expect(map.containsKey('skuModelNumber'), isFalse); - expect(map.containsKey('cpuLoad'), isFalse); - expect(map.containsKey('memoryLoad'), isFalse); - expect(map['uptimes'], equals(100)); - }); - - test('toMap includes mainRadios and guestRadios', () { - // Arrange - final radios = DashboardManagerTestData.createRadioInfoSuccess(); - final radioList = (radios.output['radios'] as List) - .map((e) => RouterRadio.fromMap(e as Map)) - .toList(); - final guestSettings = - DashboardManagerTestData.createGuestRadioSettingsSuccess(); - final guestRadioList = (guestSettings.output['radios'] as List) - .map((e) => GuestRadioInfo.fromMap(e as Map)) - .toList(); - - final state = DashboardManagerState( - mainRadios: radioList, - guestRadios: guestRadioList, - ); - - // Act - final map = state.toMap(); - - // Assert - expect(map['mainRadios'], isA()); - expect((map['mainRadios'] as List).length, equals(2)); - expect(map['guestRadios'], isA()); - expect((map['guestRadios'] as List).length, equals(2)); - }); - - test('fromMap creates state from map', () { - // Arrange - final deviceInfoOutput = - DashboardManagerTestData.createDeviceInfoSuccess().output; - final map = { - 'deviceInfo': deviceInfoOutput, - 'mainRadios': >[], - 'guestRadios': >[], - 'isGuestNetworkEnabled': true, - 'uptimes': 5000, - 'skuModelNumber': 'MX5300', - 'localTime': 9876543210, - 'cpuLoad': '25%', - 'memoryLoad': '50%', - }; - - // Act - final state = DashboardManagerState.fromMap(map); - - // Assert - expect(state.deviceInfo?.serialNumber, equals('TEST123456')); - expect(state.isGuestNetworkEnabled, isTrue); - expect(state.uptimes, equals(5000)); - expect(state.skuModelNumber, equals('MX5300')); - expect(state.localTime, equals(9876543210)); - expect(state.cpuLoad, equals('25%')); - expect(state.memoryLoad, equals('50%')); - }); - - test('fromMap handles null deviceInfo', () { - // Arrange - final map = { - 'deviceInfo': null, - 'mainRadios': >[], - 'guestRadios': >[], - 'isGuestNetworkEnabled': false, - 'uptimes': 0, - 'localTime': 0, - }; - - // Act - final state = DashboardManagerState.fromMap(map); - - // Assert - expect(state.deviceInfo, isNull); - }); - - test('fromMap handles null radios lists', () { - // Arrange - final map = { - 'mainRadios': null, - 'guestRadios': null, - 'isGuestNetworkEnabled': false, - 'uptimes': 0, - 'localTime': 0, - }; - - // Act - final state = DashboardManagerState.fromMap(map); - - // Assert - expect(state.mainRadios, isEmpty); - expect(state.guestRadios, isEmpty); - }); - - test('toJson and fromJson are reversible', () { - // Arrange - final deviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess().output, - ); - final original = DashboardManagerState( - deviceInfo: deviceInfo, - uptimes: 1000, - isGuestNetworkEnabled: true, - localTime: 1234567890, - ); - - // Act - final json = original.toJson(); - final restored = DashboardManagerState.fromJson(json); - - // Assert - expect(restored.deviceInfo?.serialNumber, - equals(original.deviceInfo?.serialNumber)); - expect(restored.uptimes, equals(original.uptimes)); - expect(restored.isGuestNetworkEnabled, - equals(original.isGuestNetworkEnabled)); - expect(restored.localTime, equals(original.localTime)); - }); - - test('toJson and fromJson preserve mainRadios and guestRadios', () { - // Arrange - final radios = DashboardManagerTestData.createRadioInfoSuccess(); - final radioList = (radios.output['radios'] as List) - .map((e) => RouterRadio.fromMap(e as Map)) - .toList(); - final guestSettings = - DashboardManagerTestData.createGuestRadioSettingsSuccess(); - final guestRadioList = (guestSettings.output['radios'] as List) - .map((e) => GuestRadioInfo.fromMap(e as Map)) - .toList(); - - final original = DashboardManagerState( - mainRadios: radioList, - guestRadios: guestRadioList, - isGuestNetworkEnabled: true, - uptimes: 0, - localTime: 0, - ); - - // Act - final json = original.toJson(); - final restored = DashboardManagerState.fromJson(json); - - // Assert - expect(restored.mainRadios.length, equals(original.mainRadios.length)); - expect(restored.guestRadios.length, equals(original.guestRadios.length)); - expect(restored.mainRadios[0].band, equals('2.4GHz')); - expect(restored.mainRadios[1].band, equals('5GHz')); - }); - }); -} diff --git a/test/core/jnap/services/dashboard_manager_service_test.dart b/test/core/jnap/services/dashboard_manager_service_test.dart deleted file mode 100644 index 3723de515..000000000 --- a/test/core/jnap/services/dashboard_manager_service_test.dart +++ /dev/null @@ -1,364 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:privacy_gui/core/errors/service_error.dart'; -import 'package:privacy_gui/core/jnap/actions/better_action.dart'; -import 'package:privacy_gui/core/jnap/models/device_info.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; -import 'package:privacy_gui/core/data/providers/polling_provider.dart'; -import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; -import 'package:privacy_gui/core/jnap/router_repository.dart'; -import 'package:privacy_gui/core/data/services/dashboard_manager_service.dart'; - -import '../../../mocks/test_data/dashboard_manager_test_data.dart'; - -class MockRouterRepository extends Mock implements RouterRepository {} - -void main() { - late DashboardManagerService service; - late MockRouterRepository mockRouterRepository; - - setUpAll(() { - registerFallbackValue(JNAPAction.getDeviceInfo); - }); - - setUp(() { - mockRouterRepository = MockRouterRepository(); - service = DashboardManagerService(mockRouterRepository); - }); - - group('DashboardManagerService - transformPollingData', () { - // T018: transformPollingData returns default state when pollingResult is null - test('returns default state when pollingResult is null', () { - // Arrange - const CoreTransactionData? pollingData = null; - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result, isA()); - expect(result.deviceInfo, isNull); - expect(result.mainRadios, isEmpty); - expect(result.guestRadios, isEmpty); - expect(result.isGuestNetworkEnabled, isFalse); - expect(result.uptimes, equals(0)); - expect(result.wanConnection, isNull); - expect(result.lanConnections, isEmpty); - expect(result.skuModelNumber, isNull); - expect(result.cpuLoad, isNull); - expect(result.memoryLoad, isNull); - }); - - // T019: transformPollingData returns complete state when all actions succeed - test('returns complete state when all actions succeed', () { - // Arrange - final pollingData = - DashboardManagerTestData.createSuccessfulPollingData(); - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result, isA()); - expect(result.deviceInfo, isNotNull); - expect(result.deviceInfo?.serialNumber, equals('TEST123456')); - expect(result.deviceInfo?.modelNumber, equals('MX5300')); - expect(result.mainRadios, isNotEmpty); - expect(result.mainRadios.length, equals(2)); // 2.4GHz and 5GHz - expect(result.guestRadios, isNotEmpty); - expect(result.guestRadios.length, equals(2)); - expect(result.isGuestNetworkEnabled, isFalse); - expect(result.uptimes, equals(86400)); - expect(result.wanConnection, equals('Linked-1000Mbps')); - expect(result.lanConnections, isNotEmpty); - expect(result.skuModelNumber, equals('MX5300-SKU')); - expect(result.localTime, isNotNull); - expect(result.localTime, isNot(equals(0))); - }); - - // T020: transformPollingData returns partial state when some actions fail - test('returns partial state when some actions fail', () { - // Arrange - final pollingData = - DashboardManagerTestData.createPartialErrorPollingData( - failedActions: { - JNAPAction.getRadioInfo, - JNAPAction.getGuestRadioSettings - }, - ); - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result, isA()); - // Device info should still be present - expect(result.deviceInfo, isNotNull); - // Radio info should be empty due to failure - expect(result.mainRadios, isEmpty); - expect(result.guestRadios, isEmpty); - // System stats should still be present - expect(result.uptimes, equals(86400)); - // Ethernet ports should still be present - expect(result.wanConnection, isNotNull); - }); - - // T021: transformPollingData correctly parses each JNAP action response - test('correctly parses each JNAP action response', () { - // Arrange - final pollingData = DashboardManagerTestData.createSuccessfulPollingData( - deviceInfo: DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'CUSTOM_SN', - modelNumber: 'CUSTOM_MODEL', - firmwareVersion: '2.0.0', - ), - systemStats: DashboardManagerTestData.createSystemStatsSuccess( - uptimeSeconds: 172800, - cpuLoad: '45%', - memoryLoad: '60%', - ), - ethernetPortConnections: - DashboardManagerTestData.createEthernetPortConnectionsSuccess( - lanPortConnections: ['Linked-100Mbps', 'None'], - wanPortConnection: 'Linked-100Mbps', - ), - ); - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result.deviceInfo?.serialNumber, equals('CUSTOM_SN')); - expect(result.deviceInfo?.modelNumber, equals('CUSTOM_MODEL')); - expect(result.deviceInfo?.firmwareVersion, equals('2.0.0')); - expect(result.uptimes, equals(172800)); - expect(result.cpuLoad, equals('45%')); - expect(result.memoryLoad, equals('60%')); - expect(result.wanConnection, equals('Linked-100Mbps')); - expect(result.lanConnections, equals(['Linked-100Mbps', 'None'])); - }); - - // T022: transformPollingData uses default localTime when parsing fails - test('uses current time when localTime parsing fails', () { - // Arrange - final pollingData = - DashboardManagerTestData.createPollingDataWithInvalidTime(); - final beforeTest = DateTime.now().millisecondsSinceEpoch; - - // Act - final result = service.transformPollingData(pollingData); - final afterTest = DateTime.now().millisecondsSinceEpoch; - - // Assert - expect(result.localTime, isNotNull); - expect(result.localTime, greaterThanOrEqualTo(beforeTest)); - expect(result.localTime, lessThanOrEqualTo(afterTest)); - }); - - test('correctly parses radio info with multiple bands', () { - // Arrange - final pollingData = - DashboardManagerTestData.createSuccessfulPollingData(); - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result.mainRadios.length, equals(2)); - expect(result.mainRadios.any((r) => r.band == '2.4GHz'), isTrue); - expect(result.mainRadios.any((r) => r.band == '5GHz'), isTrue); - }); - - test('correctly parses guest radio settings', () { - // Arrange - final pollingData = DashboardManagerTestData.createSuccessfulPollingData( - guestRadioSettings: - DashboardManagerTestData.createGuestRadioSettingsSuccess( - isGuestNetworkEnabled: true, - ), - ); - - // Act - final result = service.transformPollingData(pollingData); - - // Assert - expect(result.isGuestNetworkEnabled, isTrue); - expect(result.guestRadios.length, equals(2)); - }); - }); - - group('DashboardManagerService - checkRouterIsBack', () { - // T034: checkRouterIsBack returns NodeDeviceInfo when SN matches - test('returns NodeDeviceInfo when serial number matches', () async { - // Arrange - const expectedSN = 'TEST123456'; - final deviceInfoOutput = DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: expectedSN) - .output; - - when(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - )) - .thenAnswer( - (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); - - // Act - final result = await service.checkRouterIsBack(expectedSN); - - // Assert - expect(result, isA()); - expect(result.serialNumber, equals(expectedSN)); - }); - - // T035: checkRouterIsBack throws SerialNumberMismatchError when SN doesn't match - test('throws SerialNumberMismatchError when serial number does not match', - () async { - // Arrange - const expectedSN = 'EXPECTED_SN'; - const actualSN = 'ACTUAL_SN'; - final deviceInfoOutput = DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: actualSN) - .output; - - when(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - )) - .thenAnswer( - (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); - - // Act & Assert - expect( - () => service.checkRouterIsBack(expectedSN), - throwsA(isA()), - ); - }); - - // T036: checkRouterIsBack throws ConnectivityError when router unreachable - test('throws ConnectivityError when router is unreachable', () async { - // Arrange - when(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - )).thenThrow(Exception('Network error')); - - // Act & Assert - expect( - () => service.checkRouterIsBack('TEST123456'), - throwsA(isA()), - ); - }); - - // T037: checkRouterIsBack maps JNAPError to ServiceError correctly - test('maps JNAPError to ServiceError correctly', () async { - // Arrange - when(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - )) - .thenThrow(const JNAPError( - result: '_ErrorUnauthorized', error: 'Unauthorized')); - - // Act & Assert - expect( - () => service.checkRouterIsBack('TEST123456'), - throwsA(isA()), - ); - }); - - test('returns NodeDeviceInfo when expected serial number is empty', - () async { - // Arrange - empty SN should skip validation - final deviceInfoOutput = DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'ANY_SN') - .output; - - when(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - fetchRemote: true, - retries: 0, - )) - .thenAnswer( - (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); - - // Act - final result = await service.checkRouterIsBack(''); - - // Assert - expect(result, isA()); - expect(result.serialNumber, equals('ANY_SN')); - }); - }); - - group('DashboardManagerService - checkDeviceInfo', () { - // T044: checkDeviceInfo returns cached value immediately when available - test('returns cached value immediately when available', () async { - // Arrange - final cachedDeviceInfo = NodeDeviceInfo.fromJson( - DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'CACHED_SN') - .output, - ); - - // Act - final result = await service.checkDeviceInfo(cachedDeviceInfo); - - // Assert - expect(result, equals(cachedDeviceInfo)); - expect(result.serialNumber, equals('CACHED_SN')); - // No API call should be made when cached value exists - verifyZeroInteractions(mockRouterRepository); - }); - - // T045: checkDeviceInfo makes API call when cached value is null - test('makes API call when cached value is null', () async { - // Arrange - final deviceInfoOutput = DashboardManagerTestData.createDeviceInfoSuccess( - serialNumber: 'FRESH_SN') - .output; - - when(() => mockRouterRepository.send( - any(), - retries: any(named: 'retries'), - timeoutMs: any(named: 'timeoutMs'), - )) - .thenAnswer( - (_) async => JNAPSuccess(result: 'OK', output: deviceInfoOutput)); - - // Act - final result = await service.checkDeviceInfo(null); - - // Assert - expect(result, isA()); - expect(result.serialNumber, equals('FRESH_SN')); - verify(() => mockRouterRepository.send( - JNAPAction.getDeviceInfo, - retries: 0, - timeoutMs: 3000, - )).called(1); - }); - - // T046: checkDeviceInfo throws ServiceError on API failure - test('throws ServiceError on API failure', () async { - // Arrange - when(() => mockRouterRepository.send( - any(), - retries: any(named: 'retries'), - timeoutMs: any(named: 'timeoutMs'), - )) - .thenThrow(const JNAPError( - result: 'ErrorDeviceNotFound', error: 'Device not found')); - - // Act & Assert - expect( - () => service.checkDeviceInfo(null), - throwsA(isA()), - ); - }); - }); -} diff --git a/test/mocks/_index.dart b/test/mocks/_index.dart index 5a9192066..f9069fe59 100644 --- a/test/mocks/_index.dart +++ b/test/mocks/_index.dart @@ -2,7 +2,7 @@ export 'add_nodes_notifier_mocks.dart'; export 'administration_settings_notifier_mocks.dart'; export 'apps_and_gaming_view_notifier_mocks.dart'; export 'dashboard_home_notifier_mocks.dart'; -export 'dashboard_manager_notifier_mocks.dart'; +export 'session_notifier_mocks.dart'; export 'ddns_notifier_mocks.dart'; export 'device_filter_config_notifier_mocks.dart'; export 'device_list_notifier_mock.dart'; diff --git a/test/mocks/dashboard_manager_notifier_mocks.dart b/test/mocks/dashboard_manager_notifier_mocks.dart deleted file mode 100644 index 21430eeb7..000000000 --- a/test/mocks/dashboard_manager_notifier_mocks.dart +++ /dev/null @@ -1,230 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in privacy_gui/test/mocks/mockito_specs/dashboard_manager_notifier_spec.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; - -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; -import 'package:mockito/mockito.dart' as _i1; -import 'package:privacy_gui/core/jnap/models/device_info.dart' as _i4; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart' - as _i10; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart' - as _i3; - -// 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 _FakeNotifierProviderRef_0 extends _i1.SmartFake - implements _i2.NotifierProviderRef { - _FakeNotifierProviderRef_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeDashboardManagerState_1 extends _i1.SmartFake - implements _i3.DashboardManagerState { - _FakeDashboardManagerState_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeNodeDeviceInfo_2 extends _i1.SmartFake - implements _i4.NodeDeviceInfo { - _FakeNodeDeviceInfo_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [DashboardManagerNotifier]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockDashboardManagerNotifier - extends _i2.Notifier<_i3.DashboardManagerState> - with _i1.Mock - implements _i10.DashboardManagerNotifier { - @override - _i2.NotifierProviderRef<_i3.DashboardManagerState> get ref => - (super.noSuchMethod( - Invocation.getter(#ref), - returnValue: _FakeNotifierProviderRef_0<_i3.DashboardManagerState>( - this, - Invocation.getter(#ref), - ), - returnValueForMissingStub: - _FakeNotifierProviderRef_0<_i3.DashboardManagerState>( - this, - Invocation.getter(#ref), - ), - ) as _i2.NotifierProviderRef<_i3.DashboardManagerState>); - - @override - _i3.DashboardManagerState get state => (super.noSuchMethod( - Invocation.getter(#state), - returnValue: _FakeDashboardManagerState_1( - this, - Invocation.getter(#state), - ), - returnValueForMissingStub: _FakeDashboardManagerState_1( - this, - Invocation.getter(#state), - ), - ) as _i3.DashboardManagerState); - - @override - set state(_i3.DashboardManagerState? value) => super.noSuchMethod( - Invocation.setter( - #state, - value, - ), - returnValueForMissingStub: null, - ); - - @override - _i3.DashboardManagerState build() => (super.noSuchMethod( - Invocation.method( - #build, - [], - ), - returnValue: _FakeDashboardManagerState_1( - this, - Invocation.method( - #build, - [], - ), - ), - returnValueForMissingStub: _FakeDashboardManagerState_1( - this, - Invocation.method( - #build, - [], - ), - ), - ) as _i3.DashboardManagerState); - - @override - _i6.Future saveSelectedNetwork( - String? serialNumber, - String? networkId, - ) => - (super.noSuchMethod( - Invocation.method( - #saveSelectedNetwork, - [ - serialNumber, - networkId, - ], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - - @override - _i6.Future<_i4.NodeDeviceInfo> checkRouterIsBack() => (super.noSuchMethod( - Invocation.method( - #checkRouterIsBack, - [], - ), - returnValue: _i6.Future<_i4.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_2( - this, - Invocation.method( - #checkRouterIsBack, - [], - ), - )), - returnValueForMissingStub: - _i6.Future<_i4.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_2( - this, - Invocation.method( - #checkRouterIsBack, - [], - ), - )), - ) as _i6.Future<_i4.NodeDeviceInfo>); - - @override - _i6.Future<_i4.NodeDeviceInfo> checkDeviceInfo(String? serialNumber) => - (super.noSuchMethod( - Invocation.method( - #checkDeviceInfo, - [serialNumber], - ), - returnValue: _i6.Future<_i4.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_2( - this, - Invocation.method( - #checkDeviceInfo, - [serialNumber], - ), - )), - returnValueForMissingStub: - _i6.Future<_i4.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_2( - this, - Invocation.method( - #checkDeviceInfo, - [serialNumber], - ), - )), - ) as _i6.Future<_i4.NodeDeviceInfo>); - - @override - void listenSelf( - void Function( - _i3.DashboardManagerState?, - _i3.DashboardManagerState, - )? listener, { - void Function( - Object, - StackTrace, - )? onError, - }) => - super.noSuchMethod( - Invocation.method( - #listenSelf, - [listener], - {#onError: onError}, - ), - returnValueForMissingStub: null, - ); - - @override - bool updateShouldNotify( - _i3.DashboardManagerState? previous, - _i3.DashboardManagerState? next, - ) => - (super.noSuchMethod( - Invocation.method( - #updateShouldNotify, - [ - previous, - next, - ], - ), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); -} diff --git a/test/mocks/mockito_specs/dashboard_manager_notifier_spec.dart b/test/mocks/mockito_specs/dashboard_manager_notifier_spec.dart deleted file mode 100644 index 296beeae8..000000000 --- a/test/mocks/mockito_specs/dashboard_manager_notifier_spec.dart +++ /dev/null @@ -1,5 +0,0 @@ -@GenerateNiceMocks([ - MockSpec(), -]) -import 'package:mockito/annotations.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; diff --git a/test/mocks/mockito_specs/session_notifier_spec.dart b/test/mocks/mockito_specs/session_notifier_spec.dart new file mode 100644 index 000000000..cfbea4dc2 --- /dev/null +++ b/test/mocks/mockito_specs/session_notifier_spec.dart @@ -0,0 +1,5 @@ +@GenerateNiceMocks([ + MockSpec(), +]) +import 'package:mockito/annotations.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; diff --git a/test/mocks/session_notifier_mocks.dart b/test/mocks/session_notifier_mocks.dart new file mode 100644 index 000000000..75e61386c --- /dev/null +++ b/test/mocks/session_notifier_mocks.dart @@ -0,0 +1,168 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in privacy_gui/test/mocks/mockito_specs/session_notifier_spec.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +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/device_info.dart' as _i3; +import 'package:privacy_gui/core/data/providers/session_provider.dart' as _i4; + +// 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 _FakeNotifierProviderRef_0 extends _i1.SmartFake + implements _i2.NotifierProviderRef { + _FakeNotifierProviderRef_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeNodeDeviceInfo_1 extends _i1.SmartFake + implements _i3.NodeDeviceInfo { + _FakeNodeDeviceInfo_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [SessionNotifier]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSessionNotifier extends _i2.Notifier + with _i1.Mock + implements _i4.SessionNotifier { + @override + _i2.NotifierProviderRef get ref => (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _FakeNotifierProviderRef_0( + this, + Invocation.getter(#ref), + ), + returnValueForMissingStub: _FakeNotifierProviderRef_0( + this, + Invocation.getter(#ref), + ), + ) as _i2.NotifierProviderRef); + + @override + void build() => super.noSuchMethod( + Invocation.method( + #build, + [], + ), + returnValueForMissingStub: null, + ); + + @override + _i5.Future saveSelectedNetwork( + String? serialNumber, + String? networkId, + ) => + (super.noSuchMethod( + Invocation.method( + #saveSelectedNetwork, + [ + serialNumber, + networkId, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future<_i3.NodeDeviceInfo> checkRouterIsBack() => (super.noSuchMethod( + Invocation.method( + #checkRouterIsBack, + [], + ), + returnValue: _i5.Future<_i3.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_1( + this, + Invocation.method( + #checkRouterIsBack, + [], + ), + )), + returnValueForMissingStub: + _i5.Future<_i3.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_1( + this, + Invocation.method( + #checkRouterIsBack, + [], + ), + )), + ) as _i5.Future<_i3.NodeDeviceInfo>); + + @override + _i5.Future<_i3.NodeDeviceInfo> checkDeviceInfo(String? serialNumber) => + (super.noSuchMethod( + Invocation.method( + #checkDeviceInfo, + [serialNumber], + ), + returnValue: _i5.Future<_i3.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_1( + this, + Invocation.method( + #checkDeviceInfo, + [serialNumber], + ), + )), + returnValueForMissingStub: + _i5.Future<_i3.NodeDeviceInfo>.value(_FakeNodeDeviceInfo_1( + this, + Invocation.method( + #checkDeviceInfo, + [serialNumber], + ), + )), + ) as _i5.Future<_i3.NodeDeviceInfo>); + + @override + void listenSelf( + void Function( + void, + void, + )? listener, { + void Function( + Object, + StackTrace, + )? onError, + }) => + super.noSuchMethod( + Invocation.method( + #listenSelf, + [listener], + {#onError: onError}, + ), + returnValueForMissingStub: null, + ); + + @override + bool updateShouldNotify( + void previous, + void next, + ) => + false; +} diff --git a/test/mocks/test_data/dashboard_home_test_data.dart b/test/mocks/test_data/dashboard_home_test_data.dart index c18a02b10..bbb9b34ec 100644 --- a/test/mocks/test_data/dashboard_home_test_data.dart +++ b/test/mocks/test_data/dashboard_home_test_data.dart @@ -3,65 +3,92 @@ import 'package:privacy_gui/core/jnap/models/device_info.dart'; import 'package:privacy_gui/core/jnap/models/guest_radio_settings.dart'; import 'package:privacy_gui/core/jnap/models/radio_info.dart'; import 'package:privacy_gui/core/jnap/models/wan_status.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_state.dart'; /// Test data builder for DashboardHomeService tests. /// -/// Provides factory methods to create DashboardManagerState and DeviceManagerState +/// Provides factory methods to create domain provider states and DeviceManagerState /// with sensible defaults for testing the service transformation logic. /// /// Per constitution Section 1.6.2 class DashboardHomeTestData { // ============================================ - // DashboardManagerState Builders + // DeviceInfoState Builders // ============================================ - /// Create a DashboardManagerState with default values - static DashboardManagerState createDashboardManagerState({ + /// Create a DeviceInfoState with default values + static DeviceInfoState createDeviceInfoState({ NodeDeviceInfo? deviceInfo, + String? skuModelNumber, + }) { + return DeviceInfoState( + deviceInfo: deviceInfo ?? createNodeDeviceInfo(), + skuModelNumber: skuModelNumber, + ); + } + + // ============================================ + // WifiRadiosState Builders + // ============================================ + + /// Create a WifiRadiosState with default values + static WifiRadiosState createWifiRadiosState({ List? mainRadios, List? guestRadios, bool isGuestNetworkEnabled = false, - int uptimes = 86400, - String? wanConnection = 'Linked-1000Mbps', - List lanConnections = const ['Linked-1000Mbps', 'None', 'None'], }) { - return DashboardManagerState( - deviceInfo: deviceInfo ?? createNodeDeviceInfo(), + return WifiRadiosState( mainRadios: mainRadios ?? createDefaultMainRadios(), guestRadios: guestRadios ?? const [], isGuestNetworkEnabled: isGuestNetworkEnabled, - uptimes: uptimes, - wanConnection: wanConnection, - lanConnections: lanConnections, ); } - /// Create DashboardManagerState with guest network enabled - static DashboardManagerState createDashboardManagerStateWithGuest({ - NodeDeviceInfo? deviceInfo, + /// Create WifiRadiosState with guest network enabled + static WifiRadiosState createWifiRadiosStateWithGuest({ List? mainRadios, List? guestRadios, - int uptimes = 86400, }) { - return createDashboardManagerState( - deviceInfo: deviceInfo, + return createWifiRadiosState( mainRadios: mainRadios, guestRadios: guestRadios ?? createDefaultGuestRadios(), isGuestNetworkEnabled: true, - uptimes: uptimes, ); } - /// Create empty DashboardManagerState (no radios) - static DashboardManagerState createEmptyDashboardManagerState() { - return const DashboardManagerState( - mainRadios: [], - guestRadios: [], - isGuestNetworkEnabled: false, - uptimes: 0, - lanConnections: [], + // ============================================ + // EthernetPortsState Builders + // ============================================ + + /// Create a EthernetPortsState with default values + static EthernetPortsState createEthernetPortsState({ + String? wanConnection = 'Linked-1000Mbps', + List lanConnections = const ['Linked-1000Mbps', 'None', 'None'], + }) { + return EthernetPortsState( + wanConnection: wanConnection, + lanConnections: lanConnections, + ); + } + + // ============================================ + // SystemStatsState Builders + // ============================================ + + /// Create a SystemStatsState with default values + static SystemStatsState createSystemStatsState({ + int uptimes = 86400, + String? cpuLoad, + String? memoryLoad, + }) { + return SystemStatsState( + uptimes: uptimes, + cpuLoad: cpuLoad, + memoryLoad: memoryLoad, ); } diff --git a/test/mocks/test_data/dashboard_manager_test_data.dart b/test/mocks/test_data/dashboard_manager_test_data.dart index 7e94b8c96..b95024b6f 100644 --- a/test/mocks/test_data/dashboard_manager_test_data.dart +++ b/test/mocks/test_data/dashboard_manager_test_data.dart @@ -2,13 +2,13 @@ import 'package:privacy_gui/core/jnap/actions/better_action.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/core/jnap/result/jnap_result.dart'; -/// Test data builder for DashboardManagerService tests. +/// Test data builder for SessionManagerService tests. /// /// Provides factory methods to create JNAP mock responses with sensible defaults. /// This centralizes test data and makes tests more readable. /// /// Per constitution Section 1.6.2 -class DashboardManagerTestData { +class SessionTestData { // === Individual JNAP Response Builders === /// Create default getDeviceInfo success response diff --git a/test/page/dashboard/localizations/dashboard_home_view_test.dart b/test/page/dashboard/localizations/dashboard_home_view_test.dart index 50929a3d2..26465831d 100644 --- a/test/page/dashboard/localizations/dashboard_home_view_test.dart +++ b/test/page/dashboard/localizations/dashboard_home_view_test.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:privacy_gui/core/jnap/models/node_light_settings.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; import 'package:privacy_gui/core/data/providers/firmware_update_state.dart'; import 'package:privacy_gui/core/data/providers/node_internet_status_provider.dart'; import 'package:privacy_gui/page/dashboard/_dashboard.dart'; @@ -367,10 +366,6 @@ void main() { DashboardHomeState.fromMap(dashboardHomeCherry7TestState) .copyWith(wanPortConnection: () => 'None'), ); - when(testHelper.mockDashboardManagerNotifier.build()).thenReturn( - DashboardManagerState.fromMap(dashboardManagerChrry7TestState) - .copyWith(wanConnection: 'None'), - ); final context = await pumpDashboard( tester, diff --git a/test/page/dashboard/providers/dashboard_home_provider_test.dart b/test/page/dashboard/providers/dashboard_home_provider_test.dart index 19ab1b964..07efd8e73 100644 --- a/test/page/dashboard/providers/dashboard_home_provider_test.dart +++ b/test/page/dashboard/providers/dashboard_home_provider_test.dart @@ -1,9 +1,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/device_info_provider.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; +import 'package:privacy_gui/core/data/providers/system_stats_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_state.dart'; +import 'package:privacy_gui/core/jnap/models/device.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_provider.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_state.dart'; import 'package:privacy_gui/page/dashboard/services/dashboard_home_service.dart'; @@ -12,20 +15,6 @@ import 'package:privacy_gui/page/health_check/providers/health_check_state.dart' import '../../../mocks/test_data/dashboard_home_test_data.dart'; -/// Mock DashboardManagerNotifier for testing -class MockDashboardManagerNotifier extends Notifier - implements DashboardManagerNotifier { - final DashboardManagerState _state; - - MockDashboardManagerNotifier(this._state); - - @override - DashboardManagerState build() => _state; - - @override - dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); -} - /// Mock DeviceManagerNotifier for testing class MockDeviceManagerNotifier extends Notifier implements DeviceManagerNotifier { @@ -63,19 +52,28 @@ class MockHealthCheckNotifier extends Notifier class MockDashboardHomeService implements DashboardHomeService { DashboardHomeState? returnState; int buildCallCount = 0; - DashboardManagerState? lastDashboardManagerState; + DeviceInfoState? lastDeviceInfoState; + WifiRadiosState? lastWifiRadiosState; + EthernetPortsState? lastEthernetPortsState; + SystemStatsState? lastSystemStatsState; DeviceManagerState? lastDeviceManagerState; List? lastDeviceList; @override DashboardHomeState buildDashboardHomeState({ - required DashboardManagerState dashboardManagerState, + required DeviceInfoState deviceInfoState, + required WifiRadiosState wifiRadiosState, + required EthernetPortsState ethernetPortsState, + required SystemStatsState systemStatsState, required DeviceManagerState deviceManagerState, required String Function(LinksysDevice device) getBandForDevice, required List deviceList, }) { buildCallCount++; - lastDashboardManagerState = dashboardManagerState; + lastDeviceInfoState = deviceInfoState; + lastWifiRadiosState = wifiRadiosState; + lastEthernetPortsState = ethernetPortsState; + lastSystemStatsState = systemStatsState; lastDeviceManagerState = deviceManagerState; lastDeviceList = deviceList; return returnState ?? const DashboardHomeState(); @@ -97,8 +95,11 @@ void main() { group('DashboardHomeNotifier', () { test('build() calls service.buildDashboardHomeState', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); @@ -112,8 +113,10 @@ void main() { container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -128,21 +131,32 @@ void main() { expect(mockService.buildCallCount, 1); }); - test('build() passes correct dashboardManagerState to service', () { + test('build() passes correct domain states to service', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( - uptimes: 172800, + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState( + deviceInfo: DashboardHomeTestData.createNodeDeviceInfo( + modelNumber: 'LN16', + ), + ); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState( + mainRadios: DashboardHomeTestData.createDefaultMainRadios(), + ); + final ethernetPortsState = DashboardHomeTestData.createEthernetPortsState( wanConnection: 'Linked-100Mbps', ); + final systemStatsState = DashboardHomeTestData.createSystemStatsState( + uptimes: 172800, + ); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -153,16 +167,22 @@ void main() { container.read(dashboardHomeProvider); // Assert - expect(mockService.lastDashboardManagerState, dashboardManagerState); - expect(mockService.lastDashboardManagerState?.uptimes, 172800); - expect(mockService.lastDashboardManagerState?.wanConnection, - 'Linked-100Mbps'); + expect(mockService.lastDeviceInfoState, deviceInfoState); + expect(mockService.lastDeviceInfoState?.deviceInfo?.modelNumber, 'LN16'); + expect(mockService.lastWifiRadiosState, wifiRadiosState); + expect(mockService.lastWifiRadiosState?.mainRadios.length, 2); + expect( + mockService.lastEthernetPortsState?.wanConnection, 'Linked-100Mbps'); + expect(mockService.lastSystemStatsState?.uptimes, 172800); }); test('build() passes correct deviceManagerState to service', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( lastUpdateTime: 1234567890, ); @@ -170,8 +190,10 @@ void main() { container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -188,8 +210,11 @@ void main() { test('build() passes deviceList from deviceManagerProvider', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceList = [ DashboardHomeTestData.createMasterDevice(), DashboardHomeTestData.createMainWifiDevice(deviceId: 'device-001'), @@ -202,8 +227,10 @@ void main() { container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -220,8 +247,11 @@ void main() { test('returns service result directly', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); @@ -251,8 +281,10 @@ void main() { container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -276,18 +308,25 @@ void main() { expect(state.detectedWANType, 'DHCP'); }); - test('provider listens to dashboardManagerProvider changes', () { + test('provider listens to domain providers changes', () { // Arrange - final dashboardManagerState1 = - DashboardHomeTestData.createDashboardManagerState(uptimes: 1000); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState( + uptimes: 1000, + ); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState1)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -299,13 +338,16 @@ void main() { // Assert - service was called expect(mockService.buildCallCount, 1); - expect(mockService.lastDashboardManagerState?.uptimes, 1000); + expect(mockService.lastSystemStatsState?.uptimes, 1000); }); test('provider listens to deviceManagerProvider changes', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( lastUpdateTime: 0, // First polling ); @@ -313,8 +355,10 @@ void main() { container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), @@ -331,16 +375,21 @@ void main() { test('provider listens to healthCheckProvider (for reactivity)', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); container = ProviderContainer( overrides: [ dashboardHomeServiceProvider.overrideWithValue(mockService), - dashboardManagerProvider.overrideWith( - () => MockDashboardManagerNotifier(dashboardManagerState)), + deviceInfoProvider.overrideWithValue(deviceInfoState), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(ethernetPortsState), + systemStatsProvider.overrideWithValue(systemStatsState), deviceManagerProvider.overrideWith( () => MockDeviceManagerNotifier(deviceManagerState)), healthCheckProvider.overrideWith(() => MockHealthCheckNotifier()), diff --git a/test/page/dashboard/services/dashboard_home_service_test.dart b/test/page/dashboard/services/dashboard_home_service_test.dart index 11f7aa575..c51cf6542 100644 --- a/test/page/dashboard/services/dashboard_home_service_test.dart +++ b/test/page/dashboard/services/dashboard_home_service_test.dart @@ -15,10 +15,13 @@ void main() { group('buildDashboardHomeState', () { test('returns correct state with main WiFi networks grouped by band', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState( mainRadios: DashboardHomeTestData.createDefaultMainRadios(), ); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( deviceList: [ @@ -42,7 +45,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -62,8 +68,12 @@ void main() { test('returns correct state with guest WiFi when guest radios exist', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerStateWithGuest(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = + DashboardHomeTestData.createWifiRadiosStateWithGuest(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( deviceList: [ @@ -78,7 +88,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -95,8 +108,17 @@ void main() { test('returns empty WiFi list when no radios exist', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createEmptyDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState( + mainRadios: const [], + ); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState( + wanConnection: null, + lanConnections: const [], + ); + final systemStatsState = + DashboardHomeTestData.createSystemStatsState(uptimes: 0); final deviceManagerState = DashboardHomeTestData.createEmptyDeviceManagerState(); final getBandForDevice = @@ -104,7 +126,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -116,8 +141,11 @@ void main() { test('sets isAnyNodesOffline true when nodes are offline', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerStateWithOfflineNodes(); final getBandForDevice = @@ -125,7 +153,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -137,8 +168,11 @@ void main() { test('sets isAnyNodesOffline false when all nodes are online', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( deviceList: [ @@ -151,7 +185,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -163,8 +200,11 @@ void main() { test('sets isFirstPolling true when lastUpdateTime is zero', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createFirstPollingDeviceManagerState(); final getBandForDevice = @@ -172,7 +212,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -184,8 +227,11 @@ void main() { test('sets isFirstPolling false when lastUpdateTime is non-zero', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( lastUpdateTime: 1234567890, @@ -195,7 +241,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -207,10 +256,13 @@ void main() { test('handles null deviceInfo for port layout determination', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState( deviceInfo: null, ); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); final getBandForDevice = @@ -218,7 +270,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -237,10 +292,13 @@ void main() { test('correctly counts connected devices per band', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState( mainRadios: DashboardHomeTestData.createDefaultMainRadios(), ); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( deviceList: [ @@ -274,7 +332,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -293,12 +354,15 @@ void main() { test('does not add guest WiFi when guest radios are empty', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState( guestRadios: const [], isGuestNetworkEnabled: true, // Even if enabled, no radios = no guest WiFi ); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); final getBandForDevice = @@ -306,7 +370,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -318,8 +385,11 @@ void main() { test('correctly extracts WAN type from wanStatus', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( wanStatus: DashboardHomeTestData.createWanStatus(wanType: 'PPPoE'), @@ -329,7 +399,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -341,8 +414,11 @@ void main() { test('correctly extracts detectedWANType from wanStatus', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState( wanStatus: DashboardHomeTestData.createWanStatus( @@ -354,7 +430,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -366,8 +445,11 @@ void main() { test('correctly determines master icon from deviceList', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState(); + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceList = [ DashboardHomeTestData.createMasterDevice(modelNumber: 'MX5300'), ]; @@ -380,7 +462,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceList, @@ -394,13 +479,16 @@ void main() { test('correctly determines horizontal port layout', () { // Arrange - LN11 has horizontal ports - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState( deviceInfo: DashboardHomeTestData.createNodeDeviceInfo( modelNumber: 'LN11', hardwareVersion: '1', ), ); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState(); + final systemStatsState = DashboardHomeTestData.createSystemStatsState(); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); final getBandForDevice = @@ -408,7 +496,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, @@ -421,12 +512,16 @@ void main() { test('passes through uptime, wanConnection, lanConnections correctly', () { // Arrange - final dashboardManagerState = - DashboardHomeTestData.createDashboardManagerState( - uptimes: 172800, // 2 days in seconds + final deviceInfoState = DashboardHomeTestData.createDeviceInfoState(); + final wifiRadiosState = DashboardHomeTestData.createWifiRadiosState(); + final ethernetPortsState = + DashboardHomeTestData.createEthernetPortsState( wanConnection: 'Linked-100Mbps', lanConnections: ['Linked-1000Mbps', 'Linked-100Mbps', 'None', 'None'], ); + final systemStatsState = DashboardHomeTestData.createSystemStatsState( + uptimes: 172800, // 2 days in seconds + ); final deviceManagerState = DashboardHomeTestData.createDeviceManagerState(); final getBandForDevice = @@ -434,7 +529,10 @@ void main() { // Act final result = service.buildDashboardHomeState( - dashboardManagerState: dashboardManagerState, + deviceInfoState: deviceInfoState, + wifiRadiosState: wifiRadiosState, + ethernetPortsState: ethernetPortsState, + systemStatsState: systemStatsState, deviceManagerState: deviceManagerState, getBandForDevice: getBandForDevice, deviceList: deviceManagerState.deviceList, diff --git a/test/page/login/localizations/login_local_view_test.dart b/test/page/login/localizations/login_local_view_test.dart index e393c7d38..921c5ca01 100644 --- a/test/page/login/localizations/login_local_view_test.dart +++ b/test/page/login/localizations/login_local_view_test.dart @@ -26,8 +26,7 @@ void main() async { setUp(() { testHelper.setup(); - when(testHelper.mockDashboardManagerNotifier.checkDeviceInfo(null)) - .thenAnswer( + when(testHelper.mockSessionNotifier.checkDeviceInfo(null)).thenAnswer( (_) async => NodeDeviceInfo.fromJson(jsonDecode(testDeviceInfo)['output']), ); diff --git a/test/page/wifi_settings/providers/wifi_bundle_provider_test.dart b/test/page/wifi_settings/providers/wifi_bundle_provider_test.dart index 2b241fde2..35c7bb020 100644 --- a/test/page/wifi_settings/providers/wifi_bundle_provider_test.dart +++ b/test/page/wifi_settings/providers/wifi_bundle_provider_test.dart @@ -4,10 +4,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'package:mocktail/mocktail.dart'; import 'package:privacy_gui/core/jnap/actions/jnap_service_supported.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/wifi_radios_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_state.dart'; +import 'package:privacy_gui/core/data/providers/ethernet_ports_provider.dart'; import 'package:privacy_gui/core/jnap/router_repository.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_provider.dart'; import 'package:privacy_gui/page/dashboard/providers/dashboard_home_state.dart'; @@ -24,10 +24,6 @@ class MockWifiSettingsService extends Mock implements WifiSettingsService {} class FakeLinksysDevice extends Fake implements LinksysDevice {} -class MockDashboardManagerNotifier extends Notifier - with Mock - implements DashboardManagerNotifier {} - class MockDashboardHomeNotifier extends Notifier with Mock implements DashboardHomeNotifier {} @@ -42,19 +38,19 @@ class MockServiceHelper extends Mock implements ServiceHelper {} void main() { late MockWifiSettingsService mockWifiSettingsService; - late MockDashboardManagerNotifier mockDashboardManagerNotifier; late MockDashboardHomeNotifier mockDashboardHomeNotifier; late MockDeviceManagerNotifier mockDeviceManagerNotifier; late MockRouterRepository mockRouterRepository; late ProviderContainer container; + late WifiRadiosState wifiRadiosState; setUp(() { // Mock Init mockWifiSettingsService = MockWifiSettingsService(); - mockDashboardManagerNotifier = MockDashboardManagerNotifier(); mockDashboardHomeNotifier = MockDashboardHomeNotifier(); mockDeviceManagerNotifier = MockDeviceManagerNotifier(); mockRouterRepository = MockRouterRepository(); + wifiRadiosState = const WifiRadiosState(); // Stub ServiceHelper final helper = MockServiceHelper(); @@ -86,8 +82,6 @@ void main() { )); // Default Stubs - when(() => mockDashboardManagerNotifier.build()) - .thenReturn(const DashboardManagerState()); when(() => mockDashboardHomeNotifier.build()) .thenReturn(const DashboardHomeState()); when(() => mockDeviceManagerNotifier.build()) @@ -120,8 +114,8 @@ void main() { container = ProviderContainer( overrides: [ wifiSettingsServiceProvider.overrideWithValue(mockWifiSettingsService), - dashboardManagerProvider - .overrideWith(() => mockDashboardManagerNotifier), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(const EthernetPortsState()), dashboardHomeProvider.overrideWith(() => mockDashboardHomeNotifier), deviceManagerProvider.overrideWith(() => mockDeviceManagerNotifier), routerRepositoryProvider.overrideWithValue(mockRouterRepository), @@ -133,14 +127,15 @@ void main() { GetIt.I.reset(); }); - void seedWithRadios() { + /// Creates test data for radios and returns both RouterRadios and WiFiItems. + (List, List) createRadiosTestData() { final radios = [ RouterRadio( radioID: 'RADIO_2.4GHz', physicalRadioID: '1', bssid: '00:00:00:00:00:01', band: '2.4GHz', - supportedModes: const [], // Empty to avoid crash + supportedModes: const [], supportedChannelsForChannelWidths: const [], supportedSecurityTypes: [WifiSecurityType.wpa2Personal.value], maxRadiusSharedKeyLength: 64, @@ -170,10 +165,7 @@ void main() { channel: 36, security: WifiSecurityType.wpa2Personal.value)), ]; - when(() => mockDashboardManagerNotifier.build()) - .thenReturn(DashboardManagerState(mainRadios: radios)); - // Create WiFiItems matching the seeded radios for the mock final wifiItems = [ WiFiItem.fromMap(const { 'radioID': 'RADIO_2.4GHz', @@ -201,7 +193,20 @@ void main() { }), ]; - // Stub createInitialWifiListSettings to return the matching WiFiItems + return (radios, wifiItems); + } + + /// Seeds the container with radio data by: + /// 1. Creating test data first + /// 2. Setting up the mock stub + /// 3. Creating a NEW container with the proper state override + void seedWithRadios() { + final (radios, wifiItems) = createRadiosTestData(); + + // First: Prepare the state BEFORE creating the container + wifiRadiosState = WifiRadiosState(mainRadios: radios); + + // Second: Set up mock stub to return matching WiFiItems when(() => mockWifiSettingsService.createInitialWifiListSettings( mainRadios: any(named: 'mainRadios'), isGuestNetworkEnabled: any(named: 'isGuestNetworkEnabled'), @@ -218,18 +223,19 @@ void main() { simpleModeWifi: wifiItems.first, )); - // Re-create container to pick up new stub + // Third: Create container with the prepared state container = ProviderContainer( overrides: [ wifiSettingsServiceProvider.overrideWithValue(mockWifiSettingsService), - dashboardManagerProvider - .overrideWith(() => mockDashboardManagerNotifier), + wifiRadiosProvider.overrideWithValue(wifiRadiosState), + ethernetPortsProvider.overrideWithValue(const EthernetPortsState()), dashboardHomeProvider.overrideWith(() => mockDashboardHomeNotifier), deviceManagerProvider.overrideWith(() => mockDeviceManagerNotifier), routerRepositoryProvider.overrideWithValue(mockRouterRepository), ], ); - // Trigger build + + // Trigger build to initialize provider container.read(wifiBundleProvider); } diff --git a/test/providers/auth/auth_provider_test.dart b/test/providers/auth/auth_provider_test.dart index 0880a6735..a04b1256a 100644 --- a/test/providers/auth/auth_provider_test.dart +++ b/test/providers/auth/auth_provider_test.dart @@ -7,7 +7,7 @@ import 'package:privacy_gui/constants/pref_key.dart'; import 'package:privacy_gui/core/cloud/model/cloud_session_model.dart'; import 'package:privacy_gui/core/cloud/model/guardians_remote_assistance.dart'; import 'package:privacy_gui/core/errors/service_error.dart'; -import 'package:privacy_gui/core/data/providers/dashboard_manager_provider.dart'; +import 'package:privacy_gui/core/data/providers/session_provider.dart'; import 'package:privacy_gui/core/data/providers/device_manager_provider.dart'; import 'package:privacy_gui/core/data/providers/polling_provider.dart'; import 'package:privacy_gui/providers/auth/auth_provider.dart'; @@ -23,8 +23,7 @@ class MockPollingNotifier extends Mock implements PollingNotifier {} class MockDeviceManagerNotifier extends Mock implements DeviceManagerNotifier {} -class MockDashboardManagerNotifier extends Mock - implements DashboardManagerNotifier {} +class MockSessionNotifier extends Mock implements SessionNotifier {} class MockRASessionNotifier extends Mock implements RASessionNotifier {} @@ -32,14 +31,14 @@ void main() { late MockAuthService mockAuthService; late MockPollingNotifier mockPollingNotifier; late MockDeviceManagerNotifier mockDeviceManagerNotifier; - late MockDashboardManagerNotifier mockDashboardManagerNotifier; + late MockSessionNotifier mockSessionNotifier; late MockRASessionNotifier mockRaSessionNotifier; setUp(() { mockAuthService = MockAuthService(); mockPollingNotifier = MockPollingNotifier(); mockDeviceManagerNotifier = MockDeviceManagerNotifier(); - mockDashboardManagerNotifier = MockDashboardManagerNotifier(); + mockSessionNotifier = MockSessionNotifier(); mockRaSessionNotifier = MockRASessionNotifier(); // Setup default behaviors @@ -49,7 +48,7 @@ void main() { when(() => mockRaSessionNotifier.stopMonitorSession()).thenReturn(null); when(() => mockRaSessionNotifier.raLogout()) .thenAnswer((_) async => Future.value()); - when(() => mockDashboardManagerNotifier.saveSelectedNetwork(any(), any())) + when(() => mockSessionNotifier.saveSelectedNetwork(any(), any())) .thenAnswer((_) async => Future.value()); }); @@ -59,8 +58,7 @@ void main() { authServiceProvider.overrideWithValue(mockAuthService), pollingProvider.overrideWith(() => mockPollingNotifier), deviceManagerProvider.overrideWith(() => mockDeviceManagerNotifier), - dashboardManagerProvider - .overrideWith(() => mockDashboardManagerNotifier), + sessionProvider.overrideWith(() => mockSessionNotifier), raSessionProvider.overrideWith(() => mockRaSessionNotifier), ], ); @@ -308,7 +306,7 @@ void main() { }); }); - // Note: raLogin() tests require complex provider mocking with DashboardManager + // Note: raLogin() tests require complex provider mocking with SessionProvider // These are better covered by integration tests // Note: logout() tests are complex due to provider dependencies diff --git a/test/test_data/dashboard_manager_test_state.dart b/test/test_data/dashboard_manager_test_state.dart index 44b34272d..773d44dbb 100644 --- a/test/test_data/dashboard_manager_test_state.dart +++ b/test/test_data/dashboard_manager_test_state.dart @@ -1,4 +1,4 @@ -const dashboardManagerChrry7TestState = { +const sessionProviderCherry7TestState = { "deviceInfo": { "modelNumber": "LN16", "firmwareVersion": "1.0.4.216421", @@ -347,7 +347,7 @@ const dashboardManagerChrry7TestState = { "memoryLoad": "0.79" }; -const dashboardManagerPinnacleTestState = { +const sessionProviderPinnacleTestState = { "deviceInfo": { "modelNumber": "SPNM60", "firmwareVersion": "1.0.4.216421", @@ -696,7 +696,7 @@ const dashboardManagerPinnacleTestState = { "memoryLoad": "0.79" }; -const dashboardManagerTestState = { +const sessionProviderTestState = { "deviceInfo": { "modelNumber": "MBE70", "firmwareVersion": "1.0.12.216226", diff --git a/tools/run_screenshot_tests.dart b/tools/run_screenshot_tests.dart index 92bd55bbe..43d6d3fe1 100644 --- a/tools/run_screenshot_tests.dart +++ b/tools/run_screenshot_tests.dart @@ -35,7 +35,12 @@ void main(List arguments) async { abbr: 'o', help: 'Path to an output file to write logs to instead of stdout.') ..addOption('filter', - abbr: 'f', help: 'Filter test paths by key (e.g., "pnp").'); + abbr: 'f', help: 'Filter test paths by key (e.g., "pnp").') + ..addFlag('collect', + abbr: 'p', + help: + 'Collect all golden files to snapshots folder after tests complete.', + defaultsTo: false); ArgResults argResults = parser.parse(arguments); @@ -207,6 +212,11 @@ void main(List arguments) async { await _runTests(selectedFiles, selectedLanguages, selectedResolutions, projectRoot, outputSink); + + // Collect golden files to snapshots folder if requested + if (argResults['collect'] as bool) { + await _copyGoldensToSnapshots(projectRoot, outputSink); + } } catch (e) { _log('An error occurred: $e', outputSink); exit(1); @@ -548,3 +558,33 @@ Future _runTests(List files, List languages, _log('\nAll tests passed successfully!', outputSink); } } + +/// Copies all golden files from test directories to the snapshots folder. +Future _copyGoldensToSnapshots( + String projectRoot, IOSink? outputSink) async { + _log('\n--- Copying goldens to snapshots folder ---', outputSink, true); + + final snapshotsDir = Directory(p.join(projectRoot, 'snapshots')); + if (!await snapshotsDir.exists()) { + await snapshotsDir.create(); + } + + int fileCount = 0; + final testDir = Directory(p.join(projectRoot, 'test', 'page')); + await for (final entity + in testDir.list(recursive: true, followLinks: false)) { + if (entity is Directory && p.basename(entity.path) == 'goldens') { + _log('Copying from: ${p.relative(entity.path, from: projectRoot)}', + outputSink); + await for (final file in entity.list()) { + if (file is File) { + final destPath = p.join(snapshotsDir.path, p.basename(file.path)); + await file.copy(destPath); + fileCount++; + } + } + } + } + + _log('Copied $fileCount golden files to snapshots folder.', outputSink, true); +}