Skip to content

Commit c69ae15

Browse files
watkynjjpritzlCopilotCopilotCopilot
authored
002 add pppc keys (#151)
* AI generated removal of the big sur compatibility logic. Needs vetted and tested * big-sur story/JPCFM-5531 Add minor changes to update minimum supported OS and remove remaining Big Sur things * big-sur story/JPCFM-5531 Remove swiftlint disable call due to reduced file length * big-sur story/JPCFM-5531 Add Copilot suggestion for generating test results * big-sur story/JPCFM-5531 Add Copilot refactor to loadExecutable function * big-sur story/JPCFM-5531 Remove code coverage addition * added detailed plan for getting to swift 6 in stages * Initial plan * Stage 2: Convert NetworkAuthManager from actor to class With SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, all types are MainActor by default. The actor's synchronization purpose (single-flight token refresh) is now provided by MainActor serialization. Changes: - actor NetworkAuthManager → class NetworkAuthManager - bearerAuthSupported(): drop async (no longer needed without actor) - basicAuthString(): drop nonisolated (no longer an actor) - JamfProAPIClient: drop await from bearerAuthSupported() calls - NetworkAuthManagerTests: drop await from bearerAuthSupported() calls Side effect: Resolves Token.isValid cross-isolation warning because Token and NetworkAuthManager are now both on MainActor. Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 3a: Remove DispatchQueue.main.async wrappers With MainActor default isolation, manual dispatch to the main thread is redundant. All three locations are already MainActor-isolated. Changes: - Alert.swift: Remove DispatchQueue.main.async wrapper in display() - OpenViewController.swift: Remove wrapper in tableView selectionIndexes - SaveViewController.swift: Remove wrapper in savePressed panel callback Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 3b: Convert UploadManager to async throws Replace Task/completion handler pattern with direct async throws methods. Changes: - UploadManager.verifyConnection: async throws -> VerificationInfo - UploadManager.upload: async throws (no completion handler) - UploadInfoView.verifyConnection: now async, uses try await - UploadInfoView.performUpload: now async, uses try await - Button action bridges to async with Task { await ... } Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 3c: Convert Model.loadExecutable to direct return Replace completion handler pattern with throws -> Executable since the work is synchronous (reads bundle info and code requirements). Changes: - Model.loadExecutable(url:) now throws -> Executable (no completion) - Remove LoadExecutableCompletion typealias - findExecutableOnComputerUsing → findExecutable, returns directly - getExecutableFrom: uses do/catch instead of completion handler - getAppleEventChoices: uses do/catch for each loadExecutable call - TCCProfileViewController: promptForExecutables and acceptDrop updated - OpenViewController.prompt: updated to use try/catch Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 3d: Convert TCCProfileImporter to direct return Replace completion handler pattern with throws -> TCCProfile since the work is synchronous (decode data, read file). Changes: - TCCProfileImporter.decodeTCCProfile(data:) now throws -> TCCProfile - TCCProfileImporter.decodeTCCProfile(fileUrl:) now throws -> TCCProfile - Remove TCCProfileImportCompletion typealias - TCCProfileConfigurationPanel: uses try/catch in panel callback - TCCProfileImporterTests: updated to use try/catch pattern Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 4: Add @Concurrent for background I/O Mark SecurityWrapper and Model methods with @Concurrent to move disk I/O and security operations off MainActor onto the cooperative pool. Changes: - SecurityWrapper.copyDesignatedRequirement: @Concurrent async throws - SecurityWrapper.sign: @Concurrent async throws - SecurityWrapper.loadSigningIdentities: @Concurrent async throws - Model.loadExecutable: @Concurrent async throws -> Executable - Model.getAppleEventChoices: now async (calls loadExecutable) - Model.getExecutableFrom/findExecutable: now async - Model.getExecutablesFromAllPolicies: now async - Model.importProfile: now async - All callers updated to use Task/await where needed - ModelTests: updated to async test methods Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Stage 5: Enable Swift 6 language mode Flip SWIFT_VERSION from 5.0 to 6.0 so all concurrency warnings become hard errors. Fix override isolation mismatches. Changes: - SWIFT_VERSION = 6.0 in all 4 build configurations - SaveViewController.observeValue: add nonisolated + MainActor.assumeIsolated - ModelTests.setUp: add nonisolated + MainActor.assumeIsolated - Update plan document to reflect all stages complete Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/4235dbf5-feb0-4744-839d-543eee3b8ee5 Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * create-separate story/JPCFM-5564 Fix failing build and tests * create-separate story/JPCFM-5564 Fix code signing mistake * create-separate story/JPCFM-5564 Fix bad syntax in build settings * create-separate story/JPCFM-5564 Fix bad syntax in project settings for last time * create-separate story/JPCFM-5564 Try to fix failing tests on GH PR * create-separate story/JPCFM-5564 Try to fix unit test check issue * create-separate story/JPCFM-5564 Make actual change with the plan to fix Unit Test issues * removed some uneeded concurrency annotations * removed a couple more spots * Update Source/SwiftUI/UploadInfoView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * code review changes * put the apple events selection back onto the main queue * disabled the drop down until it is populated. * fixed the sizing issue of the save window * added width to the save prompt * changed sequence of model on save * reverted main story board * added timing check for apple events loading * reverted back to sync on the drag drop executables * try macos 26 * fixed the upload crashes * claude plan 1 * updated plan and claude.md * added some simple unit test coverage that was missing * Phase 2: URLSession injection + MockURLProtocol infrastructure Production changes: - Networking.swift: Add session:URLSession parameter (default .shared), replace all 5 URLSession.shared calls with injected session - JamfProAPIClient.swift: Replace URLSession.shared in HTML version fallback with inherited session property - UploadManager.swift: Add session:URLSession parameter, pass through to JamfProAPIClient constructors Test infrastructure: - MockURLProtocol: Simple URLProtocol subclass with single static requestHandler, URLSession.mock(handler:) convenience, and HTTPURLResponse.ok/status helpers - MockURLProtocolTests: 3 tests verifying interception, error status codes, and reset behavior (.serialized suite) Convention updates: - CLAUDE.md: Add 'always use Swift Concurrency' rule, update mock convention to serialized suites with simple static handler - docs/plans: Matching updates to Phase 2 & 4 descriptions No behavioral change — URLSession.shared remains the default for all production paths. 69 tests pass, no new compiler warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * added more networking unit tests * Fix sendBearerAuthorized retry bug and add 401 retry tests Agent-Logs-Url: https://github.com/jamf/PPPC-Utility/sessions/398e2e82-5660-412c-9150-b6c79a4bb21e Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> * Phase 4: Add UploadManager and TCCProfile XML tests - New UploadManagerTests with verifyConnection tests (mustSign true/false, credential errors, unavailability errors) and upload tests verifying site XML presence/absence through the full network mock flow - Expand TCCProfileTests with XML-parsing-based assertions for jamfProAPIData structure, site element inclusion, and site exclusion - 96 tests total, all passing, no new compiler warnings - App target coverage: 45.03% Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * updated claude.md * added ui testing plan * added UI test framework and first test for the main window * split the UI tests from the unit test * fixed the project file ref * trying out speckit on a feature * finished implementing the spec * added spec for the 3 new keys * implemented the spec locally since the agent does not have xcode access * addressed copilot comments (except the one about splitting the pr) --------- Co-authored-by: JJ Pritzl <jj.pritzl@jamf.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: watkyn <40115+watkyn@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Mike Anderson <mike.anderson@jamf.com>
1 parent 25166cd commit c69ae15

29 files changed

+2063
-149
lines changed

.specify/feature.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"feature_directory": "specs/001-fix-deprecations"
2+
"feature_directory": "specs/002-add-pppc-keys"
33
}

