This commit is contained in:
B. Ruschill 2026-04-20 09:13:17 +00:00 committed by GitHub
commit 61dca302d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 448 additions and 15 deletions

View file

@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
0516B3FE2F68892000D0FC40 /* NodeFilterParametersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */; };
102B5EAB2E172F41003D191E /* DatadogCore in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAA2E172F41003D191E /* DatadogCore */; };
102B5EAD2E172F41003D191E /* DatadogCrashReporting in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAC2E172F41003D191E /* DatadogCrashReporting */; };
102B5EAF2E172F41003D191E /* DatadogLogs in Frameworks */ = {isa = PBXBuildFile; productRef = 102B5EAE2E172F41003D191E /* DatadogLogs */; };
@ -347,6 +348,7 @@
/* Begin PBXFileReference section */
01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = "<group>"; };
05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParametersTests.swift; sourceTree = "<group>"; };
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = "<group>"; };
09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = "<group>"; };
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
@ -904,6 +906,7 @@
children = (
AA00010022E2730EC0060000 /* ConnectViewTests.swift */,
25F5D5D02C4375DF008036E3 /* RouterTests.swift */,
05DCA1AE2F646B3B00D0724C /* NodeFilterParametersTests.swift */,
);
path = MeshtasticTests;
sourceTree = "<group>";
@ -1662,6 +1665,7 @@
files = (
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */,
25F5D5D12C4375DF008036E3 /* RouterTests.swift in Sources */,
0516B3FE2F68892000D0FC40 /* NodeFilterParametersTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -9,22 +9,27 @@ import SwiftUI
@MainActor
final class NodeFilterParameters: ObservableObject {
// Public variables
@Published var searchText = ""
@Published var isOnline = false
@Published var isPkiEncrypted = false
@Published var isFavorite = false
@Published var isIgnored = false
@Published var isEnvironment = false
@Published var distanceFilter = false
@Published var maxDistance: Double = 800_000
@Published var hopsAway: Double = -1.0
@Published var roleFilter = false
@Published var deviceRoles: Set<Int> = []
@AppStorage("nodeFilter.searchText") var searchText = ""
@AppStorage("nodeFilter.isOnline") var isOnline = false
@AppStorage("nodeFilter.isPkiEncrypted") var isPkiEncrypted = false
@AppStorage("nodeFilter.isFavorite") var isFavorite = false
@AppStorage("nodeFilter.isIgnored") var isIgnored = false
@AppStorage("nodeFilter.isEnvironment") var isEnvironment = false
@AppStorage("nodeFilter.distanceFilter") var distanceFilter = false
@AppStorage("nodeFilter.maxDistance") var maxDistance: Double = 800_000
@AppStorage("nodeFilter.hopsAway") var hopsAway: Double = -1.0
@AppStorage("nodeFilter.roleFilter") var roleFilter = false
// Private backing vars
@Published private var _viaLora = true
@Published private var _viaMqtt = true
// deviceRoles requires custom storage since Set<Int> isn't directly supported by @AppStorage
@Published var deviceRoles: Set<Int> = [] {
didSet {
let array = Array(deviceRoles)
UserDefaults.standard.set(array, forKey: "nodeFilter.deviceRoles")
}
}
@AppStorage("nodeFilter.viaLora") private var _viaLora = true
@AppStorage("nodeFilter.viaMqtt") private var _viaMqtt = true
// Public computed wrappers with enforcement
var viaLora: Bool {
@ -48,4 +53,11 @@ final class NodeFilterParameters: ObservableObject {
}
}
}
// Initialize and load the deviceRoles from UserDefaults
init() {
if let storedRoles = UserDefaults.standard.array(forKey: "nodeFilter.deviceRoles") as? [Int] {
self.deviceRoles = Set(storedRoles)
}
}
}

View file

@ -0,0 +1,417 @@
//
// NodeFilterParametersTests.swift
// Meshtastic
//
// Created on 3/16/26.
//
import Foundation
import XCTest
@testable import Meshtastic
@MainActor
class NodeFilterParametersTests: XCTestCase {
// MARK: - Initialization Tests
func testDefaultInitialization() async throws {
// Clean up UserDefaults before test
clearAllFilterDefaults()
let filters = NodeFilterParameters()
XCTAssertEqual(filters.searchText, "")
XCTAssertEqual(filters.isOnline, false)
XCTAssertEqual(filters.isPkiEncrypted, false)
XCTAssertEqual(filters.isFavorite, false)
XCTAssertEqual(filters.isIgnored, false)
XCTAssertEqual(filters.isEnvironment, false)
XCTAssertEqual(filters.distanceFilter, false)
XCTAssertEqual(filters.maxDistance, 800_000)
XCTAssertEqual(filters.hopsAway, -1.0)
XCTAssertEqual(filters.roleFilter, false)
XCTAssertTrue(filters.deviceRoles.isEmpty)
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, true)
}
func testInitializationWithPersistedDeviceRoles() async throws {
clearAllFilterDefaults()
// Store device roles in UserDefaults
let expectedRoles = [1, 2, 3, 5, 8]
UserDefaults.standard.set(expectedRoles, forKey: "nodeFilter.deviceRoles")
let filters = NodeFilterParameters()
XCTAssertEqual(filters.deviceRoles, Set(expectedRoles))
}
// MARK: - @AppStorage Persistence Tests
func testSearchTextPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.searchText = "Test Node"
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.searchText, "Test Node")
}
func testBooleanFiltersPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.isOnline = true
filters1.isPkiEncrypted = true
filters1.isFavorite = true
filters1.isIgnored = true
filters1.isEnvironment = true
filters1.distanceFilter = true
filters1.roleFilter = true
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.isOnline, true)
XCTAssertEqual(filters2.isPkiEncrypted, true)
XCTAssertEqual(filters2.isFavorite, true)
XCTAssertEqual(filters2.isIgnored, true)
XCTAssertEqual(filters2.isEnvironment, true)
XCTAssertEqual(filters2.distanceFilter, true)
XCTAssertEqual(filters2.roleFilter, true)
}
func testNumericFiltersPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.maxDistance = 500_000
filters1.hopsAway = 3.0
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.maxDistance, 500_000)
XCTAssertEqual(filters2.hopsAway, 3.0)
}
// MARK: - Device Roles Tests
func testDeviceRolesPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.deviceRoles = [1, 3, 5, 7]
// Verify it's stored in UserDefaults
let storedRoles = UserDefaults.standard.array(forKey: "nodeFilter.deviceRoles") as? [Int]
XCTAssertNotNil(storedRoles)
XCTAssertEqual(Set(storedRoles!), Set([1, 3, 5, 7]))
// Verify it persists to new instance
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.deviceRoles, Set([1, 3, 5, 7]))
}
func testAddingDeviceRoles() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.deviceRoles.insert(2)
filters.deviceRoles.insert(4)
filters.deviceRoles.insert(6)
let newFilters = NodeFilterParameters()
XCTAssertTrue(newFilters.deviceRoles.contains(2))
XCTAssertTrue(newFilters.deviceRoles.contains(4))
XCTAssertTrue(newFilters.deviceRoles.contains(6))
XCTAssertEqual(newFilters.deviceRoles.count, 3)
}
func testRemovingDeviceRoles() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.deviceRoles = [1, 2, 3, 4, 5]
filters1.deviceRoles.remove(2)
filters1.deviceRoles.remove(4)
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.deviceRoles, Set([1, 3, 5]))
XCTAssertFalse(filters2.deviceRoles.contains(2))
XCTAssertFalse(filters2.deviceRoles.contains(4))
}
func testEmptyDeviceRolesPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.deviceRoles = [1, 2, 3]
// Clear the set
filters1.deviceRoles = []
let filters2 = NodeFilterParameters()
XCTAssertTrue(filters2.deviceRoles.isEmpty)
}
// MARK: - Via Lora/MQTT Enforcement Tests
func testViaLoraEnforcesViaMqtt() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
// Start with both true
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, true)
// Set viaLora to false
filters.viaLora = false
// viaMqtt should remain true
XCTAssertEqual(filters.viaLora, false)
XCTAssertEqual(filters.viaMqtt, true)
// Try to set viaMqtt to false - it should enforce viaLora to true
filters.viaMqtt = false
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, false)
}
func testViaMqttEnforcesViaLora() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
// Start with both true
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, true)
// Set viaMqtt to false
filters.viaMqtt = false
// viaLora should remain true
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, false)
// Try to set viaLora to false - it should enforce viaMqtt to true
filters.viaLora = false
XCTAssertEqual(filters.viaLora, false)
XCTAssertEqual(filters.viaMqtt, true)
}
func testBothViaTrue() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.viaLora = true
filters.viaMqtt = true
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, true)
}
func testViaSettingsPersistence() async throws {
clearAllFilterDefaults()
let filters1 = NodeFilterParameters()
filters1.viaLora = false
// viaMqtt should be enforced to true
let filters2 = NodeFilterParameters()
XCTAssertEqual(filters2.viaLora, false)
XCTAssertEqual(filters2.viaMqtt, true)
}
func testCannotHaveBothViaFalse() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
// Set viaLora to false first
filters.viaLora = false
XCTAssertEqual(filters.viaLora, false)
XCTAssertEqual(filters.viaMqtt, true)
// Try to set viaMqtt to false
filters.viaMqtt = false
// viaLora should be enforced back to true
XCTAssertEqual(filters.viaLora, true)
XCTAssertEqual(filters.viaMqtt, false)
}
// MARK: - ObservableObject Tests
func testObjectWillChange() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
var changeCount = 0
let cancellable = filters.objectWillChange.sink {
changeCount += 1
}
// Modify various properties
filters.searchText = "Test"
filters.isOnline = true
filters.deviceRoles.insert(1)
filters.viaLora = false
// Should have triggered changes
XCTAssertGreaterThan(changeCount, 0)
cancellable.cancel()
}
// MARK: - Edge Cases
func testLargeMaxDistance() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.maxDistance = 10_000_000
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.maxDistance, 10_000_000)
}
func testNegativeHopsAway() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.hopsAway = -1.0
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.hopsAway, -1.0)
}
func testZeroHopsAway() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.hopsAway = 0.0
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.hopsAway, 0.0)
}
func testSpecialCharactersInSearchText() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.searchText = "Test!@#$%^&*()_+-=[]{}|;':\",./<>?"
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.searchText, "Test!@#$%^&*()_+-=[]{}|;':\",./<>?")
}
func testUnicodeInSearchText() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.searchText = "测试 Тест 🎉🚀"
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.searchText, "测试 Тест 🎉🚀")
}
func testLongSearchText() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
let longText = String(repeating: "A", count: 1000)
filters.searchText = longText
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.searchText, longText)
}
func testLargeDeviceRolesSet() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
filters.deviceRoles = Set(0...100)
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.deviceRoles, Set(0...100))
XCTAssertEqual(newFilters.deviceRoles.count, 101)
}
// MARK: - Reset/Clear Tests
func testResetAllFilters() async throws {
clearAllFilterDefaults()
let filters = NodeFilterParameters()
// Set all values to non-default
filters.searchText = "Test"
filters.isOnline = true
filters.isPkiEncrypted = true
filters.isFavorite = true
filters.isIgnored = true
filters.isEnvironment = true
filters.distanceFilter = true
filters.maxDistance = 500_000
filters.hopsAway = 5.0
filters.roleFilter = true
filters.deviceRoles = [1, 2, 3]
filters.viaLora = false
// Reset to defaults
filters.searchText = ""
filters.isOnline = false
filters.isPkiEncrypted = false
filters.isFavorite = false
filters.isIgnored = false
filters.isEnvironment = false
filters.distanceFilter = false
filters.maxDistance = 800_000
filters.hopsAway = -1.0
filters.roleFilter = false
filters.deviceRoles = []
filters.viaLora = true
filters.viaMqtt = true
// Verify all are back to defaults
let newFilters = NodeFilterParameters()
XCTAssertEqual(newFilters.searchText, "")
XCTAssertEqual(newFilters.isOnline, false)
XCTAssertEqual(newFilters.isPkiEncrypted, false)
XCTAssertEqual(newFilters.isFavorite, false)
XCTAssertEqual(newFilters.isIgnored, false)
XCTAssertEqual(newFilters.isEnvironment, false)
XCTAssertEqual(newFilters.distanceFilter, false)
XCTAssertEqual(newFilters.maxDistance, 800_000)
XCTAssertEqual(newFilters.hopsAway, -1.0)
XCTAssertEqual(newFilters.roleFilter, false)
XCTAssertTrue(newFilters.deviceRoles.isEmpty)
XCTAssertEqual(newFilters.viaLora, true)
XCTAssertEqual(newFilters.viaMqtt, true)
}
// MARK: - Helper Functions
/// Clears all NodeFilter-related UserDefaults to ensure clean test state
private func clearAllFilterDefaults() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: "nodeFilter.searchText")
defaults.removeObject(forKey: "nodeFilter.isOnline")
defaults.removeObject(forKey: "nodeFilter.isPkiEncrypted")
defaults.removeObject(forKey: "nodeFilter.isFavorite")
defaults.removeObject(forKey: "nodeFilter.isIgnored")
defaults.removeObject(forKey: "nodeFilter.isEnvironment")
defaults.removeObject(forKey: "nodeFilter.distanceFilter")
defaults.removeObject(forKey: "nodeFilter.maxDistance")
defaults.removeObject(forKey: "nodeFilter.hopsAway")
defaults.removeObject(forKey: "nodeFilter.roleFilter")
defaults.removeObject(forKey: "nodeFilter.deviceRoles")
defaults.removeObject(forKey: "nodeFilter.viaLora")
defaults.removeObject(forKey: "nodeFilter.viaMqtt")
}
}