diff --git a/changes/49777-mdm-enrolled-host-id-apple.md b/changes/49777-mdm-enrolled-host-id-apple.md
new file mode 100644
index 0000000000..3e36511e45
--- /dev/null
+++ b/changes/49777-mdm-enrolled-host-id-apple.md
@@ -0,0 +1 @@
+- Added `host_id` and `host_serial` to the `mdm_enrolled` activity for Apple (macOS, iOS, iPadOS) enrollments, and the activity now appears on the host's activity timeline.
diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts
index c01b3d39a1..bc40e3fa33 100644
--- a/frontend/interfaces/activity.ts
+++ b/frontend/interfaces/activity.ts
@@ -215,6 +215,7 @@ export type IHostPastActivityType =
| ActivityType.WipedHost
| ActivityType.FailedWipe
| ActivityType.MdmUnenrolled
+ | ActivityType.MdmEnrolled
| ActivityType.ReadHostDiskEncryptionKey
| ActivityType.RetrievedHostMyDeviceURL
| ActivityType.ViewedHostRecoveryLockPassword
diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx
index 17978fcb2f..d7f31090c2 100644
--- a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx
+++ b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx
@@ -35,6 +35,7 @@ import RotatedManagedLocalAccountPasswordActivityItem from "./ActivityItems/Rota
import FailedToRotateManagedLocalAccountPasswordActivityItem from "./ActivityItems/FailedToRotateManagedLocalAccountPassword";
import FailedEnrollmentProfileRenewalActivityItem from "./ActivityItems/FailedEnrollmentProfileRenewalActivityItem";
import MdmUnenrolledActivityItem from "./ActivityItems/MdmUnenrolledActivityItem";
+import MdmEnrolledActivityItem from "./ActivityItems/MdmEnrolledActivityItem";
import RanCustomMdmCommandActivityItem from "./ActivityItems/RanCustomMdmCommandActivityItem";
import EditedCustomHostVitalValueActivityItem from "./ActivityItems/EditedCustomHostVitalValueActivityItem";
import PolicyAutomationActivityItem from "./ActivityItems/PolicyAutomationActivityItem";
@@ -94,6 +95,7 @@ export const pastActivityComponentMap: Record<
[ActivityType.FailedToRotateManagedLocalAccountPassword]: FailedToRotateManagedLocalAccountPasswordActivityItem,
[ActivityType.FailedEnrollmentProfileRenewal]: FailedEnrollmentProfileRenewalActivityItem,
[ActivityType.MdmUnenrolled]: MdmUnenrolledActivityItem,
+ [ActivityType.MdmEnrolled]: MdmEnrolledActivityItem,
[ActivityType.RanCustomMdmCommand]: RanCustomMdmCommandActivityItem,
[ActivityType.EditedCustomHostVitalValue]: EditedCustomHostVitalValueActivityItem,
[ActivityType.RanAutomationWebhook]: PolicyAutomationActivityItem,
diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tests.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tests.tsx
new file mode 100644
index 0000000000..b945a84bf7
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tests.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { createMockHostPastActivity } from "__mocks__/activityMock";
+
+import { ActivityType } from "interfaces/activity";
+import { Platform } from "interfaces/platform";
+
+import MdmEnrolledActivityItem from "./MdmEnrolledActivityItem";
+
+const renderItem = (platform: Platform, actor: string) =>
+ render(
+
+ );
+
+describe("MdmEnrolledActivityItem", () => {
+ const cases: Array<[Platform, string, RegExp]> = [
+ ["ios", "Admin User", /told Fleet to enroll this host/i],
+ ["android", "Admin User", /told Fleet to enroll this host/i],
+ ["android", "", /This host enrolled to Fleet/i],
+ [
+ "darwin",
+ "Admin User",
+ /told Fleet to turn on mobile device management \(MDM\) for this host/i,
+ ],
+ [
+ "darwin",
+ "",
+ /Mobile device management \(MDM\) was turned on for this host/i,
+ ],
+ ];
+
+ it.each(cases)("renders %s copy (actor=%j)", (platform, actor, expected) => {
+ renderItem(platform, actor);
+ if (actor) expect(screen.getByText(actor)).toBeVisible();
+ expect(screen.getByText(expected)).toBeVisible();
+ });
+
+ it("does not render the cancel or show-details icons", () => {
+ renderItem("darwin", "Admin User");
+ expect(screen.queryByTestId("close-icon")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("info-outline-icon")).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tsx
new file mode 100644
index 0000000000..9733277f78
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/MdmEnrolledActivityItem.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+
+import { isAndroid, isIPadOrIPhone } from "interfaces/platform";
+
+import ActivityItem from "components/ActivityItem";
+
+import { IHostActivityItemComponentProps } from "../../ActivityConfig";
+
+const baseClass = "mdm-enrolled-activity-item";
+
+const MdmEnrolledActivityItem = ({
+ activity,
+}: IHostActivityItemComponentProps) => {
+ const { actor_full_name } = activity;
+ const platform = activity.details?.platform ?? "";
+
+ let content: React.ReactNode;
+ if (isAndroid(platform) || isIPadOrIPhone(platform)) {
+ content = actor_full_name ? (
+ <>
+ {actor_full_name} told Fleet to enroll this host.
+ >
+ ) : (
+ <>This host enrolled to Fleet.>
+ );
+ } else {
+ content = actor_full_name ? (
+ <>
+ {actor_full_name} told Fleet to turn on mobile device management
+ (MDM) for this host.
+ >
+ ) : (
+ <>Mobile device management (MDM) was turned on for this host.>
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+};
+
+export default MdmEnrolledActivityItem;
diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/index.ts
new file mode 100644
index 0000000000..948b139b14
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/MdmEnrolledActivityItem/index.ts
@@ -0,0 +1 @@
+export { default } from "./MdmEnrolledActivityItem";
diff --git a/server/fleet/activities.go b/server/fleet/activities.go
index 8569a7cfc4..3fe09e2338 100644
--- a/server/fleet/activities.go
+++ b/server/fleet/activities.go
@@ -421,6 +421,10 @@ func (a ActivityTypeFleetEnrolled) ActivityName() string {
}
type ActivityTypeMDMEnrolled struct {
+ // HostID is omitted when zero so Windows enrollments (which don't set it;
+ // see #47874) keep their existing activity payload. It is always set for
+ // Apple enrollments.
+ HostID uint `json:"host_id,omitempty"`
HostSerial *string `json:"host_serial"`
HostDisplayName string `json:"host_display_name"`
InstalledFromDEP bool `json:"installed_from_dep"`
@@ -434,6 +438,16 @@ func (a ActivityTypeMDMEnrolled) ActivityName() string {
return "mdm_enrolled"
}
+// HostIDs links this activity to the host on the host details timeline. Returns nil when the host
+// is unknown (eg the enrollment is being processed before the host record exists) so the global
+// activity is still recorded but no activity_host_past row is inserted.
+func (a ActivityTypeMDMEnrolled) HostIDs() []uint {
+ if a.HostID == 0 {
+ return nil
+ }
+ return []uint{a.HostID}
+}
+
// TODO(BMAA): Should we add enrollment_id for BYOD unenrollments?
type ActivityTypeMDMUnenrolled struct {
HostID uint `json:"host_id"`
diff --git a/server/mdm/lifecycle/lifecycle.go b/server/mdm/lifecycle/lifecycle.go
index 060f07451c..410c42f5f8 100644
--- a/server/mdm/lifecycle/lifecycle.go
+++ b/server/mdm/lifecycle/lifecycle.go
@@ -249,13 +249,18 @@ func (t *HostLifecycle) turnOnApple(ctx context.Context, opts HostOptions) error
// create MDM enrolled activity if not in the middle of a SCEP renewal
if !info.SCEPRenewalInProgress {
mdmEnrolledActivity := &fleet.ActivityTypeMDMEnrolled{
+ HostID: info.HostID,
HostDisplayName: info.DisplayName,
InstalledFromDEP: info.DEPAssignedToFleet,
MDMPlatform: fleet.MDMPlatformApple,
Platform: info.Platform,
}
if nanoEnroll.Type == userEnrollmentDeviceType {
- mdmEnrolledActivity.EnrollmentID = ptr.String(opts.UserEnrollmentID)
+ // Account-driven user (BYOD) enrollments have no hardware serial, so
+ // report the enrollment ID as the serial too, keeping host_serial
+ // populated for automations regardless of enrollment type.
+ mdmEnrolledActivity.EnrollmentID = new(opts.UserEnrollmentID)
+ mdmEnrolledActivity.HostSerial = new(opts.UserEnrollmentID)
} else {
mdmEnrolledActivity.HostSerial = ptr.String(info.HardwareSerial)
}
diff --git a/server/mdm/lifecycle/lifecycle_test.go b/server/mdm/lifecycle/lifecycle_test.go
index c6f8a02745..fbeb897171 100644
--- a/server/mdm/lifecycle/lifecycle_test.go
+++ b/server/mdm/lifecycle/lifecycle_test.go
@@ -7,6 +7,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/stretchr/testify/require"
)
@@ -173,6 +174,65 @@ func TestReconcileHostNameEnforcementOnEnrollment(t *testing.T) {
// duplicate DEP hosts (same serial) does not recreate a pending "ghost" host
// when another DEP-assigned host with that serial still exists, while a host
// with no duplicate is still restored as before.
+// TestMDMEnrolledActivityHostIDAndSerial verifies the Apple mdm_enrolled activity
+// carries host_id (so it lands on the host's activity timeline via HostIDs) and a
+// populated host_serial for both device and account-driven user (BYOD)
+// enrollments. Regression coverage for #49777.
+func TestMDMEnrolledActivityHostIDAndSerial(t *testing.T) {
+ ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
+ const hostID = uint(99)
+
+ // runTurnOn drives an Apple turn-on enrollment of the given nano enrollment
+ // type and returns the mdm_enrolled activity that was recorded.
+ runTurnOn := func(t *testing.T, enrollType, hardwareSerial string, opts HostOptions) *fleet.ActivityTypeMDMEnrolled {
+ ds := new(mock.Store)
+ ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, uuid string) (*fleet.NanoEnrollment, error) {
+ return &fleet.NanoEnrollment{Enabled: true, Type: enrollType, TokenUpdateTally: 1}, nil
+ }
+ ds.GetHostMDMCheckinInfoFunc = func(ctx context.Context, uuid string) (*fleet.HostMDMCheckinInfo, error) {
+ return &fleet.HostMDMCheckinInfo{HostID: hostID, Platform: opts.Platform, HardwareSerial: hardwareSerial}, nil
+ }
+ ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) { return job, nil }
+ ds.ReconcileHostDeviceNamesForHostsFunc = func(ctx context.Context, hostIDs []uint) error { return nil }
+
+ var captured *fleet.ActivityTypeMDMEnrolled
+ newAct := func(ctx context.Context, user *fleet.User, details fleet.ActivityDetails) error {
+ if a, ok := details.(*fleet.ActivityTypeMDMEnrolled); ok {
+ captured = a
+ }
+ return nil
+ }
+
+ lc := New(ds, slog.New(slog.DiscardHandler), newAct)
+ require.NoError(t, lc.Do(ctx, opts))
+ require.NotNil(t, captured, "an mdm_enrolled activity should have been recorded")
+ return captured
+ }
+
+ t.Run("device enrollment: host_id set, host_serial is the hardware serial", func(t *testing.T) {
+ a := runTurnOn(t, mdm.EnrollType(mdm.Device).String(), "C08VQ2AXHT96",
+ HostOptions{Action: HostActionTurnOn, Platform: "darwin", UUID: "host-uuid"})
+
+ require.Equal(t, hostID, a.HostID)
+ require.Equal(t, []uint{hostID}, a.HostIDs())
+ require.NotNil(t, a.HostSerial)
+ require.Equal(t, "C08VQ2AXHT96", *a.HostSerial)
+ require.Nil(t, a.EnrollmentID)
+ })
+
+ t.Run("account-driven user enrollment: host_id set, host_serial is the enrollment id", func(t *testing.T) {
+ a := runTurnOn(t, mdm.EnrollType(mdm.UserEnrollmentDevice).String(), "",
+ HostOptions{Action: HostActionTurnOn, Platform: "ios", UUID: "ADUE-ENROLL-ID", UserEnrollmentID: "ADUE-ENROLL-ID"})
+
+ require.Equal(t, hostID, a.HostID)
+ require.Equal(t, []uint{hostID}, a.HostIDs())
+ require.NotNil(t, a.EnrollmentID)
+ require.Equal(t, "ADUE-ENROLL-ID", *a.EnrollmentID)
+ require.NotNil(t, a.HostSerial)
+ require.Equal(t, "ADUE-ENROLL-ID", *a.HostSerial)
+ })
+}
+
func TestDeleteAppleDuplicateDEPHost(t *testing.T) {
ctx := license.NewContext(context.Background(), &fleet.LicenseInfo{Tier: fleet.TierPremium})
diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go
index ec4ddcc92d..d0c41f224c 100644
--- a/server/service/integration_mdm_dep_test.go
+++ b/server/service/integration_mdm_dep_test.go
@@ -1175,11 +1175,13 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
found = true
require.Nil(t, activity.ActorID)
require.Nil(t, activity.ActorFullName)
+ depHost, err := s.ds.HostByIdentifier(context.Background(), devices[0].SerialNumber)
+ require.NoError(t, err)
require.JSONEq(
t,
fmt.Sprintf(
- `{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": true, "mdm_platform": "apple", "platform": "darwin"}`,
- devices[0].SerialNumber, devices[0].Model, devices[0].SerialNumber,
+ `{"host_id": %d, "host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": true, "mdm_platform": "apple", "platform": "darwin"}`,
+ depHost.ID, devices[0].SerialNumber, devices[0].Model, devices[0].SerialNumber,
),
string(*activity.Details),
)
@@ -1408,11 +1410,13 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() {
s.awaitRunAppleMDMWorkerSchedule()
// The last activity should have `installed_from_dep=true`.
+ depReenrollHost, err := s.ds.HostByIdentifier(context.Background(), mdmDevice.SerialNumber)
+ require.NoError(t, err)
s.lastActivityMatches(
"mdm_enrolled",
fmt.Sprintf(
- `{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": true, "mdm_platform": "apple", "platform": "darwin"}`,
- mdmDevice.SerialNumber, mdmDevice.Model, mdmDevice.SerialNumber,
+ `{"host_id": %d, "host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": true, "mdm_platform": "apple", "platform": "darwin"}`,
+ depReenrollHost.ID, mdmDevice.SerialNumber, mdmDevice.Model, mdmDevice.SerialNumber,
),
0,
)
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index e63727c446..bfc608a086 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -1855,13 +1855,17 @@ func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")
err := mdmDeviceA.Enroll()
require.NoError(t, err)
+ hostA, err := s.ds.HostByIdentifier(context.Background(), mdmDeviceA.SerialNumber)
+ require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
- fmt.Sprintf(`{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0)
+ fmt.Sprintf(`{"host_id": %d, "host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, hostA.ID, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0)
mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")
err = mdmDeviceB.Enroll()
require.NoError(t, err)
+ hostB, err := s.ds.HostByIdentifier(context.Background(), mdmDeviceB.SerialNumber)
+ require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
- fmt.Sprintf(`{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0)
+ fmt.Sprintf(`{"host_id": %d, "host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, hostB.ID, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0)
// Find the ID of Fleet's MDM solution
var mdmID uint
mysqltest.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
@@ -16235,8 +16239,10 @@ func (s *integrationMDMTestSuite) TestAppleMDMAccountDrivenUserEnrollment() {
require.NoError(t, iPhoneMdmDevice.Enroll())
assert.Equal(t, "sso_user@example.com", iPhoneMdmDevice.EnrollInfo.AssignedManagedAppleID)
+ iPhoneHost, err := s.ds.HostByIdentifier(context.Background(), iPhoneMdmDevice.EnrollmentID())
+ require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
- fmt.Sprintf(`{"host_serial": null, "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ios"}`, iPhoneMdmDevice.EnrollmentID(), iPhoneMdmDevice.Model, iPhoneMdmDevice.EnrollmentID()), 0)
+ fmt.Sprintf(`{"host_id": %d, "host_serial": "%s", "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ios"}`, iPhoneHost.ID, iPhoneMdmDevice.EnrollmentID(), iPhoneMdmDevice.EnrollmentID(), iPhoneMdmDevice.Model, iPhoneMdmDevice.EnrollmentID()), 0)
linkedIDPAccount, err := s.ds.GetMDMIdPAccountByHostUUID(context.Background(), iPhoneMdmDevice.EnrollmentID())
require.NoError(t, err)
require.NotNil(t, linkedIDPAccount)
@@ -16270,8 +16276,10 @@ func (s *integrationMDMTestSuite) TestAppleMDMAccountDrivenUserEnrollment() {
require.NoError(t, iPadMdmDevice.Enroll())
assert.Equal(t, "sso_user2@example.com", iPadMdmDevice.EnrollInfo.AssignedManagedAppleID)
+ iPadHost, err := s.ds.HostByIdentifier(context.Background(), iPadMdmDevice.EnrollmentID())
+ require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
- fmt.Sprintf(`{"host_serial": null, "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ipados"}`, iPadMdmDevice.EnrollmentID(), iPadMdmDevice.Model, iPadMdmDevice.EnrollmentID()), 0)
+ fmt.Sprintf(`{"host_id": %d, "host_serial": "%s", "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ipados"}`, iPadHost.ID, iPadMdmDevice.EnrollmentID(), iPadMdmDevice.EnrollmentID(), iPadMdmDevice.Model, iPadMdmDevice.EnrollmentID()), 0)
linkedIDPAccount, err = s.ds.GetMDMIdPAccountByHostUUID(context.Background(), iPadMdmDevice.EnrollmentID())
require.NoError(t, err)
require.NotNil(t, linkedIDPAccount)
@@ -16285,8 +16293,10 @@ func (s *integrationMDMTestSuite) TestAppleMDMAccountDrivenUserEnrollment() {
require.NoError(t, oldUrlIphoneMdmDevice.Enroll())
assert.Equal(t, "sso_user2@example.com", oldUrlIphoneMdmDevice.EnrollInfo.AssignedManagedAppleID)
+ oldUrlIphoneHost, err := s.ds.HostByIdentifier(context.Background(), oldUrlIphoneMdmDevice.EnrollmentID())
+ require.NoError(t, err)
s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
- fmt.Sprintf(`{"host_serial": null, "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ios"}`, oldUrlIphoneMdmDevice.EnrollmentID(), oldUrlIphoneMdmDevice.Model, oldUrlIphoneMdmDevice.EnrollmentID()), 0)
+ fmt.Sprintf(`{"host_id": %d, "host_serial": "%s", "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ios"}`, oldUrlIphoneHost.ID, oldUrlIphoneMdmDevice.EnrollmentID(), oldUrlIphoneMdmDevice.EnrollmentID(), oldUrlIphoneMdmDevice.Model, oldUrlIphoneMdmDevice.EnrollmentID()), 0)
linkedIDPAccount, err = s.ds.GetMDMIdPAccountByHostUUID(context.Background(), oldUrlIphoneMdmDevice.EnrollmentID())
require.NoError(t, err)
require.NotNil(t, linkedIDPAccount)
diff --git a/server/service/integration_vpp_install_test.go b/server/service/integration_vpp_install_test.go
index acae1dc712..636161367b 100644
--- a/server/service/integration_vpp_install_test.go
+++ b/server/service/integration_vpp_install_test.go
@@ -1395,14 +1395,21 @@ func (s *integrationMDMTestSuite) TestVPPAppActivitiesOnCancelInstall() {
listPastResp = listActivitiesResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", mdmHost2.ID), nil, http.StatusOK, &listPastResp)
require.GreaterOrEqual(t, len(listPastResp.Activities), 2)
+ // mdm_unenrolled is emitted last in the unenroll flow, so it heads the (descending) feed.
require.Equal(t, fleet.ActivityTypeMDMUnenrolled{}.ActivityName(), listPastResp.Activities[0].Type)
- require.Equal(t, fleet.ActivityInstalledAppStoreApp{}.ActivityName(), listPastResp.Activities[1].Type)
- require.Contains(t, string(*listPastResp.Activities[1].Details), fmt.Sprintf(`"app_store_id": %q`, app1.AdamID))
- require.Contains(t, string(*listPastResp.Activities[1].Details), `"status": "failed_install"`)
- if len(listPastResp.Activities) > 2 {
- // the third activity should not be the cancellation of the second app
- require.Equal(t, fleet.ActivityInstalledAppStoreApp{}.ActivityName(), listPastResp.Activities[2].Type)
+ // Only the first VPP app was activated, so exactly one installed_app_store_app cancellation
+ // should appear (the second app was never activated). Filter by type, since the feed also
+ // includes the host's mdm_enrolled activity.
+ appStoreType := fleet.ActivityInstalledAppStoreApp{}.ActivityName()
+ var appStoreActs []*fleet.Activity
+ for _, act := range listPastResp.Activities {
+ if act.Type == appStoreType {
+ appStoreActs = append(appStoreActs, act)
+ }
}
+ require.Len(t, appStoreActs, 1)
+ require.Contains(t, string(*appStoreActs[0].Details), fmt.Sprintf(`"app_store_id": %q`, app1.AdamID))
+ require.Contains(t, string(*appStoreActs[0].Details), `"status": "failed_install"`)
// listing the host's software available for install shows the cancelled app as failed
getHostSw = getHostSoftwareResponse{}
@@ -1890,10 +1897,18 @@ func (s *integrationMDMTestSuite) TestInHouseAppSelfInstall() {
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", iosHost.ID), nil, http.StatusOK, &listUpcomingAct)
require.Len(t, listUpcomingAct.Activities, 0)
- // host has the past activity for the installed app
+ // host has the past activity for the installed app (the feed also includes the host's
+ // mdm_enrolled activity, so filter by type).
var listPastResp listActivitiesResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", iosHost.ID), nil, http.StatusOK, &listPastResp)
- require.Len(t, listPastResp.Activities, 1)
+ installedSoftwareType := fleet.ActivityTypeInstalledSoftware{}.ActivityName()
+ installedCount := 0
+ for _, act := range listPastResp.Activities {
+ if act.Type == installedSoftwareType {
+ installedCount++
+ }
+ }
+ require.Equal(t, 1, installedCount)
// update the app to have a label condition
clr := fleet.CreateLabelResponse{}