@capgo/capacitor-network-diagnostics 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CapgoCapacitorNetworkDiagnostics.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +467 -0
  5. package/android/build.gradle +59 -0
  6. package/android/src/main/AndroidManifest.xml +4 -0
  7. package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnostics.java +681 -0
  8. package/android/src/main/java/app/capgo/networkdiagnostics/NetworkDiagnosticsPlugin.java +141 -0
  9. package/android/src/main/res/.gitkeep +0 -0
  10. package/dist/docs.json +961 -0
  11. package/dist/esm/definitions.d.ts +276 -0
  12. package/dist/esm/definitions.js +2 -0
  13. package/dist/esm/definitions.js.map +1 -0
  14. package/dist/esm/index.d.ts +4 -0
  15. package/dist/esm/index.js +7 -0
  16. package/dist/esm/index.js.map +1 -0
  17. package/dist/esm/web.d.ts +24 -0
  18. package/dist/esm/web.js +388 -0
  19. package/dist/esm/web.js.map +1 -0
  20. package/dist/plugin.cjs.js +402 -0
  21. package/dist/plugin.cjs.js.map +1 -0
  22. package/dist/plugin.js +405 -0
  23. package/dist/plugin.js.map +1 -0
  24. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Download.swift +71 -0
  25. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+PacketLoss.swift +91 -0
  26. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Run.swift +163 -0
  27. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics+Utils.swift +202 -0
  28. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnostics.swift +151 -0
  29. package/ios/Sources/NetworkDiagnosticsPlugin/NetworkDiagnosticsPlugin.swift +139 -0
  30. package/ios/Tests/NetworkDiagnosticsPluginTests/NetworkDiagnosticsTests.swift +11 -0
  31. package/package.json +92 -0
