mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41741 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Software setup items are now ordered using custom display names when available. * **Bug Fixes** * Software installations now process sequentially for improved reliability and predictability. * Enhanced handling of missing installation tracking data to prevent failures. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
1521 lines
50 KiB
Go
1521 lines
50 KiB
Go
package worker
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/optjson"
|
|
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
|
nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
|
|
mock "github.com/fleetdm/fleet/v4/server/mock/mdm"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockPusher struct {
|
|
response *nanomdm_push.Response
|
|
err error
|
|
}
|
|
|
|
func (m mockPusher) Push(context.Context, []string) (map[string]*nanomdm_push.Response, error) {
|
|
var res map[string]*nanomdm_push.Response
|
|
if m.response != nil {
|
|
res = map[string]*nanomdm_push.Response{
|
|
m.response.Id: m.response,
|
|
}
|
|
}
|
|
return res, m.err
|
|
}
|
|
|
|
type installAppResponse struct {
|
|
CommandUUID string
|
|
Error error
|
|
}
|
|
|
|
type mockVPPInstaller struct {
|
|
t *testing.T
|
|
ds *mysql.Datastore
|
|
installedApps []*fleet.VPPApp
|
|
appInstallResponses map[string]installAppResponse
|
|
getTokenErr error
|
|
}
|
|
|
|
func (m *mockVPPInstaller) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) {
|
|
require.True(m.t, appleDevice)
|
|
if m.getTokenErr != nil {
|
|
return "", m.getTokenErr
|
|
}
|
|
return "valid-token", nil
|
|
}
|
|
|
|
func (m *mockVPPInstaller) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, opts fleet.HostSoftwareInstallOptions) (string, error) {
|
|
require.True(m.t, opts.ForSetupExperience)
|
|
resp, ok := m.appInstallResponses[vppApp.AdamID]
|
|
require.True(m.t, ok)
|
|
m.installedApps = append(m.installedApps, vppApp)
|
|
if resp.Error == nil {
|
|
mysql.ExecAdhocSQL(m.t, m.ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `
|
|
INSERT INTO nano_commands (command_uuid, request_type, command)
|
|
VALUES (?, 'InstallApplication', '<?xml')
|
|
`, resp.CommandUUID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = q.ExecContext(ctx, `
|
|
INSERT INTO nano_enrollment_queue (id, command_uuid, active)
|
|
VALUES (?, ?, 1)
|
|
`, host.UUID, resp.CommandUUID)
|
|
return err
|
|
})
|
|
}
|
|
return resp.CommandUUID, resp.Error
|
|
}
|
|
|
|
func TestAppleMDM(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// use a real mysql datastore so that the test does not rely so much on
|
|
// specific internals (sequence and number of calls, etc.). The MDM storage
|
|
// and pusher are mocks.
|
|
ds := mysql.CreateMySQLDS(t)
|
|
// call TruncateTables immediately as a DB migation may have created jobs
|
|
mysql.TruncateTables(t, ds)
|
|
|
|
mdmStorage, err := ds.NewMDMAppleMDMStorage()
|
|
require.NoError(t, err)
|
|
|
|
// nopLog := slog.New(slog.DiscardHandler)
|
|
// use this to debug/verify details of calls
|
|
slogLog := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
|
|
testOrgName := "fleet-test"
|
|
|
|
createEnrolledHost := func(t *testing.T, i int, teamID *uint, depAssignedToFleet bool, platform string) *fleet.Host {
|
|
// create the host
|
|
h, err := ds.NewHost(ctx, &fleet.Host{
|
|
Hostname: fmt.Sprintf("test-host%d-name", i),
|
|
OsqueryHostID: ptr.String(fmt.Sprintf("osquery-%d", i)),
|
|
NodeKey: ptr.String(fmt.Sprintf("nodekey-%d", i)),
|
|
UUID: uuid.New().String(),
|
|
Platform: platform,
|
|
HardwareSerial: fmt.Sprintf("serial-%d", i),
|
|
TeamID: teamID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// create the nano_device and enrollment
|
|
var abmTokenID uint
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO nano_devices (id, serial_number, authenticate) VALUES (?, ?, ?)`, h.UUID, h.HardwareSerial, "test")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = q.ExecContext(ctx, `INSERT INTO nano_enrollments (id, device_id, type, topic, push_magic, token_hex, last_seen_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, h.UUID, h.UUID, "device", "topic", "push_magic", "token_hex", time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
encTok := uuid.NewString()
|
|
abmToken, err := ds.InsertABMToken(ctx, &fleet.ABMToken{
|
|
OrganizationName: "unused",
|
|
EncryptedToken: []byte(encTok),
|
|
RenewAt: time.Now().Add(30 * 24 * time.Hour), // 30 days from now
|
|
})
|
|
abmTokenID = abmToken.ID
|
|
|
|
return err
|
|
})
|
|
if depAssignedToFleet {
|
|
err := ds.UpsertMDMAppleHostDEPAssignments(ctx, []fleet.Host{*h}, abmTokenID, make(map[uint]time.Time))
|
|
require.NoError(t, err)
|
|
}
|
|
err = ds.SetOrUpdateMDMData(ctx, h.ID, false, true, "http://example.com", depAssignedToFleet, fleet.WellKnownMDMFleet, "", false)
|
|
require.NoError(t, err)
|
|
return h
|
|
}
|
|
|
|
getEnqueuedCommandTypes := func(t *testing.T) []string {
|
|
var commands []string
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.SelectContext(ctx, q, &commands, "SELECT request_type FROM nano_commands")
|
|
})
|
|
return commands
|
|
}
|
|
|
|
enableManualRelease := func(t *testing.T, teamID *uint) {
|
|
if teamID == nil {
|
|
enableAppCfg := func(enable bool) {
|
|
ac, err := ds.AppConfig(ctx)
|
|
require.NoError(t, err)
|
|
ac.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(enable)
|
|
err = ds.SaveAppConfig(ctx, ac)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
enableAppCfg(true)
|
|
t.Cleanup(func() { enableAppCfg(false) })
|
|
} else {
|
|
enableTm := func(enable bool) {
|
|
tm, err := ds.TeamWithExtras(ctx, *teamID) // TODO see if we can convert to TeamLite (will require a new save DS method)
|
|
require.NoError(t, err)
|
|
tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(enable)
|
|
_, err = ds.SaveTeam(ctx, tm)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
enableTm(true)
|
|
t.Cleanup(func() { enableTm(false) })
|
|
}
|
|
}
|
|
|
|
t.Run("no-op with nil commander", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
// create a host and enqueue the job
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should mark the job as done
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
require.Empty(t, jobs)
|
|
})
|
|
|
|
t.Run("fails with unknown task", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
// create a host and enqueue the job
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should mark the job as failed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Time{})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
require.Contains(t, jobs[0].Error, "unknown task: no-such-task")
|
|
require.Equal(t, fleet.JobStateQueued, jobs[0].State)
|
|
require.Equal(t, 1, jobs[0].Retries)
|
|
})
|
|
|
|
t.Run("installs default manifest", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
// use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support.
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// there is no post-DEP release device job anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
|
|
t.Run("installs default manifest, manual release", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
t.Cleanup(func() { mysql.TruncateTables(t, ds) })
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
enableManualRelease(t, nil)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// there is no post-DEP release device job pending
|
|
require.Empty(t, jobs)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
|
|
t.Run("installs custom bootstrap manifest", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-bootstrap",
|
|
TeamID: 0, // no-team
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "custom-bootstrap", ms.BootstrapPackageName)
|
|
})
|
|
|
|
t.Run("installs custom bootstrap manifest of a team", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
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, "darwin")
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-team-bootstrap",
|
|
TeamID: tm.ID,
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "custom-team-bootstrap", ms.BootstrapPackageName)
|
|
})
|
|
|
|
t.Run("installs custom bootstrap manifest of a team, manual release", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
t.Cleanup(func() { mysql.TruncateTables(t, ds) })
|
|
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
|
require.NoError(t, err)
|
|
enableManualRelease(t, &tm.ID)
|
|
|
|
h := createEnrolledHost(t, 1, &tm.ID, true, "darwin")
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-team-bootstrap",
|
|
TeamID: tm.ID,
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// there is no post-DEP release device job pending
|
|
require.Empty(t, jobs)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
ms, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "custom-team-bootstrap", ms.BootstrapPackageName)
|
|
})
|
|
|
|
t.Run("skips install of custom bootstrap manifest during migration", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-bootstrap",
|
|
TeamID: 0, // no-team
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false, true)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
// Only the fleetd install is enqueued
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
var nfe fleet.NotFoundError
|
|
_, err = ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.ErrorAs(t, err, &nfe)
|
|
})
|
|
|
|
t.Run("skips install of custom bootstrap manifest of a team during migration", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
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, "darwin")
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-team-bootstrap",
|
|
TeamID: tm.ID,
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false, true)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
// Only the fleetd install is enqueued
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
var nfe fleet.NotFoundError
|
|
_, err = ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.ErrorAs(t, err, &nfe)
|
|
})
|
|
|
|
t.Run("installs custom bootstrap package during migration when FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION is set", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION")
|
|
})
|
|
os.Setenv("FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION", "1")
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
err := ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-bootstrap",
|
|
TeamID: 0, // no-team
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false, true)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
// The fleetd install and bootstrap package install are enqueued
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
setup, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "custom-bootstrap", setup.BootstrapPackageName)
|
|
})
|
|
|
|
t.Run("installs custom bootstrap package of a team during migration when FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION is set", func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION")
|
|
})
|
|
os.Setenv("FLEET_ALLOW_BOOTSTRAP_PACKAGE_DURING_MIGRATION", "1")
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
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, "darwin")
|
|
err = ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
|
|
Name: "custom-team-bootstrap",
|
|
TeamID: tm.ID,
|
|
Bytes: []byte("test"),
|
|
Sha256: []byte("test"),
|
|
Token: "token",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false, true)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
// Fleetd install and bootstrap package install are enqueued
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
setup, err := ds.GetHostMDMMacOSSetup(ctx, h.ID)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "custom-team-bootstrap", setup.BootstrapPackageName)
|
|
})
|
|
|
|
t.Run("unknown enroll reference", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Time{})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
require.Contains(t, jobs[0].Error, "MDMIdPAccount with uuid abcd was not found")
|
|
require.Equal(t, fleet.JobStateQueued, jobs[0].State)
|
|
require.Equal(t, 1, jobs[0].Retries)
|
|
})
|
|
|
|
t.Run("enroll reference but SSO disabled", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
err := ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{
|
|
Username: "test",
|
|
Fullname: "test",
|
|
Email: "test@example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
|
|
require.NoError(t, err)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID, false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
// confirm that AccountConfiguration command was not enqueued
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
|
|
t.Run("enroll reference with SSO enabled", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
err := ds.InsertMDMIdPAccount(ctx, &fleet.MDMIdPAccount{
|
|
Username: "test",
|
|
Fullname: "test",
|
|
Email: "test@example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
idpAcc, err := ds.GetMDMIdPAccountByEmail(ctx, "test@example.com")
|
|
require.NoError(t, err)
|
|
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
|
require.NoError(t, err)
|
|
tm, err = ds.TeamWithExtras(ctx, tm.ID) // TODO see if we can convert to TeamLite (will require a new save DS method)
|
|
require.NoError(t, err)
|
|
tm.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
|
|
_, err = ds.SaveTeam(ctx, tm)
|
|
require.NoError(t, err)
|
|
|
|
h := createEnrolledHost(t, 1, &tm.ID, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID, false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
|
|
// the post-DEP release device job is not queued anymore
|
|
require.Len(t, jobs, 0)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "AccountConfiguration"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
|
|
t.Run("installs fleetd for manual enrollments", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "", false, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
require.Empty(t, jobs)
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
|
|
t.Run("use worker for automatic release", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
// the release device job got enqueued
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().Add(time.Minute)) // release job is always added with a delay
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
require.Equal(t, fleet.JobStateQueued, jobs[0].State)
|
|
require.Equal(t, appleMDMJobName, jobs[0].Name)
|
|
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
})
|
|
|
|
t.Run("automatic release retries and give up", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
// the release device job got enqueued, and it will constantly re-enqueue
|
|
// itself because the command is never acknowledged
|
|
var (
|
|
previousID uint
|
|
firstStartedAt time.Time
|
|
)
|
|
for i := 0; i <= 10; i++ {
|
|
jobs, err := ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute)) // release job is always added with a delay
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
|
|
releaseJob := jobs[0]
|
|
require.Equal(t, fleet.JobStateQueued, releaseJob.State)
|
|
require.Equal(t, appleMDMJobName, releaseJob.Name)
|
|
require.NotEqual(t, previousID, releaseJob.ID)
|
|
previousID = releaseJob.ID
|
|
|
|
var args appleMDMArgs
|
|
err = json.Unmarshal([]byte(*releaseJob.Args), &args)
|
|
require.NoError(t, err)
|
|
require.Equal(t, args.Task, AppleMDMPostDEPReleaseDeviceTask)
|
|
require.EqualValues(t, i, args.ReleaseDeviceAttempt)
|
|
|
|
if i == 0 {
|
|
// first time, there is no release device started at
|
|
require.Nil(t, args.ReleaseDeviceStartedAt)
|
|
} else {
|
|
require.NotNil(t, args.ReleaseDeviceStartedAt)
|
|
if i == 1 {
|
|
firstStartedAt = *args.ReleaseDeviceStartedAt
|
|
} else {
|
|
require.True(t, firstStartedAt.Equal(*args.ReleaseDeviceStartedAt))
|
|
}
|
|
}
|
|
|
|
if i == 10 {
|
|
// finally, after 10 attempts, update the release started at to make it
|
|
// meet the maximum wait time and actually do the release on the next
|
|
// processing.
|
|
startedAt := firstStartedAt.Add(-time.Hour)
|
|
args.ReleaseDeviceStartedAt = &startedAt
|
|
b, err := json.Marshal(args)
|
|
require.NoError(t, err)
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE jobs SET args = ? WHERE id = ?`, string(b), releaseJob.ID)
|
|
return err
|
|
})
|
|
}
|
|
// update the job to make it available to run immediately
|
|
releaseJob.NotBefore = time.Now().UTC().Add(-time.Minute)
|
|
_, err = ds.UpdateJob(ctx, releaseJob.ID, releaseJob)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed and re-enqueue a new job with the same args
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// on the last processing, it did end up releasing the device due to the
|
|
// limit of attempts and wait delay being reached.
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "DeviceConfigured"}, getEnqueuedCommandTypes(t))
|
|
|
|
// job queue is now empty
|
|
jobs, err := ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute))
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 0)
|
|
})
|
|
|
|
t.Run("automatic release succeeds after a few attempts", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
for i := 0; i <= 4; i++ {
|
|
jobs, err := ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute)) // release job is always added with a delay
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
|
|
releaseJob := jobs[0]
|
|
require.Equal(t, fleet.JobStateQueued, releaseJob.State)
|
|
require.Equal(t, appleMDMJobName, releaseJob.Name)
|
|
|
|
if i == 4 {
|
|
// after 4 attempts, record a result for the command so it gets released
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result)
|
|
SELECT ?, command_uuid, ?, ? FROM nano_commands`,
|
|
h.UUID, "Acknowledged", `<?xml`)
|
|
return err
|
|
})
|
|
}
|
|
// update the job to make it available to run immediately
|
|
releaseJob.NotBefore = time.Now().UTC().Add(-time.Minute)
|
|
_, err = ds.UpdateJob(ctx, releaseJob.ID, releaseJob)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed and re-enqueue a new job with the same args
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// on the last processing, it did release the device due to all pending
|
|
// commands being completed.
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "DeviceConfigured"}, getEnqueuedCommandTypes(t))
|
|
|
|
// job queue is now empty
|
|
jobs, err := ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute))
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 0)
|
|
})
|
|
|
|
t.Run("installs enqueued VPP apps", 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")
|
|
|
|
expectedAppInstalls := []*fleet.VPPApp{}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
idx := fmt.Sprint(i)
|
|
vppApp := &fleet.VPPApp{
|
|
Name: "vpp_worker-" + idx, LatestVersion: "1.0.0", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "depworker-" + idx, Platform: fleet.IOSPlatform}},
|
|
BundleIdentifier: "b" + idx,
|
|
}
|
|
vppAppWithTeam, err := ds.InsertVPPAppWithTeam(ctx, vppApp, &tm.ID)
|
|
require.NoError(t, err)
|
|
expectedAppInstalls = append(expectedAppInstalls, vppAppWithTeam)
|
|
}
|
|
|
|
appInstallResponses := make(map[string]installAppResponse, len(expectedAppInstalls))
|
|
|
|
for _, appWithTeam := range expectedAppInstalls {
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
stmt := `
|
|
INSERT INTO setup_experience_status_results (
|
|
host_uuid,
|
|
name,
|
|
status,
|
|
vpp_app_team_id
|
|
) VALUES (?, ?, ?, ?)
|
|
`
|
|
_, err = q.ExecContext(ctx, stmt, h.UUID, appWithTeam.Name, fleet.SetupExperienceStatusPending, appWithTeam.VPPAppTeam.AppTeamID)
|
|
return err
|
|
})
|
|
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: uuid.NewString(), Error: nil}
|
|
}
|
|
|
|
vppInstaller := &mockVPPInstaller{t: t, ds: ds, appInstallResponses: appInstallResponses}
|
|
|
|
mdmWorker := &AppleMDM{
|
|
VPPInstaller: vppInstaller,
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, h.Platform, nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, jobs)
|
|
var releaseJob *fleet.Job
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
// THere should only be one release job
|
|
require.Nil(t, releaseJob)
|
|
releaseJob = job
|
|
}
|
|
}
|
|
// We should have found a release job
|
|
require.NotNil(t, releaseJob)
|
|
// It should be the release task
|
|
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
// And it should contain the command IDs for the installs
|
|
expectedAdamIDs := make([]string, 0, len(expectedAppInstalls))
|
|
installedAdamIDs := make([]string, 0, len(vppInstaller.installedApps))
|
|
for _, app := range expectedAppInstalls {
|
|
require.Contains(t, string(*releaseJob.Args), appInstallResponses[app.AdamID].CommandUUID)
|
|
expectedAdamIDs = append(expectedAdamIDs, app.AdamID)
|
|
}
|
|
for _, installed := range vppInstaller.installedApps {
|
|
installedAdamIDs = append(installedAdamIDs, installed.AdamID)
|
|
}
|
|
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
|
|
|
|
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID, tm.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, results, len(expectedAppInstalls))
|
|
for _, result := range results {
|
|
require.Equal(t, fleet.SetupExperienceStatusRunning, result.Status)
|
|
}
|
|
|
|
// Acknowledge the commands - the release job should still re-enqueue itself and await the installs
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result)
|
|
SELECT ?, command_uuid, ?, ? FROM nano_commands`,
|
|
h.UUID, "Acknowledged", `<?xml`)
|
|
return err
|
|
})
|
|
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before=? WHERE id=?`, time.Now().Add(-time.Minute), releaseJob.ID)
|
|
return err
|
|
})
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
releaseJob = nil
|
|
|
|
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute+time.Second)) // look in the future to catch any delayed job
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
// THere should only be one release job
|
|
require.Nil(t, releaseJob)
|
|
releaseJob = job
|
|
}
|
|
}
|
|
// We should have found a release job
|
|
require.NotNil(t, releaseJob)
|
|
// It should be the release task
|
|
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
|
|
// Now update setup_experience_status as if the installs succeeded
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status=? WHERE host_uuid=?`, fleet.SetupExperienceStatusSuccess, h.UUID)
|
|
return err
|
|
})
|
|
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before=? WHERE id=?`, time.Now().Add(-time.Minute), releaseJob.ID)
|
|
return err
|
|
})
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute+time.Second)) // look in the future to catch any delayed job
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
require.Fail(t, "there should be no more release jobs queued")
|
|
}
|
|
}
|
|
|
|
require.Contains(t, getEnqueuedCommandTypes(t), "DeviceConfigured")
|
|
})
|
|
|
|
t.Run("marks failed VPP installs as failed, runs all others", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
test.CreateInsertGlobalVPPToken(t, ds)
|
|
defer mysql.TruncateTables(t, ds)
|
|
badCommandUUID := "bad-command-uuid"
|
|
|
|
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "test"})
|
|
require.NoError(t, err)
|
|
|
|
h := createEnrolledHost(t, 1, &tm.ID, true, "ios")
|
|
|
|
expectedAppInstalls := []*fleet.VPPApp{}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
idx := fmt.Sprint(i)
|
|
vppApp := &fleet.VPPApp{
|
|
Name: "vpp_worker-" + idx, LatestVersion: "1.0.0", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "depworker-" + idx, Platform: fleet.IOSPlatform}},
|
|
BundleIdentifier: "b" + idx,
|
|
}
|
|
vppAppWithTeam, err := ds.InsertVPPAppWithTeam(ctx, vppApp, &tm.ID)
|
|
require.NoError(t, err)
|
|
expectedAppInstalls = append(expectedAppInstalls, vppAppWithTeam)
|
|
}
|
|
|
|
appInstallResponses := make(map[string]installAppResponse, len(expectedAppInstalls))
|
|
|
|
for _, appWithTeam := range expectedAppInstalls {
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
stmt := `
|
|
INSERT INTO setup_experience_status_results (
|
|
host_uuid,
|
|
name,
|
|
status,
|
|
vpp_app_team_id
|
|
) VALUES (?, ?, ?, ?)
|
|
`
|
|
_, err = q.ExecContext(ctx, stmt, h.UUID, appWithTeam.Name, fleet.SetupExperienceStatusPending, appWithTeam.VPPAppTeam.AppTeamID)
|
|
return err
|
|
})
|
|
if len(appInstallResponses) == 0 {
|
|
// first one, simulate a failure. It shouldn't actually
|
|
// return a command UUID here but even if it does we
|
|
// should not wait on it
|
|
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: badCommandUUID, Error: errors.New("test error")}
|
|
continue
|
|
}
|
|
// rest succeed
|
|
appInstallResponses[appWithTeam.AdamID] = installAppResponse{CommandUUID: uuid.NewString(), Error: nil}
|
|
}
|
|
|
|
vppInstaller := &mockVPPInstaller{t: t, ds: ds, appInstallResponses: appInstallResponses}
|
|
|
|
mdmWorker := &AppleMDM{
|
|
VPPInstaller: vppInstaller,
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, h.Platform, nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
jobs, err := ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute)) // look in the future to catch any delayed job
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, jobs)
|
|
var releaseJob *fleet.Job
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
// THere should only be one release job
|
|
require.Nil(t, releaseJob)
|
|
releaseJob = job
|
|
}
|
|
}
|
|
// We should have found a release job
|
|
require.NotNil(t, releaseJob)
|
|
// It should be the release task
|
|
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
|
|
// And it should contain the command IDs for the installs that didn't error
|
|
expectedAdamIDs := make([]string, 0, len(expectedAppInstalls))
|
|
installedAdamIDs := make([]string, 0, len(vppInstaller.installedApps))
|
|
for _, app := range expectedAppInstalls {
|
|
expectedAdamIDs = append(expectedAdamIDs, app.AdamID)
|
|
if appInstallResponses[app.AdamID].Error != nil {
|
|
// this one failed, so it should not be in the release command
|
|
continue
|
|
}
|
|
require.Contains(t, string(*releaseJob.Args), appInstallResponses[app.AdamID].CommandUUID)
|
|
}
|
|
require.NotContains(t, string(*releaseJob.Args), badCommandUUID)
|
|
for _, installed := range vppInstaller.installedApps {
|
|
installedAdamIDs = append(installedAdamIDs, installed.AdamID)
|
|
}
|
|
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
|
|
|
|
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID, tm.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, results, len(expectedAppInstalls))
|
|
for _, result := range results {
|
|
require.NotNil(t, result.VPPAppAdamID)
|
|
if *result.VPPAppAdamID == expectedAppInstalls[0].AdamID {
|
|
// this is the one we simulated a failure for
|
|
require.Equal(t, fleet.SetupExperienceStatusFailure, result.Status)
|
|
continue
|
|
}
|
|
require.Equal(t, fleet.SetupExperienceStatusRunning, result.Status)
|
|
}
|
|
|
|
// Acknowledge the commands - the release job should still re-enqueue itself and await the remaining installs
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result)
|
|
SELECT ?, command_uuid, ?, ? FROM nano_commands`,
|
|
h.UUID, "Acknowledged", `<?xml`)
|
|
return err
|
|
})
|
|
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before=? WHERE id=?`, time.Now().Add(-time.Minute), releaseJob.ID)
|
|
return err
|
|
})
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
releaseJob = nil
|
|
|
|
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute+time.Second)) // look in the future to catch any delayed job
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
// THere should only be one release job
|
|
require.Nil(t, releaseJob)
|
|
releaseJob = job
|
|
}
|
|
}
|
|
// We should have found a release job
|
|
require.NotNil(t, releaseJob)
|
|
// It should be the release task
|
|
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
|
|
// Now update setup_experience_status as if the installs succeeded
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status=? WHERE host_uuid=? AND status <> ?`, fleet.SetupExperienceStatusSuccess, h.UUID, fleet.SetupExperienceStatusFailure)
|
|
return err
|
|
})
|
|
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `UPDATE jobs SET not_before=? WHERE id=?`, time.Now().Add(-time.Minute), releaseJob.ID)
|
|
return err
|
|
})
|
|
|
|
// run the worker, should succeed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
jobs, err = ds.GetQueuedJobs(ctx, 10, time.Now().UTC().Add(time.Minute+time.Second)) // look in the future to catch any delayed job
|
|
|
|
for _, job := range jobs {
|
|
if job.Name == appleMDMJobName {
|
|
assert.Fail(t, "there should be no more release jobs queued")
|
|
}
|
|
}
|
|
|
|
require.Contains(t, getEnqueuedCommandTypes(t), "DeviceConfigured")
|
|
})
|
|
|
|
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)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err := QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should succeed and enqueue the release job
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run again
|
|
time.Sleep(time.Second)
|
|
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
|
|
// get the release job
|
|
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute))
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
|
|
releaseJob := jobs[0]
|
|
require.Equal(t, fleet.JobStateQueued, releaseJob.State)
|
|
require.Equal(t, appleMDMJobName, releaseJob.Name)
|
|
require.Contains(t, string(*releaseJob.Args), AppleMDMPostDEPReleaseDeviceTask)
|
|
|
|
// record a "NotNow" result for the command - this should be treated as completed
|
|
// and should not block device release
|
|
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO nano_command_results (id, command_uuid, status, result)
|
|
SELECT ?, command_uuid, ?, ? FROM nano_commands`,
|
|
h.UUID, "NotNow", `<?xml`)
|
|
return err
|
|
})
|
|
|
|
// update the job to make it available to run immediately
|
|
releaseJob.NotBefore = time.Now().UTC().Add(-time.Minute)
|
|
_, err = ds.UpdateJob(ctx, releaseJob.ID, releaseJob)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker - should release the device immediately since NotNow is treated as completed
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// the device should be released (DeviceConfigured command enqueued)
|
|
require.ElementsMatch(t, []string{"InstallEnterpriseApplication", "DeviceConfigured"}, getEnqueuedCommandTypes(t))
|
|
|
|
// job queue should be empty - no re-enqueue because NotNow is a final state
|
|
jobs, err = ds.GetQueuedJobs(ctx, 1, time.Now().UTC().Add(time.Minute))
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 0)
|
|
})
|
|
|
|
t.Run("installs profiles on post dep enrollment", func(t *testing.T) {
|
|
mysql.SetTestABMAssets(t, ds, testOrgName)
|
|
defer mysql.TruncateTables(t, ds)
|
|
|
|
profile1 := []byte("profile1")
|
|
profile2 := []byte("profile2")
|
|
profile3 := []byte("profile3")
|
|
|
|
_, err := ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{
|
|
Mobileconfig: profile1,
|
|
Identifier: "profile1",
|
|
Name: "Profile 1",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{
|
|
Mobileconfig: profile2,
|
|
Identifier: "profile2",
|
|
Name: "Profile 2",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ds.NewMDMAppleConfigProfile(ctx, fleet.MDMAppleConfigProfile{
|
|
Mobileconfig: profile3,
|
|
Identifier: "profile3",
|
|
Name: "Profile 3",
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
h := createEnrolledHost(t, 1, nil, true, "darwin")
|
|
|
|
mdmWorker := &AppleMDM{
|
|
Datastore: ds,
|
|
Log: slogLog,
|
|
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
|
|
}
|
|
w := NewWorker(ds, slogLog)
|
|
w.Register(mdmWorker)
|
|
|
|
err = QueueAppleMDMJob(ctx, ds, slogLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true, false)
|
|
require.NoError(t, err)
|
|
|
|
// run the worker, should send install profiles commands, and a ddm request
|
|
err = w.ProcessJobs(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// ensure the job's not_before allows it to be returned if it were to run
|
|
// again
|
|
time.Sleep(time.Second)
|
|
|
|
// check all commands that were enqueued
|
|
require.ElementsMatch(t, []string{"InstallProfile", "DeclarativeManagement", "InstallProfile", "InstallProfile", "InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
|
|
})
|
|
}
|
|
|
|
func TestGetSignedURL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
meta := &fleet.MDMAppleBootstrapPackage{
|
|
Sha256: []byte{1, 2, 3},
|
|
}
|
|
|
|
var data []byte
|
|
buf := bytes.NewBuffer(data)
|
|
logger := slog.New(slog.NewTextHandler(buf, nil))
|
|
a := &AppleMDM{Log: logger}
|
|
|
|
// S3 not configured
|
|
assert.Empty(t, a.getSignedURL(ctx, meta))
|
|
assert.Empty(t, buf.String())
|
|
|
|
// Signer not configured
|
|
mockStore := &mock.MDMBootstrapPackageStore{}
|
|
a.BootstrapPackageStore = mockStore
|
|
mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
|
|
return "bozo", fleet.ErrNotConfigured
|
|
}
|
|
assert.Empty(t, a.getSignedURL(ctx, meta))
|
|
assert.Empty(t, buf.String())
|
|
|
|
// Test happy path
|
|
mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
|
|
return "signed", nil
|
|
}
|
|
mockStore.ExistsFunc = func(ctx context.Context, packageID string) (bool, error) {
|
|
assert.Equal(t, "010203", packageID)
|
|
return true, nil
|
|
}
|
|
assert.Equal(t, "signed", a.getSignedURL(ctx, meta))
|
|
assert.Empty(t, buf.String())
|
|
assert.True(t, mockStore.SignFuncInvoked)
|
|
assert.True(t, mockStore.ExistsFuncInvoked)
|
|
mockStore.SignFuncInvoked = false
|
|
mockStore.ExistsFuncInvoked = false
|
|
|
|
// Test error -- sign failed
|
|
mockStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
|
|
return "", errors.New("test error")
|
|
}
|
|
assert.Empty(t, a.getSignedURL(ctx, meta))
|
|
assert.Contains(t, buf.String(), "test error")
|
|
assert.True(t, mockStore.SignFuncInvoked)
|
|
assert.False(t, mockStore.ExistsFuncInvoked)
|
|
}
|