Skip to content

Commit bf5b1d5

Browse files
committed
new unitary tests
1 parent 2f4e41a commit bf5b1d5

File tree

5 files changed

+252
-0
lines changed

5 files changed

+252
-0
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
- name: Build
2525
run: swift build -c release
2626

27+
- name: Run tests
28+
run: swift test
29+
2730
- name: Upload binary
2831
uses: actions/upload-artifact@v4
2932
with:

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ jobs:
2020
- name: Get version
2121
id: version
2222
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
23+
24+
- name: Run tests
25+
run: swift test
2326

2427
- name: Build and Sign app bundle
2528
run: |

Sources/ScriptRunner.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ final class ScriptRunner: @unchecked Sendable {
9191

9292
/// Runs a script synchronously and returns the output (useful for testing)
9393
func runScriptSynchronously(at path: String, withArgument argument: String? = nil) -> (exitCode: Int32, output: String, error: String) {
94+
let fileManager = FileManager.default
95+
var isDirectory: ObjCBool = false
96+
97+
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory),
98+
!isDirectory.boolValue else {
99+
return (-1, "", "Failed to execute: Script not found or is a directory")
100+
}
101+
94102
let process = Process()
95103
process.executableURL = URL(fileURLWithPath: "/bin/sh")
96104
process.arguments = [path]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import XCTest
2+
@testable import ntfy_macos
3+
4+
final class NtfyClientTests: XCTestCase {
5+
// MARK: - URL Construction
6+
7+
func testClientInitialization() {
8+
let client = NtfyClient(serverURL: "https://ntfy.sh", topics: ["test"])
9+
XCTAssertNotNil(client)
10+
}
11+
12+
func testClientWithMultipleTopics() {
13+
let client = NtfyClient(serverURL: "https://ntfy.sh", topics: ["topic1", "topic2", "topic3"])
14+
XCTAssertNotNil(client)
15+
}
16+
17+
func testClientWithAuthToken() {
18+
let client = NtfyClient(serverURL: "https://ntfy.sh", topics: ["test"], authToken: "tk_secret123")
19+
XCTAssertNotNil(client)
20+
}
21+
22+
func testClientWithCustomServer() {
23+
let client = NtfyClient(serverURL: "https://my-ntfy.example.com", topics: ["alerts"])
24+
XCTAssertNotNil(client)
25+
}
26+
27+
// MARK: - Exponential Backoff Calculation
28+
29+
func testExponentialBackoffCalculation() {
30+
// Test the exponential backoff formula: min(baseDelay * 2^attempts, maxDelay)
31+
let baseDelay: TimeInterval = 2.0
32+
let maxDelay: TimeInterval = 300.0
33+
34+
// Attempt 0: 2 * 2^0 = 2
35+
XCTAssertEqual(min(baseDelay * pow(2.0, 0), maxDelay), 2.0)
36+
37+
// Attempt 1: 2 * 2^1 = 4
38+
XCTAssertEqual(min(baseDelay * pow(2.0, 1), maxDelay), 4.0)
39+
40+
// Attempt 2: 2 * 2^2 = 8
41+
XCTAssertEqual(min(baseDelay * pow(2.0, 2), maxDelay), 8.0)
42+
43+
// Attempt 3: 2 * 2^3 = 16
44+
XCTAssertEqual(min(baseDelay * pow(2.0, 3), maxDelay), 16.0)
45+
46+
// Attempt 4: 2 * 2^4 = 32
47+
XCTAssertEqual(min(baseDelay * pow(2.0, 4), maxDelay), 32.0)
48+
49+
// Attempt 5: 2 * 2^5 = 64
50+
XCTAssertEqual(min(baseDelay * pow(2.0, 5), maxDelay), 64.0)
51+
52+
// Attempt 6: 2 * 2^6 = 128
53+
XCTAssertEqual(min(baseDelay * pow(2.0, 6), maxDelay), 128.0)
54+
55+
// Attempt 7: 2 * 2^7 = 256
56+
XCTAssertEqual(min(baseDelay * pow(2.0, 7), maxDelay), 256.0)
57+
58+
// Attempt 8: 2 * 2^8 = 512, capped at 300
59+
XCTAssertEqual(min(baseDelay * pow(2.0, 8), maxDelay), 300.0)
60+
61+
// Attempt 9: still capped at 300
62+
XCTAssertEqual(min(baseDelay * pow(2.0, 9), maxDelay), 300.0)
63+
}
64+
65+
func testJitterRange() {
66+
// Verify jitter calculation stays within ±10%
67+
let baseDelay: TimeInterval = 100.0
68+
69+
for _ in 0..<100 {
70+
let jitter = baseDelay * Double.random(in: -0.1...0.1)
71+
let delayWithJitter = baseDelay + jitter
72+
73+
XCTAssertGreaterThanOrEqual(delayWithJitter, 90.0)
74+
XCTAssertLessThanOrEqual(delayWithJitter, 110.0)
75+
}
76+
}
77+
78+
// MARK: - Disconnect
79+
80+
func testDisconnectDoesNotCrash() {
81+
let client = NtfyClient(serverURL: "https://ntfy.sh", topics: ["test"])
82+
// Should not crash even if never connected
83+
client.disconnect()
84+
}
85+
86+
func testMultipleDisconnects() {
87+
let client = NtfyClient(serverURL: "https://ntfy.sh", topics: ["test"])
88+
// Multiple disconnects should be safe
89+
client.disconnect()
90+
client.disconnect()
91+
client.disconnect()
92+
}
93+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import XCTest
2+
@testable import ntfy_macos
3+
4+
final class ScriptRunnerTests: XCTestCase {
5+
var runner: ScriptRunner!
6+
var tempDir: URL!
7+
8+
override func setUp() {
9+
super.setUp()
10+
runner = ScriptRunner()
11+
tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("script-tests-\(UUID().uuidString)")
12+
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
13+
}
14+
15+
override func tearDown() {
16+
if let dir = tempDir {
17+
try? FileManager.default.removeItem(at: dir)
18+
}
19+
super.tearDown()
20+
}
21+
22+
// MARK: - Script Validation
23+
24+
func testValidateScriptNotFound() {
25+
let result = runner.validateScript(at: "/nonexistent/path/script.sh")
26+
XCTAssertFalse(result)
27+
}
28+
29+
func testValidateScriptIsDirectory() {
30+
let result = runner.validateScript(at: tempDir.path)
31+
XCTAssertFalse(result)
32+
}
33+
34+
func testValidateScriptNotExecutable() throws {
35+
let scriptPath = tempDir.appendingPathComponent("not-executable.sh")
36+
try "#!/bin/bash\necho hello".write(to: scriptPath, atomically: true, encoding: .utf8)
37+
38+
// File exists but is not executable
39+
let result = runner.validateScript(at: scriptPath.path)
40+
XCTAssertFalse(result)
41+
}
42+
43+
func testValidateScriptExecutable() throws {
44+
let scriptPath = tempDir.appendingPathComponent("executable.sh")
45+
try "#!/bin/bash\necho hello".write(to: scriptPath, atomically: true, encoding: .utf8)
46+
47+
// Make it executable
48+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
49+
50+
let result = runner.validateScript(at: scriptPath.path)
51+
XCTAssertTrue(result)
52+
}
53+
54+
// MARK: - Script Execution
55+
56+
func testRunScriptSynchronouslySimple() throws {
57+
let scriptPath = tempDir.appendingPathComponent("simple.sh")
58+
try "#!/bin/bash\necho 'Hello World'".write(to: scriptPath, atomically: true, encoding: .utf8)
59+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
60+
61+
let result = runner.runScriptSynchronously(at: scriptPath.path)
62+
63+
XCTAssertEqual(result.exitCode, 0)
64+
XCTAssertTrue(result.output.contains("Hello World"))
65+
XCTAssertTrue(result.error.isEmpty)
66+
}
67+
68+
func testRunScriptSynchronouslyWithArgument() throws {
69+
let scriptPath = tempDir.appendingPathComponent("with-arg.sh")
70+
try "#!/bin/bash\necho \"Received: $1\"".write(to: scriptPath, atomically: true, encoding: .utf8)
71+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
72+
73+
let result = runner.runScriptSynchronously(at: scriptPath.path, withArgument: "test-message")
74+
75+
XCTAssertEqual(result.exitCode, 0)
76+
XCTAssertTrue(result.output.contains("Received: test-message"))
77+
}
78+
79+
func testRunScriptSynchronouslyExitCode() throws {
80+
let scriptPath = tempDir.appendingPathComponent("exit-code.sh")
81+
try "#!/bin/bash\nexit 42".write(to: scriptPath, atomically: true, encoding: .utf8)
82+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
83+
84+
let result = runner.runScriptSynchronously(at: scriptPath.path)
85+
86+
XCTAssertEqual(result.exitCode, 42)
87+
}
88+
89+
func testRunScriptSynchronouslyStderr() throws {
90+
let scriptPath = tempDir.appendingPathComponent("stderr.sh")
91+
try "#!/bin/bash\necho 'Error message' >&2".write(to: scriptPath, atomically: true, encoding: .utf8)
92+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
93+
94+
let result = runner.runScriptSynchronously(at: scriptPath.path)
95+
96+
XCTAssertEqual(result.exitCode, 0)
97+
XCTAssertTrue(result.error.contains("Error message"))
98+
}
99+
100+
func testRunScriptSynchronouslyNotFound() {
101+
let result = runner.runScriptSynchronously(at: "/nonexistent/script.sh")
102+
103+
XCTAssertEqual(result.exitCode, -1)
104+
XCTAssertTrue(result.error.contains("Failed to execute"))
105+
}
106+
107+
func testRunScriptWithEnvironmentPath() throws {
108+
// Test that the enhanced PATH is set
109+
let scriptPath = tempDir.appendingPathComponent("check-path.sh")
110+
try "#!/bin/bash\necho $PATH".write(to: scriptPath, atomically: true, encoding: .utf8)
111+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
112+
113+
let result = runner.runScriptSynchronously(at: scriptPath.path)
114+
115+
XCTAssertEqual(result.exitCode, 0)
116+
XCTAssertTrue(result.output.contains("/opt/homebrew/bin"))
117+
XCTAssertTrue(result.output.contains("/usr/local/bin"))
118+
}
119+
120+
// MARK: - Special Characters
121+
122+
func testRunScriptWithSpecialCharactersInArgument() throws {
123+
let scriptPath = tempDir.appendingPathComponent("special-chars.sh")
124+
try "#!/bin/bash\necho \"Arg: $1\"".write(to: scriptPath, atomically: true, encoding: .utf8)
125+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
126+
127+
let specialArg = "Hello 'World' with \"quotes\" and $pecial chars!"
128+
let result = runner.runScriptSynchronously(at: scriptPath.path, withArgument: specialArg)
129+
130+
XCTAssertEqual(result.exitCode, 0)
131+
XCTAssertTrue(result.output.contains("Hello"))
132+
}
133+
134+
func testRunScriptWithUnicodeArgument() throws {
135+
let scriptPath = tempDir.appendingPathComponent("unicode.sh")
136+
try "#!/bin/bash\necho \"Message: $1\"".write(to: scriptPath, atomically: true, encoding: .utf8)
137+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: scriptPath.path)
138+
139+
let unicodeArg = "🔔 Notification: été, naïve, 日本語"
140+
let result = runner.runScriptSynchronously(at: scriptPath.path, withArgument: unicodeArg)
141+
142+
XCTAssertEqual(result.exitCode, 0)
143+
XCTAssertTrue(result.output.contains("🔔"))
144+
}
145+
}

0 commit comments

Comments
 (0)