diff --git a/NetMonitor-iOS/Platform/BackgroundTaskService.swift b/NetMonitor-iOS/Platform/BackgroundTaskService.swift index bc6cb72..97fe160 100644 --- a/NetMonitor-iOS/Platform/BackgroundTaskService.swift +++ b/NetMonitor-iOS/Platform/BackgroundTaskService.swift @@ -25,22 +25,34 @@ final class BackgroundTaskService { Self.logger.info("Registering background tasks...") BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.refreshTaskIdentifier, using: .main) { task in + guard let refreshTask = task as? BGAppRefreshTask else { + task.setTaskCompleted(success: false) + return + } Task { @MainActor in - await self.handleRefreshTask(task as! BGAppRefreshTask) + await self.handleRefreshTask(refreshTask) } } Self.logger.debug("✅ Registered: \(Self.refreshTaskIdentifier)") BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.syncTaskIdentifier, using: .main) { task in + guard let syncTask = task as? BGProcessingTask else { + task.setTaskCompleted(success: false) + return + } Task { @MainActor in - await self.handleSyncTask(task as! BGProcessingTask) + await self.handleSyncTask(syncTask) } } Self.logger.debug("✅ Registered: \(Self.syncTaskIdentifier)") BGTaskScheduler.shared.register(forTaskWithIdentifier: Self.scheduledNetworkScanTaskIdentifier, using: .main) { task in + guard let scanTask = task as? BGProcessingTask else { + task.setTaskCompleted(success: false) + return + } Task { @MainActor in - await self.handleScheduledNetworkScanTask(task as! BGProcessingTask) + await self.handleScheduledNetworkScanTask(scanTask) } } Self.logger.debug("✅ Registered: \(Self.scheduledNetworkScanTaskIdentifier)") @@ -375,7 +387,7 @@ final class BackgroundTaskService { } // Wait for first result; group.next() returns (Bool?)? because element type is Bool? - let nextResult: Bool? = await (group.next()) ?? nil + let nextResult: Bool? = (await group.next()).flatMap { $0 } let result = nextResult ?? false group.cancelAll() connection.cancel() diff --git a/NetMonitor-iOS/Platform/PublicIPService.swift b/NetMonitor-iOS/Platform/PublicIPService.swift index d401f41..120cc77 100644 --- a/NetMonitor-iOS/Platform/PublicIPService.swift +++ b/NetMonitor-iOS/Platform/PublicIPService.swift @@ -48,7 +48,9 @@ final class PublicIPService: PublicIPServiceProtocol { private func fetchFromIPAPI() async throws -> ISPInfo { // Step 1: Get guaranteed IPv4 address from ipify (IPv4-only service) - let ipv4URL = URL(string: "https://api.ipify.org")! + guard let ipv4URL = URL(string: "https://api.ipify.org") else { + throw PublicIPError.invalidResponse + } let (ipData, ipResponse) = try await session.data(from: ipv4URL) guard let ipHTTP = ipResponse as? HTTPURLResponse, ipHTTP.statusCode == 200, let ipv4 = String(data: ipData, encoding: .utf8)? @@ -58,7 +60,9 @@ final class PublicIPService: PublicIPServiceProtocol { } // Step 2: Look up ISP details for that IPv4 address - let url = URL(string: "https://ipapi.co/\(ipv4)/json/")! + guard let url = URL(string: "https://ipapi.co/\(ipv4)/json/") else { + throw PublicIPError.invalidResponse + } let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, diff --git a/NetMonitor-iOS/Platform/RateAppService.swift b/NetMonitor-iOS/Platform/RateAppService.swift index 5eb01b7..1ba68f2 100644 --- a/NetMonitor-iOS/Platform/RateAppService.swift +++ b/NetMonitor-iOS/Platform/RateAppService.swift @@ -8,7 +8,7 @@ enum RateAppService { // MARK: - App Store ID - /// TODO: Update with real App Store ID once live + /// Replace with the real App Store ID before shipping. static let appStoreID: String = "APP_STORE_ID_PLACEHOLDER" private static let reviewURL = "itms-apps://itunes.apple.com/app/id\(appStoreID)?action=write-review" private static let ratingsURL = "itms-apps://itunes.apple.com/app/id\(appStoreID)" diff --git a/NetMonitor-iOS/Platform/WiFiInfoService.swift b/NetMonitor-iOS/Platform/WiFiInfoService.swift index 40cfc33..d15b39b 100644 --- a/NetMonitor-iOS/Platform/WiFiInfoService.swift +++ b/NetMonitor-iOS/Platform/WiFiInfoService.swift @@ -129,8 +129,9 @@ final class WiFiInfoService: NSObject, WiFiInfoServiceProtocol { if strength > 0 { let clamped = max(0.0, min(1.0, strength)) - signalStrengthPercent = Int(clamped * 100) - signalDBm = Self.percentToApproxDBm(signalStrengthPercent!) + let percent = Int(clamped * 100) + signalStrengthPercent = percent + signalDBm = Self.percentToApproxDBm(percent) } else { // signalStrength is 0.0 - could be very weak or iOS not reporting // Return nil to indicate unavailable, rather than -100 dBm diff --git a/NetMonitor-iOS/ViewModels/DeviceDetailViewModel.swift b/NetMonitor-iOS/ViewModels/DeviceDetailViewModel.swift index 978cf63..68e7b9b 100644 --- a/NetMonitor-iOS/ViewModels/DeviceDetailViewModel.swift +++ b/NetMonitor-iOS/ViewModels/DeviceDetailViewModel.swift @@ -76,7 +76,7 @@ final class DeviceDetailViewModel { } let result = await group.next() group.cancelAll() - return result ?? nil + return result.flatMap { $0 } } if !Task.isCancelled { device.manufacturer = manufacturer } } @@ -95,7 +95,7 @@ final class DeviceDetailViewModel { } let result = await group.next() group.cancelAll() - return result ?? nil + return result.flatMap { $0 } } if !Task.isCancelled { device.resolvedHostname = hostname } } diff --git a/NetMonitor-iOS/Views/Components/StatusBadge.swift b/NetMonitor-iOS/Views/Components/StatusBadge.swift index 46c8300..78145ac 100644 --- a/NetMonitor-iOS/Views/Components/StatusBadge.swift +++ b/NetMonitor-iOS/Views/Components/StatusBadge.swift @@ -2,7 +2,7 @@ import SwiftUI import NetMonitorCore // MARK: - Status Badge -/// A pill-shaped badge showing connection/device status +// A pill-shaped badge showing connection/device status struct StatusBadge: View { let status: StatusType var showLabel: Bool = true @@ -65,7 +65,7 @@ struct StatusBadge: View { } // MARK: - Status Dot -/// A simple status indicator dot without label +// A simple status indicator dot without label struct StatusDot: View { let status: StatusType var size: CGFloat = 10 diff --git a/NetMonitor-iOS/Views/Tools/WebBrowserToolView.swift b/NetMonitor-iOS/Views/Tools/WebBrowserToolView.swift index 9f0e649..6385c1f 100644 --- a/NetMonitor-iOS/Views/Tools/WebBrowserToolView.swift +++ b/NetMonitor-iOS/Views/Tools/WebBrowserToolView.swift @@ -15,7 +15,12 @@ struct WebBrowserToolView: View { BookmarkItem(name: "Speed Test", url: "https://speed.cloudflare.com", icon: "speedometer", description: "Cloudflare speed test"), BookmarkItem(name: "DNS Checker", url: "https://dns.google", icon: "globe", description: "Google DNS tools"), BookmarkItem(name: "What's My IP", url: "https://whatismyipaddress.com", icon: "location", description: "Check public IP"), - BookmarkItem(name: "Port Checker", url: "https://www.yougetsignal.com/tools/open-ports/", icon: "door.left.hand.open", description: "Test port connectivity"), + BookmarkItem( + name: "Port Checker", + url: "https://www.yougetsignal.com/tools/open-ports/", + icon: "door.left.hand.open", + description: "Test port connectivity" + ), BookmarkItem(name: "Ping Test", url: "https://tools.pingdom.com", icon: "arrow.up.arrow.down", description: "Online ping tools") ] } diff --git a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/CompanionMessageEdgeCaseTests.swift b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/CompanionMessageEdgeCaseTests.swift index 17fc0fb..46277a6 100644 --- a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/CompanionMessageEdgeCaseTests.swift +++ b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/CompanionMessageEdgeCaseTests.swift @@ -68,7 +68,8 @@ struct CompanionMessageEdgeCaseTests { return } #expect(l.onlineTargets == 999_999) - #expect(abs(l.averageLatency! - 99999.99) < 0.01) + guard let latency = l.averageLatency else { Issue.record("Expected non-nil averageLatency"); return } + #expect(abs(latency - 99999.99) < 0.01) } // MARK: - Empty String and Empty Dictionary Payloads @@ -91,8 +92,8 @@ struct CompanionMessageEdgeCaseTests { guard case .error(let e) = errDecoded else { Issue.record("Expected .error") return } - #expect(e.code == "") - #expect(e.message == "") + #expect(e.code.isEmpty) + #expect(e.message.isEmpty) // toolResult with empty result let trPayload = ToolResultPayload(tool: "ping", success: true, result: "", timestamp: fixedDate) @@ -101,7 +102,7 @@ struct CompanionMessageEdgeCaseTests { guard case .toolResult(let t) = trDecoded else { Issue.record("Expected .toolResult") return } - #expect(t.result == "") + #expect(t.result.isEmpty) } // MARK: - All-Nil Optional Fields diff --git a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/Helpers/MockURLProtocol.swift b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/Helpers/MockURLProtocol.swift index 016c46d..b1b92d6 100644 --- a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/Helpers/MockURLProtocol.swift +++ b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/Helpers/MockURLProtocol.swift @@ -124,22 +124,26 @@ final class MockURLProtocol: URLProtocol, @unchecked Sendable { let path = request.url?.absoluteString ?? "" for (key, value) in responses { if path.contains(key) { + guard let requestURL = request.url else { continue } let response = HTTPURLResponse( - url: request.url!, + url: requestURL, statusCode: value.0, httpVersion: nil, headerFields: ["Content-Type": "application/json"] - )! - return (response, value.1) + ) + if let response { + return (response, value.1) + } } } + let fallbackURL = request.url ?? URL(string: "https://example.com")! let response = HTTPURLResponse( - url: request.url!, + url: fallbackURL, statusCode: 404, httpVersion: nil, headerFields: nil - )! - return (response, Data()) + ) + return (response ?? HTTPURLResponse(), Data()) } } @@ -163,12 +167,13 @@ final class MockURLProtocol: URLProtocol, @unchecked Sendable { /// Sets the global `requestHandler` — use inside `.serialized` suites only. static func stub(json: String, statusCode: Int = 200) { requestHandler = { request in + let url = request.url ?? URL(string: "https://example.com")! let response = HTTPURLResponse( - url: request.url ?? URL(string: "https://example.com")!, + url: url, statusCode: statusCode, httpVersion: nil, headerFields: ["Content-Type": "application/json"] - )! + ) ?? HTTPURLResponse() return (response, Data(json.utf8)) } } @@ -179,12 +184,13 @@ final class MockURLProtocol: URLProtocol, @unchecked Sendable { requestHandler = { request in let path = request.url?.absoluteString ?? "" let json = routes.first(where: { path.contains($0.key) })?.value ?? "{}" + let url = request.url ?? URL(string: "https://example.com")! let response = HTTPURLResponse( - url: request.url ?? URL(string: "https://example.com")!, + url: url, statusCode: statusCode, httpVersion: nil, headerFields: ["Content-Type": "application/json"] - )! + ) ?? HTTPURLResponse() return (response, Data(json.utf8)) } } diff --git a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/PingServiceTests.swift b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/PingServiceTests.swift index 936d871..aeb1cd4 100644 --- a/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/PingServiceTests.swift +++ b/Packages/NetMonitorCore/Tests/NetMonitorCoreTests/PingServiceTests.swift @@ -43,8 +43,7 @@ struct PingServiceTests { makeResult(sequence: 3, time: 0, isTimeout: true), ] let stats = await service.calculateStatistics(results, requestedCount: 3) - #expect(stats != nil) - let s = stats! + guard let s = stats else { Issue.record("Expected non-nil stats"); return } #expect(s.received == 0) #expect(s.transmitted == 3) #expect(s.packetLoss == 100.0) @@ -60,8 +59,7 @@ struct PingServiceTests { let service = PingService() let results = [makeResult(sequence: 1, time: 42.0)] let stats = await service.calculateStatistics(results, requestedCount: 1) - #expect(stats != nil) - let s = stats! + guard let s = stats else { Issue.record("Expected non-nil stats"); return } #expect(s.received == 1) #expect(s.transmitted == 1) #expect(s.packetLoss == 0.0) @@ -83,8 +81,7 @@ struct PingServiceTests { makeResult(sequence: 4, time: 0, isTimeout: true), ] let stats = await service.calculateStatistics(results, requestedCount: 4) - #expect(stats != nil) - let s = stats! + guard let s = stats else { Issue.record("Expected non-nil stats"); return } #expect(s.transmitted == 4) #expect(s.received == 2) #expect(s.packetLoss == 50.0) @@ -104,8 +101,7 @@ struct PingServiceTests { makeResult(sequence: 2, time: 20.0), ] let stats = await service.calculateStatistics(results, requestedCount: 4) - #expect(stats != nil) - let s = stats! + guard let s = stats else { Issue.record("Expected non-nil stats"); return } #expect(s.transmitted == 4) #expect(s.received == 2) #expect(s.packetLoss == 50.0) @@ -118,8 +114,8 @@ struct PingServiceTests { let service = PingService() let results = [makeResult(sequence: 1, time: 100.0)] let stats = await service.calculateStatistics(results, requestedCount: 1) - #expect(stats != nil) - #expect(stats!.stdDev == 0.0) + guard let stats else { Issue.record("Expected non-nil stats"); return } + #expect(stats.stdDev == 0.0) } @Test("stdDev is computed correctly for multiple results") @@ -132,10 +128,10 @@ struct PingServiceTests { makeResult(sequence: 3, time: 30.0), ] let stats = await service.calculateStatistics(results, requestedCount: 3) - #expect(stats != nil) - let s = stats! + guard let s = stats else { Issue.record("Expected non-nil stats"); return } let expectedStdDev = Foundation.sqrt(200.0 / 3.0) - #expect(abs(s.stdDev! - expectedStdDev) < 0.001) + guard let stdDev = s.stdDev else { Issue.record("Expected non-nil stdDev"); return } + #expect(abs(stdDev - expectedStdDev) < 0.001) } @Test("stdDev is zero when all times are identical") @@ -147,8 +143,8 @@ struct PingServiceTests { makeResult(sequence: 3, time: 5.0), ] let stats = await service.calculateStatistics(results, requestedCount: 3) - #expect(stats != nil) - #expect(stats!.stdDev == 0.0) + guard let stats else { Issue.record("Expected non-nil stats"); return } + #expect(stats.stdDev == 0.0) } // MARK: - Host name propagation