Clean up setup experience cancellation behavior (#43437)

Fixes #34288.

# 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.

## Testing

- [x] Added/updated automated tests

- [ ] QA'd all new/changed functionality manually

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

* **New Features**
* Setup experience cancellations now create explicit cancellation
activities for skipped/failed software and VPP app installs, plus a new
"Canceled setup experience" activity type and a from_setup_experience
flag. Activity text and host activity views now indicate "during setup
experience" when applicable.
* **Tests**
* Added and updated tests for cancellation activity creation, VPP
license-failure handling, and WasFromAutomation/from_setup_experience
behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ian Littman
2026-04-14 09:39:26 -05:00
committed by GitHub
parent a3b7e29e26
commit 3675f8ff90
31 changed files with 1208 additions and 221 deletions
+6 -42
View File
@@ -861,68 +861,32 @@ WHERE host_uuid = ?
}
func (ds *Datastore) MaybeUpdateSetupExperienceVPPStatus(ctx context.Context, hostUUID string, nanoCommandUUID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
selectStmt := "SELECT id FROM setup_experience_status_results WHERE host_uuid = ? AND nano_command_uuid = ?"
updateStmt := "UPDATE setup_experience_status_results SET status = ? WHERE id = ?"
var id uint
if err := ds.writer(ctx).GetContext(ctx, &id, selectStmt, hostUUID, nanoCommandUUID); err != nil {
// TODO: maybe we can use the reader instead for this query
if errors.Is(err, sql.ErrNoRows) {
// return early if no results found
return false, nil
}
return false, err
}
res, err := ds.writer(ctx).ExecContext(ctx, updateStmt, status, id)
stmt := `UPDATE setup_experience_status_results SET status = ? WHERE host_uuid = ? AND nano_command_uuid = ? AND status NOT IN (?, ?, ?)`
res, err := ds.writer(ctx).ExecContext(ctx, stmt, status, hostUUID, nanoCommandUUID, fleet.SetupExperienceStatusSuccess, fleet.SetupExperienceStatusFailure, fleet.SetupExperienceStatusCancelled)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
func (ds *Datastore) MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx context.Context, hostUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
selectStmt := "SELECT id FROM setup_experience_status_results WHERE host_uuid = ? AND host_software_installs_execution_id = ?"
updateStmt := "UPDATE setup_experience_status_results SET status = ? WHERE id = ?"
var id uint
if err := ds.writer(ctx).GetContext(ctx, &id, selectStmt, hostUUID, executionID); err != nil {
// TODO: maybe we can use the reader instead for this query
if errors.Is(err, sql.ErrNoRows) {
// return early if no results found
return false, nil
}
return false, err
}
res, err := ds.writer(ctx).ExecContext(ctx, updateStmt, status, id)
stmt := `UPDATE setup_experience_status_results SET status = ? WHERE host_uuid = ? AND host_software_installs_execution_id = ? AND status NOT IN (?, ?, ?)`
res, err := ds.writer(ctx).ExecContext(ctx, stmt, status, hostUUID, executionID, fleet.SetupExperienceStatusSuccess, fleet.SetupExperienceStatusFailure, fleet.SetupExperienceStatusCancelled)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
func (ds *Datastore) MaybeUpdateSetupExperienceScriptStatus(ctx context.Context, hostUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
selectStmt := "SELECT id FROM setup_experience_status_results WHERE host_uuid = ? AND script_execution_id = ?"
updateStmt := "UPDATE setup_experience_status_results SET status = ? WHERE id = ?"
var id uint
if err := ds.writer(ctx).GetContext(ctx, &id, selectStmt, hostUUID, executionID); err != nil {
// TODO: maybe we can use the reader instead for this query
if errors.Is(err, sql.ErrNoRows) {
// return early if no results found
return false, nil
}
return false, err
}
res, err := ds.writer(ctx).ExecContext(ctx, updateStmt, status, id)
stmt := `UPDATE setup_experience_status_results SET status = ? WHERE host_uuid = ? AND script_execution_id = ? AND status NOT IN (?, ?, ?)`
res, err := ds.writer(ctx).ExecContext(ctx, stmt, status, hostUUID, executionID, fleet.SetupExperienceStatusSuccess, fleet.SetupExperienceStatusFailure, fleet.SetupExperienceStatusCancelled)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
@@ -34,6 +34,7 @@ func TestSetupExperience(t *testing.T) {
{"TestUpdateSetupExperienceScriptWhileEnqueued", testUpdateSetupExperienceScriptWhileEnqueued},
{"TestEnqueueSetupExperienceItemsWindows", testEnqueueSetupExperienceItemsWindows},
{"EnqueueSetupExperienceItemsWithDisplayName", testEnqueueSetupExperienceItemsWithDisplayName},
{"UpdateStatusGuardsTerminalStates", testUpdateStatusGuardsTerminalStates},
}
for _, c := range cases {
@@ -1794,6 +1795,185 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) {
require.False(t, inSetupExperience)
}
func testUpdateStatusGuardsTerminalStates(t *testing.T, ds *Datastore) {
ctx := context.Background()
hostUUID := uuid.NewString()
// --- Set up foreign-key references ---
// User (required for software installer)
user, err := ds.NewUser(ctx, &fleet.User{
Name: "GuardTest",
Email: "guard@example.com",
GlobalRole: new("admin"),
Password: []byte("12characterslong!"),
})
require.NoError(t, err)
// Software installer
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Filename: "guard_test.pkg",
Title: "Guard Test Software",
Version: "1.0.0",
Source: "apps",
Platform: "darwin",
Extension: "pkg",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// VPP token + app
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Guard Kong", "GuardJungle")
require.NoError(t, err)
tok, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok.ID, []uint{})
require.NoError(t, err)
vppApp, err := ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
BundleIdentifier: "com.guard.test",
Name: "guard_test.app",
LatestVersion: "1.0.0",
}, nil)
require.NoError(t, err)
var vppAppsTeamsID uint
err = sqlx.GetContext(ctx, ds.reader(ctx), &vppAppsTeamsID,
`SELECT id FROM vpp_apps_teams WHERE adam_id = ?`, vppApp.AdamID)
require.NoError(t, err)
// Setup experience script (raw SQL, same pattern as testSetupExperienceStatusResults)
var scriptID uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
res, err := q.ExecContext(ctx, `INSERT INTO setup_experience_scripts (name) VALUES (?)`, "guard_test_script")
require.NoError(t, err)
id, err := res.LastInsertId()
require.NoError(t, err)
scriptID = uint(id) //nolint: gosec
return nil
})
// --- Helpers ---
insertRow := func(sesr *fleet.SetupExperienceStatusResult) {
stmt := `INSERT INTO setup_experience_status_results
(id, host_uuid, name, status, software_installer_id,
host_software_installs_execution_id, vpp_app_team_id,
nano_command_uuid, setup_experience_script_id,
script_execution_id, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
res, err := q.ExecContext(ctx, stmt,
sesr.ID, sesr.HostUUID, sesr.Name, sesr.Status,
sesr.SoftwareInstallerID,
sesr.HostSoftwareInstallsExecutionID,
sesr.VPPAppTeamID, sesr.NanoCommandUUID,
sesr.SetupExperienceScriptID,
sesr.ScriptExecutionID, sesr.Error)
require.NoError(t, err)
id, err := res.LastInsertId()
require.NoError(t, err)
sesr.ID = uint(id) //nolint: gosec
return nil
})
}
readStatus := func(id uint) fleet.SetupExperienceStatusResultStatus {
var status fleet.SetupExperienceStatusResultStatus
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &status,
"SELECT status FROM setup_experience_status_results WHERE id = ?", id)
})
return status
}
// --- Negative tests: terminal states must not be overwritten ---
terminalStatuses := []fleet.SetupExperienceStatusResultStatus{
fleet.SetupExperienceStatusCancelled,
fleet.SetupExperienceStatusFailure,
fleet.SetupExperienceStatusSuccess,
}
for _, termStatus := range terminalStatuses {
// Software installer row
execID := uuid.NewString()
row := &fleet.SetupExperienceStatusResult{
HostUUID: hostUUID,
Name: "sw-" + string(termStatus),
Status: termStatus,
SoftwareInstallerID: new(installerID),
HostSoftwareInstallsExecutionID: new(execID),
}
insertRow(row)
updated, err := ds.MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx, hostUUID, execID, fleet.SetupExperienceStatusFailure)
require.NoError(t, err)
require.False(t, updated, "software installer row in %s should not be updated", termStatus)
require.Equal(t, termStatus, readStatus(row.ID))
// VPP row
nanoUUID := uuid.NewString()
row = &fleet.SetupExperienceStatusResult{
HostUUID: hostUUID,
Name: "vpp-" + string(termStatus),
Status: termStatus,
VPPAppTeamID: new(vppAppsTeamsID),
NanoCommandUUID: new(nanoUUID),
}
insertRow(row)
updated, err = ds.MaybeUpdateSetupExperienceVPPStatus(ctx, hostUUID, nanoUUID, fleet.SetupExperienceStatusFailure)
require.NoError(t, err)
require.False(t, updated, "VPP row in %s should not be updated", termStatus)
require.Equal(t, termStatus, readStatus(row.ID))
// Script row
scriptExecID := uuid.NewString()
row = &fleet.SetupExperienceStatusResult{
HostUUID: hostUUID,
Name: "script-" + string(termStatus),
Status: termStatus,
SetupExperienceScriptID: new(scriptID),
ScriptExecutionID: new(scriptExecID),
}
insertRow(row)
updated, err = ds.MaybeUpdateSetupExperienceScriptStatus(ctx, hostUUID, scriptExecID, fleet.SetupExperienceStatusFailure)
require.NoError(t, err)
require.False(t, updated, "script row in %s should not be updated", termStatus)
require.Equal(t, termStatus, readStatus(row.ID))
}
// --- Positive control: pending row CAN be updated ---
pendingExecID := uuid.NewString()
pendingRow := &fleet.SetupExperienceStatusResult{
HostUUID: hostUUID,
Name: "sw-pending-positive",
Status: fleet.SetupExperienceStatusPending,
SoftwareInstallerID: new(installerID),
HostSoftwareInstallsExecutionID: new(pendingExecID),
}
insertRow(pendingRow)
updated, err := ds.MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx, hostUUID, pendingExecID, fleet.SetupExperienceStatusFailure)
require.NoError(t, err)
require.True(t, updated, "pending row should be updated")
require.Equal(t, fleet.SetupExperienceStatusFailure, readStatus(pendingRow.ID))
// --- Bug-scenario test: canceled VPP row must not flip to failure ---
cancelledNanoUUID := uuid.NewString()
cancelledVPPRow := &fleet.SetupExperienceStatusResult{
HostUUID: hostUUID,
Name: "vpp-canceled-bug",
Status: fleet.SetupExperienceStatusCancelled,
VPPAppTeamID: new(vppAppsTeamsID),
NanoCommandUUID: new(cancelledNanoUUID),
}
insertRow(cancelledVPPRow)
updated, err = ds.MaybeUpdateSetupExperienceVPPStatus(ctx, hostUUID, cancelledNanoUUID, fleet.SetupExperienceStatusFailure)
require.NoError(t, err)
require.False(t, updated, "cancelled VPP row must not be overwritten by late failure result")
require.Equal(t, fleet.SetupExperienceStatusCancelled, readStatus(cancelledVPPRow.ID))
}
func testGetSetupExperienceScriptByID(t *testing.T, ds *Datastore) {
ctx := context.Background()
+46 -16
View File
@@ -233,6 +233,8 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeEditedHostIdpData{},
ActivityTypeEditedEnrollSecrets{},
ActivityTypeCanceledSetupExperience{},
}
// ActivityDetails is an alias for the canonical ActivityDetails interface defined in server/activity/api.
@@ -877,7 +879,7 @@ type ActivityTypeRanScript struct {
Async bool `json:"async"`
PolicyID *uint `json:"policy_id"`
PolicyName *string `json:"policy_name"`
FromSetupExperience bool `json:"-"`
FromSetupExperience bool `json:"from_setup_experience"`
}
func (a ActivityTypeRanScript) ActivityName() string {
@@ -1086,7 +1088,7 @@ type ActivityTypeInstalledSoftware struct {
Source *string `json:"source,omitempty"`
PolicyID *uint `json:"policy_id"`
PolicyName *string `json:"policy_name"`
FromSetupExperience bool `json:"-"`
FromSetupExperience bool `json:"from_setup_experience"`
CommandUUID string `json:"command_uuid,omitempty"`
}
@@ -1329,7 +1331,7 @@ type ActivityInstalledAppStoreApp struct {
PolicyID *uint `json:"policy_id"`
PolicyName *string `json:"policy_name"`
HostPlatform string `json:"host_platform"`
FromSetupExperience bool `json:"-"`
FromSetupExperience bool `json:"from_setup_experience"`
FromAutoUpdate bool `json:"from_auto_update"`
}
@@ -1346,11 +1348,10 @@ func (a ActivityInstalledAppStoreApp) WasFromAutomation() bool {
}
func (a ActivityInstalledAppStoreApp) MustActivateNextUpcomingActivity() bool {
// for VPP apps, we only activate the next upcoming activity if the installation
// failed, because if it succeeded (and in this case, it only means the command to
// install succeeded), we only activate the next activity when we verify the
// app is actually installed.
return a.Status != string(SoftwareInstalled)
// For VPP apps, we only activate the next upcoming activity if the installation
// failed; successes are activated on install verification.
// More info on the blank command UUID skip at https://github.com/fleetdm/fleet/pull/43437#discussion_r3074297749
return a.CommandUUID != "" && a.Status != string(SoftwareInstalled)
}
func (a ActivityInstalledAppStoreApp) ActivateNextUpcomingActivityArgs() (uint, string) {
@@ -1541,10 +1542,11 @@ func (a ActivityTypeCanceledRunScript) HostIDs() []uint {
}
type ActivityTypeCanceledInstallSoftware struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
FromSetupExperience bool `json:"from_setup_experience"`
}
func (a ActivityTypeCanceledInstallSoftware) ActivityName() string {
@@ -1555,6 +1557,10 @@ func (a ActivityTypeCanceledInstallSoftware) HostIDs() []uint {
return []uint{a.HostID}
}
func (a ActivityTypeCanceledInstallSoftware) WasFromAutomation() bool {
return a.FromSetupExperience
}
type ActivityTypeCanceledUninstallSoftware struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
@@ -1571,10 +1577,11 @@ func (a ActivityTypeCanceledUninstallSoftware) HostIDs() []uint {
}
type ActivityTypeCanceledInstallAppStoreApp struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
FromSetupExperience bool `json:"from_setup_experience"`
}
func (a ActivityTypeCanceledInstallAppStoreApp) HostIDs() []uint {
@@ -1585,6 +1592,10 @@ func (a ActivityTypeCanceledInstallAppStoreApp) ActivityName() string {
return "canceled_install_app_store_app"
}
func (a ActivityTypeCanceledInstallAppStoreApp) WasFromAutomation() bool {
return a.FromSetupExperience
}
type ActivityTypeRanScriptBatch struct {
ScriptName string `json:"script_name"`
BatchExecutionID string `json:"batch_execution_id"`
@@ -1865,3 +1876,22 @@ func (a ActivityTypeClearedPasscode) ActivityName() string {
func (a ActivityTypeClearedPasscode) HostIDs() []uint {
return []uint{a.HostID}
}
type ActivityTypeCanceledSetupExperience struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
SoftwareTitle string `json:"software_title"`
SoftwareTitleID uint `json:"software_title_id"`
}
func (a ActivityTypeCanceledSetupExperience) ActivityName() string {
return "canceled_setup_experience"
}
func (a ActivityTypeCanceledSetupExperience) HostIDs() []uint {
return []uint{a.HostID}
}
func (a ActivityTypeCanceledSetupExperience) WasFromAutomation() bool {
return true
}
+82
View File
@@ -0,0 +1,82 @@
package fleet
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestVPPInstallFailureEmptyCommandUUIDDoesNotActivateNext exercises the
// scenario where a VPP install is attempted during setup experience for a
// host that has other upcoming activities queued. If the VPP call fails
// before an MDM command is sent (e.g. no available licenses), the
// CommandUUID is empty. In that case the next upcoming activity must NOT
// be activated, because the current activity was never truly started —
// activating the next one would break the intended sequential ordering.
//
// See commit 159194acc9d92843bb2de933309f159c84a501aa for the fix.
func TestVPPInstallFailureEmptyCommandUUIDDoesNotActivateNext(t *testing.T) {
t.Parallel()
tests := []struct {
name string
activity ActivityInstalledAppStoreApp
expectActivate bool
}{
{
name: "failed VPP install with empty command UUID must not activate next upcoming activity",
activity: ActivityInstalledAppStoreApp{
HostID: 42,
HostDisplayName: "ios-host",
SoftwareTitle: "Licensed App",
AppStoreID: "99999",
CommandUUID: "", // no MDM command was sent
Status: string(SoftwareInstallFailed),
FromSetupExperience: true,
},
expectActivate: false,
},
{
name: "failed VPP install with command UUID activates next upcoming activity",
activity: ActivityInstalledAppStoreApp{
HostID: 42,
HostDisplayName: "ios-host",
SoftwareTitle: "Licensed App",
AppStoreID: "99999",
CommandUUID: "cmd-uuid-abc",
Status: string(SoftwareInstallFailed),
FromSetupExperience: true,
},
expectActivate: true,
},
{
name: "successful VPP install must not activate next (handled by install verification)",
activity: ActivityInstalledAppStoreApp{
HostID: 42,
HostDisplayName: "ios-host",
SoftwareTitle: "Licensed App",
AppStoreID: "99999",
CommandUUID: "cmd-uuid-abc",
Status: string(SoftwareInstalled),
FromSetupExperience: true,
},
expectActivate: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expectActivate, tt.activity.MustActivateNextUpcomingActivity(),
"MustActivateNextUpcomingActivity() = %v, want %v",
tt.activity.MustActivateNextUpcomingActivity(), tt.expectActivate)
if tt.expectActivate {
hostID, cmdUUID := tt.activity.ActivateNextUpcomingActivityArgs()
assert.Equal(t, tt.activity.HostID, hostID)
assert.Equal(t, tt.activity.CommandUUID, cmdUUID)
}
})
}
}
+4
View File
@@ -121,6 +121,10 @@ func (s *SetupExperienceStatusResult) IsForSoftwarePackage() bool {
return s.SoftwareInstallerID != nil
}
func (s *SetupExperienceStatusResult) IsForVPPApp() bool {
return s.VPPAppTeamID != nil
}
func (s *SetupExperienceStatusResult) ForMyDevicePage(token string) {
// convert api style iconURL to device token URL
if s.IconURL != "" && s.SoftwareTitleID != nil {
+1 -1
View File
@@ -3960,7 +3960,7 @@ func (svc *MDMAppleCheckinAndCommandService) CommandAndReportResults(r *mdm.Requ
HostUUID: cmdResult.Identifier(),
CommandUUID: cmdResult.CommandUUID,
CommandStatus: cmdResult.Status,
}, true); err != nil {
}, fleet.NewActivityFunc(svc.newActivityFn)); err != nil {
return nil, ctxerr.Wrap(r.Context, err, "updating setup experience status from VPP install result")
} else if updated {
// TODO: call next step of setup experience?
+1 -1
View File
@@ -171,7 +171,7 @@ func NewInstalledApplicationListResultsHandler(
HostUUID: installedAppResult.HostUUID(),
CommandUUID: expectedInstall.InstallCommandUUID,
CommandStatus: terminalStatus,
}, true); err != nil {
}, fleet.NewActivityFunc(newActivityFn)); err != nil {
return ctxerr.Wrap(ctx, err, "updating setup experience status from VPP install result")
} else if updated {
fromSetupExperience = true
+11 -14
View File
@@ -7968,7 +7968,8 @@ func (s *integrationEnterpriseTestSuite) TestRunBatchScript() {
"batch_execution_id": "%s",
"script_execution_id": "%s",
"script_name": "%s",
"host_display_name": "%s"
"host_display_name": "%s",
"from_setup_experience": false
}
`, host1.ID, batchRes.BatchExecutionID, orbitRespHost1.Notifications.PendingScriptExecutionIDs[0], script.Name, host1.DisplayName())
require.Len(t, hostPastActivitiesResp.Activities, 1)
@@ -8183,7 +8184,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostSavedScript() {
s.lastActivityMatches(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null}`,
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null, "from_setup_experience": false}`,
host.ID, host.DisplayName(), savedNoTmScript.Name, scriptResultResp.ExecutionID,
),
0,
@@ -15701,7 +15702,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
s.lastActivityOfTypeMatches(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null}`,
`{"host_id": %d, "host_display_name": %q, "script_name": "", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null, "from_setup_experience": false}`,
host.ID, host.DisplayName(), scriptExecID), 0)
// create a saved script execution request
@@ -15723,7 +15724,7 @@ func (s *integrationEnterpriseTestSuite) TestHostScriptSoftDelete() {
http.StatusOK)
s.lastActivityOfTypeMatches(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null}`,
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "script1.sh", "script_execution_id": %q, "async": true, "policy_id": null, "policy_name": null, "batch_execution_id": null, "from_setup_experience": false}`,
host.ID, host.DisplayName(), savedScriptExecID), 0)
// get the anonymous script result details
@@ -17979,7 +17980,8 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
"status": "installed",
"source": "apps",
"policy_id": %d,
"policy_name": "%s"
"policy_name": "%s",
"from_setup_experience": false
}`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp", "dummy_installer.pkg", host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0)
var activityCount int
@@ -23331,18 +23333,13 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
require.NotEmpty(t, executionIDs["vim"])
require.NotEmpty(t, executionIDs["test.tar.gz"])
s.lastActivityOfTypeMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallSoftware{}.ActivityName(), fmt.Sprintf(`{
"host_id": %d,
"host_display_name": %q,
"software_title": %q,
"software_package": %q,
"install_uuid": %q,
"status": "failed",
"self_service": false,
"source": "deb_packages",
"policy_name": null,
"policy_id": null
}`, ubuntuHost.ID, ubuntuHost.DisplayName(), "vim", "vim.deb", executionIDs["vim"]), 0)
"software_title_id": %d,
"from_setup_experience": true
}`, ubuntuHost.ID, ubuntuHost.DisplayName(), "vim", debVimTitleID), 0)
// Record a result for test.tar.gz.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
@@ -500,7 +500,8 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
"status": "installed",
"source": "apps",
"policy_id": null,
"policy_name": null
"policy_name": null,
"from_setup_experience": true
}
`, enrolledHost.ID, getHostResp.Host.DisplayName, statusResp.Results.Software[0].Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, installUUID)
@@ -580,7 +581,8 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
"script_name": "%s",
"host_display_name": "%s",
"script_execution_id": "%s",
"batch_execution_id": null
"batch_execution_id": null,
"from_setup_experience": true
}
`, enrolledHost.ID, statusResp.Results.Script.Name, getHostResp.Host.DisplayName, execID)
@@ -931,7 +933,8 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithFMAAndVersionRollba
"status": "installed",
"source": "apps",
"policy_id": null,
"policy_name": null
"policy_name": null,
"from_setup_experience": true
}
`, enrolledHost.ID, getHostResp.Host.DisplayName, titleDetail.SoftwareTitle.SoftwarePackage.Name, installUUID)
s.lastActivityMatchesExtended(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), expectedActivityDetail, 0, ptr.Bool(true))
@@ -4146,7 +4149,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceAndroid() {
req := android_service.PubSubPushRequest{PubSubMessage: *reportMsg}
s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubSubToken.Value))
s.lastActivityOfTypeMatches(fleet.ActivityInstalledAppStoreApp{}.ActivityName(), fmt.Sprintf(`{"app_store_id":%q,
"command_uuid":%q, "host_display_name":%q, "host_id":%d, "host_platform":%q, "policy_id":null, "policy_name":null, "self_service":false, "from_auto_update": false, "software_title":%q,
"command_uuid":%q, "from_auto_update": false, "from_setup_experience": true, "host_display_name":%q, "host_id":%d, "host_platform":%q, "policy_id":null, "policy_name":null, "self_service":false, "software_title":%q,
"status":%q}`, app1.AdamID, app1CmdUUID, host.DisplayName(), host.ID, host.Platform, app1.Name, fleet.SoftwareInstalled), 0)
// the pending install should now be verified
@@ -4366,7 +4369,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceAndroidCancelOnUnenroll() {
{"enrollment_id": null, "host_display_name": %q, "host_serial": %q, "installed_from_dep": false, "platform": %q}`,
host1.DisplayName(), "", host1.Platform), 0) // for some reason the serial is force-set to empty string when we create this activity
s.lastActivityOfTypeMatches(fleet.ActivityInstalledAppStoreApp{}.ActivityName(), fmt.Sprintf(`{"app_store_id":%q,
"command_uuid":%q, "host_display_name":%q, "host_id":%d, "host_platform":%q, "policy_id":null, "policy_name":null, "self_service":false, "from_auto_update": false, "software_title":%q,
"command_uuid":%q, "from_auto_update": false, "from_setup_experience": true, "host_display_name":%q, "host_id":%d, "host_platform":%q, "policy_id":null, "policy_name":null, "self_service":false, "software_title":%q,
"status":%q}`, app1.AdamID, app1CmdUUID, host1.DisplayName(), host1.ID, host1.Platform, app1.Name, fleet.SoftwareInstallFailed), 0)
// host2 and host3 haven't been unenrolled, app install is still pending
+11 -11
View File
@@ -14265,7 +14265,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
errApp.Name,
@@ -14351,7 +14351,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
macOSApp.Name,
@@ -14427,7 +14427,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
addedApp.Name,
@@ -14685,7 +14685,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
installHost.ID,
installHost.DisplayName(),
app.Name,
@@ -15235,7 +15235,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
s.lastActivityMatchesExtended(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null, "from_setup_experience": false}`,
mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
),
0,
@@ -15304,7 +15304,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
s.lastActivityMatchesExtended(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null, "from_setup_experience": false}`,
mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
),
0,
@@ -15421,7 +15421,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
s.lastActivityMatchesExtended(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s", "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s", "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
macOSApp.Name,
@@ -15473,7 +15473,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
s.lastActivityMatchesExtended(
fleet.ActivityTypeRanScript{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null, "from_setup_experience": false}`,
mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
),
0,
@@ -15539,7 +15539,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
s.lastActivityMatchesExtended(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "host_platform": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "host_platform": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s", "from_setup_experience": false, "from_auto_update": false}`,
mdmHost2.ID,
mdmHost2.DisplayName(),
mdmHost2.Platform,
@@ -19706,7 +19706,7 @@ func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
// cancel the VPP app install, confirm canceled activity
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[0].UUID), nil, http.StatusNoContent)
lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallAppStoreApp{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "App 1", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), vppAppTitleID), 0)
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "App 1", "software_title_id": %d, "from_setup_experience": false}`, mdmHost.ID, mdmHost.DisplayName(), vppAppTitleID), 0)
// to be able to simulate the host sending a MDM result post-cancelation,
// we need to re-activate the MDM command.
@@ -19740,7 +19740,7 @@ func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
// cancel the software install, confirm canceled activity
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[1].UUID), nil, http.StatusNoContent)
lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallSoftware{}.ActivityName(),
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d, "from_setup_experience": false}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)
// record a software install result post-cancelation
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
+10 -10
View File
@@ -435,7 +435,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
errApp.Name,
@@ -483,7 +483,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
addedApp.Name,
@@ -600,7 +600,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
addedApp.Name,
@@ -650,7 +650,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
mdmHost.ID,
mdmHost.DisplayName(),
addedApp.Name,
@@ -900,7 +900,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
iosHost.ID,
iosHost.DisplayName(),
iOSApp.Name,
@@ -1008,7 +1008,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "from_auto_update": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
ipodHost.ID,
ipodHost.DisplayName(),
iOSApp.Name,
@@ -1164,7 +1164,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": true, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_auto_update": false}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": true, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false, "from_auto_update": false}`,
data.host.ID,
data.host.DisplayName(),
data.app.Name,
@@ -1847,7 +1847,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppSelfInstall() {
// installed activity is now created
activityData = fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "command_uuid": %q, "install_uuid": "",
"software_title": "ipa_test", "software_package": "", "self_service": true, "status": "installed",
"policy_id": null, "policy_name": null}`, iosHost.ID, iosHost.DisplayName(), installCmdUUID)
"policy_id": null, "policy_name": null, "from_setup_experience": false}`, iosHost.ID, iosHost.DisplayName(), installCmdUUID)
s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), activityData, 0)
// host has no more upcoming activities
@@ -2179,7 +2179,7 @@ func (s *integrationMDMTestSuite) TestVPPAppScheduledUpdates() {
s.lastActivityMatches(
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "from_auto_update": false, "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "from_auto_update": false, "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
host.ID,
host.DisplayName(),
"App 1",
@@ -2457,7 +2457,7 @@ func (s *integrationMDMTestSuite) TestVPPAppScheduledUpdates() {
fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
fmt.Sprintf(
// See `"from_auto_update": true`.
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "from_auto_update": true, "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s"}`,
`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "from_auto_update": true, "status": "%s", "self_service": false, "policy_id": null, "policy_name": null, "host_platform": "%s", "from_setup_experience": false}`,
host.ID,
host.DisplayName(),
"App 1",
+2 -2
View File
@@ -952,7 +952,7 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
HostUUID: host.UUID,
ExecutionID: result.ExecutionID,
ExitCode: result.ExitCode,
}, true); err != nil {
}, svc.NewActivity); err != nil {
return ctxerr.Wrap(ctx, err, "update setup experience status")
} else if updated {
svc.logger.DebugContext(ctx, "setup experience script result updated", "host_uuid", host.UUID, "execution_id", result.ExecutionID)
@@ -1415,7 +1415,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f
HostUUID: hostUUID,
ExecutionID: result.InstallUUID,
InstallerStatus: result.Status(),
}, true); err != nil {
}, svc.NewActivity); err != nil {
return ctxerr.Wrap(ctx, err, "update setup experience status")
} else if updated {
svc.logger.DebugContext(ctx,
+29 -10
View File
@@ -266,10 +266,10 @@ func isAllSetupExperienceSoftwareRequired(ctx context.Context, ds fleet.Datastor
}
func (svc *Service) MaybeCancelPendingSetupExperienceSteps(ctx context.Context, host *fleet.Host) error {
return maybeCancelPendingSetupExperienceSteps(ctx, svc.ds, host)
return maybeCancelPendingSetupExperienceSteps(ctx, svc.ds, host, svc.NewActivity)
}
func maybeCancelPendingSetupExperienceSteps(ctx context.Context, ds fleet.Datastore, host *fleet.Host) error {
func maybeCancelPendingSetupExperienceSteps(ctx context.Context, ds fleet.Datastore, host *fleet.Host, newActivityFn fleet.NewActivityFunc) error {
// Only macOS and Windows support canceling setup experience steps.
if host.Platform != "darwin" && host.Platform != "windows" {
return nil
@@ -318,6 +318,26 @@ func maybeCancelPendingSetupExperienceSteps(ctx context.Context, ds fleet.Datast
if err := ds.CancelPendingSetupExperienceSteps(ctx, hostUUID); err != nil {
return ctxerr.Wrap(ctx, err, "cancelling pending setup experience steps")
}
// Emit the canceled_setup_experience activity once at cancellation time.
// Find the software item that failed and triggered this cancellation from the
// already-loaded statuses (no extra DB call).
if newActivityFn != nil {
for _, s := range statuses {
if s.Status == fleet.SetupExperienceStatusFailure && s.IsForSoftware() {
if err := newActivityFn(ctx, nil, fleet.ActivityTypeCanceledSetupExperience{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
SoftwareTitle: s.Name,
SoftwareTitleID: ptr.ValOrZero(s.SoftwareTitleID),
}); err != nil {
return ctxerr.Wrap(ctx, err, "creating canceled setup experience activity")
}
break
}
}
}
return nil
}
@@ -326,9 +346,8 @@ func maybeCancelPendingSetupExperienceSteps(ctx context.Context, ds fleet.Datast
// SetupExperienceSoftwareInstallResult, and SetupExperienceVPPInstallResult), it returns a boolean
// indicating whether the datastore was updated and an error if one occurred. If the result is not of a
// supported type, it returns false and an error indicated that the type is not supported.
// If the skipPending parameter is true, the datastore will only be updated if the given result
// status is not pending.
func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, result interface{}, requireTerminalStatus bool) (bool, error) {
// The datastore will only be updated if the given result status is a terminal status.
func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, result any, newActivityFn fleet.NewActivityFunc) (bool, error) {
var updated bool
var err error
var status fleet.SetupExperienceStatusResultStatus
@@ -338,7 +357,7 @@ func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, r
status = v.SetupExperienceStatus()
if !status.IsValid() {
return false, fmt.Errorf("invalid status: %s", status)
} else if requireTerminalStatus && !status.IsTerminalStatus() {
} else if !status.IsTerminalStatus() {
return false, nil
}
return ds.MaybeUpdateSetupExperienceScriptStatus(ctx, v.HostUUID, v.ExecutionID, status)
@@ -348,7 +367,7 @@ func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, r
hostUUID = v.HostUUID
if !status.IsValid() {
return false, fmt.Errorf("invalid status: %s", status)
} else if requireTerminalStatus && !status.IsTerminalStatus() {
} else if !status.IsTerminalStatus() {
return false, nil
}
updated, err = ds.MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx, v.HostUUID, v.ExecutionID, status)
@@ -360,7 +379,7 @@ func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, r
hostUUID = v.HostUUID
if !status.IsValid() {
return false, fmt.Errorf("invalid status: %s", status)
} else if requireTerminalStatus && !status.IsTerminalStatus() {
} else if !status.IsTerminalStatus() {
return false, nil
}
updated, err = ds.MaybeUpdateSetupExperienceVPPStatus(ctx, v.HostUUID, v.CommandUUID, status)
@@ -377,9 +396,9 @@ func maybeUpdateSetupExperienceStatus(ctx context.Context, ds fleet.Datastore, r
if getHostUUIDErr != nil {
return updated, fmt.Errorf("getting host by UUID: %w", getHostUUIDErr)
}
cancelErr := maybeCancelPendingSetupExperienceSteps(ctx, ds, host)
cancelErr := maybeCancelPendingSetupExperienceSteps(ctx, ds, host, newActivityFn)
if cancelErr != nil {
return updated, fmt.Errorf("cancel setup experience after macos software install failure: %w", cancelErr)
return updated, fmt.Errorf("cancel setup experience after software install failure: %w", cancelErr)
}
}
return updated, err
+229 -57
View File
@@ -305,7 +305,7 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
vppUUID := "vpp-uuid"
t.Run("unsupported result type", func(t *testing.T) {
_, err := maybeUpdateSetupExperienceStatus(ctx, ds, map[string]interface{}{"key": "value"}, true)
_, err := maybeUpdateSetupExperienceStatus(ctx, ds, map[string]any{"key": "value"}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported result type")
})
@@ -351,7 +351,7 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
ExecutionID: scriptUUID,
ExitCode: tt.exitCode,
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, true)
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, nil)
require.NoError(t, err)
require.Equal(t, tt.alwaysUpdated, updated)
require.Equal(t, tt.alwaysUpdated, ds.MaybeUpdateSetupExperienceScriptStatusFuncInvoked)
@@ -388,8 +388,6 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
requireTerminalStatus := true // when this flag is true, we don't expect pending status to update
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFunc = func(ctx context.Context, hostUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hostUUID)
require.Equal(t, executionID, softwareUUID)
@@ -409,34 +407,16 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
ExecutionID: softwareUUID,
InstallerStatus: tt.status,
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, requireTerminalStatus)
activityFnCalled := false
activityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
activityFnCalled = true
return nil
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, activityFn)
require.NoError(t, err)
require.Equal(t, tt.alwaysUpdated, updated)
require.Equal(t, tt.alwaysUpdated, ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFuncInvoked)
requireTerminalStatus = false // when this flag is false, we do expect pending status to update
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFunc = func(ctx context.Context, hostUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hostUUID)
require.Equal(t, executionID, softwareUUID)
require.Equal(t, tt.expectStatus, status)
require.True(t, status.IsValid())
if status.IsTerminalStatus() {
require.True(t, status == fleet.SetupExperienceStatusSuccess || status == fleet.SetupExperienceStatusFailure)
} else {
require.True(t, status == fleet.SetupExperienceStatusPending || status == fleet.SetupExperienceStatusRunning)
}
return true, nil
}
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFuncInvoked = false
updated, err = maybeUpdateSetupExperienceStatus(ctx, ds, result, requireTerminalStatus)
require.NoError(t, err)
shouldUpdate := tt.alwaysUpdated
if tt.expectStatus == fleet.SetupExperienceStatusPending || tt.expectStatus == fleet.SetupExperienceStatusRunning {
shouldUpdate = true
}
require.Equal(t, shouldUpdate, updated)
require.Equal(t, shouldUpdate, ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFuncInvoked)
require.False(t, activityFnCalled)
})
}
})
@@ -476,8 +456,6 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
requireTerminalStatus := true // when this flag is true, we don't expect pending status to update
ds.MaybeUpdateSetupExperienceVPPStatusFunc = func(ctx context.Context, hostUUID string, cmdUUID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hostUUID)
require.Equal(t, cmdUUID, vppUUID)
@@ -496,36 +474,230 @@ func TestMaybeUpdateSetupExperience(t *testing.T) {
CommandUUID: vppUUID,
CommandStatus: tt.status,
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, requireTerminalStatus)
activityFnCalled := false
activityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
activityFnCalled = true
return nil
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, activityFn)
require.NoError(t, err)
require.Equal(t, tt.alwaysUpdated, updated)
require.Equal(t, tt.alwaysUpdated, ds.MaybeUpdateSetupExperienceVPPStatusFuncInvoked)
requireTerminalStatus = false // when this flag is false, we do expect pending status to update
ds.MaybeUpdateSetupExperienceVPPStatusFunc = func(ctx context.Context, hostUUID string, cmdUUID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hostUUID)
require.Equal(t, cmdUUID, vppUUID)
require.Equal(t, tt.expected, status)
require.True(t, status.IsValid())
if status.IsTerminalStatus() {
require.True(t, status == fleet.SetupExperienceStatusSuccess || status == fleet.SetupExperienceStatusFailure)
} else {
require.True(t, status == fleet.SetupExperienceStatusPending || status == fleet.SetupExperienceStatusRunning)
}
return true, nil
}
ds.MaybeUpdateSetupExperienceVPPStatusFuncInvoked = false
updated, err = maybeUpdateSetupExperienceStatus(ctx, ds, result, requireTerminalStatus)
require.NoError(t, err)
shouldUpdate := tt.alwaysUpdated
if tt.expected == fleet.SetupExperienceStatusPending || tt.expected == fleet.SetupExperienceStatusRunning {
shouldUpdate = true
}
require.Equal(t, shouldUpdate, updated)
require.Equal(t, shouldUpdate, ds.MaybeUpdateSetupExperienceVPPStatusFuncInvoked)
require.False(t, activityFnCalled)
})
}
})
t.Run("software install failure triggers cancel and activity", func(t *testing.T) {
teamID := uint(1)
failedSoftwareTitleID := uint(42)
failedSoftwareName := "FailedApp"
pendingExecID := "pending-exec-id"
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFunc = func(ctx context.Context, hUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hUUID)
require.Equal(t, softwareUUID, executionID)
require.Equal(t, fleet.SetupExperienceStatusFailure, status)
return true, nil
}
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
return &fleet.Host{
ID: 1,
UUID: hostUUID,
Platform: "darwin",
TeamID: &teamID,
}, nil
}
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
require.Equal(t, teamID, tid)
return &fleet.TeamLite{
ID: teamID,
Config: fleet.TeamConfigLite{
MDM: fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
RequireAllSoftware: true,
},
},
},
}, nil
}
installerID := uint(10)
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hUUID string, tID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return []*fleet.SetupExperienceStatusResult{
{
ID: 1,
HostUUID: hostUUID,
Name: failedSoftwareName,
Status: fleet.SetupExperienceStatusFailure,
SoftwareInstallerID: &installerID,
HostSoftwareInstallsExecutionID: &softwareUUID,
SoftwareTitleID: &failedSoftwareTitleID,
},
{
ID: 2,
HostUUID: hostUUID,
Name: "PendingApp",
Status: fleet.SetupExperienceStatusPending,
SoftwareInstallerID: &installerID,
HostSoftwareInstallsExecutionID: &pendingExecID,
},
}, nil
}
ds.CancelHostUpcomingActivityFunc = func(ctx context.Context, hID uint, executionID string) (fleet.ActivityDetails, error) {
require.Equal(t, uint(1), hID)
require.Equal(t, pendingExecID, executionID)
return nil, nil
}
ds.CancelPendingSetupExperienceStepsFunc = func(ctx context.Context, hUUID string) error {
require.Equal(t, hostUUID, hUUID)
return nil
}
var activityFnCalled bool
var recordedActivity fleet.ActivityDetails
activityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
activityFnCalled = true
recordedActivity = activity
return nil
}
result := fleet.SetupExperienceSoftwareInstallResult{
HostUUID: hostUUID,
ExecutionID: softwareUUID,
InstallerStatus: fleet.SoftwareInstallFailed,
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, activityFn)
require.NoError(t, err)
require.True(t, updated)
require.True(t, activityFnCalled)
require.True(t, ds.CancelPendingSetupExperienceStepsFuncInvoked)
require.True(t, ds.CancelHostUpcomingActivityFuncInvoked)
canceledActivity, ok := recordedActivity.(fleet.ActivityTypeCanceledSetupExperience)
require.True(t, ok)
require.Equal(t, uint(1), canceledActivity.HostID)
require.Equal(t, failedSoftwareName, canceledActivity.SoftwareTitle)
require.Equal(t, failedSoftwareTitleID, canceledActivity.SoftwareTitleID)
})
t.Run("late arriving result for canceled item does not trigger duplicate activity", func(t *testing.T) {
// See https://github.com/fleetdm/fleet/pull/43437#discussion_r3074297752
// 1. Software install A fails → triggers cancel of pending VPP install B + emits activity
// 2. Later, B's MDM command result (Error) arrives. The datastore guard returns
// updated=false because B is already in "canceled" state, so the cancel/activity
// path is NOT entered a second time.
teamID := uint(1)
failedSoftwareTitleID := uint(42)
failedSoftwareName := "FailedApp"
pendingVPPCommandUUID := "pending-vpp-cmd"
installerID := uint(10)
vppTeamID := uint(1)
// ---- Step 1: Software install A fails ----
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFunc = func(ctx context.Context, hUUID string, executionID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hUUID)
require.Equal(t, softwareUUID, executionID)
require.Equal(t, fleet.SetupExperienceStatusFailure, status)
return true, nil // updated
}
ds.MaybeUpdateSetupExperienceSoftwareInstallStatusFuncInvoked = false
ds.HostByIdentifierFunc = func(ctx context.Context, identifier string) (*fleet.Host, error) {
return &fleet.Host{
ID: 1,
UUID: hostUUID,
Platform: "darwin",
TeamID: &teamID,
}, nil
}
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
return &fleet.TeamLite{
ID: teamID,
Config: fleet.TeamConfigLite{
MDM: fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
RequireAllSoftware: true,
},
},
},
}, nil
}
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hUUID string, tID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return []*fleet.SetupExperienceStatusResult{
{
ID: 1,
HostUUID: hostUUID,
Name: failedSoftwareName,
Status: fleet.SetupExperienceStatusFailure,
SoftwareInstallerID: &installerID,
HostSoftwareInstallsExecutionID: &softwareUUID,
SoftwareTitleID: &failedSoftwareTitleID,
},
{
ID: 2,
HostUUID: hostUUID,
Name: "PendingVPPApp",
Status: fleet.SetupExperienceStatusPending,
VPPAppTeamID: &vppTeamID,
NanoCommandUUID: &pendingVPPCommandUUID,
},
}, nil
}
ds.CancelHostUpcomingActivityFunc = func(ctx context.Context, hID uint, executionID string) (fleet.ActivityDetails, error) {
return nil, nil
}
ds.CancelPendingSetupExperienceStepsFunc = func(ctx context.Context, hUUID string) error {
require.Equal(t, hostUUID, hUUID)
return nil
}
ds.CancelPendingSetupExperienceStepsFuncInvoked = false
ds.CancelHostUpcomingActivityFuncInvoked = false
activityCallCount := 0
activityFn := func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
activityCallCount++
return nil
}
result := fleet.SetupExperienceSoftwareInstallResult{
HostUUID: hostUUID,
ExecutionID: softwareUUID,
InstallerStatus: fleet.SoftwareInstallFailed,
}
updated, err := maybeUpdateSetupExperienceStatus(ctx, ds, result, activityFn)
require.NoError(t, err)
require.True(t, updated)
require.True(t, ds.CancelPendingSetupExperienceStepsFuncInvoked)
require.Equal(t, 1, activityCallCount, "activity should have been emitted exactly once")
// ---- Step 2: Late-arriving VPP result for B (already canceled) ----
// The datastore guard returns (false, nil) because B's row is already "canceled".
ds.MaybeUpdateSetupExperienceVPPStatusFunc = func(ctx context.Context, hUUID string, cmdUUID string, status fleet.SetupExperienceStatusResultStatus) (bool, error) {
require.Equal(t, hostUUID, hUUID)
require.Equal(t, vppUUID, cmdUUID)
require.Equal(t, fleet.SetupExperienceStatusFailure, status)
return false, nil // guard blocked: row already canceled
}
ds.MaybeUpdateSetupExperienceVPPStatusFuncInvoked = false
// Reset invoked flags so we can assert they are NOT set again.
ds.CancelPendingSetupExperienceStepsFuncInvoked = false
ds.CancelHostUpcomingActivityFuncInvoked = false
vppResult := fleet.SetupExperienceVPPInstallResult{
HostUUID: hostUUID,
CommandUUID: vppUUID,
CommandStatus: fleet.MDMAppleStatusError,
}
updated, err = maybeUpdateSetupExperienceStatus(ctx, ds, vppResult, activityFn)
require.NoError(t, err)
require.False(t, updated, "update should be blocked by datastore guard")
require.False(t, ds.CancelPendingSetupExperienceStepsFuncInvoked, "cancel should NOT be called again")
require.False(t, ds.CancelHostUpcomingActivityFuncInvoked, "cancel upcoming activity should NOT be called again")
require.Equal(t, 1, activityCallCount, "activity should still have been emitted only once (no duplicate)")
})
}
+17
View File
@@ -46,6 +46,7 @@ type AppleMDM struct {
Commander *apple_mdm.MDMAppleCommander
BootstrapPackageStore fleet.MDMBootstrapPackageStore
VPPInstaller fleet.AppleMDMVPPInstaller
NewActivityFn fleet.NewActivityFunc
}
// Name returns the name of the job.
@@ -583,6 +584,7 @@ func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context,
cmdUUID, err := a.installSoftwareFromVPP(ctx, host, vppApp, true, opts)
failedBeforeCommandSend := err != nil
if err != nil {
// if we get an error (e.g. no available licenses) while attempting to enqueue the
// install, then we should immediately go to an error state so setup experience
@@ -598,6 +600,21 @@ func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context,
if err := a.Datastore.UpdateSetupExperienceStatusResult(ctx, app); err != nil {
return nil, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid")
}
// Emit activity for the VPP app install failure, if one occurred
if failedBeforeCommandSend && a.NewActivityFn != nil {
failActivity := fleet.ActivityInstalledAppStoreApp{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
SoftwareTitle: app.Name,
AppStoreID: ptr.ValOrZero(app.VPPAppAdamID),
Status: string(fleet.SoftwareInstallFailed),
HostPlatform: host.Platform,
FromSetupExperience: true,
}
if actErr := a.NewActivityFn(ctx, nil, failActivity); actErr != nil {
a.Log.WarnContext(ctx, "failed to create activity for VPP app install failure during setup experience", "err", actErr)
}
}
}
}
+65
View File
@@ -1351,6 +1351,71 @@ INSERT INTO setup_experience_status_results (
require.Contains(t, getEnqueuedCommandTypes(t), "DeviceConfigured")
})
t.Run("emits activity on VPP install failure during setup experience", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds, testOrgName)
test.CreateInsertGlobalVPPToken(t, ds)
defer mysql.TruncateTables(t, ds)
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
require.NoError(t, err)
h := createEnrolledHost(t, 1, &tm.ID, true, "ios")
vppApp := &fleet.VPPApp{
Name: "fail-app", LatestVersion: "1.0.0",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "fail-adam-id", Platform: fleet.IOSPlatform}},
BundleIdentifier: "com.example.fail",
}
vppAppWithTeam, err := ds.InsertVPPAppWithTeam(ctx, vppApp, &tm.ID)
require.NoError(t, err)
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err = q.ExecContext(ctx, `
INSERT INTO setup_experience_status_results (host_uuid, name, status, vpp_app_team_id)
VALUES (?, ?, ?, ?)`, h.UUID, vppAppWithTeam.Name, fleet.SetupExperienceStatusPending, vppAppWithTeam.VPPAppTeam.AppTeamID)
return err
})
appInstallResponses := map[string]installAppResponse{
vppAppWithTeam.AdamID: {CommandUUID: "bad-cmd", Error: errors.New("no available licenses")},
}
vppInstaller := &mockVPPInstaller{t: t, ds: ds, appInstallResponses: appInstallResponses}
var capturedActivities []fleet.ActivityDetails
mdmWorker := &AppleMDM{
VPPInstaller: vppInstaller,
Datastore: ds,
Log: slogLog,
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
NewActivityFn: func(_ context.Context, _ *fleet.User, activity fleet.ActivityDetails) error {
capturedActivities = append(capturedActivities, activity)
return nil
},
}
w := NewWorker(ds, slogLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, h.Platform, nil, "", true, false)
require.NoError(t, err)
err = w.ProcessJobs(ctx)
require.NoError(t, err)
// Exactly one activity should have been emitted for the failed VPP install
require.Len(t, capturedActivities, 1)
act, ok := capturedActivities[0].(fleet.ActivityInstalledAppStoreApp)
require.True(t, ok, "expected ActivityInstalledAppStoreApp, got %T", capturedActivities[0])
assert.Equal(t, h.ID, act.HostID)
assert.Equal(t, h.DisplayName(), act.HostDisplayName)
assert.Equal(t, vppAppWithTeam.Name, act.SoftwareTitle)
assert.Equal(t, vppAppWithTeam.AdamID, act.AppStoreID)
assert.Equal(t, string(fleet.SoftwareInstallFailed), act.Status)
assert.Equal(t, h.Platform, act.HostPlatform)
assert.True(t, act.FromSetupExperience)
assert.False(t, act.SelfService)
})
t.Run("treats NotNow status as a finished command status that does not block device release", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds, testOrgName)
defer mysql.TruncateTables(t, ds)