mirror of
https://github.com/fleetdm/fleet
synced 2026-05-06 06:48:54 +00:00
For #29478, sans GitOps. --------- Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Co-authored-by: Konstantin Sykulev <konst@sykulev.com>
901 lines
37 KiB
Go
901 lines
37 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
|
|
"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/jmoiron/sqlx"
|
|
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
|
"github.com/micromdm/plist"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func (s *integrationMDMTestSuite) setVPPTokenForTeam(teamID uint) {
|
|
t := s.T()
|
|
// Valid token
|
|
orgName := "Fleet Device Management Inc."
|
|
token := "mycooltoken"
|
|
expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
|
|
expDate := expTime.Format(fleet.VPPTimeFormat)
|
|
tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
|
|
t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
|
|
var validToken uploadVPPTokenResponse
|
|
s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)
|
|
|
|
s.lastActivityMatches(fleet.ActivityEnabledVPP{}.ActivityName(), "", 0)
|
|
|
|
// Get the token
|
|
var resp getVPPTokensResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
|
|
require.NoError(t, resp.Err)
|
|
|
|
// Associate team to the VPP token.
|
|
var resPatchVPP patchVPPTokensTeamsResponse
|
|
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{teamID}}, http.StatusOK, &resPatchVPP)
|
|
}
|
|
|
|
func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
|
|
// ===============================
|
|
// Initial setup
|
|
// ===============================
|
|
|
|
t := s.T()
|
|
s.setSkipWorkerJobs(t)
|
|
|
|
// Create a team
|
|
var newTeamResp teamResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
|
|
team := newTeamResp.Team
|
|
|
|
s.setVPPTokenForTeam(team.ID)
|
|
|
|
getSoftwareTitleIDFromApp := func(app *fleet.VPPApp) uint {
|
|
var titleID uint
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
ctx := context.Background()
|
|
return sqlx.GetContext(ctx, q, &titleID, `SELECT title_id FROM vpp_apps WHERE adam_id = ? AND platform = ?`, app.AdamID, app.Platform)
|
|
})
|
|
|
|
return titleID
|
|
}
|
|
|
|
// Add macOS and iOS apps to team 1
|
|
macOSApp := &fleet.VPPApp{
|
|
VPPAppTeam: fleet.VPPAppTeam{
|
|
VPPAppID: fleet.VPPAppID{
|
|
AdamID: "1",
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
},
|
|
Name: "App 1",
|
|
BundleIdentifier: "a-1",
|
|
IconURL: "https://example.com/images/1",
|
|
LatestVersion: "1.0.0",
|
|
}
|
|
|
|
iOSApp := &fleet.VPPApp{
|
|
VPPAppTeam: fleet.VPPAppTeam{
|
|
VPPAppID: fleet.VPPAppID{
|
|
AdamID: "2",
|
|
Platform: fleet.IOSPlatform,
|
|
},
|
|
},
|
|
Name: "App 2",
|
|
BundleIdentifier: "b-2",
|
|
IconURL: "https://example.com/images/2",
|
|
LatestVersion: "2.0.0",
|
|
}
|
|
var addAppResp addAppStoreAppResponse
|
|
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: macOSApp.AdamID, SelfService: true}, http.StatusOK, &addAppResp)
|
|
|
|
s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
|
|
fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": true}`, team.Name,
|
|
macOSApp.Name, getSoftwareTitleIDFromApp(macOSApp), macOSApp.AdamID, team.ID, macOSApp.Platform), 0)
|
|
|
|
// Add iOS app to team
|
|
addAppResp = addAppStoreAppResponse{}
|
|
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: iOSApp.AdamID, Platform: iOSApp.Platform}, http.StatusOK, &addAppResp)
|
|
|
|
s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
|
|
fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": false}`, team.Name,
|
|
iOSApp.Name, getSoftwareTitleIDFromApp(iOSApp), iOSApp.AdamID, team.ID, iOSApp.Platform), 0)
|
|
|
|
checkInstallFleetdCommandSent := func(mdmDevice *mdmtest.TestAppleMDMClient, wantCommand bool) {
|
|
foundInstallFleetdCommand := false
|
|
cmd, err := mdmDevice.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
if manifest := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil {
|
|
foundInstallFleetdCommand = true
|
|
require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
|
|
require.Contains(t, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL())
|
|
}
|
|
cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
}
|
|
require.Equal(t, wantCommand, foundInstallFleetdCommand)
|
|
}
|
|
|
|
// Create hosts for testing
|
|
orbitHost := createOrbitEnrolledHost(t, "darwin", "nonmdm", s.ds)
|
|
mdmHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
|
setOrbitEnrollment(t, mdmHost, s.ds)
|
|
s.runWorker()
|
|
checkInstallFleetdCommandSent(mdmDevice, true)
|
|
selfServiceHost, selfServiceDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
|
|
s.runWorker()
|
|
setOrbitEnrollment(t, selfServiceHost, s.ds)
|
|
selfServiceToken := "selfservicetoken"
|
|
checkInstallFleetdCommandSent(selfServiceDevice, true)
|
|
updateDeviceTokenForHost(t, s.ds, selfServiceHost.ID, selfServiceToken)
|
|
s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, selfServiceDevice.SerialNumber)
|
|
|
|
// Create and enroll an iOS device
|
|
// ensure a valid alternate device token for self-service status access checking later
|
|
updateDeviceTokenForHost(t, s.ds, mdmHost.ID, "foobar")
|
|
|
|
// Add serial number to our fake Apple server
|
|
s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmHost.HardwareSerial)
|
|
s.Do("POST", "/api/latest/fleet/hosts/transfer",
|
|
&addHostsToTeamRequest{HostIDs: []uint{mdmHost.ID, orbitHost.ID, selfServiceHost.ID}, TeamID: &team.ID}, http.StatusOK)
|
|
|
|
// Add all apps to the team
|
|
errApp := &fleet.VPPApp{
|
|
VPPAppTeam: fleet.VPPAppTeam{
|
|
VPPAppID: fleet.VPPAppID{
|
|
AdamID: "2",
|
|
Platform: fleet.MacOSPlatform,
|
|
},
|
|
},
|
|
Name: "App 2",
|
|
BundleIdentifier: "b-2",
|
|
IconURL: "https://example.com/images/2",
|
|
LatestVersion: "2.0.0",
|
|
}
|
|
expectedApps := []*fleet.VPPApp{macOSApp, errApp, iOSApp}
|
|
expectedAppsByBundleID := map[string]*fleet.VPPApp{
|
|
macOSApp.BundleIdentifier: macOSApp,
|
|
errApp.BundleIdentifier: errApp,
|
|
iOSApp.BundleIdentifier: iOSApp,
|
|
}
|
|
addedApp := expectedApps[0]
|
|
|
|
var listSw listSoftwareTitlesResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
|
|
"available_for_install", "true")
|
|
// Add remaining as non-self-service
|
|
for _, app := range expectedApps {
|
|
addAppResp = addAppStoreAppResponse{}
|
|
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
|
|
&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: app.AdamID, Platform: app.Platform},
|
|
http.StatusOK, &addAppResp)
|
|
s.lastActivityMatches(
|
|
fleet.ActivityAddedAppStoreApp{}.ActivityName(),
|
|
fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": false}`, team.Name,
|
|
app.Name, getSoftwareTitleIDFromApp(app), app.AdamID, team.ID, app.Platform),
|
|
0,
|
|
)
|
|
}
|
|
|
|
listSw = listSoftwareTitlesResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "available_for_install", "true")
|
|
require.Len(t, listSw.SoftwareTitles, len(expectedApps))
|
|
var errTitleID, macOSTitleID uint
|
|
for _, sw := range listSw.SoftwareTitles {
|
|
require.NotNil(t, sw.AppStoreApp)
|
|
switch {
|
|
case sw.Name == addedApp.Name && sw.Source == "apps":
|
|
macOSTitleID = sw.ID
|
|
case sw.Name == errApp.Name && sw.Source == "apps":
|
|
errTitleID = sw.ID
|
|
}
|
|
}
|
|
|
|
// ================================
|
|
// Install attempts
|
|
// ================================
|
|
|
|
checkCommandsInFlight := func(expectedCount int) {
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var count int
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_mdm_commands WHERE command_type = ?", fleet.VerifySoftwareInstallVPPPrefix)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expectedCount, count)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
type vppInstallOpts struct {
|
|
failOnInstall bool
|
|
appInstallVerified bool
|
|
appInstallTimeout bool
|
|
bundleID string
|
|
}
|
|
|
|
processVPPInstallOnClient := func(mdmClient *mdmtest.TestAppleMDMClient, opts vppInstallOpts) string {
|
|
var installCmdUUID string
|
|
|
|
// Process the InstallApplication command
|
|
s.runWorker()
|
|
cmd, err := mdmClient.Idle()
|
|
require.NoError(t, err)
|
|
|
|
app, ok := expectedAppsByBundleID[opts.bundleID]
|
|
require.Truef(t, ok, "unexpected bundle ID: %s", opts.bundleID)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
switch cmd.Command.RequestType {
|
|
case "InstallApplication":
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
installCmdUUID = cmd.CommandUUID
|
|
if opts.failOnInstall {
|
|
t.Logf("Failed command UUID: %s", installCmdUUID)
|
|
cmd, err = mdmClient.Err(cmd.CommandUUID, []mdm.ErrorChain{{ErrorCode: 1234}})
|
|
require.NoError(t, err)
|
|
continue
|
|
}
|
|
|
|
cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
|
|
require.NoError(t, err)
|
|
case "InstalledApplicationList":
|
|
// If we are polling to verify the install, we should get an
|
|
// InstalledApplicationList command instead of an InstallApplication command.
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
_, err = mdmClient.AcknowledgeInstalledApplicationList(
|
|
mdmClient.UUID,
|
|
cmd.CommandUUID,
|
|
[]fleet.Software{
|
|
{
|
|
Name: "RandomApp",
|
|
BundleIdentifier: "com.example.randomapp",
|
|
Version: "9.9.9",
|
|
Installed: false,
|
|
},
|
|
{
|
|
Name: app.Name,
|
|
BundleIdentifier: app.BundleIdentifier,
|
|
Version: app.LatestVersion,
|
|
Installed: opts.appInstallVerified,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
return ""
|
|
default:
|
|
require.Fail(t, "unexpected MDM command on client", cmd.Command.RequestType)
|
|
}
|
|
}
|
|
|
|
if opts.failOnInstall {
|
|
return installCmdUUID
|
|
}
|
|
|
|
if opts.appInstallTimeout {
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(context.Background(), "UPDATE nano_command_results SET updated_at = ? WHERE command_uuid = ?", time.Now().Add(-11*time.Minute), installCmdUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Process the verification command (InstalledApplicationList)
|
|
s.runWorker()
|
|
// Check that there is now a verify command in flight
|
|
checkCommandsInFlight(1)
|
|
cmd, err = mdmClient.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
switch cmd.Command.RequestType {
|
|
case "InstalledApplicationList":
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
cmd, err = mdmClient.AcknowledgeInstalledApplicationList(
|
|
mdmClient.UUID,
|
|
cmd.CommandUUID,
|
|
[]fleet.Software{
|
|
{
|
|
Name: "RandomApp",
|
|
BundleIdentifier: "com.example.randomapp",
|
|
Version: "9.9.9",
|
|
Installed: false,
|
|
},
|
|
{
|
|
Name: app.Name,
|
|
BundleIdentifier: app.BundleIdentifier,
|
|
Version: app.LatestVersion,
|
|
Installed: opts.appInstallVerified,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
default:
|
|
require.Fail(t, "unexpected MDM command on client", cmd.Command.RequestType)
|
|
}
|
|
}
|
|
|
|
return installCmdUUID
|
|
}
|
|
|
|
checkVPPApp := func(got *fleet.HostSoftwareWithInstaller, expected *fleet.VPPApp, expectedCmdUUID string, expectedStatus fleet.SoftwareInstallerStatus) {
|
|
require.Equal(t, expected.Name, got.Name)
|
|
require.NotNil(t, got.AppStoreApp)
|
|
require.Equal(t, expected.AdamID, got.AppStoreApp.AppStoreID)
|
|
require.Equal(t, ptr.String(expected.IconURL), got.IconUrl)
|
|
require.Empty(t, got.AppStoreApp.Name) // Name is only present for installer packages
|
|
require.Equal(t, expected.LatestVersion, got.AppStoreApp.Version)
|
|
require.NotNil(t, got.Status)
|
|
require.Equal(t, expectedStatus, *got.Status)
|
|
require.Equal(t, expectedCmdUUID, got.AppStoreApp.LastInstall.CommandUUID)
|
|
require.NotNil(t, got.AppStoreApp.LastInstall.InstalledAt)
|
|
}
|
|
|
|
// ================================
|
|
// Install command failed
|
|
// ================================
|
|
|
|
// Cancel any pending refetch requests
|
|
require.NoError(t, s.ds.UpdateHostRefetchRequested(context.Background(), mdmHost.ID, false))
|
|
|
|
// Trigger install to the host
|
|
installResp := installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, errTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
|
|
// Check if the host is listed as pending
|
|
var listResp listHostsResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", fmt.Sprint(team.ID),
|
|
"software_title_id", fmt.Sprint(errTitleID))
|
|
require.Len(t, listResp.Hosts, 1)
|
|
require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID)
|
|
var countResp countHostsResponse
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(errTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
// Simulate failed installation on the host
|
|
opts := vppInstallOpts{
|
|
failOnInstall: true,
|
|
appInstallVerified: false,
|
|
appInstallTimeout: false,
|
|
bundleID: addedApp.BundleIdentifier,
|
|
}
|
|
failedCmdUUID := processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
// We should have cleared out upcoming_activies since the install failed
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var count uint
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM upcoming_activities WHERE host_id = ?", mdmHost.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
return nil
|
|
})
|
|
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", fmt.Sprint(team.ID),
|
|
"software_title_id", fmt.Sprint(errTitleID))
|
|
require.Len(t, listResp.Hosts, 1)
|
|
require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(errTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
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}`,
|
|
mdmHost.ID,
|
|
mdmHost.DisplayName(),
|
|
errApp.Name,
|
|
errApp.AdamID,
|
|
failedCmdUUID,
|
|
fleet.SoftwareInstallFailed,
|
|
),
|
|
0,
|
|
)
|
|
|
|
// No refetch requested since the install failed
|
|
var hostResp getHostResponse
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost.ID), nil, http.StatusOK, &hostResp)
|
|
require.False(t, hostResp.Host.RefetchRequested, "RefetchRequested should be false after failed software install")
|
|
|
|
// ================================================
|
|
// Successful install and immediate verification
|
|
// ================================================
|
|
|
|
// Trigger install to the host
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
// Simulate successful installation on the host
|
|
opts.appInstallTimeout = false
|
|
opts.failOnInstall = false
|
|
opts.appInstallVerified = true
|
|
installCmdUUID := processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Len(t, listResp.Hosts, 1)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
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}`,
|
|
mdmHost.ID,
|
|
mdmHost.DisplayName(),
|
|
addedApp.Name,
|
|
addedApp.AdamID,
|
|
installCmdUUID,
|
|
fleet.SoftwareInstalled,
|
|
),
|
|
0,
|
|
)
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost.ID), nil, http.StatusOK, &hostResp)
|
|
require.True(t, hostResp.Host.RefetchRequested, "RefetchRequested should be true after successful software install")
|
|
|
|
s.lq.On("QueriesForHost", mdmHost.ID).Return(map[string]string{fmt.Sprintf("%d", mdmHost.ID): "select 1 from osquery;"}, nil)
|
|
|
|
req := getDistributedQueriesRequest{NodeKey: *mdmHost.NodeKey}
|
|
var dqResp getDistributedQueriesResponse
|
|
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
|
|
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_macos")
|
|
require.NoError(t, s.ds.UpdateHostRefetchRequested(context.Background(), mdmHost.ID, false))
|
|
|
|
// Check list host software
|
|
getHostSw := getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW := getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
got1, got2 := gotSW[0], gotSW[1]
|
|
|
|
checkVPPApp(got1, addedApp, installCmdUUID, fleet.SoftwareInstalled)
|
|
checkVPPApp(got2, errApp, failedCmdUUID, fleet.SoftwareInstallFailed)
|
|
|
|
// ================================================
|
|
// Successful install and delayed verification
|
|
// ================================================
|
|
|
|
// Trigger install to the host
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
// Install is ACK, but not verified yet
|
|
opts.appInstallTimeout = false
|
|
opts.appInstallVerified = false
|
|
opts.failOnInstall = false
|
|
installCmdUUID = processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
// We should have 0 installed, because the verification is not done yet
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Len(t, listResp.Hosts, 0)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 0, countResp.Count)
|
|
|
|
// We should instead have 1 pending
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
checkVPPApp(gotSW[0], addedApp, installCmdUUID, fleet.SoftwareInstallPending)
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Len(t, listResp.Hosts, 1)
|
|
|
|
// Install is ACK, but not verified yet
|
|
// Don't update the command UUID because we didn't trigger a new install command
|
|
// (the command UUID is the same as the one we got when we triggered the install)
|
|
processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
// We should have 0 installed, because the verification is not done yet
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Len(t, listResp.Hosts, 0)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 0, countResp.Count)
|
|
|
|
// We should instead have 1 pending
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
checkVPPApp(gotSW[0], addedApp, installCmdUUID, fleet.SoftwareInstallPending)
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Len(t, listResp.Hosts, 1)
|
|
|
|
// Install is ACK, and now it's verified
|
|
// Don't update the command UUID because we didn't trigger a new install command
|
|
// (the command UUID is the same as the one we got when we triggered the install)
|
|
opts.appInstallVerified = true
|
|
processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
checkCommandsInFlight(0)
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
checkVPPApp(gotSW[0], addedApp, installCmdUUID, fleet.SoftwareInstalled)
|
|
|
|
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}`,
|
|
mdmHost.ID,
|
|
mdmHost.DisplayName(),
|
|
addedApp.Name,
|
|
addedApp.AdamID,
|
|
installCmdUUID,
|
|
fleet.SoftwareInstalled,
|
|
),
|
|
0,
|
|
)
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost.ID), nil, http.StatusOK, &hostResp)
|
|
require.True(t, hostResp.Host.RefetchRequested, "RefetchRequested should be true after successful software install")
|
|
|
|
s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
|
|
require.Contains(t, dqResp.Queries, "fleet_detail_query_software_macos")
|
|
require.NoError(t, s.ds.UpdateHostRefetchRequested(context.Background(), mdmHost.ID, false))
|
|
|
|
// ========================================================
|
|
// Install command succeeds, but verification fails
|
|
// ========================================================
|
|
|
|
// Trigger install to the host
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
opts.failOnInstall = false
|
|
opts.appInstallVerified = false
|
|
opts.appInstallTimeout = true
|
|
installCmdUUID = processVPPInstallOnClient(mdmDevice, opts)
|
|
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Empty(t, listResp.Hosts)
|
|
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
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}`,
|
|
mdmHost.ID,
|
|
mdmHost.DisplayName(),
|
|
addedApp.Name,
|
|
addedApp.AdamID,
|
|
installCmdUUID,
|
|
fleet.SoftwareInstallFailed,
|
|
),
|
|
0,
|
|
)
|
|
|
|
// No refetch requested since the install failed
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost.ID), nil, http.StatusOK, &hostResp)
|
|
require.False(t, hostResp.Host.RefetchRequested, "RefetchRequested should be false after failed software install")
|
|
|
|
// Check list host software
|
|
getHostSw = getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
got1, got2 = gotSW[0], gotSW[1]
|
|
checkVPPApp(got1, addedApp, installCmdUUID, fleet.SoftwareInstallFailed)
|
|
checkVPPApp(got2, errApp, failedCmdUUID, fleet.SoftwareInstallFailed)
|
|
|
|
// ========================================================
|
|
// Mark installs as failed when MDM turned off on host
|
|
// ========================================================
|
|
|
|
// Trigger install to the host
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
// Trigger install to the self-service device (its data shouldn't be changed)
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", selfServiceHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 2, countResp.Count)
|
|
|
|
// Trigger verification on other host
|
|
opts.failOnInstall = false
|
|
opts.appInstallVerified = false
|
|
opts.appInstallTimeout = false
|
|
processVPPInstallOnClient(selfServiceDevice, opts)
|
|
|
|
s.runWorker()
|
|
|
|
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", mdmHost.ID), nil, http.StatusNoContent)
|
|
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
// We should have cleared out upcoming_activies when disabling MDM
|
|
var count int
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM upcoming_activities WHERE host_id = ?", mdmHost.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
|
|
installCmdUUID = ""
|
|
// Get the UUID for the latest install
|
|
err = sqlx.GetContext(
|
|
context.Background(),
|
|
q,
|
|
&installCmdUUID,
|
|
"SELECT command_uuid FROM host_vpp_software_installs WHERE host_id = ? AND adam_id = ? ORDER BY verification_failed_at DESC",
|
|
mdmHost.ID,
|
|
addedApp.AdamID,
|
|
)
|
|
require.NotEmpty(t, installCmdUUID)
|
|
require.NoError(t, err)
|
|
|
|
count = 99999
|
|
|
|
// We also should have cleared out host_mdm_commands to avoid a deadlocked state
|
|
err = sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_mdm_commands WHERE host_id = ? AND command_type = ?", mdmHost.ID, fleet.VerifySoftwareInstallVPPPrefix)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
|
|
// The other host should have a verification command pending
|
|
err = sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_mdm_commands WHERE host_id = ? AND command_type = ?", selfServiceHost.ID, fleet.VerifySoftwareInstallVPPPrefix)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count)
|
|
|
|
return nil
|
|
})
|
|
|
|
// Cancel the install for the other host, we don't need it anymore
|
|
var listUpcomingAct listHostUpcomingActivitiesResponse
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", selfServiceHost.ID), nil, http.StatusOK, &listUpcomingAct)
|
|
require.Len(t, listUpcomingAct.Activities, 1)
|
|
|
|
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", selfServiceHost.ID, listUpcomingAct.Activities[0].UUID), nil, http.StatusNoContent)
|
|
|
|
getHostSw = getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
got1, got2 = gotSW[0], gotSW[1]
|
|
checkVPPApp(got1, addedApp, installCmdUUID, fleet.SoftwareInstallFailed)
|
|
checkVPPApp(got2, errApp, failedCmdUUID, fleet.SoftwareInstallFailed)
|
|
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 0, countResp.Count)
|
|
|
|
// ========================================================
|
|
// Mark installs as failed when MDM turned off globally
|
|
// ========================================================
|
|
|
|
// Re-enroll host in MDM
|
|
mdmDevice = enrollMacOSHostInMDM(t, mdmHost, s.ds, s.server.URL)
|
|
s.runWorker()
|
|
checkInstallFleetdCommandSent(mdmDevice, true)
|
|
|
|
// Trigger install to the host
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
s.Do("DELETE", "/api/latest/fleet/mdm/apple/apns_certificate", nil, http.StatusOK)
|
|
|
|
t.Cleanup(s.appleCoreCertsSetup)
|
|
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
// We should have cleared out upcoming_activies when disabling MDM
|
|
var count uint
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM upcoming_activities WHERE host_id = ?", mdmHost.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
|
|
installCmdUUID = ""
|
|
// Get the UUID for the latest install
|
|
err = sqlx.GetContext(
|
|
context.Background(),
|
|
q,
|
|
&installCmdUUID,
|
|
"SELECT command_uuid FROM host_vpp_software_installs WHERE host_id = ? AND adam_id = ? ORDER BY verification_failed_at DESC",
|
|
mdmHost.ID,
|
|
addedApp.AdamID,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, installCmdUUID)
|
|
|
|
return nil
|
|
})
|
|
|
|
getHostSw = getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
|
|
gotSW = getHostSw.Software
|
|
require.Len(t, gotSW, 2) // App 1 and App 2
|
|
got1, got2 = gotSW[0], gotSW[1]
|
|
checkVPPApp(got1, addedApp, installCmdUUID, fleet.SoftwareInstallFailed)
|
|
checkVPPApp(got2, errApp, failedCmdUUID, fleet.SoftwareInstallFailed)
|
|
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
|
|
require.Equal(t, 0, countResp.Count)
|
|
|
|
// Re-enable MDM
|
|
s.appleCoreCertsSetup()
|
|
|
|
// ========================================================
|
|
// Test iOS VPP app installation
|
|
// ========================================================
|
|
|
|
// Enroll iOS device, add serial number to fake Apple server, and transfer to team
|
|
iosHost, iosDevice := s.createAppleMobileHostThenEnrollMDM("ios")
|
|
s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, iosDevice.SerialNumber)
|
|
s.Do("POST", "/api/latest/fleet/hosts/transfer",
|
|
&addHostsToTeamRequest{HostIDs: []uint{iosHost.ID}, TeamID: &team.ID}, http.StatusOK)
|
|
|
|
var iosTitleID uint
|
|
for _, sw := range listSw.SoftwareTitles {
|
|
if sw.Name == iOSApp.Name && sw.Source == "ios_apps" {
|
|
iosTitleID = sw.ID
|
|
break
|
|
}
|
|
}
|
|
require.NotZero(t, iosTitleID)
|
|
|
|
// Trigger install to the iOS device
|
|
installResp = installSoftwareResponse{}
|
|
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", iosHost.ID, iosTitleID), &installSoftwareRequest{},
|
|
http.StatusAccepted, &installResp)
|
|
|
|
// Verify pending status
|
|
countResp = countHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(iosTitleID))
|
|
require.Equal(t, 1, countResp.Count)
|
|
|
|
// Before installation, we should have 0 refetch commands
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var count int
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_mdm_commands WHERE host_id = ? AND command_type = ?", iosHost.ID, fleet.RefetchAppsCommandUUIDPrefix)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
return nil
|
|
})
|
|
|
|
// Simulate successful installation on iOS device
|
|
opts.appInstallTimeout = false
|
|
opts.appInstallVerified = true
|
|
opts.failOnInstall = false
|
|
opts.bundleID = iOSApp.BundleIdentifier
|
|
installCmdUUID = processVPPInstallOnClient(iosDevice, opts)
|
|
|
|
// Verify successful installation
|
|
listResp = listHostsResponse{}
|
|
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
|
|
fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(iosTitleID))
|
|
assert.Len(t, listResp.Hosts, 1)
|
|
assert.Equal(t, iosHost.ID, listResp.Hosts[0].ID)
|
|
|
|
// Verify activity log entry
|
|
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}`,
|
|
iosHost.ID,
|
|
iosHost.DisplayName(),
|
|
iOSApp.Name,
|
|
iOSApp.AdamID,
|
|
installCmdUUID,
|
|
fleet.SoftwareInstalled,
|
|
),
|
|
0,
|
|
)
|
|
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", iosHost.ID), nil, http.StatusOK, &hostResp)
|
|
require.False(t, hostResp.Host.RefetchRequested, "RefetchRequested should be false after successful software install for iDevice")
|
|
|
|
// Now we have a refetch apps command in flight to update the host software inventory
|
|
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
|
var count int
|
|
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_mdm_commands WHERE host_id = ?", iosHost.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, count, 1)
|
|
return nil
|
|
})
|
|
|
|
s.runWorker()
|
|
cmd, err := iosDevice.Idle()
|
|
require.NoError(t, err)
|
|
for cmd != nil {
|
|
var fullCmd micromdm.CommandPayload
|
|
switch cmd.Command.RequestType {
|
|
case "InstalledApplicationList":
|
|
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
|
|
require.True(t, strings.HasPrefix(cmd.CommandUUID, fleet.RefetchAppsCommandUUIDPrefix))
|
|
cmd, err = iosDevice.AcknowledgeInstalledApplicationList(
|
|
iosDevice.UUID,
|
|
cmd.CommandUUID,
|
|
[]fleet.Software{
|
|
{
|
|
Name: "RandomApp",
|
|
BundleIdentifier: "com.example.randomapp",
|
|
Version: "9.9.9",
|
|
Installed: false,
|
|
},
|
|
{
|
|
Name: iOSApp.Name,
|
|
BundleIdentifier: iOSApp.BundleIdentifier,
|
|
Version: iOSApp.LatestVersion,
|
|
Installed: true,
|
|
},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
default:
|
|
require.Fail(t, "unexpected MDM command on client", cmd.Command.RequestType)
|
|
}
|
|
}
|
|
|
|
// we should also have the installed version, because we update host software inventory on verification
|
|
getHostSw = getHostSoftwareResponse{}
|
|
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", iosHost.ID), nil, http.StatusOK, &getHostSw, "available_for_install", "true")
|
|
assert.Len(t, getHostSw.Software, 1)
|
|
assert.Equal(t, iosTitleID, getHostSw.Software[0].ID)
|
|
assert.NotNil(t, getHostSw.Software[0].AppStoreApp)
|
|
assert.Len(t, getHostSw.Software[0].InstalledVersions, 1)
|
|
assert.Equal(t, iOSApp.LatestVersion, getHostSw.Software[0].InstalledVersions[0].Version)
|
|
}
|