fleet/server/datastore/mysql/android_test.go
Ian Littman 8e4e89f4e9
API + auth + UI changes for team labels (#37208)
Covers #36760, #36758.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually
2025-12-29 21:28:45 -06:00

2713 lines
102 KiB
Go

package mysql
import (
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAndroid(t *testing.T) {
ds := CreateMySQLDS(t)
TruncateTables(t, ds)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"NewAndroidHost", testNewAndroidHost},
{"UpdateAndroidHost", testUpdateAndroidHost},
{"AndroidMDMStats", testAndroidMDMStats},
{"AndroidHostStorageData", testAndroidHostStorageData},
{"NewMDMAndroidConfigProfile", testNewMDMAndroidConfigProfile},
{"GetMDMAndroidConfigProfile", testGetMDMAndroidConfigProfile},
{"DeleteMDMAndroidConfigProfile", testDeleteMDMAndroidConfigProfile},
{"GetMDMAndroidProfilesSummary", testMDMAndroidProfilesSummary},
{"ListMDMAndroidProfilesToSend", testListMDMAndroidProfilesToSend},
{"ListMDMAndroidProfilesToSend_WithExcludeAny", testListMDMAndroidProfilesToSendWithExcludeAny},
{"GetMDMAndroidProfilesContents", testGetMDMAndroidProfilesContents},
{"BulkUpsertMDMAndroidHostProfiles", testBulkUpsertMDMAndroidHostProfiles},
{"BulkUpsertMDMAndroidHostProfiles", testBulkUpsertMDMAndroidHostProfiles2},
{"BulkUpsertMDMAndroidHostProfiles", testBulkUpsertMDMAndroidHostProfiles3},
{"GetHostMDMAndroidProfiles", testGetHostMDMAndroidProfiles},
{"GetAndroidPolicyRequestByUUID", testGetAndroidPolicyRequestByUUID},
{"ListHostMDMAndroidProfilesPendingInstallWithVersion", testListHostMDMAndroidProfilesPendingInstallWithVersion},
{"BulkDeleteMDMAndroidHostProfiles", testBulkDeleteMDMAndroidHostProfiles},
{"BatchSetMDMAndroidProfiles_Associations", testBatchSetMDMAndroidProfiles_Associations},
{"NewAndroidHostWithIdP", testNewAndroidHostWithIdP},
{"AndroidBYODDetection", testAndroidBYODDetection},
{"SetAndroidHostUnenrolled", testSetAndroidHostUnenrolled},
{"BulkSetAndroidHostsUnenrolled", testBulkSetAndroidHostsUnenrolled},
{"InsertAndGetAndroidAppConfiguration", testInsertAndGetAndroidAppConfiguration},
{"UpdateAndroidAppConfiguration", testUpdateAndroidAppConfiguration},
{"DeleteAndroidAppConfiguration", testDeleteAndroidAppConfiguration},
{"GetAndroidAppConfiguration_NotFound", testGetAndroidAppConfigurationNotFound},
{"UpdateAndroidAppConfiguration_NotFound", testUpdateAndroidAppConfigurationNotFound},
{"DeleteAndroidAppConfiguration_NotFound", testDeleteAndroidAppConfigurationNotFound},
{"InsertAndroidAppConfiguration_Duplicate", testInsertAndroidAppConfigurationDuplicate},
{"AndroidAppConfiguration_CascadeDeleteTeam", testAndroidAppConfigurationCascadeDeleteTeam},
{"AndroidAppConfiguration_GlobalVsTeam", testAndroidAppConfigurationGlobalVsTeam},
{"AddDeleteAndroidAppWithConfiguration", testAddDeleteAndroidAppWithConfiguration},
{"HasAndroidAppConfigurationChanged", testHasAndroidAppConfigurationChanged},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testNewAndroidHost(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
const enterpriseSpecificID = "enterprise_specific_id"
host := createAndroidHost(enterpriseSpecificID)
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.NotZero(t, result.Host.ID)
assert.NotZero(t, result.Device.ID)
lbls, err := ds.ListLabelsForHost(testCtx(), result.Host.ID)
require.NoError(t, err)
require.Len(t, lbls, 2)
names := []string{lbls[0].Name, lbls[1].Name}
require.ElementsMatch(t, []string{fleet.BuiltinLabelNameAllHosts, fleet.BuiltinLabelNameAndroid}, names)
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultLite.Host.ID)
assert.Equal(t, result.Device.ID, resultLite.Device.ID)
resultLite, err = ds.AndroidHostLiteByHostUUID(testCtx(), result.Host.UUID)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultLite.Host.ID)
assert.Equal(t, result.Device.ID, resultLite.Device.ID)
_, err = ds.AndroidHostLite(testCtx(), "non-existent")
require.Error(t, err)
_, err = ds.AndroidHostLiteByHostUUID(testCtx(), "no-such-host")
require.Error(t, err)
// Inserting the same host again should be fine.
// This may occur when 2 Fleet servers received the same host information via pubsub.
resultCopy, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultCopy.Host.ID)
assert.Equal(t, result.Device.ID, resultCopy.Device.ID)
// create another host, this time delete the Android label
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(testCtx(), `DELETE FROM labels WHERE name = ?`, fleet.BuiltinLabelNameAndroid)
return err
})
const enterpriseSpecificID2 = "enterprise_specific_id2"
host2 := createAndroidHost(enterpriseSpecificID2)
// still passes, but no label membership was recorded
result, err = ds.NewAndroidHost(testCtx(), host2)
require.NoError(t, err)
lbls, err = ds.ListLabelsForHost(testCtx(), result.Host.ID)
require.NoError(t, err)
require.Empty(t, lbls)
}
func createAndroidHost(enterpriseSpecificID string) *fleet.AndroidHost {
// Device ID needs to be unique per device
deviceID := md5ChecksumBytes([]byte(enterpriseSpecificID))[:16]
host := &fleet.AndroidHost{
Host: &fleet.Host{
Hostname: "hostname",
ComputerName: "computer_name",
Platform: "android",
OSVersion: "Android 14",
Build: "build",
Memory: 1024,
TeamID: nil,
HardwareSerial: "hardware_serial",
CPUType: "cpu_type",
HardwareModel: "hardware_model",
HardwareVendor: "hardware_vendor",
UUID: enterpriseSpecificID,
},
Device: &android.Device{
DeviceID: deviceID,
EnterpriseSpecificID: ptr.String(enterpriseSpecificID),
AppliedPolicyID: ptr.String("1"),
AppliedPolicyVersion: ptr.Int64(1),
LastPolicySyncTime: ptr.Time(time.Now().UTC().Truncate(time.Millisecond)),
},
}
host.SetNodeKey(enterpriseSpecificID)
return host
}
func testCtx() context.Context {
return context.Background()
}
func testUpdateAndroidHost(t *testing.T, ds *Datastore) {
const enterpriseSpecificID = "es_id_update"
host := createAndroidHost(enterpriseSpecificID)
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
assert.NotZero(t, result.Host.ID)
assert.NotZero(t, result.Device.ID)
// Dummy update
err = ds.UpdateAndroidHost(testCtx(), result, false)
require.NoError(t, err)
host = result
host.Host.DetailUpdatedAt = time.Now()
host.Host.LabelUpdatedAt = time.Now()
host.Host.Hostname = "hostname_updated"
host.Host.ComputerName = "computer_name_updated"
host.Host.Platform = "android_updated"
host.Host.OSVersion = "Android 15"
host.Host.Build = "build_updated"
host.Host.Memory = 2048
host.Host.HardwareSerial = "hardware_serial_updated"
host.Host.CPUType = "cpu_type_updated"
host.Host.HardwareModel = "hardware_model_updated"
host.Host.HardwareVendor = "hardware_vendor_updated"
host.Device.AppliedPolicyID = ptr.String("2")
// Make sure host UUID is preserved during update
host.Host.UUID = enterpriseSpecificID
err = ds.UpdateAndroidHost(testCtx(), host, false)
require.NoError(t, err)
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
assert.Equal(t, host.Host.ID, resultLite.Host.ID)
assert.EqualValues(t, host.Device, resultLite.Device)
// Make sure UUID was preserved after update
assert.Equal(t, enterpriseSpecificID, resultLite.Host.UUID, "UUID should be preserved after UpdateAndroidHost")
// Regression: empty UUID doesn't corrupt existing data
// This simulates a scenario where updateHost might not set UUID, resulting in empty value
t.Run("Empty UUID regression test", func(t *testing.T) {
const regressionESID = "regression-uuid-test"
regressionHost := createAndroidHost(regressionESID)
createdHost, err := ds.NewAndroidHost(testCtx(), regressionHost)
require.NoError(t, err)
require.Equal(t, regressionESID, createdHost.Host.UUID)
// Simulate update where UUID might be accidentally cleared
hostWithEmptyUUID := createdHost
hostWithEmptyUUID.Host.UUID = ""
hostWithEmptyUUID.Host.Hostname = "regression-hostname"
// This should still work but UUID should be empty
err = ds.UpdateAndroidHost(testCtx(), hostWithEmptyUUID, false)
require.NoError(t, err)
// UUID is now empty
resultAfterBug, err := ds.AndroidHostLite(testCtx(), regressionESID)
require.NoError(t, err)
assert.Equal(t, "", resultAfterBug.Host.UUID, "UUID should be empty after update without UUID set (documents the bug)")
// Update with UUID properly set
hostWithUUID := resultAfterBug
hostWithUUID.Host.UUID = regressionESID
hostWithUUID.Host.Hostname = "fixed-hostname"
err = ds.UpdateAndroidHost(testCtx(), hostWithUUID, false)
require.NoError(t, err)
// UUID is restored
resultAfterFix, err := ds.AndroidHostLite(testCtx(), regressionESID)
require.NoError(t, err)
assert.Equal(t, regressionESID, resultAfterFix.Host.UUID, "UUID should be restored after fix")
})
}
func testAndroidMDMStats(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
const appleMDMURL = "/mdm/apple/mdm"
const serverURL = "http://androidmdm.example.com"
appCfg, err := ds.AppConfig(testCtx())
require.NoError(t, err)
appCfg.ServerSettings.ServerURL = serverURL
err = ds.SaveAppConfig(testCtx(), appCfg)
require.NoError(t, err)
// create a few android hosts
hosts := make([]*fleet.Host, 3)
var androidHost0 *fleet.AndroidHost
for i := range hosts {
host := createAndroidHost(uuid.NewString())
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
hosts[i] = result.Host
if androidHost0 == nil {
androidHost0 = host
}
}
// create a non-android host
macHost, err := ds.NewHost(testCtx(), &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
Platform: "darwin",
HardwareSerial: uuid.NewString(),
})
require.NoError(t, err)
nanoEnroll(t, ds, macHost, false)
err = ds.MDMAppleUpsertHost(testCtx(), macHost, false)
require.NoError(t, err)
// create a non-mdm host
linuxHost, err := ds.NewHost(testCtx(), &fleet.Host{
Hostname: "test-host2-name",
OsqueryHostID: ptr.String("1338"),
NodeKey: ptr.String("1338"),
UUID: "test-uuid-2",
Platform: "linux",
HardwareSerial: uuid.NewString(),
})
require.NoError(t, err)
require.NotNil(t, linuxHost)
// stats not computed yet
statusStats, _, err := ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err := ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{}, statusStats)
require.Equal(t, []fleet.AggregatedMDMSolutions(nil), solutionsStats)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
// 3 Android hosts with UUID are counted as personal enrollment, 1 macOS host as manual
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 4, EnrolledManualHostsCount: 1, EnrolledPersonalHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 2)
// both solutions are Fleet
require.Equal(t, fleet.WellKnownMDMFleet, solutionsStats[0].Name)
require.Equal(t, fleet.WellKnownMDMFleet, solutionsStats[1].Name)
// one is the Android server URL, one is the Apple URL
for _, sol := range solutionsStats {
switch sol.ServerURL {
case serverURL:
require.Equal(t, 3, sol.HostsCount)
case serverURL + appleMDMURL:
require.Equal(t, 1, sol.HostsCount)
default:
require.Failf(t, "unexpected server URL: %v", sol.ServerURL)
}
}
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
// All 3 Android hosts with UUID are counted as personal enrollment
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, EnrolledPersonalHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 3, solutionsStats[0].HostsCount)
require.Equal(t, serverURL, solutionsStats[0].ServerURL)
// turn MDM off for android
err = ds.DeleteAllEnterprises(testCtx())
require.NoError(t, err)
err = ds.BulkSetAndroidHostsUnenrolled(testCtx())
require.NoError(t, err)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 4, EnrolledManualHostsCount: 1, UnenrolledHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 1, solutionsStats[0].HostsCount)
require.Equal(t, serverURL+appleMDMURL, solutionsStats[0].ServerURL)
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, UnenrolledHostsCount: 3}, statusStats)
require.Len(t, solutionsStats, 0)
// simulate an android host that re-enrolls
err = ds.UpdateAndroidHost(testCtx(), androidHost0, true)
require.NoError(t, err)
// compute stats
err = ds.GenerateAggregatedMunkiAndMDM(testCtx())
require.NoError(t, err)
// filter on android
statusStats, _, err = ds.AggregatedMDMStatus(testCtx(), nil, "android")
require.NoError(t, err)
solutionsStats, _, err = ds.AggregatedMDMSolutions(testCtx(), nil, "android")
require.NoError(t, err)
// After re-enrollment, 1 Android host with UUID is counted as personal enrollment
require.Equal(t, fleet.AggregatedMDMStatus{HostsCount: 3, UnenrolledHostsCount: 2, EnrolledPersonalHostsCount: 1}, statusStats)
require.Len(t, solutionsStats, 1)
require.Equal(t, 1, solutionsStats[0].HostsCount)
require.Equal(t, serverURL, solutionsStats[0].ServerURL)
}
// Test that BatchSetMDMProfiles properly inserts Android profiles when the
// incoming profiles have empty ProfileUUIDs and still applies label
// associations (i.e. matching by team_id + name works).
func testBatchSetMDMAndroidProfiles_Associations(t *testing.T, ds *Datastore) {
// Ensure builtin labels exist
test.AddBuiltinLabels(t, ds)
// Prepare an incoming Android profile without ProfileUUID and with a label
teamID := uint(0)
profName := "test-android-profile"
incoming := &fleet.MDMAndroidConfigProfile{
ProfileUUID: "", // intentionally empty to exercise DB-generated uuid flow
Name: profName,
RawJSON: json.RawMessage(`{"k":"v"}`),
TeamID: nil,
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{
LabelName: fleet.BuiltinLabelNameAndroid,
}},
}
// Look up the builtin Android label id and set it on the incoming profile
var lblID uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &lblID, `SELECT id FROM labels WHERE name = ?`, fleet.BuiltinLabelNameAndroid)
})
// assign the id so the label association insertion uses a valid FK
if len(incoming.LabelsIncludeAll) > 0 {
incoming.LabelsIncludeAll[0].LabelID = lblID
}
// Call BatchSetMDMProfiles with only android profiles populated
_, err := ds.BatchSetMDMProfiles(testCtx(), &teamID, nil, nil, nil, []*fleet.MDMAndroidConfigProfile{incoming}, nil)
require.NoError(t, err)
// Verify the profile exists in the DB
var dbCount int
err = sqlx.GetContext(testCtx(), ds.writer(testCtx()), &dbCount, `SELECT COUNT(1) FROM mdm_android_configuration_profiles WHERE name = ? AND team_id = ?`, profName, teamID)
require.NoError(t, err)
assert.Equal(t, 1, dbCount)
// Verify that a label association was created for the profile by querying
// mdm_configuration_profile_labels joined to mdm_android_configuration_profiles
var assocCount int
query := `SELECT COUNT(1) FROM mdm_configuration_profile_labels l JOIN mdm_android_configuration_profiles p ON l.android_profile_uuid = p.profile_uuid WHERE p.name = ? AND p.team_id = ? AND l.label_name = ?`
err = sqlx.GetContext(testCtx(), ds.writer(testCtx()), &assocCount, query, profName, teamID, fleet.BuiltinLabelNameAndroid)
require.NoError(t, err)
assert.Equal(t, 1, assocCount, "expected a label association for the inserted android profile")
}
func testAndroidHostStorageData(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
// Android host with storage data
const enterpriseSpecificID = "storage_test_enterprise"
host := &fleet.AndroidHost{
Host: &fleet.Host{
Hostname: "android-storage-test",
ComputerName: "Android Storage Test Device",
Platform: "android",
OSVersion: "Android 14",
Build: "UPB4.230623.005",
Memory: 8192, // 8GB RAM
TeamID: nil,
HardwareSerial: "STORAGE-TEST-SERIAL",
CPUType: "arm64-v8a",
HardwareModel: "Google Pixel 8 Pro",
HardwareVendor: "Google",
GigsTotalDiskSpace: 128.0, // 64GB system + 64GB external
GigsDiskSpaceAvailable: 35.0, // 10GB + 25GB available
PercentDiskSpaceAvailable: 27.34, // 35/128 * 100
},
Device: &android.Device{
DeviceID: "storage-test-device-id",
EnterpriseSpecificID: ptr.String(enterpriseSpecificID),
AppliedPolicyID: ptr.String("1"),
LastPolicySyncTime: ptr.Time(time.Now().UTC().Truncate(time.Millisecond)),
},
}
host.SetNodeKey(enterpriseSpecificID)
// NewAndroidHost with storage data
result, err := ds.NewAndroidHost(testCtx(), host)
require.NoError(t, err)
require.NotZero(t, result.Host.ID)
// storage data was saved correctly
assert.Equal(t, 128.0, result.Host.GigsTotalDiskSpace, "Total disk space should be saved")
assert.Equal(t, 35.0, result.Host.GigsDiskSpaceAvailable, "Available disk space should be saved")
assert.Equal(t, 27.34, result.Host.PercentDiskSpaceAvailable, "Disk space percentage should be saved")
// AndroidHostLite provides lightweight Android data (no storage data)
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
assert.Equal(t, result.Host.ID, resultLite.Host.ID)
// UpdateAndroidHost preserves storage data
updatedHost := result
updatedHost.Host.Hostname = "updated-hostname"
updatedHost.Host.GigsTotalDiskSpace = 256.0 // Updated: 128GB system + 128GB external
updatedHost.Host.GigsDiskSpaceAvailable = 64.0 // Updated: 20GB + 44GB available
updatedHost.Host.PercentDiskSpaceAvailable = 25.0 // Updated: 64/256 * 100
err = ds.UpdateAndroidHost(testCtx(), updatedHost, false)
require.NoError(t, err)
// verify updated host data via host query (includes storage from host_disks)
finalResult, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
require.NoError(t, err)
// get host data to check storage updates
updatedFullHost, err := ds.Host(testCtx(), finalResult.Host.ID)
require.NoError(t, err)
assert.Equal(t, "updated-hostname", updatedFullHost.Hostname, "Hostname should be updated")
assert.Equal(t, 256.0, updatedFullHost.GigsTotalDiskSpace, "Updated total disk space should be saved in host_disks")
assert.Equal(t, 64.0, updatedFullHost.GigsDiskSpaceAvailable, "Updated available disk space should be saved in host_disks")
assert.Equal(t, 25.0, updatedFullHost.PercentDiskSpaceAvailable, "Updated disk space percentage should be saved in host_disks")
}
func testNewMDMAndroidConfigProfile(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := testCtx()
// create some labels to test
lblExcl, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-label-1", Query: "select 1"})
require.NoError(t, err)
lblInclAny, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-label-2", Query: "select 2"})
require.NoError(t, err)
lblInclAll, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclall-label-3", Query: "select 3"})
require.NoError(t, err)
// New Android MDM config profile
profile := fleet.MDMAndroidConfigProfile{
Name: "testAndroid",
TeamID: nil,
RawJSON: []byte(`{"hello": "world"}`),
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{
LabelID: lblInclAll.ID,
LabelName: lblInclAll.Name,
RequireAll: true,
}},
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{{
LabelID: lblInclAny.ID,
LabelName: lblInclAny.Name,
RequireAll: false,
}},
LabelsExcludeAny: []fleet.ConfigurationProfileLabel{{
LabelID: lblExcl.ID,
LabelName: lblExcl.Name,
RequireAll: false,
Exclude: true,
}},
}
// Create the profile
result, err := ds.NewMDMAndroidConfigProfile(ctx, profile)
require.NoError(t, err)
assert.NotEmpty(t, result.ProfileUUID)
// Create another profile just to have multiple entries
profile2 := fleet.MDMAndroidConfigProfile{
Name: "testAndroid2",
TeamID: nil,
RawJSON: []byte(`{"hello2": "world2"}`),
}
result2, err := ds.NewMDMAndroidConfigProfile(ctx, profile2)
require.NoError(t, err)
assert.NotEmpty(t, result2.ProfileUUID)
returnedProfile, err := ds.GetMDMAndroidConfigProfile(ctx, result.ProfileUUID)
require.NoError(t, err)
require.NotNil(t, returnedProfile)
// Verify the profile was created correctly
assert.Equal(t, profile.RawJSON, returnedProfile.RawJSON)
assert.Equal(t, profile.Name, returnedProfile.Name)
require.NotNil(t, returnedProfile.TeamID)
assert.Equal(t, uint(0), *returnedProfile.TeamID)
require.ElementsMatch(t, profile.LabelsIncludeAll, returnedProfile.LabelsIncludeAll)
require.ElementsMatch(t, profile.LabelsIncludeAny, returnedProfile.LabelsIncludeAny)
require.ElementsMatch(t, profile.LabelsExcludeAny, returnedProfile.LabelsExcludeAny)
// Create a Windows profile with a name, then make sure an error is returned when creating an
// Android profile with that name
windowsProfile := fleet.MDMWindowsConfigProfile{
Name: "testWindowsAndroidConflict",
TeamID: nil,
SyncML: []byte(`hello`),
}
_, err = ds.NewMDMWindowsConfigProfile(ctx, windowsProfile, nil)
require.NoError(t, err)
androidProfile := fleet.MDMAndroidConfigProfile{
Name: "testWindowsAndroidConflict",
TeamID: nil,
RawJSON: []byte(`{"hello3": "world3"}`),
}
_, err = ds.NewMDMAndroidConfigProfile(ctx, androidProfile)
require.ErrorContains(t, err, "already exists")
// Create that same conflicting android profile but on a different team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "test team"})
require.NoError(t, err)
require.NotNil(t, team)
androidProfile.TeamID = ptr.Uint(team.ID)
otherTeamProfile, err := ds.NewMDMAndroidConfigProfile(ctx, androidProfile)
require.NoError(t, err)
// Verify we can GET the newly created profile
otherTeamProfile, err = ds.GetMDMAndroidConfigProfile(ctx, otherTeamProfile.ProfileUUID)
require.NoError(t, err)
require.NotNil(t, otherTeamProfile)
assert.Equal(t, androidProfile.RawJSON, otherTeamProfile.RawJSON)
assert.Equal(t, androidProfile.Name, otherTeamProfile.Name)
require.NotNil(t, otherTeamProfile.TeamID)
assert.Equal(t, *androidProfile.TeamID, *otherTeamProfile.TeamID)
}
func testGetMDMAndroidConfigProfile(t *testing.T, ds *Datastore) {
ctx := testCtx()
profile, err := ds.GetMDMAndroidConfigProfile(ctx, "some-fake-uuid")
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
require.Nil(t, profile)
}
func testDeleteMDMAndroidConfigProfile(t *testing.T, ds *Datastore) {
ctx := testCtx()
err := ds.DeleteMDMAndroidConfigProfile(ctx, "some-fake-uuid")
var nfe fleet.NotFoundError
require.ErrorAs(t, err, &nfe)
profile1 := &fleet.MDMAndroidConfigProfile{
Name: "testAndroid",
TeamID: nil,
RawJSON: []byte(`{"hello": "world"}`),
}
profile1, err = ds.NewMDMAndroidConfigProfile(ctx, *profile1)
require.NoError(t, err)
require.NotNil(t, profile1)
profile2 := &fleet.MDMAndroidConfigProfile{
Name: "testAndroid2",
TeamID: nil,
RawJSON: []byte(`{"hello": "world"}`),
}
profile2, err = ds.NewMDMAndroidConfigProfile(ctx, *profile2)
require.NoError(t, err)
require.NotNil(t, profile2)
// set a host profile to mimic reconcilation has yet to run
err = ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: "test-host-1",
ProfileUUID: profile1.ProfileUUID,
Status: nil,
OperationType: fleet.MDMOperationTypeInstall,
},
{
HostUUID: "test-host-2",
ProfileUUID: profile2.ProfileUUID,
Status: &fleet.MDMDeliveryPending,
OperationType: fleet.MDMOperationTypeInstall,
},
})
require.NoError(t, err)
// Delete the first profile
err = ds.DeleteMDMAndroidConfigProfile(ctx, profile1.ProfileUUID)
require.NoError(t, err)
// Verify the first profile is deleted and respective host profile is cancelled
profile1, err = ds.GetMDMAndroidConfigProfile(ctx, profile1.ProfileUUID)
require.ErrorAs(t, err, &nfe)
require.Nil(t, profile1)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `SELECT host_uuid, profile_uuid FROM host_mdm_android_profiles`
var hosts []struct {
HostUUID string `db:"host_uuid"`
ProfileUUID string `db:"profile_uuid"`
}
err := sqlx.SelectContext(ctx, q, &hosts, stmt)
if err != nil {
return err
}
require.NoError(t, err)
require.Len(t, hosts, 1)
require.Equal(t, "test-host-2", hosts[0].HostUUID)
return nil
})
// Verify the second profile is untouched
profile2, err = ds.GetMDMAndroidConfigProfile(ctx, profile2.ProfileUUID)
require.NoError(t, err)
require.NotNil(t, profile2)
require.Equal(t, "testAndroid2", profile2.Name)
}
func testMDMAndroidProfilesSummary(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := context.Background()
checkMDMProfilesSummary := func(t *testing.T, teamID *uint, expected fleet.MDMProfilesSummary) {
ps, err := ds.GetMDMAndroidProfilesSummary(ctx, teamID)
require.NoError(t, err)
require.NotNil(t, ps)
require.Equal(t, expected, *ps)
}
checkListHostsFilterOSSettings := func(t *testing.T, teamID *uint, status fleet.OSSettingsStatus, expectedIDs []uint) {
gotHosts, err := ds.ListHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
require.NoError(t, err)
if len(expectedIDs) != len(gotHosts) {
gotIDs := make([]uint, len(gotHosts))
for i, h := range gotHosts {
gotIDs[i] = h.ID
}
require.Len(t, gotHosts, len(expectedIDs), fmt.Sprintf("status: %s expected: %v got: %v", status, expectedIDs, gotIDs))
}
for _, h := range gotHosts {
require.Contains(t, expectedIDs, h.ID)
}
count, err := ds.CountHosts(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.HostListOptions{TeamFilter: teamID, OSSettingsFilter: status})
require.NoError(t, err)
require.Equal(t, len(expectedIDs), count, "status: %s", status)
}
type hostIDsByProfileStatus map[fleet.MDMDeliveryStatus][]uint
checkExpected := func(t *testing.T, teamID *uint, ep hostIDsByProfileStatus) {
checkMDMProfilesSummary(t, teamID, fleet.MDMProfilesSummary{
Pending: uint(len(ep[fleet.MDMDeliveryPending])),
Failed: uint(len(ep[fleet.MDMDeliveryFailed])),
Verifying: uint(len(ep[fleet.MDMDeliveryVerifying])),
Verified: uint(len(ep[fleet.MDMDeliveryVerified])),
})
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerified, ep[fleet.MDMDeliveryVerified])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsVerifying, ep[fleet.MDMDeliveryVerifying])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsFailed, ep[fleet.MDMDeliveryFailed])
checkListHostsFilterOSSettings(t, teamID, fleet.OSSettingsPending, ep[fleet.MDMDeliveryPending])
}
upsertHostProfileStatus := func(t *testing.T, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `INSERT INTO host_mdm_android_profiles (host_uuid, profile_uuid, status, operation_type) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`
_, err := q.ExecContext(ctx, stmt, hostUUID, profUUID, status, fleet.MDMOperationTypeInstall, status)
return err
})
}
cleanupTables := func(t *testing.T) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM host_mdm_android_profiles`)
return err
})
}
// Create some hosts
var hosts []*fleet.Host
for i := 0; i < 5; i++ {
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-%d", i))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
require.NotNil(t, newHost)
hosts = append(hosts, newHost.Host)
}
t.Run("profiles summary empty when there are no hosts with statuses", func(t *testing.T) {
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{},
}
checkExpected(t, nil, expected)
})
t.Run("profiles summary accounts for host profiles with mixed statuses", func(t *testing.T) {
for i := 0; i < 5; i++ {
// upsert five profiles for hosts[0] with nil statuses
upsertHostProfileStatus(t, hosts[0].UUID, fmt.Sprintf("some-android-profile-%d", i), nil)
// upsert five profiles for hosts[1] with pending statuses
upsertHostProfileStatus(t, hosts[1].UUID, fmt.Sprintf("some-android-profile-%d", i), &fleet.MDMDeliveryPending)
// upsert five profiles for hosts[2] with verifying statuses
upsertHostProfileStatus(t, hosts[2].UUID, fmt.Sprintf("some-android-profile-%d", i), &fleet.MDMDeliveryVerifying)
// upsert five profiles for hosts[3] with verified statuses
upsertHostProfileStatus(t, hosts[3].UUID, fmt.Sprintf("some-android-profile-%d", i), &fleet.MDMDeliveryVerified)
// upsert five profiles for hosts[4] with failed statuses
upsertHostProfileStatus(t, hosts[4].UUID, fmt.Sprintf("some-android-profile-%d", i), &fleet.MDMDeliveryFailed)
}
expected := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
fleet.MDMDeliveryVerified: []uint{hosts[3].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// add some other android hosts that won't be be assigned any profiles
for i := 0; i < 5; i++ {
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-other-%d", i))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
require.NotNil(t, newHost)
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to failed status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-android-profile-0", &fleet.MDMDeliveryFailed)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID, hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to pending status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-android-profile-0", &fleet.MDMDeliveryPending)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID, hosts[2].ID, hosts[3].ID},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to verifying status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-android-profile-0", &fleet.MDMDeliveryVerifying)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID, hosts[3].ID},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// upsert some-profile-0 to verified status for hosts[0:4]
for i := 0; i < 5; i++ {
upsertHostProfileStatus(t, hosts[i].UUID, "some-android-profile-0", &fleet.MDMDeliveryVerified)
}
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[0].ID, hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
fleet.MDMDeliveryVerified: []uint{hosts[3].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
// create a new team
t1, err := ds.NewTeam(ctx, &fleet.Team{Name: uuid.NewString()})
require.NoError(t, err)
require.NotNil(t, t1)
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{},
fleet.MDMDeliveryVerifying: []uint{},
fleet.MDMDeliveryVerified: []uint{},
fleet.MDMDeliveryFailed: []uint{},
}
checkExpected(t, &t1.ID, expected)
// transfer hosts[1:2] to the team
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&t1.ID, []uint{hosts[1].ID, hosts[2].ID})))
// hosts[1:2] now counted for the team, hosts[2] is counted as verifying again because
// disk encryption is not enabled for the team
expectedTeam1 := hostIDsByProfileStatus{
fleet.MDMDeliveryPending: []uint{hosts[1].ID},
fleet.MDMDeliveryVerifying: []uint{hosts[2].ID},
}
checkExpected(t, &t1.ID, expectedTeam1)
// set MDM to off for hosts[0]
require.NoError(t, ds.SetOrUpdateMDMData(ctx, hosts[0].ID, false, false, "", false, "", "", false))
// hosts[0] is no longer counted
expected = hostIDsByProfileStatus{
fleet.MDMDeliveryVerified: []uint{hosts[3].ID},
fleet.MDMDeliveryFailed: []uint{hosts[4].ID},
}
checkExpected(t, nil, expected)
cleanupTables(t)
})
}
func testGetHostMDMAndroidProfiles(t *testing.T, ds *Datastore) {
ctx := context.Background()
// Create a host
host := createAndroidHost("host-mdm-profiles-test")
newHost, err := ds.NewAndroidHost(ctx, host)
require.NoError(t, err)
require.NotNil(t, newHost)
// No profiles initially
profiles, err := ds.GetHostMDMAndroidProfiles(ctx, newHost.UUID)
require.NoError(t, err)
require.Empty(t, profiles)
// Create some profiles
profile1 := androidProfileForTest("profile1")
profile1, err = ds.NewMDMAndroidConfigProfile(ctx, *profile1)
require.NoError(t, err)
require.NotNil(t, profile1)
profile2 := androidProfileForTest("profile2")
profile2, err = ds.NewMDMAndroidConfigProfile(ctx, *profile2)
require.NoError(t, err)
require.NotNil(t, profile2)
profile3 := androidProfileForTest("profile3")
profile3, err = ds.NewMDMAndroidConfigProfile(ctx, *profile3)
require.NoError(t, err)
require.NotNil(t, profile3)
// Assign profiles to host with different statuses
upsertAndroidHostProfileStatus(t, ds, newHost.UUID, profile1.ProfileUUID, &fleet.MDMDeliveryVerified)
upsertAndroidHostProfileStatus(t, ds, newHost.UUID, profile2.ProfileUUID, &fleet.MDMDeliveryPending)
upsertAndroidHostProfileStatus(t, ds, newHost.UUID, profile3.ProfileUUID, nil)
// Retrieve host profiles
profiles, err = ds.GetHostMDMAndroidProfiles(ctx, newHost.UUID)
require.NoError(t, err)
require.Len(t, profiles, 3)
byProfileUUID := make(map[string]fleet.HostMDMAndroidProfile)
for _, p := range profiles {
require.NotNil(t, p.Status)
byProfileUUID[p.ProfileUUID] = p
}
require.Len(t, byProfileUUID, 3)
require.Equal(t, fleet.MDMDeliveryVerified, *byProfileUUID[profile1.ProfileUUID].Status)
require.Equal(t, fleet.MDMDeliveryPending, *byProfileUUID[profile2.ProfileUUID].Status)
require.Equal(t, fleet.MDMDeliveryPending, *byProfileUUID[profile3.ProfileUUID].Status)
// Change status of two profiles
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
// delivery failed
_, err := q.ExecContext(ctx, `UPDATE host_mdm_android_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?`,
fleet.MDMDeliveryFailed, newHost.UUID, profile2.ProfileUUID)
require.NoError(t, err)
// removal verifying
_, err = q.ExecContext(ctx, `UPDATE host_mdm_android_profiles SET operation_type = ?, status = ? WHERE host_uuid = ? AND profile_uuid = ?`,
fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying, newHost.UUID, profile3.ProfileUUID)
return err
})
// Retrieve host profiles
profiles, err = ds.GetHostMDMAndroidProfiles(ctx, newHost.UUID)
require.NoError(t, err)
require.Len(t, profiles, 2) // verifying removal profile not returned
byProfileUUID = make(map[string]fleet.HostMDMAndroidProfile)
for _, p := range profiles {
require.NotNil(t, p.Status)
byProfileUUID[p.ProfileUUID] = p
}
require.Len(t, byProfileUUID, 2)
require.Equal(t, fleet.MDMDeliveryVerified, *byProfileUUID[profile1.ProfileUUID].Status)
require.Equal(t, fleet.MDMDeliveryFailed, *byProfileUUID[profile2.ProfileUUID].Status)
// Non-existent host returns empty slice
profiles, err = ds.GetHostMDMAndroidProfiles(ctx, "non-existent-uuid")
require.NoError(t, err)
require.Empty(t, profiles)
}
func androidProfileForTest(name string, labels ...*fleet.Label) *fleet.MDMAndroidConfigProfile {
payload := `{
"maximumTimeToLock": "1234"
}`
profile := &fleet.MDMAndroidConfigProfile{
RawJSON: []byte(payload),
Name: name,
}
for _, l := range labels {
switch {
case strings.HasPrefix(l.Name, "exclude-"):
profile.LabelsExcludeAny = append(profile.LabelsExcludeAny, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
case strings.HasPrefix(l.Name, "inclany-"):
profile.LabelsIncludeAny = append(profile.LabelsIncludeAny, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
default:
profile.LabelsIncludeAll = append(profile.LabelsIncludeAll, fleet.ConfigurationProfileLabel{LabelName: l.Name, LabelID: l.ID})
}
}
return profile
}
func upsertAndroidHostProfileStatus(t *testing.T, ds *Datastore, hostUUID string, profUUID string, status *fleet.MDMDeliveryStatus) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
stmt := `INSERT INTO host_mdm_android_profiles (host_uuid, profile_uuid, status, operation_type) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = ?`
_, err := q.ExecContext(context.Background(), stmt, hostUUID, profUUID, status, fleet.MDMOperationTypeInstall, status)
return err
})
}
func expectAndroidProfiles(
t *testing.T,
ds *Datastore,
tmID *uint,
want []*fleet.MDMAndroidConfigProfile,
) {
if tmID == nil {
tmID = ptr.Uint(0)
}
ctx := t.Context()
var gotUUIDs []string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.SelectContext(ctx, q, &gotUUIDs,
`SELECT profile_uuid FROM mdm_android_configuration_profiles WHERE team_id = ?`,
tmID)
})
// load each profile, this will also load its labels
var got []*fleet.MDMAndroidConfigProfile
for _, profileUUID := range gotUUIDs {
profile, err := ds.GetMDMAndroidConfigProfile(ctx, profileUUID)
require.NoError(t, err)
got = append(got, profile)
}
// create map of expected uuids keyed by name
wantMap := make(map[string]*fleet.MDMAndroidConfigProfile, len(want))
for _, cp := range want {
wantMap[cp.Name] = cp
}
JSONRemarshal := func(bytes []byte) ([]byte, error) {
var ifce interface{}
err := json.Unmarshal(bytes, &ifce)
if err != nil {
return nil, err
}
return json.Marshal(ifce)
}
// compare only the fields we care about, and build the resulting map of
// profile identifier as key to profile UUID as value
for _, gotA := range got {
wantA := wantMap[gotA.Name]
if gotA.TeamID != nil && *gotA.TeamID == 0 {
gotA.TeamID = nil
}
// ProfileUUID is non-empty and starts with "g", but otherwise we don't
// care about it for test assertions.
require.NotEmpty(t, gotA.ProfileUUID)
require.True(t, strings.HasPrefix(gotA.ProfileUUID, fleet.MDMAndroidProfileUUIDPrefix))
gotA.ProfileUUID = ""
gotA.CreatedAt = time.Time{}
gotA.AutoIncrement = 0
gotBytes, err := JSONRemarshal(gotA.RawJSON)
require.NoError(t, err)
gotA.RawJSON = gotBytes
// if an expected uploaded_at timestamp is provided for this profile, keep
// its value, otherwise clear it as we don't care about asserting its
// value.
if wantA.UploadedAt.IsZero() {
gotA.UploadedAt = time.Time{}
}
}
require.ElementsMatch(t, want, got)
}
func testListMDMAndroidProfilesToSend(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := t.Context()
// Create some hosts
hosts := make([]*fleet.Host, 2)
for i := range hosts {
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-%d", i))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
hosts[i] = newHost.Host
}
// without any profile, should return empty
profs, toRemoveProfs, err := ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, profs)
require.Empty(t, toRemoveProfs)
// create a couple profiles for no team, and one for a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"})
require.NoError(t, err)
p1, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-1"))
require.NoError(t, err)
p2, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-2"))
require.NoError(t, err)
tmP3 := androidProfileForTest("team-1")
tmP3.TeamID = &tm.ID
p3, err := ds.NewMDMAndroidConfigProfile(ctx, *tmP3)
require.NoError(t, err)
// both no-team profiles should be applicable to both hosts
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p2.Name},
}, profs)
// transfer host 1 to the team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[1].ID}))
require.NoError(t, err)
// profiles for host 1 change to p3
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 3)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// test the include all labels condition
lblIncAll1, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclall-1", Query: "select 1"})
require.NoError(t, err)
lblIncAll2, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclall-2", Query: "select 1"})
require.NoError(t, err)
p4, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-4", lblIncAll1, lblIncAll2))
require.NoError(t, err)
// no change, host is not a member of both labels
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 3)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// make host[0] a member of only one of the labels
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblIncAll1, []uint{hosts[0].ID}, fleet.TeamFilter{})
require.NoError(t, err)
// no change, host is not a member of both labels
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 3)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// make host[0] a member of the other label
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblIncAll2, []uint{hosts[0].ID}, fleet.TeamFilter{})
require.NoError(t, err)
// now p4 is applicable to host 0
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// test the include any labels condition
lblIncAny1, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclany-1", Query: "select 1"})
require.NoError(t, err)
lblIncAny2, err := ds.NewLabel(ctx, &fleet.Label{Name: "inclany-2", Query: "select 1"})
require.NoError(t, err)
p5, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-5", lblIncAny1, lblIncAny2))
require.NoError(t, err)
// no change, host 0 not a member yet
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// make host[0] a member of one of the labels
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblIncAny1, []uint{hosts[0].ID}, fleet.TeamFilter{})
require.NoError(t, err)
// now p5 is applicable to host 0
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 5)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// test the exclude any labels condition
lblExclAny1, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-1", Query: "select 1"})
require.NoError(t, err)
lblExclAny2, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-2", LabelMembershipType: fleet.LabelMembershipTypeManual})
require.NoError(t, err)
p6, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-6", lblExclAny1, lblExclAny2))
require.NoError(t, err)
// no change, label membership was not updated after labels created
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 5)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// update the timestamp of when host label membership was updated
hosts[0].LabelUpdatedAt = time.Now().UTC().Add(time.Second) // just to be extra safe in tests
hosts[0].PolicyUpdatedAt = time.Now().UTC()
err = ds.UpdateHost(ctx, hosts[0])
require.NoError(t, err)
// host 0 is _not_ a member of the excluded labels, so p6 is applicable
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 6)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p6.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p6.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// make host[0] a member of one of the exclude labels
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblExclAny2, []uint{hosts[0].ID}, fleet.TeamFilter{})
require.NoError(t, err)
// p6 is not applicable anymore
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 5)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// add another host in team
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-%d", 2))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
hosts = append(hosts, newHost.Host)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[2].ID}))
require.NoError(t, err)
// it is not included in noProfHosts as it has p3
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 6)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[2].UUID, ProfileName: p3.Name},
}, profs)
// simulate that host 2 already has p3 installed
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_android_profiles
(host_uuid, profile_uuid, profile_name, included_in_policy_version, operation_type, status)
VALUES (?, ?, ?, ?, ?, ?)`, hosts[2].UUID, p3.ProfileUUID, p3.Name, 1, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerified)
return err
})
// host 2 is not included in the results as it has p3 installed
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 5)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p3.Name},
}, profs)
// delete profile p3
err = ds.DeleteMDMAndroidConfigProfile(ctx, p3.ProfileUUID)
require.NoError(t, err)
// host 2 is now a host with no profile (profile 3 needs to be cleared), host 1 is unlisted as it didn't have p3 installed
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[2].UUID, ProfileName: p3.Name},
}, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
}, profs)
// Turn off MDM on host 2 - it should no longer have any operations listed
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_mdm SET enrolled=0 WHERE host_id=?`, hosts[2].ID)
return err
})
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p5.Name},
}, profs)
// Turn off MDM on host 0 - no more profiles to send
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE host_mdm SET enrolled=0 WHERE host_id=?`, hosts[0].ID)
return err
})
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, profs)
require.Empty(t, toRemoveProfs)
}
// Specific test for "exclude any" logic which can be tricky because manual
// labels apply immediately whereas dynamic labels only apply after label membership
// has been determined for the host(as signified by the LabelUpdatedAt timestamp).
// Base test covers some of this but it's a good area for extra testing in light of
// https://github.com/fleetdm/fleet/issues/33132
func testListMDMAndroidProfilesToSendWithExcludeAny(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
ctx := t.Context()
// Create some hosts
hosts := make([]*fleet.Host, 2)
for i := range hosts {
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-%d", i))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
hosts[i] = newHost.Host
}
// without any profile, should return empty
profs, toRemoveProfs, err := ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, profs)
require.Empty(t, toRemoveProfs)
// Create a team
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"})
require.NoError(t, err)
// transfer host 1 to the team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[1].ID}))
require.NoError(t, err)
// test the exclude any labels condition
lblExclAny1, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-1", Query: "select 1"})
require.NoError(t, err)
lblExclAny2, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-2", LabelMembershipType: fleet.LabelMembershipTypeManual})
require.NoError(t, err)
// Dynamic exclude-any label
p1, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-1", lblExclAny1))
require.NoError(t, err)
// Manual exclude-any label only
p2, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-2", lblExclAny2))
require.NoError(t, err)
// Both manual and dynamic label exclusion
p3, err := ds.NewMDMAndroidConfigProfile(ctx, *androidProfileForTest("no-team-3", lblExclAny1, lblExclAny2))
require.NoError(t, err)
// p2 becomes immediately applicable because it only has a manual label
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 1)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
}, profs)
// update the timestamp of when host label membership was updated
hosts[0].LabelUpdatedAt = time.Now().UTC().Add(time.Second) // just to be extra safe in tests
hosts[0].PolicyUpdatedAt = time.Now().UTC()
err = ds.UpdateHost(ctx, hosts[0])
require.NoError(t, err)
// host 0 dynamic labels now apply, and this host is _not_ a member of the excluded labels, so p1, p2 and p3 are now applicable
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 3)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p3.Name},
}, profs)
tmP4 := androidProfileForTest("team-4", lblExclAny1)
tmP4.TeamID = &tm.ID
tmP5 := androidProfileForTest("team-5", lblExclAny2)
tmP5.TeamID = &tm.ID
tmP6 := androidProfileForTest("team-6", lblExclAny1, lblExclAny2)
tmP6.TeamID = &tm.ID
// Dynamic exclude-any label
p4, err := ds.NewMDMAndroidConfigProfile(ctx, *tmP4)
require.NoError(t, err)
// Manual exclude-any label only
p5, err := ds.NewMDMAndroidConfigProfile(ctx, *tmP5)
require.NoError(t, err)
// Both manual and dynamic label exclusion
p6, err := ds.NewMDMAndroidConfigProfile(ctx, *tmP6)
require.NoError(t, err)
// p5 becomes immediately applicable to host 1 because it only has a manual label
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p3.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p5.Name},
}, profs)
// Set the hosts label_updated_at causing p4-p6 to become applicable to host 1
hosts[1].LabelUpdatedAt = time.Now().UTC().Add(time.Second) // just to be extra safe in tests
hosts[1].PolicyUpdatedAt = time.Now().UTC()
hosts[1].TeamID = &tm.ID
err = ds.UpdateHost(ctx, hosts[1])
require.NoError(t, err)
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 6)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p2.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p2.Name},
{ProfileUUID: p3.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p3.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p5.Name},
{ProfileUUID: p6.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p6.Name},
}, profs)
// Make host 0 a member of labelExclAny2 which excludes everything except p1 for it
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblExclAny2, []uint{hosts[0].ID}, fleet.TeamFilter{})
require.NoError(t, err)
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 4)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p1.ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: p1.Name},
{ProfileUUID: p4.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p4.Name},
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p5.Name},
{ProfileUUID: p6.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p6.Name},
}, profs)
// Make hosts 0 and 1 members of labelExclAny1 which excludes everything except p5 for host p1. Android doesn't
// currently support dynamic labels but this ensures the datastore processes it right if somehow an Android host
// becomes a member of one
_, _, err = ds.UpdateLabelMembershipByHostIDs(ctx, *lblExclAny1, []uint{hosts[0].ID, hosts[1].ID}, fleet.TeamFilter{})
require.NoError(t, err)
profs, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.Len(t, profs, 1)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: p5.ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: p5.Name},
}, profs)
}
func testGetMDMAndroidProfilesContents(t *testing.T, ds *Datastore) {
ctx := t.Context()
p1 := androidProfileForTest("p1")
p1.RawJSON = []byte(`{"v": 1}`)
p2 := androidProfileForTest("p2")
p2.RawJSON = []byte(`{"v": 2}`)
p3 := androidProfileForTest("p3")
p3.RawJSON = []byte(`{"v": 3}`)
p1, err := ds.NewMDMAndroidConfigProfile(ctx, *p1)
require.NoError(t, err)
p2, err = ds.NewMDMAndroidConfigProfile(ctx, *p2)
require.NoError(t, err)
p3, err = ds.NewMDMAndroidConfigProfile(ctx, *p3)
require.NoError(t, err)
cases := []struct {
uuids []string
want map[string]json.RawMessage
}{
{[]string{}, nil},
{nil, nil},
{[]string{p1.ProfileUUID}, map[string]json.RawMessage{p1.ProfileUUID: p1.RawJSON}},
{[]string{p1.ProfileUUID, p2.ProfileUUID}, map[string]json.RawMessage{
p1.ProfileUUID: p1.RawJSON,
p2.ProfileUUID: p2.RawJSON,
}},
{[]string{p1.ProfileUUID, p2.ProfileUUID, p3.ProfileUUID}, map[string]json.RawMessage{
p1.ProfileUUID: p1.RawJSON,
p2.ProfileUUID: p2.RawJSON,
p3.ProfileUUID: p3.RawJSON,
}},
{[]string{p1.ProfileUUID, p2.ProfileUUID, "no-such-uuid"}, map[string]json.RawMessage{
p1.ProfileUUID: p1.RawJSON,
p2.ProfileUUID: p2.RawJSON,
}},
}
for _, c := range cases {
t.Run(fmt.Sprintf("%v", c.uuids), func(t *testing.T) {
out, err := ds.GetMDMAndroidProfilesContents(ctx, c.uuids)
require.NoError(t, err)
require.Equal(t, c.want, out)
})
}
}
func testBulkUpsertMDMAndroidHostProfiles(t *testing.T, ds *Datastore) {
testBulkUpsertMDMAndroidHostProfilesN(t, ds, 0)
}
func testBulkUpsertMDMAndroidHostProfiles2(t *testing.T, ds *Datastore) {
testBulkUpsertMDMAndroidHostProfilesN(t, ds, 2)
}
func testBulkUpsertMDMAndroidHostProfiles3(t *testing.T, ds *Datastore) {
testBulkUpsertMDMAndroidHostProfilesN(t, ds, 3)
}
func testBulkUpsertMDMAndroidHostProfilesN(t *testing.T, ds *Datastore, batchSize int) {
test.AddBuiltinLabels(t, ds)
ctx := t.Context()
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team"})
require.NoError(t, err)
// Create some hosts and some profiles
hosts := make([]*fleet.Host, 3)
for i := range hosts {
androidHost := createAndroidHost(fmt.Sprintf("enterprise-id-%d", i))
newHost, err := ds.NewAndroidHost(ctx, androidHost)
require.NoError(t, err)
hosts[i] = newHost.Host
if i == len(hosts)-1 {
// last host is in a team
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[i].ID}))
require.NoError(t, err)
}
}
profiles := make([]*fleet.MDMAndroidConfigProfile, 3)
for i := range profiles {
p := androidProfileForTest(fmt.Sprintf("profile-%d", i))
if i == len(profiles)-1 {
// last profile is for a team
p.TeamID = &tm.ID
}
p, err := ds.NewMDMAndroidConfigProfile(ctx, *p)
require.NoError(t, err)
profiles[i] = p
}
err = ds.BulkUpsertMDMAndroidHostProfiles(ctx, nil)
require.NoError(t, err)
ds.testUpsertMDMDesiredProfilesBatchSize = batchSize
t.Cleanup(func() { ds.testUpsertMDMDesiredProfilesBatchSize = 0 })
hostProfiles, toRemoveProfs, err := ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: profiles[0].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[0].Name},
{ProfileUUID: profiles[1].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[1].Name},
{ProfileUUID: profiles[0].ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: profiles[0].Name},
{ProfileUUID: profiles[1].ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: profiles[1].Name},
{ProfileUUID: profiles[2].ProfileUUID, HostUUID: hosts[2].UUID, ProfileName: profiles[2].Name},
}, hostProfiles)
// mark all installed for hosts 0, profile 1 failed for host 1
err = ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hosts[0].UUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: ptr.Int(1),
},
{
HostUUID: hosts[0].UUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: ptr.Int(1),
},
{
HostUUID: hosts[1].UUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryFailed,
IncludedInPolicyVersion: ptr.Int(1),
},
})
require.NoError(t, err)
hostProfiles, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
// because host 1 still has a missing profile, it must resend both (as it merged them)
{ProfileUUID: profiles[0].ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: profiles[0].Name},
{ProfileUUID: profiles[1].ProfileUUID, HostUUID: hosts[1].UUID, ProfileName: profiles[1].Name},
{ProfileUUID: profiles[2].ProfileUUID, HostUUID: hosts[2].UUID, ProfileName: profiles[2].Name},
}, hostProfiles)
// mark host 0 profile 1 as NULL, host 1 profile 0 as installed (so both are now installed), and host 2 profile 2 as installed
err = ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hosts[0].UUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: nil,
IncludedInPolicyVersion: ptr.Int(1),
},
{
HostUUID: hosts[1].UUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: ptr.Int(1),
},
{
HostUUID: hosts[2].UUID,
ProfileUUID: profiles[2].ProfileUUID,
ProfileName: profiles[2].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: ptr.Int(1),
},
})
require.NoError(t, err)
hostProfiles, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.Empty(t, toRemoveProfs)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
// host 0 now has a profile not installed, so it needs to resend both
{ProfileUUID: profiles[0].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[0].Name},
{ProfileUUID: profiles[1].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[1].Name},
// host 1 now has both delivered, nothing to resend
// host 2 profile is delivered, nothing to resend
}, hostProfiles)
// delete profile 2, which will cause host 2 to be resent as "no profiles" to remove it as it was delivered
err = ds.DeleteMDMAndroidConfigProfile(ctx, profiles[2].ProfileUUID)
require.NoError(t, err)
hostProfiles, toRemoveProfs, err = ds.ListMDMAndroidProfilesToSend(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: profiles[2].ProfileUUID, HostUUID: hosts[2].UUID, ProfileName: profiles[2].Name},
}, toRemoveProfs)
require.ElementsMatch(t, []*fleet.MDMAndroidProfilePayload{
{ProfileUUID: profiles[0].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[0].Name},
{ProfileUUID: profiles[1].ProfileUUID, HostUUID: hosts[0].UUID, ProfileName: profiles[1].Name},
}, hostProfiles)
}
func testGetAndroidPolicyRequestByUUID(t *testing.T, ds *Datastore) {
ctx := t.Context()
policyRequestUUID := uuid.New().String()
t.Run("Returns not found", func(t *testing.T) {
policyRequest, err := ds.GetAndroidPolicyRequestByUUID(ctx, policyRequestUUID)
require.Contains(t, err.Error(), common_mysql.NotFound("AndroidPolicyRequest").WithName(policyRequestUUID).Error())
require.Nil(t, policyRequest)
})
t.Run("Correctly retrieves the policy request", func(t *testing.T) {
// Create a test policy request
err := ds.NewAndroidPolicyRequest(ctx, &android.MDMAndroidPolicyRequest{
RequestUUID: policyRequestUUID,
Payload: json.RawMessage(`{"key": "value"}`),
})
require.NoError(t, err)
// Retrieve the policy request by UUID
policyRequest, err := ds.GetAndroidPolicyRequestByUUID(ctx, policyRequestUUID)
require.NoError(t, err)
require.NotNil(t, policyRequest)
require.Equal(t, policyRequestUUID, policyRequest.RequestUUID)
})
}
func testListHostMDMAndroidProfilesPendingInstallWithVersion(t *testing.T, ds *Datastore) {
ctx := t.Context()
profiles := make([]*fleet.MDMAndroidConfigProfile, 3)
for i := range profiles {
p := androidProfileForTest(fmt.Sprintf("profile-%d", i))
p, err := ds.NewMDMAndroidConfigProfile(ctx, *p)
require.NoError(t, err)
profiles[i] = p
}
hostUUID := uuid.NewString()
clearOutHostMDMAndroidProfilesTable := func() {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "DELETE FROM host_mdm_android_profiles WHERE host_uuid = ?", hostUUID)
return err
})
}
t.Run("Does not list other install statuses", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryFailed,
IncludedInPolicyVersion: policyVersion,
},
{
HostUUID: hostUUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryVerified,
IncludedInPolicyVersion: policyVersion,
},
{
HostUUID: hostUUID,
ProfileUUID: profiles[2].ProfileUUID,
ProfileName: profiles[2].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryVerifying,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
hostProfiles, err := ds.ListHostMDMAndroidProfilesPendingInstallWithVersion(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
require.Len(t, hostProfiles, 0)
})
t.Run("Does not list higher versions than passed", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(2)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryFailed,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
hostProfiles, err := ds.ListHostMDMAndroidProfilesPendingInstallWithVersion(ctx, hostUUID, int64(*policyVersion-1))
require.NoError(t, err)
require.Len(t, hostProfiles, 0)
})
t.Run("Does not list remove operation", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryFailed,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
hostProfiles, err := ds.ListHostMDMAndroidProfilesPendingInstallWithVersion(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
require.Len(t, hostProfiles, 0)
})
t.Run("Does list pending install profiles with version less than or equal to applied policy version", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
hostProfiles, err := ds.ListHostMDMAndroidProfilesPendingInstallWithVersion(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
require.Len(t, hostProfiles, 1)
require.Equal(t, &fleet.MDMDeliveryPending, hostProfiles[0].Status)
require.Equal(t, fleet.MDMOperationTypeInstall, hostProfiles[0].OperationType)
require.EqualValues(t, policyVersion, hostProfiles[0].IncludedInPolicyVersion)
})
}
func testBulkDeleteMDMAndroidHostProfiles(t *testing.T, ds *Datastore) {
ctx := t.Context()
profiles := make([]*fleet.MDMAndroidConfigProfile, 3)
for i := range profiles {
p := androidProfileForTest(fmt.Sprintf("profile-%d", i))
p, err := ds.NewMDMAndroidConfigProfile(ctx, *p)
require.NoError(t, err)
profiles[i] = p
}
hostUUID := uuid.NewString()
clearOutHostMDMAndroidProfilesTable := func() {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "DELETE FROM host_mdm_android_profiles WHERE host_uuid = ?", hostUUID)
return err
})
}
listAllHostMDMAndroidProfiles := func() []*fleet.MDMAndroidProfilePayload {
var hostProfiles []*fleet.MDMAndroidProfilePayload
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
err := sqlx.SelectContext(ctx, q, &hostProfiles, "SELECT profile_uuid, host_uuid, profile_name, operation_type, status, detail, included_in_policy_version, policy_request_uuid, device_request_uuid, request_fail_count FROM host_mdm_android_profiles")
require.NoError(t, err)
return err
})
return hostProfiles
}
t.Run("Does not delete profiles not associated with host", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
// Act
err = ds.BulkDeleteMDMAndroidHostProfiles(ctx, uuid.NewString(), int64(*policyVersion))
require.NoError(t, err)
// Assert
hostProfiles := listAllHostMDMAndroidProfiles()
require.Len(t, hostProfiles, 1)
})
t.Run("Does not delete install operation types", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeInstall,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
// Act
err = ds.BulkDeleteMDMAndroidHostProfiles(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
// Assert
hostProfiles := listAllHostMDMAndroidProfiles()
require.Len(t, hostProfiles, 1)
})
t.Run("Does not delete other statuses with remove operation", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(1)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryVerifying,
IncludedInPolicyVersion: policyVersion,
},
{
HostUUID: hostUUID,
ProfileUUID: profiles[2].ProfileUUID,
ProfileName: profiles[2].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryVerified,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
// Act
err = ds.BulkDeleteMDMAndroidHostProfiles(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
// Assert
hostProfiles := listAllHostMDMAndroidProfiles()
require.Len(t, hostProfiles, 2)
})
t.Run("Does not delete profiles with higher policy version than passed", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(2)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
// Act
err = ds.BulkDeleteMDMAndroidHostProfiles(ctx, hostUUID, int64(*policyVersion-1))
require.NoError(t, err)
// Assert
hostProfiles := listAllHostMDMAndroidProfiles()
require.Len(t, hostProfiles, 1)
})
t.Run("Deletes pending or failed remove profiles with policy version lower than or equal to passed", func(t *testing.T) {
// Arrange
policyVersion := ptr.Int(2)
err := ds.BulkUpsertMDMAndroidHostProfiles(ctx, []*fleet.MDMAndroidProfilePayload{
{
HostUUID: hostUUID,
ProfileUUID: profiles[0].ProfileUUID,
ProfileName: profiles[0].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: policyVersion,
},
{
HostUUID: hostUUID,
ProfileUUID: profiles[1].ProfileUUID,
ProfileName: profiles[1].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryPending,
IncludedInPolicyVersion: ptr.Int(*policyVersion - 1),
},
{
HostUUID: hostUUID,
ProfileUUID: profiles[2].ProfileUUID,
ProfileName: profiles[2].Name,
OperationType: fleet.MDMOperationTypeRemove,
Status: &fleet.MDMDeliveryFailed,
IncludedInPolicyVersion: policyVersion,
},
})
require.NoError(t, err)
t.Cleanup(clearOutHostMDMAndroidProfilesTable)
// Act
err = ds.BulkDeleteMDMAndroidHostProfiles(ctx, hostUUID, int64(*policyVersion))
require.NoError(t, err)
// Assert
hostProfiles := listAllHostMDMAndroidProfiles()
require.Len(t, hostProfiles, 0)
})
}
func testNewAndroidHostWithIdP(t *testing.T, ds *Datastore) {
ctx := t.Context()
test.AddBuiltinLabels(t, ds)
// create IdP account... InsertMDMIdPAccount generates its own UUID
idpAccount := &fleet.MDMIdPAccount{
Username: "john.doe",
Fullname: "John Doe",
Email: "john.doe@example.com",
}
err := ds.InsertMDMIdPAccount(ctx, idpAccount)
require.NoError(t, err)
// get the actual UUID that was generated
insertedAccount, err := ds.GetMDMIdPAccountByEmail(ctx, "john.doe@example.com")
require.NoError(t, err)
require.NotNil(t, insertedAccount)
idpAccount.UUID = insertedAccount.UUID
// create Android host
const enterpriseSpecificID = "enterprise_with_idp"
host := createAndroidHost(enterpriseSpecificID)
host.Host.UUID = "test-host-uuid" // Use a specific UUID for testing
result, err := ds.NewAndroidHost(ctx, host)
require.NoError(t, err)
require.NotZero(t, result.Host.ID)
// associate host with IdP account, triggering reconciliation
err = ds.AssociateHostMDMIdPAccount(ctx, "test-host-uuid", idpAccount.UUID)
require.NoError(t, err)
// host_emails table has IdP email
emails, err := ds.GetHostEmails(ctx, "test-host-uuid", fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Len(t, emails, 1)
assert.Equal(t, "john.doe@example.com", emails[0])
// is reconciliation idempotent?
err = ds.AssociateHostMDMIdPAccount(ctx, "test-host-uuid", idpAccount.UUID)
require.NoError(t, err)
// still only one email (no duplicates)
emails, err = ds.GetHostEmails(ctx, "test-host-uuid", fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Len(t, emails, 1, "Should still have exactly one email after reassociation")
assert.Equal(t, "john.doe@example.com", emails[0])
// remove IdP account association and trigger reconciliation
_, err = ds.writer(ctx).ExecContext(ctx,
`DELETE FROM host_mdm_idp_accounts WHERE host_uuid = ?`,
"test-host-uuid")
require.NoError(t, err)
// test cleanup (in production this would happen on re-enrollment)
err = ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
_, err := reconcileHostEmailsFromMdmIdpAccountsDB(ctx, tx, ds.logger, result.Host.ID)
return err
})
require.NoError(t, err)
// host_emails table no longer has IdP email
emails, err = ds.GetHostEmails(ctx, "test-host-uuid", fleet.DeviceMappingMDMIdpAccounts)
require.NoError(t, err)
require.Empty(t, emails, "IdP email should be removed when association is deleted")
}
func testAndroidBYODDetection(t *testing.T, ds *Datastore) {
ctx := context.Background()
test.AddBuiltinLabels(t, ds)
// Test 1: Android host with non-empty UUID (BYOD/personal device)
t.Run("personal enrollment with UUID", func(t *testing.T) {
const enterpriseID = "test-enterprise-id-byod"
host := createAndroidHost(enterpriseID)
// Ensure UUID is set (createAndroidHost already does this)
require.NotEmpty(t, host.Host.UUID)
require.Equal(t, enterpriseID, host.Host.UUID)
result, err := ds.NewAndroidHost(ctx, host)
require.NoError(t, err)
require.NotZero(t, result.Host.ID)
// Query host_mdm table directly to verify is_personal_enrollment = 1
var isPersonalEnrollment bool
err = sqlx.GetContext(ctx, ds.reader(ctx), &isPersonalEnrollment,
`SELECT is_personal_enrollment FROM host_mdm WHERE host_id = ?`,
result.Host.ID)
require.NoError(t, err)
assert.True(t, isPersonalEnrollment, "BYOD device with UUID should have is_personal_enrollment = 1")
})
// Test 2: Android host without UUID (company-owned device)
t.Run("company enrollment without UUID", func(t *testing.T) {
const enterpriseID = "test-enterprise-id-company"
host := createAndroidHost(enterpriseID)
// Override UUID to be empty to simulate company-owned device
host.Host.UUID = ""
result, err := ds.NewAndroidHost(ctx, host)
require.NoError(t, err)
require.NotZero(t, result.Host.ID)
// Query host_mdm table directly to verify is_personal_enrollment = 0
var isPersonalEnrollment bool
err = sqlx.GetContext(ctx, ds.reader(ctx), &isPersonalEnrollment,
`SELECT is_personal_enrollment FROM host_mdm WHERE host_id = ?`,
result.Host.ID)
require.NoError(t, err)
assert.False(t, isPersonalEnrollment, "Company device without UUID should have is_personal_enrollment = 0")
})
// Test 3: Verify update path also sets personal enrollment correctly
t.Run("update existing host enrollment status", func(t *testing.T) {
// Create a host initially without UUID
const enterpriseID = "test-enterprise-id-update"
host := createAndroidHost(enterpriseID)
host.Host.UUID = ""
result, err := ds.NewAndroidHost(ctx, host)
require.NoError(t, err)
require.NotZero(t, result.Host.ID)
// Initially should not be personal enrollment
var isPersonalEnrollment bool
err = sqlx.GetContext(ctx, ds.reader(ctx), &isPersonalEnrollment,
`SELECT is_personal_enrollment FROM host_mdm WHERE host_id = ?`,
result.Host.ID)
require.NoError(t, err)
assert.False(t, isPersonalEnrollment, "Initially should not be personal enrollment")
// Update the host with a UUID (simulating re-enrollment as BYOD)
result.Host.UUID = enterpriseID
err = ds.UpdateAndroidHost(ctx, result, true) // fromEnroll = true to trigger MDM info update
require.NoError(t, err)
// Now should be marked as personal enrollment
err = sqlx.GetContext(ctx, ds.reader(ctx), &isPersonalEnrollment,
`SELECT is_personal_enrollment FROM host_mdm WHERE host_id = ?`,
result.Host.ID)
require.NoError(t, err)
assert.True(t, isPersonalEnrollment, "After update with UUID should have is_personal_enrollment = 1")
})
}
// NEW TEST: verify single-host unenroll updates host_mdm correctly
func testSetAndroidHostUnenrolled(t *testing.T, ds *Datastore) {
// Set a non-empty server URL so initial enrolled row has data to clear
appCfg, err := ds.AppConfig(testCtx())
require.NoError(t, err)
appCfg.ServerSettings.ServerURL = "https://mdm.example.com"
require.NoError(t, ds.SaveAppConfig(testCtx(), appCfg))
// Create an Android host (this also upserts an enrolled host_mdm row)
esid := "enterprise-" + uuid.NewString()
h := createAndroidHost(esid)
res, err := ds.NewAndroidHost(testCtx(), h)
require.NoError(t, err)
// Sanity check initial host_mdm values
var enrolled int
var serverURL string
var mdmIDIsNull int
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &enrolled, `SELECT enrolled FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &serverURL, `SELECT server_url FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &mdmIDIsNull, `SELECT CASE WHEN mdm_id IS NULL THEN 1 ELSE 0 END FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
require.Equal(t, 1, enrolled)
require.NotEmpty(t, serverURL)
require.Equal(t, 0, mdmIDIsNull)
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-1", &fleet.MDMDeliveryPending)
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-2", &fleet.MDMDeliveryPending)
// Perform single-host unenroll
didUnenroll, err := ds.SetAndroidHostUnenrolled(testCtx(), res.Host.ID)
require.NoError(t, err)
require.True(t, didUnenroll)
// Calling unenrolled again returns false
didUnenroll, err = ds.SetAndroidHostUnenrolled(testCtx(), res.Host.ID)
require.NoError(t, err)
require.False(t, didUnenroll)
profileCountForHost := 0
// Validate host_mdm row updated
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &enrolled, `SELECT enrolled FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &serverURL, `SELECT server_url FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &mdmIDIsNull, `SELECT CASE WHEN mdm_id IS NULL THEN 1 ELSE 0 END FROM host_mdm WHERE host_id = ?`, res.Host.ID)
})
// validate profile records deleted
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &profileCountForHost, `SELECT COUNT(*) FROM host_mdm_android_profiles WHERE host_uuid=?`, res.Host.UUID)
})
assert.Equal(t, 0, enrolled)
assert.Equal(t, "", serverURL)
assert.Equal(t, 1, mdmIDIsNull)
assert.Equal(t, 0, profileCountForHost)
}
func testBulkSetAndroidHostsUnenrolled(t *testing.T, ds *Datastore) {
test.AddBuiltinLabels(t, ds)
// Set a non-empty server URL so initial enrolled row has data to clear
appCfg, err := ds.AppConfig(testCtx())
require.NoError(t, err)
appCfg.ServerSettings.ServerURL = "https://mdm.example.com"
require.NoError(t, ds.SaveAppConfig(testCtx(), appCfg))
// Create 5 android hosts
for i := 0; i < 5; i++ {
esid := "enterprise-" + uuid.NewString()
h := createAndroidHost(esid)
res, err := ds.NewAndroidHost(testCtx(), h)
require.NoError(t, err)
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-1", &fleet.MDMDeliveryPending)
upsertAndroidHostProfileStatus(t, ds, res.Host.UUID, "profile-2", &fleet.MDMDeliveryPending)
}
// Create a macOS host (to verify we don't unenroll non-Android hosts)
macHost, err := ds.NewHost(testCtx(), &fleet.Host{
Hostname: "test-host1-name",
OsqueryHostID: ptr.String("1337"),
NodeKey: ptr.String("1337"),
UUID: "test-uuid-1",
Platform: "darwin",
HardwareSerial: uuid.NewString(),
})
require.NoError(t, err)
nanoEnroll(t, ds, macHost, false)
err = ds.MDMAppleUpsertHost(testCtx(), macHost, false)
require.NoError(t, err)
// Initial sanity check
enrolledCount := 0
androidHostProfileCount := 0
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &enrolledCount, `SELECT COUNT(*) FROM host_mdm WHERE enrolled = 1`)
})
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &androidHostProfileCount, `SELECT COUNT(*) FROM host_mdm_android_profiles`)
})
assert.Equal(t, 10, androidHostProfileCount)
require.Equal(t, 6, enrolledCount) // 5 android + 1 macOS
err = ds.BulkSetAndroidHostsUnenrolled(testCtx())
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &enrolledCount, `SELECT COUNT(*) FROM host_mdm WHERE enrolled = 1`)
})
require.Equal(t, 1, enrolledCount)
// validate profile records deleted
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(testCtx(), q, &androidHostProfileCount, `SELECT COUNT(*) FROM host_mdm_android_profiles`)
})
assert.Equal(t, 0, androidHostProfileCount)
}
// setupTestApp creates a test Android app in vpp_apps table
func setupTestApp(t *testing.T, ds *Datastore, appID string) {
_, err := ds.writer(testCtx()).ExecContext(testCtx(), `
INSERT INTO vpp_apps (adam_id, platform, bundle_identifier, name, latest_version, icon_url)
VALUES (?, 'android', ?, 'Test App', '1.0', 'http://example.com/icon.png')
`, appID, appID)
require.NoError(t, err)
}
// setupTestTeam creates a test team
func setupTestTeam(t *testing.T, ds *Datastore) uint {
team, err := ds.NewTeam(testCtx(), &fleet.Team{Name: "Test Team"})
require.NoError(t, err)
return team.ID
}
func testInsertAndGetAndroidAppConfiguration(t *testing.T, ds *Datastore) {
appID := "com.example.testapp"
setupTestApp(t, ds, appID)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {"key": "value"}}`),
}
// Insert configuration
err := ds.InsertAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Get configuration
retrieved, err := ds.GetAndroidAppConfiguration(testCtx(), appID, 0)
require.NoError(t, err)
require.NotNil(t, retrieved)
require.Equal(t, appID, retrieved.ApplicationID)
require.Nil(t, retrieved.TeamID)
require.Equal(t, uint(0), retrieved.GlobalOrTeamID)
require.JSONEq(t, string(config.Configuration), string(retrieved.Configuration))
require.NotZero(t, retrieved.ID)
require.NotZero(t, retrieved.CreatedAt)
require.NotZero(t, retrieved.UpdatedAt)
// test bulk-get configuration
configsByAppID, err := ds.BulkGetAndroidAppConfigurations(testCtx(), []string{appID}, 0)
require.NoError(t, err)
require.Len(t, configsByAppID, 1)
require.Equal(t, string(retrieved.Configuration), string(configsByAppID[appID]))
// bulk-get configuration returns any known app config, ignores others
configsByAppID, err = ds.BulkGetAndroidAppConfigurations(testCtx(), []string{appID, "no-such-app"}, 0)
require.NoError(t, err)
require.Len(t, configsByAppID, 1)
require.Equal(t, string(retrieved.Configuration), string(configsByAppID[appID]))
}
func testUpdateAndroidAppConfiguration(t *testing.T, ds *Datastore) {
appID := "com.example.updateapp"
setupTestApp(t, ds, appID)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {"key": "value1"}}`),
}
// Insert initial configuration
err := ds.InsertAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Update configuration
newConfig := json.RawMessage(`{"managedConfiguration": {"key": "value2"}, "workProfileWidgets": true}`)
config.Configuration = newConfig
err = ds.UpdateAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Verify update
retrieved, err := ds.GetAndroidAppConfiguration(testCtx(), appID, 0)
require.NoError(t, err)
require.JSONEq(t, string(newConfig), string(retrieved.Configuration))
}
func testDeleteAndroidAppConfiguration(t *testing.T, ds *Datastore) {
appID := "com.example.deleteapp"
setupTestApp(t, ds, appID)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {}}`),
}
// Insert configuration
err := ds.InsertAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Verify it exists
_, err = ds.GetAndroidAppConfiguration(testCtx(), appID, 0)
require.NoError(t, err)
// Delete configuration
err = ds.DeleteAndroidAppConfiguration(testCtx(), appID, 0)
require.NoError(t, err)
// Verify it's deleted
_, err = ds.GetAndroidAppConfiguration(testCtx(), appID, 0)
require.Error(t, err)
require.ErrorContains(t, err, "not found")
}
func testGetAndroidAppConfigurationNotFound(t *testing.T, ds *Datastore) {
_, err := ds.GetAndroidAppConfiguration(testCtx(), "nonexistent.app", 0)
require.Error(t, err)
require.ErrorContains(t, err, "not found")
}
func testUpdateAndroidAppConfigurationNotFound(t *testing.T, ds *Datastore) {
config := &fleet.AndroidAppConfiguration{
ApplicationID: "nonexistent.app",
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{}`),
}
err := ds.UpdateAndroidAppConfiguration(testCtx(), config)
require.Error(t, err)
require.ErrorContains(t, err, "not found")
}
func testDeleteAndroidAppConfigurationNotFound(t *testing.T, ds *Datastore) {
err := ds.DeleteAndroidAppConfiguration(testCtx(), "nonexistent.app", 0)
require.Error(t, err)
require.ErrorContains(t, err, "not found")
}
func testInsertAndroidAppConfigurationDuplicate(t *testing.T, ds *Datastore) {
appID := "com.example.duplicateapp"
setupTestApp(t, ds, appID)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {}}`),
}
// Insert first time - should succeed
err := ds.InsertAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Insert duplicate - should fail due to unique constraint
err = ds.InsertAndroidAppConfiguration(testCtx(), config)
require.Error(t, err)
require.ErrorContains(t, err, "Duplicate")
}
func testAndroidAppConfigurationCascadeDeleteTeam(t *testing.T, ds *Datastore) {
appID := "com.example.teamcascadeapp"
setupTestApp(t, ds, appID)
teamID := setupTestTeam(t, ds)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: ptr.Uint(teamID),
GlobalOrTeamID: teamID,
Configuration: json.RawMessage(`{"managedConfiguration": {}}`),
}
// Insert configuration
err := ds.InsertAndroidAppConfiguration(testCtx(), config)
require.NoError(t, err)
// Verify it exists
_, err = ds.GetAndroidAppConfiguration(testCtx(), appID, teamID)
require.NoError(t, err)
// Delete the team
err = ds.DeleteTeam(testCtx(), teamID)
require.NoError(t, err)
// Verify configuration is also deleted (CASCADE)
_, err = ds.GetAndroidAppConfiguration(testCtx(), appID, teamID)
require.Error(t, err)
require.ErrorContains(t, err, "not found")
}
func testAndroidAppConfigurationGlobalVsTeam(t *testing.T, ds *Datastore) {
appID := "com.example.globalvsteamapp"
setupTestApp(t, ds, appID)
teamID := setupTestTeam(t, ds)
// Insert global configuration
globalConfig := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {"env": "global"}}`),
}
err := ds.InsertAndroidAppConfiguration(testCtx(), globalConfig)
require.NoError(t, err)
// Insert team configuration
teamConfig := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: ptr.Uint(teamID),
GlobalOrTeamID: teamID,
Configuration: json.RawMessage(`{"managedConfiguration": {"env": "team"}}`),
}
err = ds.InsertAndroidAppConfiguration(testCtx(), teamConfig)
require.NoError(t, err)
// Verify global configuration
retrievedGlobal, err := ds.GetAndroidAppConfiguration(testCtx(), appID, 0)
require.NoError(t, err)
require.JSONEq(t, `{"managedConfiguration": {"env": "global"}}`, string(retrievedGlobal.Configuration))
require.Nil(t, retrievedGlobal.TeamID)
require.Equal(t, uint(0), retrievedGlobal.GlobalOrTeamID)
// Verify team configuration
retrievedTeam, err := ds.GetAndroidAppConfiguration(testCtx(), appID, teamID)
require.NoError(t, err)
require.JSONEq(t, `{"managedConfiguration": {"env": "team"}}`, string(retrievedTeam.Configuration))
require.NotNil(t, retrievedTeam.TeamID)
require.Equal(t, teamID, *retrievedTeam.TeamID)
require.Equal(t, teamID, retrievedTeam.GlobalOrTeamID)
}
func testAddDeleteAndroidAppWithConfiguration(t *testing.T, ds *Datastore) {
ctx := context.Background()
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
require.NoError(t, err)
test.CreateInsertGlobalVPPToken(t, ds)
testConfig := json.RawMessage(`{"ManagedConfiguration": {"DisableShareScreen": true, "DisableComputerAudio": true}}`)
// Create android and VPP apps
app1, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "android1", BundleIdentifier: "android1",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "something_android_app_1", Platform: fleet.AndroidPlatform},
Configuration: testConfig,
}}, &team1.ID)
require.NoError(t, err)
app2, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
Name: "vpp1", BundleIdentifier: "com.app.vpp1",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_app_forapple_1", Platform: fleet.IOSPlatform},
Configuration: json.RawMessage(`{"ManagedConfiguration": {"ios app shouldn't have configuration": true}}`),
}}, &team1.ID)
require.NoError(t, err)
// Get android app without team
meta, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, nil, app1.TitleID)
require.NoError(t, err)
require.Zero(t, meta.Configuration)
// Get android app and configuration
meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
require.NoError(t, err)
require.NotZero(t, meta.VPPAppsTeamsID)
require.NotZero(t, meta.Configuration)
require.Equal(t, "android1", meta.BundleIdentifier)
require.Equal(t, testConfig, meta.Configuration)
// Get ios app
meta2, err := ds.GetVPPAppMetadataByTeamAndTitleID(ctx, nil, app2.TitleID)
require.NoError(t, err)
require.NotZero(t, meta2.VPPAppsTeamsID)
// Edit android app
newConfig := json.RawMessage(`{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`)
app1.VPPAppTeam.Configuration = newConfig
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
require.NoError(t, err)
// Check that configuration was changed
meta, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
require.NoError(t, err)
require.NotZero(t, meta.VPPAppsTeamsID)
require.Equal(t, newConfig, meta.Configuration)
// Add invalid configuration
badConfig := json.RawMessage(`"-": "-"`)
app1.VPPAppTeam.Configuration = badConfig
_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
require.Error(t, err)
// Delete app, should delete configuration
require.NoError(t, ds.DeleteVPPAppFromTeam(ctx, &team1.ID, app1.VPPAppID))
_, err = ds.GetVPPAppMetadataByTeamAndTitleID(ctx, &team1.ID, app1.TitleID)
require.ErrorContains(t, err, "not found")
_, err = ds.GetAndroidAppConfiguration(ctx, app1.AdamID, team1.ID)
require.ErrorContains(t, err, "not found")
}
func testHasAndroidAppConfigurationChanged(t *testing.T, ds *Datastore) {
ctx := context.Background()
appID := "com.example.testapp"
setupTestApp(t, ds, appID)
config := &fleet.AndroidAppConfiguration{
ApplicationID: appID,
TeamID: nil,
GlobalOrTeamID: 0,
Configuration: json.RawMessage(`{"managedConfiguration": {"a": 1}}`),
}
err := ds.InsertAndroidAppConfiguration(ctx, config)
require.NoError(t, err)
cases := []struct {
desc string
newConfig string
compareAppID string
changed bool
}{
{
desc: "empty new config",
newConfig: "",
compareAppID: appID,
changed: true,
},
{
desc: "empty object",
newConfig: "{}",
compareAppID: appID,
changed: true,
},
{
desc: "boolean instead of object",
newConfig: "false",
compareAppID: appID,
changed: true,
},
{
desc: "empty managedConfiguration",
newConfig: `{"managedConfiguration": {}}`,
compareAppID: appID,
changed: true,
},
{
desc: "same config",
newConfig: `{"managedConfiguration": {"a":1}}`,
compareAppID: appID,
changed: false,
},
{
desc: "slightly different config",
newConfig: `{"managedConfiguration": {"a":"b"}}`,
compareAppID: appID,
changed: true,
},
{
desc: "expanded different config",
newConfig: `{"managedConfiguration": {"a":1, "b":2}}`,
compareAppID: appID,
changed: true,
},
{
desc: "very different config",
newConfig: `{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`,
compareAppID: appID,
changed: true,
},
{
desc: "empty compared to non-existing",
newConfig: ``,
compareAppID: "com.no-such.app",
changed: false,
},
{
desc: "some config compared to non-existing",
newConfig: `{"workProfileWidgets": "WORK_PROFILE_WIDGETS_ALLOWED"}`,
compareAppID: "com.no-such.app",
changed: true,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
got, err := ds.HasAndroidAppConfigurationChanged(ctx, c.compareAppID, 0, json.RawMessage(c.newConfig))
require.NoError(t, err)
require.Equal(t, c.changed, got)
})
}
}