Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8f22710
AI generated removal of the big sur compatibility logic. Needs vetted…
watkyn Jan 29, 2026
bcb3a12
big-sur story/JPCFM-5531 Add minor changes to update minimum supporte…
jjpritzl Mar 10, 2026
5efa226
big-sur story/JPCFM-5531 Remove swiftlint disable call due to reduced…
jjpritzl Mar 10, 2026
93ed2be
big-sur story/JPCFM-5531 Add Copilot suggestion for generating test r…
jjpritzl Mar 13, 2026
dc8df63
big-sur story/JPCFM-5531 Add Copilot refactor to loadExecutable function
jjpritzl Mar 13, 2026
343399a
big-sur story/JPCFM-5531 Remove code coverage addition
jjpritzl Mar 24, 2026
1e0027e
added detailed plan for getting to swift 6 in stages
watkyn Mar 26, 2026
0a14250
Initial plan
Copilot Mar 26, 2026
6741832
Stage 2: Convert NetworkAuthManager from actor to class
Copilot Mar 26, 2026
9256bd0
Stage 3a: Remove DispatchQueue.main.async wrappers
Copilot Mar 26, 2026
7b77113
Stage 3b: Convert UploadManager to async throws
Copilot Mar 26, 2026
a64ed56
Stage 3c: Convert Model.loadExecutable to direct return
Copilot Mar 26, 2026
542a287
Stage 3d: Convert TCCProfileImporter to direct return
Copilot Mar 26, 2026
f38e4fc
Stage 4: Add @concurrent for background I/O
Copilot Mar 26, 2026
23583a4
Stage 5: Enable Swift 6 language mode
Copilot Mar 26, 2026
c24c1e9
create-separate story/JPCFM-5564 Fix failing build and tests
jjpritzl Mar 31, 2026
24d36a5
create-separate story/JPCFM-5564 Fix code signing mistake
jjpritzl Mar 31, 2026
cf78ace
create-separate story/JPCFM-5564 Fix bad syntax in build settings
jjpritzl Mar 31, 2026
60390d5
create-separate story/JPCFM-5564 Fix bad syntax in project settings f…
jjpritzl Mar 31, 2026
2f85f1b
create-separate story/JPCFM-5564 Try to fix failing tests on GH PR
jjpritzl Apr 1, 2026
2ed25d9
create-separate story/JPCFM-5564 Try to fix unit test check issue
jjpritzl Apr 1, 2026
83f107d
create-separate story/JPCFM-5564 Make actual change with the plan to …
jjpritzl Apr 1, 2026
b28109d
create-separate story/JPCFM-5564 merge main and fix merge conflicts
jjpritzl Apr 1, 2026
b165a85
Merge branch 'master' of github.com:jamf/PPPC-Utility into copilot/cr…
watkyn Apr 2, 2026
b3db45b
removed some uneeded concurrency annotations
watkyn Apr 2, 2026
113fcad
removed a couple more spots
watkyn Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` is set on both app and test targets — don't add explicit `@MainActor` to production code or test structs/functions, it's already the default

## Swift Testing Migration
## Swift Testing Conventions

- Place `@Test` and `@Suite` annotations on the line **above** the declaration, not inline
- Use `// when` and `// then` comment blocks; skip `// given` (assumed from context)
Expand Down
16 changes: 14 additions & 2 deletions PPPC Utility.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,10 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jamf.PPPC-UtilityTests";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility";
Comment on lines 560 to 568
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app target is switched to SWIFT_VERSION = 6.0 but the test target remains on Swift 5.0. This can allow concurrency issues to slip through tests (warnings vs errors) and makes compiler behavior differ between targets. If the goal is full strict concurrency enforcement, align the test target’s Swift version with the app target.

