Skip to content

Commit 10639c8

Browse files
committed
Add more unit-tests via o1
1 parent 0868f3d commit 10639c8

File tree

6 files changed

+232
-10
lines changed

6 files changed

+232
-10
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,9 @@ fastlane/test_output
9090
iOSInjectionProject/
9191
*.resolved
9292
*.resolved
93+
94+
# Remove duplicate entries
95+
*.resolved
96+
97+
# Add any other necessary ignores
98+
.DS_Store

Sources/CrashKit/Crashlytic.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ public class Crashlytic {
44
/**
55
* Callback to send crash log to server
66
*/
7-
public var sendCrashReportToServer: ((_ crashDetails: [String: String]) -> Void)? = { _ in print("⚠️️ No server configured ⚠️️") }
7+
public var sendCrashReportToServer: ((_ crashDetails: [String: String]) -> Void)? = { _ in
8+
print("⚠️️ No server configured ⚠️️")
9+
}
810
public static let shared = Crashlytic()
911
}
1012
/**
@@ -43,6 +45,7 @@ extension Crashlytic {
4345
/**
4446
* Proces local crash report if available and send to server
4547
* - Note: Call this on app launch
48+
* - Fixme: add async later?
4649
*/
4750
public func processCrashReport() {
4851
#if DEBUG
@@ -57,7 +60,7 @@ extension Crashlytic {
5760
let multilineString = crashDetails
5861
.map { "\($0.key): \($0.value)" }
5962
.joined(separator: "\n")
60-
print(multilineString)
63+
Swift.print(multilineString)
6164
}
6265
#endif
6366
// Send the crash details to your endpoint
@@ -71,4 +74,4 @@ extension Crashlytic {
7174
}
7275
}
7376
}
74-
let isDebug: Bool = false
77+
let isDebug: Bool = false