.specify/memory/constitution.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ acceptable only for components without adequate SwiftUI coverage.
5151

5252
- Minimum deployment target: macOS 13.0.
5353
- All interactive UI elements MUST have accessibility identifiers and labels.
54+
- Service keys in the main window MUST appear in alphabetical order.
5455
- Profiles MUST be saveable locally (signed or unsigned) and uploadable to
5556
Jamf Pro (bearer token, basic auth fallback, or OAuth client credentials).
5657

@@ -86,4 +87,4 @@ Git) are maintained in `CLAUDE.md`. Amendments require:
8687
All PRs and code reviews MUST verify compliance with this constitution.
8788
Complexity violations MUST be justified in the PR description.
8889

89-
**Version**: 1.1.0 | **Ratified**: 2026-04-09 | **Last Amended**: 2026-04-09
90+
**Version**: 1.2.0 | **Ratified**: 2026-04-09 | **Last Amended**: 2026-04-09

CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,16 @@
2424
- Avoid snake_case in test names (e.g., `generateDisplayName_bundleIdentifier`). If a name is getting long, use a Trait with a sentence-style description instead.
2525
- For complex tests, use a descriptive `@Test("...")` trait that explains the scenario and expected outcome so the test is understandable without reading the body.
2626
- Use parameterized tests with Traits where it reduces duplication; 1–2 args is ideal, max 3
27+
```swift
28+
@Test("Service key round-trip preserves value", arguments: [
29+
("BluetoothAlways", "Allow"),
30+
("SystemPolicyAppBundles", "Deny")
31+
])
32+
func serviceKeyRoundTrip(serviceKey: String, value: String) async { }
33+
```
2734
- Beyond 3 params: create separate tests with some values hard-coded
2835
- Use `deinit` as teardown for repeated cleanup across tests in a suite. Use `class` for suites that need `deinit`; use `struct` otherwise.
36+
- After adding or refactoring tests, look for duplication or tests that are no longer relevant. Do not add redundant tests.
2937