Copilot uses AI. Check for mistakes.
};
Expand All @@ -585,6 +588,9 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.jamf.PPPC-UtilityTests";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/PPPC Utility.app/Contents/MacOS/PPPC Utility";
};
Expand Down Expand Up @@ -727,7 +733,10 @@
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
Expand All @@ -751,7 +760,10 @@
PRODUCT_BUNDLE_IDENTIFIER = com.jamf.opensource.pppcutility;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
};
name = Release;
};
Expand Down
56 changes: 28 additions & 28 deletions PPPC UtilityTests/ModelTests/ModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ struct ModelTests {
// MARK: - tests for getExecutableFrom*

@Test
func getExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() {
func getExecutableBasedOnIdentifierAndCodeRequirement_BundleIdentifierType() async {
let identifier = "com.example.App"
let codeRequirement = "testCodeRequirement"

// when
let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)
let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)

// then
#expect(executable.displayName == "App")
Expand All @@ -52,12 +52,12 @@ struct ModelTests {
}

@Test
func getExecutableBasedOnIdentifierAndCodeRequirement_PathIdentifierType() {
func getExecutableBasedOnIdentifierAndCodeRequirement_PathIdentifierType() async {
let identifier = "/myGreatPath/Awesome/Binary"
let codeRequirement = "testCodeRequirement"

// when
let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)
let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)

// then
#expect(executable.displayName == "Binary")
Expand All @@ -66,12 +66,12 @@ struct ModelTests {
}

@Test
func getExecutableFromComputerBasedOnIdentifier() {
func getExecutableFromComputerBasedOnIdentifier() async {
let identifier = "com.apple.Safari"
let codeRequirement = "randomReq"

// when
let executable = model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)
let executable = await model.getExecutableFrom(identifier: identifier, codeRequirement: codeRequirement)

// then
#expect(executable.displayName == "Safari")
Expand All @@ -80,10 +80,10 @@ struct ModelTests {
}

@Test
func getExecutableFromSelectedExecutables() {
func getExecutableFromSelectedExecutables() async {
let expectedIdentifier = "com.something.1"
let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq")
let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2")
let executable = await model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq")
let executableSecond = await model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2")
model.selectedExecutables = [executable, executableSecond]

// when
Expand All @@ -97,11 +97,11 @@ struct ModelTests {
}

@Test
func getExecutableFromSelectedExecutables_Path() {
func getExecutableFromSelectedExecutables_Path() async {
let expectedIdentifier = "/path/something/Special"
let executableOneMore = model.getExecutableFrom(identifier: "/path/something/Special1", codeRequirement: "testReq")
let executable = model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq")
let executableSecond = model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2")
let executableOneMore = await model.getExecutableFrom(identifier: "/path/something/Special1", codeRequirement: "testReq")
let executable = await model.getExecutableFrom(identifier: expectedIdentifier, codeRequirement: "testReq")
let executableSecond = await model.getExecutableFrom(identifier: "com.something.2", codeRequirement: "testReq2")
model.selectedExecutables = [executableOneMore, executable, executableSecond]

// when
Expand Down Expand Up @@ -184,90 +184,90 @@ struct ModelTests {
// MARK: - tests for importProfile

@Test
func importProfileUsingAuthorizationKeyAllow() {
func importProfileUsingAuthorizationKeyAllow() async {
let profile = TCCProfileBuilder().buildProfile(authorization: .allow)

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Allow")
}

@Test
func importProfileUsingAuthorizationKeyDeny() {
func importProfileUsingAuthorizationKeyDeny() async {
let profile = TCCProfileBuilder().buildProfile(authorization: .deny)

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny")
}

@Test
func importProfileUsingAuthorizationKeyAllowStandardUsers() {
func importProfileUsingAuthorizationKeyAllowStandardUsers() async {
let profile = TCCProfileBuilder().buildProfile(authorization: .allowStandardUserToSetSystemService)

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Let Standard Users Approve")
}

@Test
func importProfileUsingLegacyAllowKeyTrue() {
func importProfileUsingLegacyAllowKeyTrue() async {
let profile = TCCProfileBuilder().buildProfile(allowed: true)

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Allow")
}

@Test
func importProfileUsingLegacyAllowKeyFalse() {
func importProfileUsingLegacyAllowKeyFalse() async {
let profile = TCCProfileBuilder().buildProfile(allowed: false)

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny")
}

@Test
func importProfileUsingAuthorizationKeyThatIsInvalid() {
func importProfileUsingAuthorizationKeyThatIsInvalid() async {
let profile = TCCProfileBuilder().buildProfile(authorization: "invalidkey")

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny")
}

@Test
func importProfileUsingAuthorizationKeyTranslatesToAppleEvents() {
func importProfileUsingAuthorizationKeyTranslatesToAppleEvents() async {
let profile = TCCProfileBuilder().buildProfile(authorization: "deny")

// when
model.importProfile(tccProfile: profile)
await model.importProfile(tccProfile: profile)

// then
#expect(model.selectedExecutables.count == 1)
#expect(model.selectedExecutables.first?.policy.SystemPolicyAllFiles == "Deny")
}

// MARK: - tests for profileToString
// MARK: - tests for policyFromString

@Test
func policyWhenUsingAllow() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ struct NetworkAuthManagerTests {
networking.errorToThrow = NetworkingError.serverResponse(404, "No such page")

// default is that bearer auth is supported.
let firstCheckBearerAuthSupported = await authManager.bearerAuthSupported()
let firstCheckBearerAuthSupported = authManager.bearerAuthSupported()
#expect(firstCheckBearerAuthSupported)

// when/then
Expand All @@ -94,7 +94,7 @@ struct NetworkAuthManagerTests {
}

// The authManager should now know that bearer auth is not supported
let secondCheckBearerAuthSupported = await authManager.bearerAuthSupported()
let secondCheckBearerAuthSupported = authManager.bearerAuthSupported()
#expect(!secondCheckBearerAuthSupported)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,13 @@ struct TCCProfileImporterTests {
let tccProfileImporter = TCCProfileImporter()
let resourceURL = try getResourceProfile(fileName: "TestTCCProfileSigned-Broken")

tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
switch tccProfileResult {
case .success:
Issue.record("Malformed profile should not succeed")
case .failure(let tccProfileError):
if case TCCProfileImportError.invalidProfileFile = tccProfileError {
} else {
Issue.record("Expected invalidProfileFile error, got \(tccProfileError)")
}
do {
_ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
Issue.record("Malformed profile should not succeed")
} catch {
if case TCCProfileImportError.invalidProfileFile = error {
} else {
Issue.record("Expected invalidProfileFile error, got \(error)")
}
}
}
Expand All @@ -59,13 +57,11 @@ struct TCCProfileImporterTests {
let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile-Empty")
let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "PayloadContent")

tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
switch tccProfileResult {
case .success:
Issue.record("Empty Content, it shouldn't be success")
case .failure(let tccProfileError):
#expect(tccProfileError.localizedDescription == expectedTCCProfileError.localizedDescription)
}
do {
_ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
Issue.record("Empty Content, it shouldn't be success")
} catch {
#expect(error.localizedDescription == expectedTCCProfileError.localizedDescription)
}
}

Expand All @@ -74,31 +70,19 @@ struct TCCProfileImporterTests {
let tccProfileImporter = TCCProfileImporter()
let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile")

tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
switch tccProfileResult {
case .success(let tccProfile):
#expect(!tccProfile.content.isEmpty)
#expect(!tccProfile.content[0].services.isEmpty)
case .failure(let tccProfileError):
Issue.record("Unable to read tccProfile \(tccProfileError.localizedDescription)")
}
}
let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
#expect(!tccProfile.content.isEmpty)
#expect(!tccProfile.content[0].services.isEmpty)
}

@Test
func correctUnsignedProfileContentDataAllLowercase() throws {
let tccProfileImporter = TCCProfileImporter()
let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile-allLower")

tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
switch tccProfileResult {
case .success(let tccProfile):
#expect(!tccProfile.content.isEmpty)
#expect(!tccProfile.content[0].services.isEmpty)
case .failure(let tccProfileError):
Issue.record("Unable to read tccProfile \(tccProfileError.localizedDescription)")
}
}
let tccProfile = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
#expect(!tccProfile.content.isEmpty)
#expect(!tccProfile.content[0].services.isEmpty)
}

@Test
Expand All @@ -107,13 +91,11 @@ struct TCCProfileImporterTests {
let resourceURL = try getResourceProfile(fileName: "TestTCCUnsignedProfile-Broken")
let expectedTCCProfileError = TCCProfileImportError.invalidProfileFile(description: "The given data was not a valid property list.")

tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL) { tccProfileResult in
switch tccProfileResult {
case .success:
Issue.record("Broken Unsigned Profile, it shouldn't be success")
case .failure(let tccProfileError):
#expect(tccProfileError.localizedDescription == expectedTCCProfileError.localizedDescription)
}
do {
_ = try tccProfileImporter.decodeTCCProfile(fileUrl: resourceURL)
Issue.record("Broken Unsigned Profile, it shouldn't be success")
} catch {
#expect(error.localizedDescription == expectedTCCProfileError.localizedDescription)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ struct TCCProfileTests {
// unit tests for handling both Auth and allowed keys should fail?

@Test
func settingLegacyAllowValueNullifiesAuthorization() throws {
func settingLegacyAllowValueNullifiesAuthorization() {
var tccPolicy = TCCPolicy(identifier: "id", codeRequirement: "req", receiverIdentifier: "recId", receiverCodeRequirement: "recreq")
tccPolicy.authorization = .allow

Expand All @@ -180,12 +180,12 @@ struct TCCProfileTests {
}

@Test
func jamfProAPIData() throws {
func jamfProAPIData() async throws {
let tccProfile = TCCProfileBuilder().buildProfile(allowed: false, authorization: .allow)
let expected = try loadTextFile(fileName: "TestTCCProfileForJamfProAPI").trimmingCharacters(in: .whitespacesAndNewlines)

// when
let data = try tccProfile.jamfProAPIData(signingIdentity: nil, site: nil)
let data = try await tccProfile.jamfProAPIData(signingIdentity: nil, site: nil)

// then
let xmlString = String(data: data, encoding: .utf8)
Expand Down
2 changes: 1 addition & 1 deletion Source/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import Cocoa

@NSApplicationMain
@main
class AppDelegate: NSObject, NSApplicationDelegate {

func applicationDidFinishLaunching(_ aNotification: Notification) {}
Expand Down
Loading
Loading