fleet/server/mdm/apple/util_test.go
Magnus Jensen 8dbc73685f
mark 12075 MDMErrorDomain as Profile not found (#43872)
An additional case spotted on iPhones like the 89 error code shown on
Mac.

That we want to see as a valid profile removal

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CommandUUID</key>
	<string>0c53ebff-93cf-4599-853c-db6b582ff929</string>
	<key>ErrorChain</key>
	<array>
		<dict>
			<key>ErrorCode</key>
			<integer>12075</integer>
			<key>ErrorDomain</key>
			<string>MDMErrorDomain</string>
			<key>LocalizedDescription</key>
			<string>The profile “Fleet.WiFi” is not installed.</string>
			<key>USEnglishDescription</key>
			<string>The profile “Fleet.WiFi” is not installed.</string>
		</dict>
	</array>
	<key>Status</key>
	<string>Error</string>
	<key>UDID</key>
	<string>REDACTED</string>
</dict>
</plist>
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved detection of "profile not found" cases in Apple MDM by
recognizing an additional error signature, reducing missed detections.
* **Tests**
* Added unit tests covering the new signature, negative cases, and mixed
error chains to ensure reliable behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-22 13:49:12 -06:00

231 lines
6.6 KiB
Go

package apple_mdm
import (
"strings"
"testing"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"howett.net/plist"
)
func TestMDMAppleEnrollURL(t *testing.T) {
cases := []struct {
appConfig *fleet.AppConfig
expectedURL string
}{
{
appConfig: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://foo.example.com",
},
},
expectedURL: "https://foo.example.com/api/mdm/apple/enroll?token=tok",
},
{
appConfig: &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://foo.example.com/",
},
},
expectedURL: "https://foo.example.com/api/mdm/apple/enroll?token=tok",
},
}
for _, tt := range cases {
enrollURL, err := EnrollURL("tok", tt.appConfig)
require.NoError(t, err)
require.Equal(t, tt.expectedURL, enrollURL)
}
}
func TestGenerateRandomPin(t *testing.T) {
for i := 1; i <= 100; i++ {
pin, err := GenerateRandomPin(i)
require.NoError(t, err)
require.Len(t, pin, i)
}
}
func TestIsProfileNotFoundError(t *testing.T) {
cases := []struct {
name string
chain []mdm.ErrorChain
expected bool
}{
{
name: "empty chain",
chain: nil,
expected: false,
},
{
name: "MDMClientError 89 - profile not found",
chain: []mdm.ErrorChain{
{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'com.example' not found."},
},
expected: true,
},
{
name: "different MDMClientError code",
chain: []mdm.ErrorChain{
{ErrorCode: 90, ErrorDomain: "MDMClientError", USEnglishDescription: "Some other error"},
},
expected: false,
},
{
name: "different error domain with code 89",
chain: []mdm.ErrorChain{
{ErrorCode: 89, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "Some error"},
},
expected: false,
},
{
name: "profile not found in chain with other errors",
chain: []mdm.ErrorChain{
{ErrorCode: 100, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "First error"},
{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'com.example' not found."},
},
expected: true,
},
{
name: "MDMErrorDomain 12075 - profile not installed",
chain: []mdm.ErrorChain{
{ErrorCode: 12075, ErrorDomain: "MDMErrorDomain", USEnglishDescription: "The profile 'com.example' is not installed."},
},
expected: true,
},
{
name: "different MDMErrorDomain code",
chain: []mdm.ErrorChain{
{ErrorCode: 12076, ErrorDomain: "MDMErrorDomain", USEnglishDescription: "Some other error"},
},
expected: false,
},
{
name: "different error domain with code 12075",
chain: []mdm.ErrorChain{
{ErrorCode: 12075, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "Some error"},
},
expected: false,
},
{
name: "profile not installed in chain with other errors",
chain: []mdm.ErrorChain{
{ErrorCode: 100, ErrorDomain: "SomeOtherDomain", USEnglishDescription: "First error"},
{ErrorCode: 12075, ErrorDomain: "MDMErrorDomain", USEnglishDescription: "The profile 'com.example' is not installed."},
},
expected: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
result := IsProfileNotFoundError(tt.chain)
require.Equal(t, tt.expected, result)
})
}
}
func TestIsRecoveryLockPasswordMismatchError(t *testing.T) {
cases := []struct {
name string
chain []mdm.ErrorChain
expected bool
}{
{
name: "empty chain",
chain: nil,
expected: false,
},
{
name: "MDMClientError 70 - existing password not provided",
chain: []mdm.ErrorChain{
{ErrorCode: 70, ErrorDomain: "MDMClientError", LocalizedDescription: "Existing recovery lock password not provided"},
},
expected: true,
},
{
name: "ROSLockoutServiceDaemonErrorDomain 8 - password failed to validate",
chain: []mdm.ErrorChain{
{ErrorCode: 8, ErrorDomain: "ROSLockoutServiceDaemonErrorDomain", LocalizedDescription: "The provided recovery password failed to validate."},
},
expected: true,
},
{
name: "different MDMClientError code",
chain: []mdm.ErrorChain{
{ErrorCode: 71, ErrorDomain: "MDMClientError", LocalizedDescription: "Some other error"},
},
expected: false,
},
{
name: "different error domain",
chain: []mdm.ErrorChain{
{ErrorCode: 70, ErrorDomain: "SomeOtherDomain", LocalizedDescription: "Some error"},
},
expected: false,
},
{
name: "generic transient error",
chain: []mdm.ErrorChain{
{ErrorCode: 12345, ErrorDomain: "test", LocalizedDescription: "Network timeout"},
},
expected: false,
},
{
name: "password mismatch in chain with other errors",
chain: []mdm.ErrorChain{
{ErrorCode: 100, ErrorDomain: "SomeOtherDomain", LocalizedDescription: "First error"},
{ErrorCode: 8, ErrorDomain: "ROSLockoutServiceDaemonErrorDomain", LocalizedDescription: "The provided recovery password failed to validate."},
},
expected: true,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
result := IsRecoveryLockPasswordMismatchError(tt.chain)
require.Equal(t, tt.expected, result)
})
}
}
func TestGenerateManagedAccountPassword(t *testing.T) {
pw := GenerateManagedAccountPassword()
// Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX (6 groups of 4 chars separated by dashes)
groups := strings.Split(pw, "-")
require.Len(t, groups, ManagedAccountPasswordGroupCount)
for _, g := range groups {
require.Len(t, g, ManagedAccountPasswordGroupLen)
for _, c := range g {
assert.Contains(t, RecoveryLockPasswordCharset, string(c))
}
}
// Two calls should produce different passwords (with overwhelming probability).
pw2 := GenerateManagedAccountPassword()
require.NotEqual(t, pw, pw2)
}
func TestGenerateSaltedSHA512PBKDF2Hash(t *testing.T) {
data, err := GenerateSaltedSHA512PBKDF2Hash("test-password")
require.NoError(t, err)
require.NotEmpty(t, data)
// Parse the plist and verify the structure.
var result saltedSHA512PBKDF2
_, err = plist.Unmarshal(data, &result)
require.NoError(t, err)
assert.Len(t, result.PBKDF2.Salt, pbkdf2SaltLen, "salt should be %d bytes", pbkdf2SaltLen)
assert.Len(t, result.PBKDF2.Entropy, pbkdf2KeyLen, "entropy should be %d bytes", pbkdf2KeyLen)
assert.Equal(t, pbkdf2Iterations, result.PBKDF2.Iterations)
// Two calls with the same password should produce different outputs (different random salts).
data2, err := GenerateSaltedSHA512PBKDF2Hash("test-password")
require.NoError(t, err)
require.NotEqual(t, data, data2)
}