3038
## UI Testing Conventions
3139

@@ -35,6 +43,14 @@
3543
- Use accessibility identifiers set in `setupAccessibilityIdentifiers()` to locate UI elements
3644
- The `-UITestMode` launch argument triggers test-specific setup (e.g., loading a test profile)
3745

46+
## UI Conventions
47+
48+
- Service keys (popup rows) in the main window must appear in alphabetical order.
49+
50+
## Naming
51+
52+
- Avoid "new" in test or function names — it becomes stale quickly. Describe the behavior, not the novelty.
53+
3854
## Git
3955

4056
- Do not stage or commit changes in terminal sessions

PPPC Utility.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
C07B1FB82AF596D80075E38B /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07B1FB72AF596D80075E38B /* UploadManager.swift */; };
6060
C0A2B5422B1A5D5C0007F510 /* JamfProAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */; };
6161
C0A85DB5279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig in Resources */ = {isa = PBXBuildFile; fileRef = C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */; };
62+
AA002200000000000000002B /* TestTCCUnsignedProfile-Legacy.mobileconfig in Resources */ = {isa = PBXBuildFile; fileRef = AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */; };
6263
C0DC2BB92B2263FC003A4474 /* Haversack in Frameworks */ = {isa = PBXBuildFile; productRef = C0DC2BB82B2263FC003A4474 /* Haversack */; };
6364
C0E0383F27A30C7100A23FA2 /* PPPCServiceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */; };
6465
C0E0384027A30C7100A23FA2 /* PPPCServicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */; };
@@ -151,6 +152,7 @@
151152
C07B1FB72AF596D80075E38B /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = "<group>"; };
152153
C0A2B5412B1A5D5C0007F510 /* JamfProAPIClientTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JamfProAPIClientTests.swift; sourceTree = "<group>"; };
153154
C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TestTCCUnsignedProfile-allLower.mobileconfig"; sourceTree = "<group>"; };
155+
AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "TestTCCUnsignedProfile-Legacy.mobileconfig"; sourceTree = "<group>"; };
154156
C0E0383D27A30C7100A23FA2 /* PPPCServiceInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServiceInfo.swift; sourceTree = "<group>"; };
155157
C0E0383E27A30C7100A23FA2 /* PPPCServicesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PPPCServicesManager.swift; sourceTree = "<group>"; };
156158
C0E0384127A30D1D00A23FA2 /* PPPCServices.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PPPCServices.json; sourceTree = "<group>"; };
@@ -266,6 +268,7 @@
266268
children = (
267269
5F95AE2B2315B172002E0A22 /* TestTCCProfileSigned-Broken.mobileconfig */,
268270
C0A85DB4279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig */,
271+
AA002200000000000000002A /* TestTCCUnsignedProfile-Legacy.mobileconfig */,
269272
5F95AE2A2315B172002E0A22 /* TestTCCUnsignedProfile-Broken.mobileconfig */,
270273
5F95AE2C2315B172002E0A22 /* TestTCCUnsignedProfile-Empty.mobileconfig */,
271274
5F95AE292315B172002E0A22 /* TestTCCUnsignedProfile.mobileconfig */,
@@ -536,6 +539,7 @@
536539
5F95AE312315B172002E0A22 /* TestTCCUnsignedProfile-Empty.mobileconfig in Resources */,
537540
5F95AE2F2315B172002E0A22 /* TestTCCUnsignedProfile-Broken.mobileconfig in Resources */,
538541
C0A85DB5279873C600086283 /* TestTCCUnsignedProfile-allLower.mobileconfig in Resources */,
542+
AA002200000000000000002B /* TestTCCUnsignedProfile-Legacy.mobileconfig in Resources */,
539543
C0EE9A7D28639BF800738B6B /* TestTCCProfileForJamfProAPI.txt in Resources */,
540544
5F95AE302315B172002E0A22 /* TestTCCProfileSigned-Broken.mobileconfig in Resources */,
541545
);

