Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation

This commit is contained in:
Trey t
2026-02-11 12:54:40 -06:00
parent e40275e694
commit acce712261
77 changed files with 2940 additions and 765 deletions

View File

@@ -0,0 +1,20 @@
import XCTest
@testable import SharedCore
final class RuntimeReporterTests: XCTestCase {
func testReporterInvokesSinkWithMetadata() {
let expectation = expectation(description: "sink called")
RuntimeReporter.shared.setSink { event in
XCTAssertEqual(event.severity, .error)
XCTAssertEqual(event.message, "network failure")
XCTAssertEqual(event.metadata["status"], "500")
expectation.fulfill()
}
RuntimeReporter.shared.recordError("network failure", metadata: ["status": "500"])
waitForExpectations(timeout: 1.0)
RuntimeReporter.shared.setSink(nil)
}
}

View File

@@ -0,0 +1,36 @@
import XCTest
@testable import SharedCore
final class BoundedFIFOQueueTests: XCTestCase {
func testDisconnectReconnectFlushPreservesOrder() {
var queue = BoundedFIFOQueue<Int>(maxCount: 5)
_ = queue.enqueue(10)
_ = queue.enqueue(20)
_ = queue.enqueue(30)
XCTAssertEqual(queue.dequeueAll(), [10, 20, 30])
XCTAssertTrue(queue.isEmpty)
}
func testOverflowDropsOldestPayloads() {
var queue = BoundedFIFOQueue<Int>(maxCount: 3)
XCTAssertEqual(queue.enqueue(1), 0)
XCTAssertEqual(queue.enqueue(2), 0)
XCTAssertEqual(queue.enqueue(3), 0)
XCTAssertEqual(queue.enqueue(4), 1)
XCTAssertEqual(queue.enqueue(5), 1)
XCTAssertEqual(queue.dequeueAll(), [3, 4, 5])
}
func testMaxCountHasLowerBoundOfOne() {
var queue = BoundedFIFOQueue<Int>(maxCount: 0)
_ = queue.enqueue(1)
XCTAssertEqual(queue.enqueue(2), 1)
XCTAssertEqual(queue.dequeueAll(), [2])
}
}

View File

@@ -0,0 +1,47 @@
import XCTest
@testable import SharedCore
final class WatchPayloadValidationTests: XCTestCase {
private struct MockPayload: Codable, Equatable {
let name: String
let count: Int
}
func testValidateRejectsEmptyPayload() {
let error = WatchPayloadValidation.validate(Data())
XCTAssertEqual(error, .emptyPayload)
}
func testValidateRejectsOversizedPayload() {
let payload = Data(repeating: 0, count: 9)
let error = WatchPayloadValidation.validate(payload, maxBytes: 8)
XCTAssertEqual(error, .payloadTooLarge(actualBytes: 9, maxBytes: 8))
}
func testDecodeAcceptsValidPayload() throws {
let payload = try JSONEncoder().encode(MockPayload(name: "set", count: 12))
let decoded = try WatchPayloadValidation.decode(MockPayload.self, from: payload)
XCTAssertEqual(decoded, MockPayload(name: "set", count: 12))
}
func testDecodeRejectsInvalidJSON() throws {
let invalid = Data("not-json".utf8)
XCTAssertThrowsError(try WatchPayloadValidation.decode(MockPayload.self, from: invalid)) { error in
XCTAssertEqual(error as? WatchPayloadValidationError, .decodeFailure)
}
}
func testDecodeRejectsOversizedPayloadBeforeDecoding() {
let payload = Data(repeating: 1, count: 32)
XCTAssertThrowsError(
try WatchPayloadValidation.decode(MockPayload.self, from: payload, maxBytes: 16)
) { error in
XCTAssertEqual(
error as? WatchPayloadValidationError,
.payloadTooLarge(actualBytes: 32, maxBytes: 16)
)
}
}
}

View File

@@ -0,0 +1,39 @@
import XCTest
@testable import SharedCore
final class WorkoutValidationTests: XCTestCase {
func testValidateSupersetsRejectsEmptyPayload() {
let issues = WorkoutValidation.validateSupersets([])
XCTAssertEqual(issues.first?.code, "empty_supersets")
}
func testValidateSupersetsRejectsInvalidRoundsAndExercisePayload() {
let supersets: [[String: Any]] = [
[
"rounds": 0,
"exercises": [
["reps": 0, "duration": 0]
]
]
]
let issues = WorkoutValidation.validateSupersets(supersets)
XCTAssertTrue(issues.contains(where: { $0.code == "invalid_rounds" }))
XCTAssertTrue(issues.contains(where: { $0.code == "invalid_exercise_payload" }))
}
func testValidateSupersetsAcceptsValidPayload() {
let supersets: [[String: Any]] = [
[
"rounds": 3,
"exercises": [
["reps": 12, "duration": 0],
["reps": 0, "duration": 30]
]
]
]
let issues = WorkoutValidation.validateSupersets(supersets)
XCTAssertTrue(issues.isEmpty)
}
}

View File

@@ -0,0 +1,65 @@
import XCTest
@testable import SharedCore
final class TokenSecurityTests: XCTestCase {
func testSanitizeTokenRejectsEmptyAndRedactedValues() {
XCTAssertNil(TokenSecurity.sanitizeToken(nil))
XCTAssertNil(TokenSecurity.sanitizeToken(" "))
XCTAssertNil(TokenSecurity.sanitizeToken("REDACTED_TOKEN"))
XCTAssertEqual(TokenSecurity.sanitizeToken(" abc123 "), "abc123")
}
func testSanitizeTokenNormalizesAuthorizationPrefixes() {
XCTAssertEqual(TokenSecurity.sanitizeToken("Token abc123"), "abc123")
XCTAssertEqual(TokenSecurity.sanitizeToken("bearer xyz789"), "xyz789")
XCTAssertNil(TokenSecurity.sanitizeToken("Token "))
}
func testContainsPotentialHardcodedTokenDetectsLongHexBlob() {
let content = "private let token = \"0123456789abcdef0123456789abcdef\""
XCTAssertTrue(TokenSecurity.containsPotentialHardcodedToken(in: content))
}
func testJWTExpirationAndRotationWindow() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let expiration = now.addingTimeInterval(30 * 60)
let token = try makeJWT(exp: expiration)
XCTAssertEqual(TokenSecurity.jwtExpiration(token), expiration)
XCTAssertFalse(TokenSecurity.isExpired(token, now: now))
XCTAssertTrue(TokenSecurity.shouldRotate(token, now: now, rotationWindow: 60 * 60))
}
func testExpiredJWTReturnsExpired() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let expiration = now.addingTimeInterval(-10)
let token = try makeJWT(exp: expiration)
XCTAssertTrue(TokenSecurity.isExpired(token, now: now))
}
func testMalformedTokenDoesNotCrashAndDoesNotTriggerRotation() {
let malformed = "not-a-jwt"
XCTAssertNil(TokenSecurity.jwtExpiration(malformed))
XCTAssertFalse(TokenSecurity.isExpired(malformed))
XCTAssertFalse(TokenSecurity.shouldRotate(malformed))
}
private func makeJWT(exp: Date) throws -> String {
let header = ["alg": "HS256", "typ": "JWT"]
let payload = ["exp": Int(exp.timeIntervalSince1970)]
let headerData = try JSONSerialization.data(withJSONObject: header)
let payloadData = try JSONSerialization.data(withJSONObject: payload)
return "\(base64URL(headerData)).\(base64URL(payloadData)).signature"
}
private func base64URL(_ data: Data) -> String {
data.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}