@@ -0,0 +1,163 @@
1
+ import Foundation
2
+
3
+ extension NetworkDiagnostics {
4
+ public func runDiagnostics(
5
+ urls: [[String: Any]],
6
+ ports: [[String: Any]],
7
+ websockets: [[String: Any]],
8
+ download: [String: Any]?,
9
+ packetLoss: [String: Any]?
10
+ ) async -> [String: Any] {
11
+ let status = await getNetworkStatus()
12
+ var issues = statusIssues(status)
13
+ let urlDiagnostics = await runUrlDiagnostics(urls)
14
+ let portDiagnostics = await runPortDiagnostics(ports)
15
+ let websocketDiagnostics = await runWebSocketDiagnostics(websockets)
16
+ var output: [String: Any] = ["status": status]
17
+
18
+ output["urls"] = urlDiagnostics.results
19
+ output["ports"] = portDiagnostics.results
20
+ output["websockets"] = websocketDiagnostics.results
21
+ issues += urlDiagnostics.issues + portDiagnostics.issues + websocketDiagnostics.issues
22
+
23
+ if let downloadResult = await downloadDiagnostic(download) {
24
+ output["download"] = downloadResult.result
25
+ issues += downloadResult.issues
26
+ }
27
+
28
+ if let packetLossResult = await packetLossDiagnostic(packetLoss) {
29
+ output["packetLoss"] = packetLossResult.result
30
+ issues += packetLossResult.issues
31
+ }
32
+
33
+ output["issues"] = issues
34
+ return output
35
+ }
36
+
37
+ func runUrlDiagnostics(_ urls: [[String: Any]]) async -> DiagnosticGroupResult {
38
+ var results: [[String: Any]] = []
39
+ var issues: [String] = []
40
+ for options in urls {
41
+ let result = await testUrl(
42
+ url: options["url"] as? String ?? "",
43
+ method: options["method"] as? String ?? "HEAD",
44
+ timeoutMs: options["timeoutMs"] as? Int ?? 10_000,
45
+ followRedirects: options["followRedirects"] as? Bool ?? true
46
+ )
47
+ results.append(result)
48
+ appendUrlIssue(result, to: &issues)
49
+ }
50
+ return DiagnosticGroupResult(results: results, issues: issues)
51
+ }
52
+
53
+ func runPortDiagnostics(_ ports: [[String: Any]]) async -> DiagnosticGroupResult {
54
+ var results: [[String: Any]] = []
55
+ var issues: [String] = []
56
+ for options in ports {
57
+ let result = await testPort(
58
+ host: options["host"] as? String ?? "",
59
+ port: options["port"] as? Int ?? 0,
60
+ timeoutMs: options["timeoutMs"] as? Int ?? 5_000
61
+ )
62
+ results.append(result)
63
+ appendPortIssue(result, to: &issues)
64
+ }
65
+ return DiagnosticGroupResult(results: results, issues: issues)
66
+ }
67
+
68
+ func runWebSocketDiagnostics(_ websockets: [[String: Any]]) async -> DiagnosticGroupResult {
69
+ var results: [[String: Any]] = []
70
+ var issues: [String] = []
71
+ for options in websockets {
72
+ let result = await testWebSocket(
73
+ url: options["url"] as? String ?? "",
74
+ timeoutMs: options["timeoutMs"] as? Int ?? 10_000
75
+ )
76
+ results.append(result)
77
+ appendWebSocketIssue(result, to: &issues)
78
+ }
79
+ return DiagnosticGroupResult(results: results, issues: issues)
80
+ }
81
+
82
+ func downloadDiagnostic(_ download: [String: Any]?) async -> OptionalDiagnosticResult? {
83
+ guard let download = download else {
84
+ return nil
85
+ }
86
+
87
+ let result = await testDownloadSpeed(
88
+ url: download["url"] as? String ?? "",
89
+ maxBytes: download["maxBytes"] as? Int ?? 5 * 1024 * 1024,
90
+ timeoutMs: download["timeoutMs"] as? Int ?? 30_000
91
+ )
92
+ var issues: [String] = []
93
+ if !(result["ok"] as? Bool ?? false) {
94
+ issues.append("Download speed test failed: \(result["url"] as? String ?? "")")
95
+ }
96
+ return OptionalDiagnosticResult(result: result, issues: issues)
97
+ }
98
+
99
+ func packetLossDiagnostic(_ packetLoss: [String: Any]?) async -> OptionalDiagnosticResult? {
100
+ guard let packetLoss = packetLoss else {
101
+ return nil
102
+ }
103
+
104
+ let result = await testPacketLoss(packetLossOptions(from: packetLoss))
105
+ var issues: [String] = []
106
+ if let lossPercent = result["lossPercent"] as? Double, lossPercent > 0 {
107
+ issues.append("Packet loss detected: \(lossPercent)% to \(result["target"] as? String ?? "")")
108
+ }
109
+ return OptionalDiagnosticResult(result: result, issues: issues)
110
+ }
111
+
112
+ func packetLossOptions(from options: [String: Any]) -> PacketLossOptions {
113
+ PacketLossOptions(
114
+ mode: options["mode"] as? String ?? "",
115
+ host: options["host"] as? String ?? "",
116
+ port: options["port"] as? Int ?? 0,
117
+ url: options["url"] as? String ?? "",
118
+ count: options["count"] as? Int ?? 10,
119
+ timeoutMs: options["timeoutMs"] as? Int ?? 3_000,
120
+ intervalMs: options["intervalMs"] as? Int ?? 250
121
+ )
122
+ }
123
+
124
+ func statusIssues(_ status: [String: Any]) -> [String] {
125
+ if !(status["connected"] as? Bool ?? false) {
126
+ return ["No active network connection"]
127
+ }
128
+ if !(status["internetReachable"] as? Bool ?? false) {
129
+ return ["Network is connected but internet reachability is not confirmed"]
130
+ }
131
+ return []
132
+ }
133
+
134
+ func appendUrlIssue(_ result: [String: Any], to issues: inout [String]) {
135
+ if !(result["reachable"] as? Bool ?? false) {
136
+ issues.append("URL unreachable: \(result["url"] as? String ?? "")")
137
+ } else if !(result["ok"] as? Bool ?? false) {
138
+ issues.append("URL returned non-success status: \(result["url"] as? String ?? "")")
139
+ }
140
+ }
141
+
142
+ func appendPortIssue(_ result: [String: Any], to issues: inout [String]) {
143
+ if !(result["open"] as? Bool ?? false) {
144
+ issues.append("TCP port blocked or unreachable: \(result["host"] as? String ?? ""):\(result["port"] as? Int ?? 0)")
145
+ }
146
+ }
147
+
148
+ func appendWebSocketIssue(_ result: [String: Any], to issues: inout [String]) {
149
+ if !(result["open"] as? Bool ?? false) {
150
+ issues.append("WebSocket failed: \(result["url"] as? String ?? "")")
151
+ }
152
+ }
153
+ }
154
+
155
+ struct DiagnosticGroupResult {
156
+ let results: [[String: Any]]
157
+ let issues: [String]
158
+ }
159
+
160
+ struct OptionalDiagnosticResult {
161
+ let result: [String: Any]
162
+ let issues: [String]
163
+ }
@@ -0,0 +1,202 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ extension NetworkDiagnostics {
5
+ func currentPath() async -> NWPath {
6
+ await withCheckedContinuation { continuation in
7
+ let monitor = NWPathMonitor()
8
+ let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.path")
9
+ var resumed = false
10
+
11
+ func resume(with path: NWPath) {
12
+ if resumed {
13
+ return
14
+ }
15
+ resumed = true
16
+ monitor.cancel()
17
+ continuation.resume(returning: path)
18
+ }
19
+
20
+ monitor.pathUpdateHandler = { path in
21
+ resume(with: path)
22
+ }
23
+ monitor.start(queue: queue)
24
+ queue.asyncAfter(deadline: .now() + 1) {
25
+ resume(with: monitor.currentPath)
26
+ }
27
+ }
28
+ }
29
+
30
+ func probeTcp(host: String, port: Int, timeoutMs: Int) async -> ProbeResult {
31
+ let started = Date()
32
+ guard let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
33
+ return ProbeResult(success: false, durationMs: elapsedMs(started), errorCode: "INVALID_PORT", errorMessage: "Invalid port")
34
+ }
35
+
36
+ let connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: .tcp)
37
+
38
+ return await withCheckedContinuation { continuation in
39
+ let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.tcp")
40
+ var finished = false
41
+
42
+ func finish(success: Bool, error: Error?) {
43
+ if finished {
44
+ return
45
+ }
46
+ finished = true
47
+ connection.cancel()
48
+ continuation.resume(
49
+ returning: ProbeResult(
50
+ success: success,
51
+ durationMs: elapsedMs(started),
52
+ errorCode: error.map { errorCode($0) },
53
+ errorMessage: error?.localizedDescription
54
+ )
55
+ )
56
+ }
57
+
58
+ connection.stateUpdateHandler = { state in
59
+ switch state {
60
+ case .ready:
61
+ finish(success: true, error: nil)
62
+ case .failed(let error):
63
+ finish(success: false, error: error)
64
+ case .cancelled:
65
+ if !finished {
66
+ finish(success: false, error: NetworkDiagnosticsError.cancelled)
67
+ }
68
+ default:
69
+ break
70
+ }
71
+ }
72
+
73
+ queue.asyncAfter(deadline: .now() + timeout(timeoutMs, fallback: 5)) {
74
+ finish(success: false, error: NetworkDiagnosticsError.timeout)
75
+ }
76
+ connection.start(queue: queue)
77
+ }
78
+ }
79
+
80
+ func probeHttp(url: String, timeoutMs: Int) async -> ProbeResult {
81
+ let result = await testUrl(url: url, method: "HEAD", timeoutMs: timeoutMs, followRedirects: true)
82
+ let reachable = result["reachable"] as? Bool ?? false
83
+ return ProbeResult(
84
+ success: reachable,
85
+ durationMs: result["durationMs"] as? Int ?? 0,
86
+ errorCode: result["errorCode"] as? String,
87
+ errorMessage: result["errorMessage"] as? String
88
+ )
89
+ }
90
+
91
+ func websocketResult(_ base: [String: Any], started: Date, open: Bool, error: Error?) -> [String: Any] {
92
+ var output = base
93
+ output["open"] = open
94
+ output["durationMs"] = elapsedMs(started)
95
+ if let error = error {
96
+ output["errorCode"] = errorCode(error)
97
+ output["errorMessage"] = error.localizedDescription
98
+ }
99
+ return output
100
+ }
101
+
102
+ func connectionType(_ path: NWPath) -> String {
103
+ if path.usesInterfaceType(.wifi) {
104
+ return "wifi"
105
+ }
106
+ if path.usesInterfaceType(.cellular) {
107
+ return "cellular"
108
+ }
109
+ if path.usesInterfaceType(.wiredEthernet) {
110
+ return "ethernet"
111
+ }
112
+ if path.usesInterfaceType(.loopback) || path.usesInterfaceType(.other) {
113
+ return "other"
114
+ }
115
+ return "unknown"
116
+ }
117
+
118
+ func statusLabel(_ status: NWPath.Status) -> String {
119
+ switch status {
120
+ case .satisfied:
121
+ return "satisfied"
122
+ case .unsatisfied:
123
+ return "unsatisfied"
124
+ case .requiresConnection:
125
+ return "requiresConnection"
126
+ @unknown default:
127
+ return "unknown"
128
+ }
129
+ }
130
+
131
+ func normalizeMethod(_ method: String) -> String {
132
+ method.uppercased() == "GET" ? "GET" : "HEAD"
133
+ }
134
+
135
+ func normalizePacketLossMode(mode: String, host: String, port: Int, url: String) -> String {
136
+ if mode.lowercased() == "http" {
137
+ return "http"
138
+ }
139
+ if mode.lowercased() == "tcp" {
140
+ return "tcp"
141
+ }
142
+ if !host.isEmpty && port > 0 {
143
+ return "tcp"
144
+ }
145
+ if !url.isEmpty {
146
+ return "http"
147
+ }
148
+ return "tcp"
149
+ }
150
+
151
+ func positive(_ value: Int, fallback: Int) -> Int {
152
+ value > 0 ? value : fallback
153
+ }
154
+
155
+ func timeout(_ value: Int, fallback: TimeInterval) -> TimeInterval {
156
+ TimeInterval(positive(value, fallback: Int(fallback * 1000))) / 1000.0
157
+ }
158
+
159
+ func elapsedMs(_ started: Date) -> Int {
160
+ max(Int(Date().timeIntervalSince(started) * 1000), 0)
161
+ }
162
+
163
+ func errorCode(_ error: Error) -> String {
164
+ let nsError = error as NSError
165
+ if nsError.domain == NSURLErrorDomain {
166
+ return urlErrorCode(nsError.code)
167
+ }
168
+ if error is NetworkDiagnosticsError {
169
+ return "\(error)".uppercased()
170
+ }
171
+ return String(describing: type(of: error))
172
+ }
173
+
174
+ func urlErrorCode(_ code: Int) -> String {
175
+ switch code {
176
+ case NSURLErrorTimedOut:
177
+ return "TIMEOUT"
178
+ case NSURLErrorCannotFindHost, NSURLErrorDNSLookupFailed:
179
+ return "DNS_ERROR"
180
+ case NSURLErrorCannotConnectToHost, NSURLErrorNetworkConnectionLost:
181
+ return "CONNECTION_FAILED"
182
+ case NSURLErrorSecureConnectionFailed, NSURLErrorServerCertificateUntrusted:
183
+ return "TLS_ERROR"
184
+ default:
185
+ return "URL_ERROR_\(code)"
186
+ }
187
+ }
188
+ }
189
+
190
+ enum NetworkDiagnosticsError: Error, LocalizedError {
191
+ case cancelled
192
+ case timeout
193
+
194
+ var errorDescription: String? {
195
+ switch self {
196
+ case .cancelled:
197
+ return "Connection was cancelled"
198
+ case .timeout:
199
+ return "Connection timed out"
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,151 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ @objc public class NetworkDiagnostics: NSObject {
5
+ @objc public func getPluginVersion() -> String {
6
+ return "native"
7
+ }
8
+
9
+ public func getNetworkStatus() async -> [String: Any] {
10
+ let path = await currentPath()
11
+ let connected = path.status == .satisfied
12
+ var details: [String: Any] = [
13
+ "status": statusLabel(path.status),
14
+ "interfaces": path.availableInterfaces.map { $0.name }.joined(separator: ",")
15
+ ]
16
+
17
+ if let gateway = path.gateways.first {
18
+ details["gateway"] = "\(gateway)"
19
+ }
20
+
21
+ return [
22
+ "connected": connected,
23
+ "connectionType": connected ? connectionType(path) : "none",
24
+ "internetReachable": connected,
25
+ "expensive": path.isExpensive,
26
+ "constrained": path.isConstrained,
27
+ "details": details
28
+ ]
29
+ }
30
+
31
+ public func testUrl(url: String, method: String, timeoutMs: Int, followRedirects: Bool) async -> [String: Any] {
32
+ let started = Date()
33
+ let normalizedMethod = normalizeMethod(method)
34
+ var result: [String: Any] = [
35
+ "url": url,
36
+ "method": normalizedMethod
37
+ ]
38
+
39
+ guard let parsedUrl = URL(string: url) else {
40
+ result["ok"] = false
41
+ result["reachable"] = false
42
+ result["durationMs"] = elapsedMs(started)
43
+ result["errorCode"] = "INVALID_URL"
44
+ result["errorMessage"] = "Invalid URL"
45
+ return result
46
+ }
47
+
48
+ var request = URLRequest(url: parsedUrl)
49
+ request.httpMethod = normalizedMethod
50
+ request.timeoutInterval = timeout(timeoutMs, fallback: 10)
51
+
52
+ let configuration = URLSessionConfiguration.ephemeral
53
+ configuration.timeoutIntervalForRequest = request.timeoutInterval
54
+ configuration.timeoutIntervalForResource = request.timeoutInterval
55
+ let delegate = followRedirects ? nil : NoRedirectDelegate()
56
+ let session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
57
+ defer { session.finishTasksAndInvalidate() }
58
+
59
+ do {
60
+ let (_, response) = try await session.data(for: request)
61
+ let statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
62
+ result["ok"] = statusCode >= 200 && statusCode < 400
63
+ result["reachable"] = true
64
+ result["durationMs"] = elapsedMs(started)
65
+ result["statusCode"] = statusCode
66
+ result["finalUrl"] = response.url?.absoluteString ?? url
67
+ } catch {
68
+ result["ok"] = false
69
+ result["reachable"] = false
70
+ result["durationMs"] = elapsedMs(started)
71
+ result["errorCode"] = errorCode(error)
72
+ result["errorMessage"] = error.localizedDescription
73
+ }
74
+
75
+ return result
76
+ }
77
+
78
+ public func testPort(host: String, port: Int, timeoutMs: Int) async -> [String: Any] {
79
+ let probe = await probeTcp(host: host, port: port, timeoutMs: timeoutMs)
80
+ var result: [String: Any] = [
81
+ "host": host,
82
+ "port": port,
83
+ "open": probe.success,
84
+ "durationMs": probe.durationMs
85
+ ]
86
+
87
+ if !probe.success {
88
+ result["errorCode"] = probe.errorCode
89
+ result["errorMessage"] = probe.errorMessage
90
+ }
91
+
92
+ return result
93
+ }
94
+
95
+ public func testWebSocket(url: String, timeoutMs: Int) async -> [String: Any] {
96
+ let started = Date()
97
+ var result: [String: Any] = ["url": url]
98
+
99
+ guard let parsedUrl = URL(string: url), let scheme = parsedUrl.scheme?.lowercased(), scheme == "ws" || scheme == "wss" else {
100
+ result["open"] = false
101
+ result["durationMs"] = elapsedMs(started)
102
+ result["errorCode"] = "INVALID_URL"
103
+ result["errorMessage"] = "WebSocket URL must use ws:// or wss://"
104
+ return result
105
+ }
106
+
107
+ let configuration = URLSessionConfiguration.ephemeral
108
+ configuration.timeoutIntervalForRequest = timeout(timeoutMs, fallback: 10)
109
+ configuration.timeoutIntervalForResource = timeout(timeoutMs, fallback: 10)
110
+ let session = URLSession(configuration: configuration)
111
+ let task = session.webSocketTask(with: parsedUrl)
112
+ task.resume()
113
+
114
+ return await withCheckedContinuation { continuation in
115
+ let queue = DispatchQueue(label: "app.capgo.networkdiagnostics.websocket")
116
+ var finished = false
117
+
118
+ func finish(open: Bool, error: Error?) {
119
+ if finished {
120
+ return
121
+ }
122
+ finished = true
123
+ task.cancel(with: .goingAway, reason: nil)
124
+ session.invalidateAndCancel()
125
+ continuation.resume(returning: websocketResult(result, started: started, open: open, error: error))
126
+ }
127
+
128
+ queue.asyncAfter(deadline: .now() + timeout(timeoutMs, fallback: 10)) {
129
+ finish(open: false, error: NetworkDiagnosticsError.timeout)
130
+ }
131
+
132
+ task.sendPing { error in
133
+ queue.async {
134
+ finish(open: error == nil, error: error)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ private final class NoRedirectDelegate: NSObject, URLSessionTaskDelegate {
142
+ func urlSession(
143
+ _ session: URLSession,
144
+ task: URLSessionTask,
145
+ willPerformHTTPRedirection response: HTTPURLResponse,
146
+ newRequest request: URLRequest,
147
+ completionHandler: @escaping (URLRequest?) -> Void
148
+ ) {
149
+ completionHandler(nil)
150
+ }
151
+ }
@@ -0,0 +1,139 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc(NetworkDiagnosticsPlugin)
5
+ public class NetworkDiagnosticsPlugin: CAPPlugin, CAPBridgedPlugin {
6
+ public let identifier = "NetworkDiagnosticsPlugin"
7
+ public let jsName = "NetworkDiagnostics"
8
+ public let pluginMethods: [CAPPluginMethod] = [
9
+ CAPPluginMethod(name: "getNetworkStatus", returnType: CAPPluginReturnPromise),
10
+ CAPPluginMethod(name: "testUrl", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "testPort", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "testWebSocket", returnType: CAPPluginReturnPromise),
13
+ CAPPluginMethod(name: "testDownloadSpeed", returnType: CAPPluginReturnPromise),
14
+ CAPPluginMethod(name: "testPacketLoss", returnType: CAPPluginReturnPromise),
15
+ CAPPluginMethod(name: "runDiagnostics", returnType: CAPPluginReturnPromise),
16
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
17
+ ]
18
+
19
+ private let implementation = NetworkDiagnostics()
20
+
21
+ @objc func getNetworkStatus(_ call: CAPPluginCall) {
22
+ Task {
23
+ call.resolve(await implementation.getNetworkStatus())
24
+ }
25
+ }
26
+
27
+ @objc func testUrl(_ call: CAPPluginCall) {
28
+ guard let url = call.getString("url"), !url.isEmpty else {
29
+ call.reject("URL is required")
30
+ return
31
+ }
32
+
33
+ Task {
34
+ call.resolve(await implementation.testUrl(
35
+ url: url,
36
+ method: call.getString("method") ?? "HEAD",
37
+ timeoutMs: call.getInt("timeoutMs") ?? 10_000,
38
+ followRedirects: call.getBool("followRedirects") ?? true
39
+ ))
40
+ }
41
+ }
42
+
43
+ @objc func testPort(_ call: CAPPluginCall) {
44
+ guard let host = call.getString("host"), !host.isEmpty else {
45
+ call.reject("Host is required")
46
+ return
47
+ }
48
+ let port = call.getInt("port") ?? 0
49
+ guard port > 0 && port <= 65_535 else {
50
+ call.reject("Port must be between 1 and 65535")
51
+ return
52
+ }
53
+
54
+ Task {
55
+ call.resolve(await implementation.testPort(
56
+ host: host,
57
+ port: port,
58
+ timeoutMs: call.getInt("timeoutMs") ?? 5_000
59
+ ))
60
+ }
61
+ }
62
+
63
+ @objc func testWebSocket(_ call: CAPPluginCall) {
64
+ guard let url = call.getString("url"), !url.isEmpty else {
65
+ call.reject("URL is required")
66
+ return
67
+ }
68
+
69
+ Task {
70
+ call.resolve(await implementation.testWebSocket(
71
+ url: url,
72
+ timeoutMs: call.getInt("timeoutMs") ?? 10_000
73
+ ))
74
+ }
75
+ }
76
+
77
+ @objc func testDownloadSpeed(_ call: CAPPluginCall) {
78
+ guard let url = call.getString("url"), !url.isEmpty else {
79
+ call.reject("URL is required")
80
+ return
81
+ }
82
+
83
+ Task {
84
+ call.resolve(await implementation.testDownloadSpeed(
85
+ url: url,
86
+ maxBytes: call.getInt("maxBytes") ?? 5 * 1024 * 1024,
87
+ timeoutMs: call.getInt("timeoutMs") ?? 30_000
88
+ ))
89
+ }
90
+ }
91
+
92
+ @objc func testPacketLoss(_ call: CAPPluginCall) {
93
+ let host = call.getString("host") ?? ""
94
+ let port = call.getInt("port") ?? 0
95
+ let url = call.getString("url") ?? ""
96
+
97
+ guard (!host.isEmpty && port > 0) || !url.isEmpty else {
98
+ call.reject("Either host and port, or url is required")
99
+ return
100
+ }
101
+
102
+ Task {
103
+ let options = PacketLossOptions(
104
+ mode: call.getString("mode") ?? "",
105
+ host: host,
106
+ port: port,
107
+ url: url,
108
+ count: call.getInt("count") ?? 10,
109
+ timeoutMs: call.getInt("timeoutMs") ?? 3_000,
110
+ intervalMs: call.getInt("intervalMs") ?? 250
111
+ )
112
+ call.resolve(await implementation.testPacketLoss(options))
113
+ }
114
+ }
115
+
116
+ @objc func runDiagnostics(_ call: CAPPluginCall) {
117
+ let urls = call.getArray("urls", []) as? [[String: Any]] ?? []
118
+ let ports = call.getArray("ports", []) as? [[String: Any]] ?? []
119
+ let websockets = call.getArray("websockets", []) as? [[String: Any]] ?? []
120
+ let download = call.getObject("download")
121
+ let packetLoss = call.getObject("packetLoss")
122
+
123
+ Task {
124
+ call.resolve(await implementation.runDiagnostics(
125
+ urls: urls,
126
+ ports: ports,
127
+ websockets: websockets,
128
+ download: download,
129
+ packetLoss: packetLoss
130
+ ))
131
+ }
132
+ }
133
+
134
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
135
+ call.resolve([
136
+ "version": implementation.getPluginVersion()
137
+ ])
138
+ }
139
+ }
@@ -0,0 +1,11 @@
1
+ import XCTest
2
+ @testable import NetworkDiagnosticsPlugin
3
+
4
+ class NetworkDiagnosticsTests: XCTestCase {
5
+ func testGetPluginVersion() {
6
+ let implementation = NetworkDiagnostics()
7
+ let result = implementation.getPluginVersion()
8
+
9
+ XCTAssertEqual("native", result)
10
+ }
11
+ }