PPPC UtilityTests/Helpers/TCCProfileBuilder.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ class TCCProfileBuilder: NSObject {
4747
func buildTCCPolicies(allowed: Bool?, authorization: TCCPolicyAuthorizationValue?) -> [String: [TCCPolicy]] {
4848
return [
4949
"SystemPolicyAllFiles": [buildTCCPolicy(allowed: allowed, authorization: authorization)],
50-
"AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)]
50+
"AppleEvents": [buildTCCPolicy(allowed: allowed, authorization: authorization)],
51+
"BluetoothAlways": [buildTCCPolicy(allowed: allowed, authorization: authorization)],
52+
"SystemPolicyAppBundles": [buildTCCPolicy(allowed: allowed, authorization: authorization)],
53+
"SystemPolicyAppData": [buildTCCPolicy(allowed: allowed, authorization: authorization)]
5154
]
5255
}
5356

PPPC UtilityTests/ModelTests/ExecutableTests.swift

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,60 +34,62 @@ import Testing
3434
struct ExecutableTests {
3535
let executable = Executable()
3636

37-
@Test("Display name uses last component of bundle identifier")
38-
func generateDisplayNameBundleIdentifier() {
37+
@Test(
38+
"Display name uses last component of identifier",
39+
arguments: [
40+
("com.example.MyApp", "MyApp"),
41+
("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", "Terminal"),
42+
("Terminal", "Terminal")
43+
])
44+
func generateDisplayName(identifier: String, expected: String) {
3945
// when
40-
let displayName = executable.generateDisplayName(identifier: "com.example.MyApp")
46+
let displayName = executable.generateDisplayName(identifier: identifier)
4147

4248
// then
43-
#expect(displayName == "MyApp")
49+
#expect(displayName == expected)
4450
}
4551

46-
@Test("Display name uses last component of path identifier")
47-
func generateDisplayNamePathIdentifier() {
52+
@Test(
53+
"Icon path matches identifier type",
54+
arguments: [
55+
("com.example.MyApp", "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns"),
56+
("/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ExecutableBinaryIcon.icns")
57+
])
58+
func generateIconPath(identifier: String, expected: String) {
4859
// when
49-
let displayName = executable.generateDisplayName(identifier: "/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal")
60+
let iconPath = executable.generateIconPath(identifier: identifier)
5061

5162
// then
52-
#expect(displayName == "Terminal")
63+
#expect(iconPath == expected)
5364
}
5465

55-
@Test("Display name returns identifier when single component")
56-
func generateDisplayNameSingleComponent() {
57-
// when
58-
let displayName = executable.generateDisplayName(identifier: "Terminal")
59-
60-
// then
61-
#expect(displayName == "Terminal")
62-
}
63-
64-
@Test("Icon path is application for bundle identifier")
65-
func generateIconPathForBundleIdentifier() {
66-
// when
67-
let iconPath = executable.generateIconPath(identifier: "com.example.MyApp")
68-
69-
// then
70-
#expect(iconPath == IconFilePath.application)
71-
}
72-
73-
@Test("Icon path is binary for path identifier")
74-
func generateIconPathForPathIdentifier() {
75-
// when
76-
let iconPath = executable.generateIconPath(identifier: "/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal")
77-
78-
// then
79-
#expect(iconPath == IconFilePath.binary)
80-
}
81-
82-
@Test("All policy values default to dash")
83-
func allPolicyValuesAreDefaults() {
66+
@Test("All policy properties default to dash")
67+
func policyPropertiesDefaultToDash() {
8468
let policy = Policy()
8569

86-
// when
87-
let values = policy.allPolicyValues()
88-
8970
// then
90-
#expect(values.count == 20)
91-
#expect(values.allSatisfy { $0 == "-" })
71+
#expect(policy.Accessibility == "-")
72+
#expect(policy.AddressBook == "-")
73+
#expect(policy.BluetoothAlways == "-")
74+
#expect(policy.Calendar == "-")
75+
#expect(policy.Camera == "-")
76+
#expect(policy.FileProviderPresence == "-")
77+
#expect(policy.ListenEvent == "-")
78+
#expect(policy.MediaLibrary == "-")
79+
#expect(policy.Microphone == "-")
80+
#expect(policy.Photos == "-")
81+
#expect(policy.PostEvent == "-")
82+
#expect(policy.Reminders == "-")
83+
#expect(policy.ScreenCapture == "-")
84+
#expect(policy.SpeechRecognition == "-")
85+
#expect(policy.SystemPolicyAllFiles == "-")
86+
#expect(policy.SystemPolicyAppBundles == "-")
87+
#expect(policy.SystemPolicyAppData == "-")
88+
#expect(policy.SystemPolicyDesktopFolder == "-")
89+
#expect(policy.SystemPolicyDocumentsFolder == "-")
90+
#expect(policy.SystemPolicyDownloadsFolder == "-")
91+
#expect(policy.SystemPolicyNetworkVolumes == "-")
92+
#expect(policy.SystemPolicyRemovableVolumes == "-")
93+
#expect(policy.SystemPolicySysAdminFiles == "-")
9294
}
9395
}

PPPC UtilityTests/ModelTests/ModelTests.swift

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -267,39 +267,46 @@ struct ModelTests {
267267
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny")
268268
}
269269

270-
// MARK: - tests for policyFromString
271-
272-
@Test
273-
func policyWhenUsingAllow() {
274-
let app = Executable(identifier: "id", codeRequirement: "req")
270+
// MARK: - Service key round-trips
271+
272+
@Test(
273+
"Service key round-trip preserves value",
274+
arguments: [
275+
("BluetoothAlways", "Allow"),
276+
("SystemPolicyAppBundles", "Deny"),
277+
("SystemPolicyAppData", "Allow")
278+
])
279+
func serviceKeyRoundTrip(serviceKey: String, value: String) async {
280+
let model = ModelBuilder()
281+
.addExecutable(settings: [serviceKey: value])
282+
.build()
275283

276284
// when
277-
let policy = model.policyFromString(executable: app, value: "Allow")
285+
let profile = model.exportProfile(organization: "Org", identifier: "ID", displayName: "Name", payloadDescription: "Desc")
286+
await model.importProfile(tccProfile: profile)
278287

279288
// then
280-
#expect(policy?.authorization == .allow)
289+
let result = model.selectedExecutables.last?.policy.value(forKey: serviceKey) as? String
290+
#expect(result == value)
281291
}
282292

283-
@Test
284-
func policyWhenUsingDeny() {
285-
let app = Executable(identifier: "id", codeRequirement: "req")
286-
287-
// when
288-
let policy = model.policyFromString(executable: app, value: "Deny")
289-
290-
// then
291-
#expect(policy?.authorization == .deny)
292-
}
293+
// MARK: - tests for policyFromString
293294

294-
@Test
295-
func policyWhenUsingAllowForStandardUsers() {
295+
@Test(
296+
"policyFromString maps display value to authorization",
297+
arguments: [
298+
("Allow", TCCPolicyAuthorizationValue.allow),
299+
("Deny", TCCPolicyAuthorizationValue.deny),
300+
("Let Standard Users Approve", TCCPolicyAuthorizationValue.allowStandardUserToSetSystemService)
301+
])
302+
func policyFromStringAuthorization(value: String, expected: String) {
296303
let app = Executable(identifier: "id", codeRequirement: "req")
297304

298305
// when
299-
let policy = model.policyFromString(executable: app, value: "Let Standard Users Approve")
306+
let policy = model.policyFromString(executable: app, value: value)
300307

301308
// then
302-
#expect(policy?.authorization == .allowStandardUserToSetSystemService)
309+
#expect(policy?.authorization == expected)
303310
}
304311

305312
@Test

PPPC UtilityTests/ModelTests/PPPCServicesManagerTests.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,22 @@ struct PPPCServicesManagerTests {
3939
let actual = PPPCServicesManager()
4040

4141
// then
42-
#expect(actual.allServices.count == 21)
42+
#expect(actual.allServices.count == 24)
43+
}
44+
45+
@Test(
46+
"Service is loaded with correct English name",
47+
arguments: [
48+
("BluetoothAlways", "Bluetooth Always"),
49+
("SystemPolicyAppBundles", "App Bundles"),
50+
("SystemPolicyAppData", "App Data")
51+
])
52+
func serviceIsLoaded(key: String, expectedName: String) throws {
53+
let services = PPPCServicesManager()
54+
let service = try #require(services.allServices[key])
55+
56+
// then
57+
#expect(service.englishName == expectedName)
4358
}
4459

4560
@Test("User help with entitlements")

PPPC UtilityTests/TCCProfileImporterTests/TCCProfileImporterTests.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,31 @@ struct TCCProfileImporterTests {
7272

7373
let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
7474
#expect(!tccProfile.content.isEmpty)
75-
#expect(!tccProfile.content[0].services.isEmpty)
75+
let services = tccProfile.content[0].services
76+
#expect(!services.isEmpty)
77+
#expect(services.count == 24, "Profile should contain exactly 24 services")
78+
#expect(services["BluetoothAlways"] != nil, "BluetoothAlways service should exist")
79+
#expect(services["SystemPolicyAppBundles"] != nil, "SystemPolicyAppBundles service should exist")
80+
#expect(services["SystemPolicyAppData"] != nil, "SystemPolicyAppData service should exist")
81+
}
82+
83+
@Test("Legacy profile without new keys imports with dash defaults")
84+
func legacyProfileImportsWithDashDefaults() async throws {
85+
let tccProfileImporter = TCCProfileImporter()
86+
let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile-Legacy")
87+
let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
88+
89+
// when
90+
let model = Model()
91+
await model.importProfile(tccProfile: tccProfile)
92+
93+
// then
94+
#expect(!model.selectedExecutables.isEmpty, "Legacy profile should import at least one executable")
95+
for executable in model.selectedExecutables {
96+
#expect(executable.policy.BluetoothAlways == "-", "BluetoothAlways should default to dash for legacy profiles")
97+
#expect(executable.policy.SystemPolicyAppBundles == "-", "SystemPolicyAppBundles should default to dash for legacy profiles")
98+
#expect(executable.policy.SystemPolicyAppData == "-", "SystemPolicyAppData should default to dash for legacy profiles")
99+
}
76100
}
77101

78102
@Test

PPPC UtilityTests/TCCProfileImporterTests/TCCProfileTests.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ struct TCCProfileTests {
6363
#expect(content.version == 1)
6464

6565
// then verify the services key
66-
#expect(content.services.count == 2)
66+
#expect(content.services.count == 5)
6767
let allFiles = content.services["SystemPolicyAllFiles"]
6868
#expect(allFiles?.count == 1)
6969
allFiles?.forEach { policy in
@@ -77,6 +77,9 @@ struct TCCProfileTests {
7777
#expect(policy.receiverIdentifierType == "policy receiver id type")
7878
#expect(policy.receiverCodeRequirement == "policy receiver code req")
7979
}
80+
#expect(content.services["BluetoothAlways"] != nil, "BluetoothAlways should be included")
81+
#expect(content.services["SystemPolicyAppBundles"] != nil, "SystemPolicyAppBundles should be included")
82+
#expect(content.services["SystemPolicyAppData"] != nil, "SystemPolicyAppData should be included")
8083
}
8184
}
8285

@@ -108,7 +111,7 @@ struct TCCProfileTests {
108111
#expect(content.version == 1)
109112

110113
// then verify the services key
111-
#expect(content.services.count == 2)
114+
#expect(content.services.count == 5)
112115
let allFiles = content.services["SystemPolicyAllFiles"]
113116
#expect(allFiles?.count == 1)
114117
allFiles?.forEach { policy in
@@ -141,13 +144,7 @@ struct TCCProfileTests {
141144
#expect(content.version == 1)
142145

143146
// then verify the services key
144-
#expect(content.services.count == 2)
145-
let allFiles = content.services["SystemPolicyAllFiles"]
146-
#expect(allFiles?.count == 1)
147-
allFiles?.forEach { policy in
148-
#expect(policy.allowed == false)
149-
#expect(policy.authorization == .allow)
150-
}
147+
#expect(content.services.count == 5)
151148
}
152149
}
153150

0 commit comments

Comments
 (0)