Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions NetMonitor-iOS/Platform/BackgroundTaskService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 6 additions & 2 deletions NetMonitor-iOS/Platform/PublicIPService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion NetMonitor-iOS/Platform/RateAppService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
5 changes: 3 additions & 2 deletions NetMonitor-iOS/Platform/WiFiInfoService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions NetMonitor-iOS/ViewModels/DeviceDetailViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand All @@ -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 }
}
Expand Down
4 changes: 2 additions & 2 deletions NetMonitor-iOS/Views/Components/StatusBadge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion NetMonitor-iOS/Views/Tools/WebBrowserToolView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")!
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL(string: "https://example.com")! reintroduces force_unwrapping and will likely keep SwiftLint failing. Prefer a non-force-unwrap fallback URL (e.g., nil-coalescing to a prevalidated static URL or URL(fileURLWithPath:)) so the handler stays lint-clean.

Suggested change
let fallbackURL = request.url ?? URL(string: "https://example.com")!
let fallbackURL = request.url ?? URL(fileURLWithPath: "/")

Copilot uses AI. Check for mistakes.
let response = HTTPURLResponse(
url: request.url!,
url: fallbackURL,
statusCode: 404,
httpVersion: nil,
headerFields: nil
)!
return (response, Data())
)
return (response ?? HTTPURLResponse(), Data())
Comment on lines 142 to +146
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Falling back to HTTPURLResponse() when the failable HTTPURLResponse(url:statusCode:httpVersion:headerFields:) initializer returns nil can yield a response without a URL/statusCode, which may cause confusing downstream failures. Consider handling the nil case explicitly (e.g., throw from the handler / record a test issue) and construct a deterministic HTTPURLResponse for the 404 path.

Copilot uses AI. Check for mistakes.
}
}

Expand All @@ -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(
Comment on lines 169 to 171
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL(string: "https://example.com")! in the global stub helper still violates force_unwrapping. Use a non-force-unwrap fallback URL so this file remains SwiftLint-clean.

Copilot uses AI. Check for mistakes.
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))
Comment on lines 171 to 177
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ) ?? HTTPURLResponse() hides failure to create the intended HTTP response (and produces an essentially empty response). It would be better to handle the nil response creation explicitly (e.g., throw/record an issue) so tests fail with a clear cause.

Copilot uses AI. Check for mistakes.
}
}
Expand All @@ -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(
Comment on lines 184 to 188
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL(string: "https://example.com")! in stubRoutes still violates force_unwrapping. Replace with a non-force-unwrap fallback URL to avoid reintroducing SwiftLint failures.

Copilot uses AI. Check for mistakes.
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))
Comment on lines 188 to 194
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in stub(json:), ) ?? HTTPURLResponse() can mask failures constructing the response and returns a mostly-empty HTTPURLResponse. Handle the nil case explicitly (throw/record) to keep failures deterministic and easier to diagnose.

Copilot uses AI. Check for mistakes.
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand Down
Loading