package service import ( "bytes" "context" "io" "testing" "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSetupExperienceNextStep(t *testing.T) { ctx := context.Background() ds := new(mock.Store) svc := newTestService(t, ds) requestedInstalls := make(map[uint][]uint) requestedUpdateSetupExperience := []*fleet.SetupExperienceStatusResult{} requestedScriptExecution := []*fleet.HostScriptRequestPayload{} resetIndicators := func() { ds.InsertSoftwareInstallRequestFuncInvoked = false ds.InsertHostVPPSoftwareInstallFuncInvoked = false ds.NewHostScriptExecutionRequestFuncInvoked = false ds.UpdateSetupExperienceStatusResultFuncInvoked = false clear(requestedInstalls) requestedUpdateSetupExperience = []*fleet.SetupExperienceStatusResult{} requestedScriptExecution = []*fleet.HostScriptRequestPayload{} } host1UUID := "123" host1ID := uint(1) installerID1 := uint(2) scriptID1 := uint(3) scriptContentID1 := uint(4) ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &fleet.AppConfig{ MDM: fleet.MDM{ EnabledAndConfigured: true, }, }, nil } ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { return true, nil } var mockListSetupExperience []*fleet.SetupExperienceStatusResult ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) { return mockListSetupExperience, nil } var mockListHostsLite []*fleet.Host ds.ListHostsLiteByUUIDsFunc = func(ctx context.Context, filter fleet.TeamFilter, uuids []string) ([]*fleet.Host, error) { return mockListHostsLite, nil } ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string, error) { requestedInstalls[hostID] = append(requestedInstalls[hostID], softwareInstallerID) return "install-uuid", nil } ds.UpdateSetupExperienceStatusResultFunc = func(ctx context.Context, status *fleet.SetupExperienceStatusResult) error { requestedUpdateSetupExperience = append(requestedUpdateSetupExperience, status) return nil } ds.NewHostScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) { requestedScriptExecution = append(requestedScriptExecution, request) return &fleet.HostScriptResult{ ExecutionID: "script-uuid", }, nil } mockListHostsLite = append(mockListHostsLite, &fleet.Host{UUID: host1UUID, ID: host1ID}) finished, err := svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.True(t, finished) assert.False(t, ds.InsertSoftwareInstallRequestFuncInvoked) assert.False(t, ds.InsertHostVPPSoftwareInstallFuncInvoked) assert.False(t, ds.NewHostScriptExecutionRequestFuncInvoked) assert.False(t, ds.UpdateSetupExperienceStatusResultFuncInvoked) resetIndicators() // Only installer queued mockListSetupExperience = []*fleet.SetupExperienceStatusResult{ { HostUUID: host1UUID, SoftwareInstallerID: &installerID1, Status: fleet.SetupExperienceStatusPending, }, } finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.False(t, finished) assert.True(t, ds.InsertSoftwareInstallRequestFuncInvoked) assert.False(t, ds.InsertHostVPPSoftwareInstallFuncInvoked) assert.False(t, ds.NewHostScriptExecutionRequestFuncInvoked) assert.True(t, ds.UpdateSetupExperienceStatusResultFuncInvoked) assert.Len(t, requestedInstalls, 1) assert.Len(t, requestedUpdateSetupExperience, 1) assert.Equal(t, "install-uuid", *requestedUpdateSetupExperience[0].HostSoftwareInstallsExecutionID) mockListSetupExperience[0].Status = fleet.SetupExperienceStatusSuccess finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.True(t, finished) resetIndicators() // TODO VPP app queueing is better done in an integration // test, the setup required would be too much // Only script queued mockListSetupExperience = []*fleet.SetupExperienceStatusResult{ { HostUUID: host1UUID, SetupExperienceScriptID: &scriptID1, ScriptContentID: &scriptContentID1, Status: fleet.SetupExperienceStatusPending, }, } finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.False(t, finished) assert.False(t, ds.InsertSoftwareInstallRequestFuncInvoked) assert.False(t, ds.InsertHostVPPSoftwareInstallFuncInvoked) assert.True(t, ds.NewHostScriptExecutionRequestFuncInvoked) assert.True(t, ds.UpdateSetupExperienceStatusResultFuncInvoked) assert.Len(t, requestedScriptExecution, 1) assert.Len(t, requestedUpdateSetupExperience, 1) assert.Equal(t, "script-uuid", *requestedUpdateSetupExperience[0].ScriptExecutionID) mockListSetupExperience[0].Status = fleet.SetupExperienceStatusSuccess finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.True(t, finished) resetIndicators() // Both installer and script mockListSetupExperience = []*fleet.SetupExperienceStatusResult{ { HostUUID: host1UUID, SoftwareInstallerID: &installerID1, Status: fleet.SetupExperienceStatusPending, }, { HostUUID: host1UUID, SetupExperienceScriptID: &scriptID1, ScriptContentID: &scriptContentID1, Status: fleet.SetupExperienceStatusPending, }, } // Only installer is queued finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.False(t, finished) assert.True(t, ds.InsertSoftwareInstallRequestFuncInvoked) assert.False(t, ds.InsertHostVPPSoftwareInstallFuncInvoked) assert.False(t, ds.NewHostScriptExecutionRequestFuncInvoked) assert.True(t, ds.UpdateSetupExperienceStatusResultFuncInvoked) assert.Len(t, requestedInstalls, 1) assert.Len(t, requestedScriptExecution, 0) assert.Len(t, requestedUpdateSetupExperience, 1) // install finished, call it again. This time script is queued mockListSetupExperience[0].Status = fleet.SetupExperienceStatusSuccess finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.False(t, finished) assert.True(t, ds.InsertSoftwareInstallRequestFuncInvoked) assert.False(t, ds.InsertHostVPPSoftwareInstallFuncInvoked) assert.True(t, ds.NewHostScriptExecutionRequestFuncInvoked) assert.True(t, ds.UpdateSetupExperienceStatusResultFuncInvoked) assert.Len(t, requestedInstalls, 1) assert.Len(t, requestedScriptExecution, 1) assert.Len(t, requestedUpdateSetupExperience, 2) // both finished, now we're done mockListSetupExperience[1].Status = fleet.SetupExperienceStatusFailure finished, err = svc.SetupExperienceNextStep(ctx, &fleet.Host{ UUID: host1UUID, Platform: "darwin", }) require.NoError(t, err) assert.True(t, finished) } func TestSetupExperienceSetWithManualAgentInstall(t *testing.T) { ctx := test.UserContext(context.Background(), test.UserAdmin) ds := new(mock.Store) svc, baseSvc := newTestServiceWithMock(t, ds) appConfig := fleet.AppConfig{} team := fleet.Team{} ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { return &appConfig, nil } ds.TeamFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { return &team, nil } ds.SetSetupExperienceSoftwareTitlesFunc = func(ctx context.Context, platform string, teamID uint, titleIDs []uint) error { return nil } ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error { return nil } ds.SetSetupExperienceScriptFunc = func(ctx context.Context, script *fleet.Script) error { return nil } baseSvc.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error { return nil } // No manual agent install, we good // No team err := svc.SetSetupExperienceSoftware(ctx, "darwin", 0, []uint{1, 2}) require.NoError(t, err) scriptReader := bytes.NewReader([]byte("hello")) err = svc.SetSetupExperienceScript(ctx, nil, "potato.sh", scriptReader) require.NoError(t, err) _, _ = scriptReader.Seek(0, io.SeekStart) // Team err = svc.SetSetupExperienceSoftware(ctx, "darwin", 1, []uint{1, 2}) require.NoError(t, err) err = svc.SetSetupExperienceScript(ctx, ptr.Uint(1), "potato.sh", scriptReader) require.NoError(t, err) _, _ = scriptReader.Seek(0, io.SeekStart) // Manual agent install appConfig.MDM.MacOSSetup.ManualAgentInstall = optjson.SetBool(true) team.Config.MDM.MacOSSetup.ManualAgentInstall = optjson.SetBool(true) // No team err = svc.SetSetupExperienceSoftware(ctx, "darwin", 0, []uint{1, 2}) require.ErrorContains(t, err, "Couldn’t add setup experience software. To add software, first disable manual_agent_install.") err = svc.SetSetupExperienceScript(ctx, nil, "potato.sh", scriptReader) require.ErrorContains(t, err, "Couldn’t add setup experience script. To add script, first disable manual_agent_install.") _, _ = scriptReader.Seek(0, io.SeekStart) // Team err = svc.SetSetupExperienceSoftware(ctx, "darwin", 1, []uint{1, 2}) require.ErrorContains(t, err, "Couldn’t add setup experience software. To add software, first disable manual_agent_install.") err = svc.SetSetupExperienceScript(ctx, ptr.Uint(1), "potato.sh", scriptReader) require.ErrorContains(t, err, "Couldn’t add setup experience script. To add script, first disable manual_agent_install.") _, _ = scriptReader.Seek(0, io.SeekStart) // We can still set software to none though err = svc.SetSetupExperienceSoftware(ctx, "darwin", 0, []uint{}) require.NoError(t, err) err = svc.SetSetupExperienceSoftware(ctx, "darwin", 1, []uint{}) require.NoError(t, err) }