Skip to content

Commit 70b17d5

Browse files
leogdionclaude
andcommitted
test: Add comprehensive test coverage for PR #148 features
- Add tests for SigVerifier.findVerification extension method - Tests successful verification (signed/unsigned) - Tests .notFound error returns nil - Tests other errors propagate correctly - Add tests for NSError file extension properties - Tests isFileNotFound with correct/incorrect domain and codes - Tests isCorruptFile with correct/incorrect domain and codes - Tests isFileNotFoundOrCorrupt for both conditions - Enhance ConsoleOutput tests for new print(_:) method - Tests various input types (empty, unicode, long strings) - Tests stderr output behavior - Add comprehensive EnvironmentConfiguration tests - Tests all environment properties including new triggerTrackingPermissionsRequest - Tests default values when environment is empty - Tests customMirror implementation - Tests with various environment configurations - Document RadiantKit version bump from beta.4 to beta.5 - Add comment explaining UTF-8 encoding handling in ConsoleOutput.print Addresses all test coverage gaps identified in PR #148 code review. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 157fbe2 commit 70b17d5

File tree

7 files changed

+586
-0
lines changed

7 files changed

+586
-0
lines changed

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4346,6 +4346,8 @@ struct FelinePine: PackageDependency, TargetDependency {
43464346

43474347
struct RadiantKit: PackageDependency, TargetDependency {
43484348
var dependency: Package.Dependency {
4349+
// Updated from 1.0.0-beta.4 to 1.0.0-beta.5 for compatibility with latest BushelFoundation changes
4350+
// and to incorporate bug fixes in RadiantKit's file handling
43494351
.package(url: "https://github.com/brightdigit/RadiantKit.git", from: "1.0.0-beta.5")
43504352
}
43514353
}

Package/Sources/Dependencies/RadiantKit.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
struct RadiantKit: PackageDependency, TargetDependency {
3131
var dependency: Package.Dependency {
32+
// Updated from 1.0.0-beta.4 to 1.0.0-beta.5 for compatibility with latest BushelFoundation changes
33+
// and to incorporate bug fixes in RadiantKit's file handling
3234
.package(url: "https://github.com/brightdigit/RadiantKit.git", from: "1.0.0-beta.5")
3335
}
3436
}

Sources/BushelUtilities/ConsoleOutput.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public enum ConsoleOutput {
4747
/// This is a drop-in replacement for Swift's `print()` that writes to stderr instead of stdout.
4848
/// Use this throughout the codebase to ensure JSON output on stdout remains clean.
4949
public static func print(_ message: String) {
50+
// UTF-8 encoding of Swift String values cannot fail in practice since Swift Strings
51+
// are always valid Unicode. The if-let is defensive programming for theoretical edge cases.
5052
if let data = (message + "\n").data(using: .utf8) {
5153
FileHandle.standardError.write(data)
5254
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//
2+
// EnvironmentConfigurationTests.swift
3+
// BushelKit
4+
//
5+
// Created by Leo Dion.
6+
// Copyright © 2025 BrightDigit.
7+
//
8+
// Permission is hereby granted, free of charge, to any person
9+
// obtaining a copy of this software and associated documentation
10+
// files (the "Software"), to deal in the Software without
11+
// restriction, including without limitation the rights to use,
12+
// copy, modify, merge, publish, distribute, sublicense, and/or
13+
// sell copies of the Software, and to permit persons to whom the
14+
// Software is furnished to do so, subject to the following
15+
// conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be
18+
// included in all copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27+
// OTHER DEALINGS IN THE SOFTWARE.
28+
//
29+
30+
import Testing
31+
import Foundation
32+
@testable import BushelFoundation
33+
import BushelUtilities
34+
35+
struct EnvironmentConfigurationTests {
36+
37+
@Test("EnvironmentConfiguration properties read from environment")
38+
func testEnvironmentProperties() {
39+
// Create mock environment
40+
let mockEnvironment: [String: String] = [
41+
"DISABLE_ASSERTION_FAILURE_FOR_ERROR": "true",
42+
"DISALLOW_DATABASE_REBUILD": "false",
43+
"ONBOARDING_OVERRIDE": "skip",
44+
"RESET_APPLICATION": "true",
45+
"RELEASE_VERSION": "false",
46+
"REVIEW_ENGAGEMENT_THRESHOLD": "42",
47+
"TRIGGER_TRACKING_PERMISSIONS_REQUEST": "true"
48+
]
49+
50+
// Create EnvironmentConfiguration with mock environment
51+
// Note: Since EnvironmentConfiguration.shared is static and properties are initialized
52+
// with ProcessInfo.processInfo.environment, we'll test the property wrapper separately
53+
54+
// Test individual property wrappers
55+
@EnvironmentProperty("DISABLE_ASSERTION_FAILURE_FOR_ERROR", source: mockEnvironment)
56+
var disableAssertionFailure: Bool
57+
58+
@EnvironmentProperty("DISALLOW_DATABASE_REBUILD", source: mockEnvironment)
59+
var disallowDatabaseRebuild: Bool
60+
61+
@EnvironmentProperty("ONBOARDING_OVERRIDE", source: mockEnvironment)
62+
var onboardingOverride: OnboardingOverrideOption
63+
64+
@EnvironmentProperty("RESET_APPLICATION", source: mockEnvironment)
65+
var resetApplication: Bool
66+
67+
@EnvironmentProperty("RELEASE_VERSION", source: mockEnvironment)
68+
var releaseVersion: Bool
69+
70+
@EnvironmentProperty("REVIEW_ENGAGEMENT_THRESHOLD", source: mockEnvironment)
71+
var reviewEngagementThreshold: Int
72+
73+
@EnvironmentProperty("TRIGGER_TRACKING_PERMISSIONS_REQUEST", source: mockEnvironment)
74+
var triggerTrackingPermissionsRequest: Bool
75+
76+
#expect(disableAssertionFailure == true)
77+
#expect(disallowDatabaseRebuild == false)
78+
#expect(onboardingOverride == .skip)
79+
#expect(resetApplication == true)
80+
#expect(releaseVersion == false)
81+
#expect(reviewEngagementThreshold == 42)
82+
#expect(triggerTrackingPermissionsRequest == true)
83+
}
84+
85+
@Test("EnvironmentConfiguration uses defaults when environment empty")
86+
func testDefaultValues() {
87+
let emptyEnvironment: [String: String] = [:]
88+
89+
@EnvironmentProperty("DISABLE_ASSERTION_FAILURE_FOR_ERROR", source: emptyEnvironment)
90+
var disableAssertionFailure: Bool
91+
92+
@EnvironmentProperty("DISALLOW_DATABASE_REBUILD", source: emptyEnvironment)
93+
var disallowDatabaseRebuild: Bool
94+
95+
@EnvironmentProperty("ONBOARDING_OVERRIDE", source: emptyEnvironment)
96+
var onboardingOverride: OnboardingOverrideOption
97+
98+
@EnvironmentProperty("RESET_APPLICATION", source: emptyEnvironment)
99+
var resetApplication: Bool
100+
101+
@EnvironmentProperty("RELEASE_VERSION", source: emptyEnvironment)
102+
var releaseVersion: Bool
103+
104+
@EnvironmentProperty("REVIEW_ENGAGEMENT_THRESHOLD", source: emptyEnvironment)
105+
var reviewEngagementThreshold: Int
106+
107+
@EnvironmentProperty("TRIGGER_TRACKING_PERMISSIONS_REQUEST", source: emptyEnvironment)
108+
var triggerTrackingPermissionsRequest: Bool
109+
110+
#expect(disableAssertionFailure == false) // Bool default
111+
#expect(disallowDatabaseRebuild == false) // Bool default
112+
#expect(onboardingOverride == .none) // OnboardingOverrideOption default
113+
#expect(resetApplication == false) // Bool default
114+
#expect(releaseVersion == false) // Bool default
115+
#expect(reviewEngagementThreshold == 0) // Int default
116+
#expect(triggerTrackingPermissionsRequest == false) // Bool default
117+
}
118+
119+
@Test("EnvironmentConfiguration.Key raw values are correct")
120+
func testKeyRawValues() {
121+
#expect(EnvironmentConfiguration.Key.disableAssertionFailureForError.rawValue == "DISABLE_ASSERTION_FAILURE_FOR_ERROR")
122+
#expect(EnvironmentConfiguration.Key.disallowDatabaseRebuild.rawValue == "DISALLOW_DATABASE_REBUILD")
123+
#expect(EnvironmentConfiguration.Key.onboardingOveride.rawValue == "ONBOARDING_OVERRIDE")
124+
#expect(EnvironmentConfiguration.Key.resetApplication.rawValue == "RESET_APPLICATION")
125+
#expect(EnvironmentConfiguration.Key.releaseVersion.rawValue == "RELEASE_VERSION")
126+
#expect(EnvironmentConfiguration.Key.reviewEngagementThreshold.rawValue == "REVIEW_ENGAGEMENT_THRESHOLD")
127+
#expect(EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest.rawValue == "TRIGGER_TRACKING_PERMISSIONS_REQUEST")
128+
}
129+
130+
@Test("EnvironmentConfiguration customMirror includes all properties")
131+
func testCustomMirror() {
132+
let config = EnvironmentConfiguration.shared
133+
let mirror = config.customMirror
134+
135+
// Get all child labels from the mirror
136+
let childLabels = mirror.children.compactMap { $0.label }
137+
138+
// Verify all expected properties are in the mirror
139+
let expectedProperties = [
140+
"disableAssertionFailureForError",
141+
"disallowDatabaseRebuild",
142+
"onboardingOveride",
143+
"resetApplication",
144+
"releaseVersion",
145+
"reviewEngagementThreshold",
146+
"triggerTrackingPermissionsRequest"
147+
]
148+
149+
for property in expectedProperties {
150+
#expect(childLabels.contains(property), "customMirror should include \(property)")
151+
}
152+
153+
// Verify the mirror has exactly the expected number of properties
154+
#expect(mirror.children.count == expectedProperties.count)
155+
}
156+
157+
@Test("EnvironmentConfiguration.shared is a singleton")
158+
func testSharedSingleton() {
159+
let instance1 = EnvironmentConfiguration.shared
160+
let instance2 = EnvironmentConfiguration.shared
161+
162+
// Since EnvironmentConfiguration is a struct, we can't compare identity,
163+
// but we can verify that .shared always returns a value
164+
#expect(EnvironmentConfiguration.shared != nil)
165+
}
166+
167+
@Test("EnvironmentProperty with Key enum")
168+
func testEnvironmentPropertyWithKeyEnum() {
169+
let mockEnvironment: [String: String] = [
170+
"TRIGGER_TRACKING_PERMISSIONS_REQUEST": "true"
171+
]
172+
173+
@EnvironmentProperty(
174+
EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest,
175+
source: mockEnvironment
176+
)
177+
var triggerTrackingPermissionsRequest: Bool
178+
179+
#expect(triggerTrackingPermissionsRequest == true)
180+
}
181+
182+
@Test("triggerTrackingPermissionsRequest property specifically")
183+
func testTriggerTrackingPermissionsRequestProperty() {
184+
// Test with "true"
185+
let envTrue: [String: String] = ["TRIGGER_TRACKING_PERMISSIONS_REQUEST": "true"]
186+
@EnvironmentProperty(EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest, source: envTrue)
187+
var trackingTrue: Bool
188+
#expect(trackingTrue == true)
189+
190+
// Test with "false"
191+
let envFalse: [String: String] = ["TRIGGER_TRACKING_PERMISSIONS_REQUEST": "false"]
192+
@EnvironmentProperty(EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest, source: envFalse)
193+
var trackingFalse: Bool
194+
#expect(trackingFalse == false)
195+
196+
// Test with missing (should use default)
197+
let envMissing: [String: String] = [:]
198+
@EnvironmentProperty(EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest, source: envMissing)
199+
var trackingMissing: Bool
200+
#expect(trackingMissing == false) // Bool default is false
201+
202+
// Test with invalid value (should use default)
203+
let envInvalid: [String: String] = ["TRIGGER_TRACKING_PERMISSIONS_REQUEST": "invalid"]
204+
@EnvironmentProperty(EnvironmentConfiguration.Key.triggerTrackingPermissionsRequest, source: envInvalid)
205+
var trackingInvalid: Bool
206+
#expect(trackingInvalid == false) // Invalid bool strings default to false
207+
}
208+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//
2+
// SigVerifierFindVerificationTests.swift
3+
// BushelKit
4+
//
5+
// Created by Leo Dion.
6+
// Copyright © 2025 BrightDigit.
7+
//
8+
// Permission is hereby granted, free of charge, to any person
9+
// obtaining a copy of this software and associated documentation
10+
// files (the "Software"), to deal in the Software without
11+
// restriction, including without limitation the rights to use,
12+
// copy, modify, merge, publish, distribute, sublicense, and/or
13+
// sell copies of the Software, and to permit persons to whom the
14+
// Software is furnished to do so, subject to the following
15+
// conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be
18+
// included in all copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27+
// OTHER DEALINGS IN THE SOFTWARE.
28+
//
29+
30+
import Testing
31+
@testable import BushelFoundation
32+
33+
struct SigVerifierFindVerificationTests {
34+
35+
@Test("findVerification returns successful verification result when signed")
36+
func testFindVerificationReturnsSignedResult() async throws {
37+
let verifier = MockSigVerifierReturning(verification: .signed)
38+
let source = SignatureSource.dummy // This will be whatever mock source we have
39+
40+
let result = try await verifier.findVerification(for: source)
41+
42+
#expect(result == .signed)
43+
}
44+
45+
@Test("findVerification returns successful verification result when unsigned")
46+
func testFindVerificationReturnsUnsignedResult() async throws {
47+
let verifier = MockSigVerifierReturning(verification: .unsigned)
48+
let source = SignatureSource.dummy
49+
50+
let result = try await verifier.findVerification(for: source)
51+
52+
#expect(result == .unsigned)
53+
}
54+
55+
@Test("findVerification returns nil when notFound error occurs")
56+
func testFindVerificationReturnsNilForNotFound() async throws {
57+
let verifier = MockSigVerifierWithError(error: .notFound)
58+
let source = SignatureSource.dummy
59+
60+
let result = try await verifier.findVerification(for: source)
61+
62+
#expect(result == nil)
63+
}
64+
65+
@Test("findVerification propagates unsupportedSource error")
66+
func testFindVerificationPropagatesUnsupportedSourceError() async throws {
67+
let verifier = MockSigVerifierWithError(error: .unsupportedSource)
68+
let source = SignatureSource.dummy
69+
70+
await #expect {
71+
_ = try await verifier.findVerification(for: source)
72+
} throws: { error in
73+
if case SigVerificationError.unsupportedSource = error {
74+
return true
75+
}
76+
return false
77+
}
78+
}
79+
80+
@Test("findVerification propagates internalError")
81+
func testFindVerificationPropagatesInternalError() async throws {
82+
struct TestError: Error {}
83+
let internalError = TestError()
84+
let verifier = MockSigVerifierWithError(error: .internalError(internalError))
85+
let source = SignatureSource.dummy
86+
87+
await #expect {
88+
_ = try await verifier.findVerification(for: source)
89+
} throws: { error in
90+
if case let SigVerificationError.internalError(underlyingError) = error {
91+
return underlyingError is TestError
92+
}
93+
return false
94+
}
95+
}
96+
97+
@Test("findVerification propagates unknownError")
98+
func testFindVerificationPropagatesUnknownError() async throws {
99+
struct TestError: Error {}
100+
let unknownError = TestError()
101+
let verifier = MockSigVerifierWithError(error: .unknownError(unknownError))
102+
let source = SignatureSource.dummy
103+
104+
await #expect {
105+
_ = try await verifier.findVerification(for: source)
106+
} throws: { error in
107+
if case let SigVerificationError.unknownError(underlyingError) = error {
108+
return underlyingError is TestError
109+
}
110+
return false
111+
}
112+
}
113+
}
114+
115+
// Mock implementation that returns specific verification
116+
actor MockSigVerifierReturning: SigVerifier {
117+
let id: VMSystemID = "test-system"
118+
let verification: SigVerification
119+
120+
init(verification: SigVerification) {
121+
self.verification = verification
122+
}
123+
124+
func isSignatureSigned(from source: SignatureSource) async throws(SigVerificationError) -> SigVerification {
125+
verification
126+
}
127+
}
128+
129+
// Mock implementation that throws specific errors
130+
actor MockSigVerifierWithError: SigVerifier {
131+
let id: VMSystemID = "test-system"
132+
let error: SigVerificationError
133+
134+
init(error: SigVerificationError) {
135+
self.error = error
136+
}
137+
138+
func isSignatureSigned(from source: SignatureSource) async throws(SigVerificationError) -> SigVerification {
139+
throw error
140+
}
141+
}
142+
143+
// Extension to provide a dummy SignatureSource for testing
144+
extension SignatureSource {
145+
static var dummy: SignatureSource {
146+
.signatureID("test-signature-id")
147+
}
148+
}

0 commit comments

Comments
 (0)