mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #34433 It speeds up the cron, meaning fleetd, bootstrap and now profiles should be sent within 10 seconds of being known to fleet, compared to the previous 1 minute. It's heavily based on my last PR, so the structure and changes are close to identical, with some small differences. **I did not do the redis key part in this PR, as I think that should come in it's own PR, to avoid overlooking logic bugs with that code, and since this one is already quite sized since we're moving core pieces of code around.** # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Faster macOS onboarding: device profiles are delivered and installed as part of DEP enrollment, shortening initial setup. * Improved profile handling: per-host profile preprocessing, secret detection, and clearer failure marking. * **Improvements** * Consolidated SCEP/NDES error messaging for clearer diagnostics. * Cron/work scheduling tuned to prioritize Apple MDM profile delivery. * **Tests** * Expanded MDM unit and integration tests, including DeclarativeManagement handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
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)
|
|
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)
|
|
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)
|
|
}
|