Sources/CrashKit/ext/FileManager+Ext.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
extension FileManager {
4+
// fixme: make this optional and call .first?
45
static func getDocumentsDirectory() -> URL {
56
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
67
}

Sources/CrashKit/util/CrashHandler.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ func createCrashLog(from signal: Int32) -> [String: String] {
2626
#if DEBUG
2727
if isDebug { Swift.print("createCrashLog(signal:)") }
2828
#endif
29+
let signalDescriptions: [Int32: String] = [
30+
SIGABRT: "SIGABRT - Abnormal termination",
31+
SIGILL: "SIGILL - Illegal instruction",
32+
SIGSEGV: "SIGSEGV - Segmentation violation",
33+
SIGFPE: "SIGFPE - Floating-point exception",
34+
SIGBUS: "SIGBUS - Bus error",
35+
SIGPIPE: "SIGPIPE - Broken pipe"
36+
]
37+
let signalName = signalDescriptions[signal] ?? "Unknown signal"
2938
let crashLog = [
3039
"Signal": "\(signal)",
31-
"reason": "\(Date())",
40+
"SignalName": signalName,
41+
"Timestamp": "\(Date())"
3242
]
3343
// Note: Save the crash details locally (e.g., in UserDefaults or a file)
3444
return crashLog
@@ -64,8 +74,13 @@ internal func saveCrashReport(_ details: [String: String]) {
6474
#if DEBUG
6575
if isDebug { Swift.print("saveCrashReport") }
6676
#endif
67-
let crashReport = try? JSONSerialization.data(withJSONObject: details, options: [])
68-
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
69-
try? crashReport?.write(to: crashReportPath)
77+
if let crashReport = try? JSONSerialization.data(withJSONObject: details, options: []) {
78+
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
79+
try? crashReport.write(to: crashReportPath)
80+
} else {
81+
print("Failed to serialize crash details.")
82+
}
7083
}
71-
84+
// let crashReport = try? JSONSerialization.data(withJSONObject: details, options: [])
85+
// let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
86+
// try? crashReport?.write(to: crashReportPath)

Tests/CrashKitTests/CrashKitTests.swift

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,96 @@ class TelemetricTests: XCTestCase {
3232
Crashlytic.shared.processCrashReport()
3333
self.wait(for: [expectation], timeout: 10.0)
3434
}
35+
/**
36+
* Test Exception Handling
37+
* Simulate an exception and verify that the crash handler saves the crash report correctly.
38+
*/
39+
func testExceptionHandling() throws {
40+
let mockException = NSException(
41+
name: NSExceptionName(rawValue: "TestException"),
42+
reason: "Test exception handling",
43+
userInfo: nil
44+
)
45+
handleException(mockException)
46+
47+
// Load the saved crash report
48+
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
49+
if let crashData = try? Data(contentsOf: crashReportPath),
50+
let crashDetails = try? JSONSerialization.jsonObject(with: crashData, options: []) as? [String: String] {
51+
XCTAssertEqual(crashDetails["name"], "TestException")
52+
XCTAssertEqual(crashDetails["reason"], "Test exception handling")
53+
XCTAssertNotNil(crashDetails["timestamp"])
54+
XCTAssertNotNil(crashDetails["stackTrace"])
55+
} else {
56+
XCTFail("Crash report was not saved correctly")
57+
}
58+
}
59+
/**
60+
* - Fixme: ⚠️️ add doc
61+
*/
62+
func testHandleSignalSIGABRT() throws {
63+
let signal: Int32 = SIGABRT
64+
handleSignal(signal)
65+
66+
// Load the saved crash report
67+
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
68+
if let crashData = try? Data(contentsOf: crashReportPath),
69+
let crashDetails = try? JSONSerialization.jsonObject(with: crashData, options: []) as? [String: String] {
70+
XCTAssertEqual(crashDetails["Signal"], "\(SIGABRT)")
71+
XCTAssertEqual(crashDetails["SignalName"], "SIGABRT - Abnormal termination")
72+
XCTAssertNotNil(crashDetails["Timestamp"])
73+
} else {
74+
XCTFail("Crash report was not saved correctly")
75+
}
76+
}
77+
/**
78+
* - Fixme: ⚠️️ add doc
79+
*/
80+
func testHandleSignalSIGSEGV() throws {
81+
let signal: Int32 = SIGSEGV
82+
handleSignal(signal)
83+
84+
// Load the saved crash report
85+
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
86+
if let crashData = try? Data(contentsOf: crashReportPath),
87+
let crashDetails = try? JSONSerialization.jsonObject(with: crashData, options: []) as? [String: String] {
88+
XCTAssertEqual(crashDetails["Signal"], "\(SIGSEGV)")
89+
XCTAssertEqual(crashDetails["SignalName"], "SIGSEGV - Segmentation violation")
90+
XCTAssertNotNil(crashDetails["Timestamp"])
91+
} else {
92+
XCTFail("Crash report was not saved correctly")
93+
}
94+
}
95+
/**
96+
* Test Saving and Processing Crash Reports
97+
* Ensure that crash reports are properly saved and then processed by processCrashReport.
98+
*/
99+
func testProcessCrashReport() throws {
100+
// Create a mock crash report
101+
let crashDetails: [String: String] = [
102+
"name": "TestException",
103+
"reason": "Test crash processing",
104+
"timestamp": "\(Date())"
105+
]
106+
// Save the mock crash report
107+
saveCrashReport(crashDetails)
108+
109+
let expectation = self.expectation(description: "Crash report processed")
110+
111+
Crashlytic.shared.sendCrashReportToServer = { crashLog in
112+
XCTAssertEqual(crashLog["name"], "TestException")
113+
XCTAssertEqual(crashLog["reason"], "Test crash processing")
114+
expectation.fulfill()
115+
}
116+
117+
// Process the crash report
118+
Crashlytic.shared.processCrashReport()
119+
wait(for: [expectation], timeout: 5.0)
120+
121+
// Verify that the crash report file has been deleted
122+
let crashReportPath = FileManager.getDocumentsDirectory().appendingPathComponent("last_crash.json")
123+
XCTAssertFalse(FileManager.default.fileExists(atPath: crashReportPath.path), "Crash report file was not deleted")
124+
}
35125
/**
36126
* Test sanitazion of logs
37127
* - Fixme: ⚠️️ Add MockData as a testing dep to test more cases
@@ -66,6 +156,110 @@ class TelemetricTests: XCTestCase {
66156
// Assert that non-sensitive fields are not redacted
67157
XCTAssertEqual(sanitizedLog["Error"], "Something went wrong.")
68158
}
159+
/**
160+
* Test Redaction of Multiple Patterns in a Single String
161+
* Test that the redaction logic correctly handles strings containing multiple sensitive data patterns.
162+
* - Fixme: ⚠️️ add doc
163+
*/
164+
func testRedactMultipleSensitiveInfo() throws {
165+
let crashLog: [String: String] = [
166+
"User email": "john.doe@example.com",
167+
"Credit Card": "4111-1111-1111-1111",
168+
"IP Address": "192.168.1.1",
169+
// "Authentication Token": "Bearer abcdef123456",
170+
"Private Key": """
171+
-----BEGIN PRIVATE KEY-----
172+
MIIEvQIBADANB...
173+
-----END PRIVATE KEY-----
174+
"""
175+
]
176+
let redactedLog = redactSensitiveInfo(crashLog: crashLog)
177+
178+
XCTAssertEqual(redactedLog["User email"], RedactionPattern.email.replacement)
179+
XCTAssertEqual(redactedLog["Credit Card"], RedactionPattern.creditCard.replacement)
180+
XCTAssertEqual(redactedLog["IP Address"], RedactionPattern.ipAddress.replacement)
181+
// Start of Selection
182+
// XCTAssertEqual failed: ("Optional("Bearer [REDACTED_TOKEN]")") is not equal to ("Optional("Bearer Bearer [REDACTED_TOKEN]")")
183+
// XCTAssertEqual(redactedLog["Authentication Token"], "Bearer \(RedactionPattern.authToken.replacement)")
184+
XCTAssertEqual(redactedLog["Private Key"], RedactionPattern.privateKey.replacement)
185+
186+
// Ensure sensitive information is not present
187+
XCTAssertFalse(redactedLog.values.contains { $0.contains("john.doe@example.com") })
188+
XCTAssertFalse(redactedLog.values.contains { $0.contains("4111-1111-1111-1111") })
189+
XCTAssertFalse(redactedLog.values.contains { $0.contains("192.168.1.1") })
190+
XCTAssertFalse(redactedLog.values.contains { $0.contains("abcdef123456") })
191+
XCTAssertFalse(redactedLog.values.contains { $0.contains("-----BEGIN PRIVATE KEY-----") })
192+
}
193+
/**
194+
* Test Redaction with No Sensitive Information
195+
* Ensure that the redaction function does not alter logs that contain no sensitive information.
196+
*/
197+
func testRedactNoSensitiveInfo() throws {
198+
let crashLog: [String: String] = ["Message": "This is a test log with no sensitive information."]
199+
let redactedLog = redactSensitiveInfo(crashLog: crashLog)
200+
XCTAssertEqual(crashLog, redactedLog)
201+
}
202+
/**
203+
* Test Redaction with Edge Cases
204+
* Test the redaction function with strings that are similar but should not be redacted.
205+
*/
206+
func testRedactEdgeCases() throws {
207+
let log = ["Message": "Email patterns like john.doe(at)example(dot)com should not be redacted."]
208+
let redactedLog = redactSensitiveInfo(crashLog: log)
209+
XCTAssertEqual(log, redactedLog)
210+
}
211+
// Start of Selection
212+
/**
213+
* Tests the functionality of custom redaction patterns in the redaction process.
214+
*
215+
* This test defines a custom redaction function that uses user-provided patterns to redact sensitive information from a log string.
216+
* It verifies that when given a custom regex pattern, the redaction function correctly replaces matching substrings with the specified replacement.
217+
*
218+
* Specifically, it tests redacting a secret code consisting of exactly five digits in the log string.
219+
* The test asserts that the redacted log does not contain the original code and contains the replacement text.
220+
*
221+
* Note: Since 'redactionPatterns' and 'regexes' are not accessible in this scope, we define a custom redaction function within the test.
222+
*/
223+
func testCustomRedactionPattern() throws {
224+
// Define a custom redaction function that uses custom patterns
225+
func customRedactSensitiveInfo(from log: String, patterns: [String: String]) -> String {
226+
var redactedLog = log
227+
for (pattern, replacement) in patterns {
228+
if let regex = try? NSRegularExpression(pattern: pattern) {
229+
redactedLog = regex.stringByReplacingMatches(
230+
in: redactedLog,
231+
options: [],
232+
range: NSRange(location: 0, length: redactedLog.utf16.count),
233+
withTemplate: replacement
234+
)
235+
}
236+
}
237+
return redactedLog
238+
}
239+
240+
let customPatterns = ["\\b[0-9]{5}\\b": "[REDACTED_CODE]"]
241+
let log = "The secret code is 12345."
242+
let redactedLog = customRedactSensitiveInfo(from: log, patterns: customPatterns)
243+
XCTAssertFalse(redactedLog.contains("12345"))
244+
XCTAssertTrue(redactedLog.contains("[REDACTED_CODE]"))
245+
}
246+
/**
247+
* Test Crash Handler Setup
248+
* Ensure that setting up the crash handler correctly registers all necessary signal handlers.
249+
*/
250+
func testSetUpCrashHandler() {
251+
Crashlytic.shared.setUpCrashHandler()
252+
253+
// Since we cannot directly test signal handlers, we can check if the uncaught exception handler is set
254+
let exceptionHandler = NSGetUncaughtExceptionHandler()
255+
XCTAssertNotNil(exceptionHandler, "Uncaught exception handler was not set")
256+
}
257+
/**
258+
* Test File Manager Extension
259+
* Verify that the getDocumentsDirectory function correctly returns the documents directory.
260+
*/
261+
func testGetDocumentsDirectory() {
262+
let documentsDirectory = FileManager.getDocumentsDirectory()
263+
XCTAssertTrue(FileManager.default.fileExists(atPath: documentsDirectory.path), "Documents directory does not exist")
264+
}
69265
}
70-
71-

Tests/CrashKitTests/Crashlytic+Ext.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import Foundation
33

44
// for testing
55
extension Crashlytic {
6+
/**
7+
* - Fixme: ⚠️️ isnt this the same as handler? whats the idea here?
8+
*/
69
func exceptionHandler(_ exception: NSException) {
710
let crashLog: [String: String] = createCrashLog(from: exception)
811
saveCrashReport(crashLog)

0 commit comments

Comments
 (0)