fleet/cmd/fleetctl/fleetctl/testing_utils/testing_utils.go
Victor Lyuboslavsky 913a5904c8
Move NewActivity to activity bounded context (#39521)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38536 

This PR moves all logic to create new activities to activity bounded
context.
The old service and ActivityModule methods are not facades that route to
the new activity bounded context. The facades will be removed in a
subsequent PR.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## 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**
* Added webhook support for activity events with configurable endpoint
and enable/disable settings.
* Enhanced automation-initiated activity creation without requiring a
user context.
* Improved activity service architecture with centralized creation and
management.

* **Improvements**
* Refactored activity creation to use a dedicated service layer for
better separation of concerns.
* Added support for host-specific and automation-originated activities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-25 14:11:03 -06:00

854 lines
29 KiB
Go

package testing_utils
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/aws/smithy-go/ptr"
"github.com/docker/go-units"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/cached_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
"github.com/fleetdm/fleet/v4/server/mock"
mock2 "github.com/fleetdm/fleet/v4/server/mock/mdm"
"github.com/fleetdm/fleet/v4/server/service"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)
const (
teamName = "Team Test"
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
)
// RunServerWithMockedDS runs the fleet server with several mocked DS methods.
//
// NOTE: Assumes the current session is always from the admin user (see ds.SessionByKeyFunc below).
func RunServerWithMockedDS(t *testing.T, opts ...*service.TestServerOpts) (*httptest.Server, *mock.Store) {
ds := new(mock.Store)
var users []*fleet.User
var admin *fleet.User
ds.NewUserFunc = func(ctx context.Context, user *fleet.User) (*fleet.User, error) {
if user.GlobalRole != nil && *user.GlobalRole == fleet.RoleAdmin {
admin = user
}
users = append(users, user)
return user, nil
}
ds.SessionByKeyFunc = func(ctx context.Context, key string) (*fleet.Session, error) {
return &fleet.Session{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
ID: 1,
AccessedAt: time.Now(),
UserID: admin.ID,
Key: key,
}, nil
}
ds.MarkSessionAccessedFunc = func(ctx context.Context, session *fleet.Session) error {
return nil
}
ds.UserByIDFunc = func(ctx context.Context, id uint) (*fleet.User, error) {
return admin, nil
}
ds.ListUsersFunc = func(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
return users, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{}, nil
}
ds.NewGlobalPolicyFunc = func(ctx context.Context, authorID *uint, args fleet.PolicyPayload) (*fleet.Policy, error) {
return &fleet.Policy{
PolicyData: fleet.PolicyData{
Name: args.Name,
Query: args.Query,
Critical: args.Critical,
Platform: args.Platform,
Description: args.Description,
Resolution: &args.Resolution,
AuthorID: authorID,
ConditionalAccessBypassEnabled: ptr.Bool(true),
},
}, nil
}
ds.NewScriptFunc = func(ctx context.Context, script *fleet.Script) (*fleet.Script, error) {
if !strings.HasPrefix(script.ScriptContents, "#!/") {
return nil, errors.New("script not uploaded properly")
}
return &fleet.Script{
ID: 1,
}, nil
}
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) { return nil, nil }
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes(mdmtesting.NewTestMDMAppleCertTemplate())
require.NoError(t, err)
certPEM, keyPEM, tokenBytes, err := mysql.GenerateTestABMAssets(t)
require.NoError(t, err)
ds.GetAllMDMConfigAssetsHashesFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName) (map[fleet.MDMAssetName]string, error) {
return map[fleet.MDMAssetName]string{
fleet.MDMAssetABMCert: "abmcert",
fleet.MDMAssetABMKey: "abmkey",
fleet.MDMAssetABMTokenDeprecated: "abmtoken",
fleet.MDMAssetAPNSCert: "apnscert",
fleet.MDMAssetAPNSKey: "apnskey",
fleet.MDMAssetCACert: "scepcert",
fleet.MDMAssetCAKey: "scepkey",
}, nil
}
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName,
_ sqlx.QueryerContext,
) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetABMCert: {Name: fleet.MDMAssetABMCert, Value: certPEM},
fleet.MDMAssetABMKey: {Name: fleet.MDMAssetABMKey, Value: keyPEM},
fleet.MDMAssetABMTokenDeprecated: {Name: fleet.MDMAssetABMTokenDeprecated, Value: tokenBytes},
fleet.MDMAssetAPNSCert: {Name: fleet.MDMAssetAPNSCert, Value: apnsCert},
fleet.MDMAssetAPNSKey: {Name: fleet.MDMAssetAPNSKey, Value: apnsKey},
fleet.MDMAssetCACert: {Name: fleet.MDMAssetCACert, Value: certPEM},
fleet.MDMAssetCAKey: {Name: fleet.MDMAssetCAKey, Value: keyPEM},
}, nil
}
ds.ApplyYaraRulesFunc = func(context.Context, []fleet.YaraRule) error {
return nil
}
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
return nil
}
ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
return nil, nil
}
ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
return nil, nil
}
ds.ConditionalAccessBypassedAtFunc = func(ctx context.Context, hostID uint) (*time.Time, error) {
return nil, nil
}
ds.GetGroupedCertificateAuthoritiesFunc = func(ctx context.Context, includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
return &fleet.GroupedCertificateAuthorities{}, nil
}
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, opts fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
}
ds.GetVPPAppsFunc = func(ctx context.Context, teamID *uint) ([]fleet.VPPAppResponse, error) {
return []fleet.VPPAppResponse{}, nil
}
ds.GetEnterpriseFunc = func(ctx context.Context) (*android.Enterprise, error) {
return nil, nil
}
var cachedDS fleet.Datastore
if len(opts) > 0 && opts[0].NoCacheDatastore {
cachedDS = ds
} else {
cachedDS = cached_mysql.New(ds)
}
_, server := service.RunServerForTestsWithDS(t, cachedDS, opts...)
os.Setenv("FLEET_SERVER_ADDRESS", server.URL)
return server, ds
}
func getPathRelative(relativePath string) string {
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
panic("failed to get runtime caller info")
}
sourceDir := filepath.Dir(currentFile)
return filepath.Join(sourceDir, relativePath)
}
func ServeMDMBootstrapPackage(t *testing.T, pkgPath, pkgName string) (*httptest.Server, int) {
pkgBytes, err := os.ReadFile(pkgPath)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(len(pkgBytes)))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment;filename="%s"`, pkgName))
if n, err := w.Write(pkgBytes); err != nil {
require.NoError(t, err)
require.Equal(t, len(pkgBytes), n)
}
}))
t.Cleanup(srv.Close)
return srv, len(pkgBytes)
}
func StartSoftwareInstallerServer(t *testing.T) {
// load the ruby installer to use as base bytes to repeat for the "too large" case
b, err := os.ReadFile(getPathRelative("../../../../server/service/testdata/software-installers/ruby.deb"))
require.NoError(t, err)
// get the base dir of all installers
baseDir := getPathRelative("../../../../server/service/testdata/software-installers/")
// start the web server that will serve the installer
srv := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.Contains(r.URL.Path, "notfound"):
w.WriteHeader(http.StatusNotFound)
return
case strings.HasSuffix(r.URL.Path, ".txt"):
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(`a simple text file`))
return
case strings.Contains(r.URL.Path, "toolarge"):
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
var sz int
for sz < 513*units.MiB {
n, _ := w.Write(b)
sz += n
}
case strings.Contains(r.URL.Path, "other.deb"):
// serve same content as ruby.deb
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
_, _ = w.Write(b)
default:
http.ServeFile(w, r, filepath.Join(baseDir, filepath.Base(r.URL.Path)))
}
},
),
)
t.Cleanup(srv.Close)
t.Setenv("SOFTWARE_INSTALLER_URL", srv.URL)
}
func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, map[string]**fleet.Team) {
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
fleetCfg := config.TestConfig()
config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, getPathRelative("../../../../server/service/testdata"))
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := RunServerWithMockedDS(
t, &service.TestServerOpts{
MDMStorage: new(mock2.MDMAppleStore),
MDMPusher: MockPusher{},
FleetConfig: &fleetCfg,
License: license,
NoCacheDatastore: true,
KeyValueStore: NewMemKeyValueStore(),
},
)
// Mock appConfig
savedAppConfig := &fleet.AppConfig{
MDM: fleet.MDM{
EnabledAndConfigured: true,
AndroidEnabledAndConfigured: true,
},
}
AddLabelMocks(ds)
// Add DefaultTeamConfig mocks for enterprise features
ds.DefaultTeamConfigFunc = func(ctx context.Context) (*fleet.TeamConfig, error) {
return &fleet.TeamConfig{}, nil
}
ds.SaveDefaultTeamConfigFunc = func(ctx context.Context, config *fleet.TeamConfig) error {
return nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
appConfigCopy := *savedAppConfig
return &appConfigCopy, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
appConfigCopy := *config
savedAppConfig = &appConfigCopy
return nil
}
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam, _ map[string]uint) (bool, error) {
return false, nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.ListSoftwareAutoUpdateSchedulesFunc = func(ctx context.Context, teamID uint, source string, optionalFilter ...fleet.SoftwareAutoUpdateScheduleFilter) ([]fleet.SoftwareAutoUpdateSchedule, error) {
return []fleet.SoftwareAutoUpdateSchedule{}, nil
}
savedTeams := map[string]**fleet.Team{}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
return nil
}
ds.ApplyPolicySpecsFunc = func(ctx context.Context, authorID uint, specs []*fleet.PolicySpec) error {
return nil
}
ds.ApplyQueriesFunc = func(
ctx context.Context, authorID uint, queries []*fleet.Query, queriesToDiscardResults map[uint]struct{},
) error {
return nil
}
ds.BatchSetMDMProfilesFunc = func(
ctx context.Context, tmID *uint, macProfiles []*fleet.MDMAppleConfigProfile, winProfiles []*fleet.MDMWindowsConfigProfile,
macDecls []*fleet.MDMAppleDeclaration, androidProfiles []*fleet.MDMAndroidConfigProfile, vars []fleet.MDMProfileIdentifierFleetVariables,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.BatchSetScriptsFunc = func(ctx context.Context, tmID *uint, scripts []*fleet.Script) ([]fleet.ScriptResponse, error) {
return []fleet.ScriptResponse{}, nil
}
ds.BulkSetPendingMDMHostProfilesFunc = func(
ctx context.Context, hostIDs []uint, teamIDs []uint, profileUUIDs []string, hostUUIDs []string,
) (updates fleet.MDMProfilesUpdates, err error) {
return fleet.MDMProfilesUpdates{}, nil
}
ds.SetOrUpdateMDMWindowsConfigProfileFunc = func(ctx context.Context, cp fleet.MDMWindowsConfigProfile) error {
return nil
}
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.GetMDMAppleBootstrapPackageMetaFunc = func(ctx context.Context, teamID uint) (*fleet.MDMAppleBootstrapPackage, error) {
return &fleet.MDMAppleBootstrapPackage{}, nil
}
ds.DeleteMDMAppleBootstrapPackageFunc = func(ctx context.Context, teamID uint) error {
return nil
}
ds.GetMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) (*fleet.MDMAppleSetupAssistant, error) {
return nil, nil
}
ds.DeleteMDMAppleSetupAssistantFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
ds.IsEnrollSecretAvailableFunc = func(ctx context.Context, secret string, isNew bool, teamID *uint) (bool, error) {
return true, nil
}
ds.LabelIDsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]uint, error) {
require.ElementsMatch(t, names, []string{fleet.BuiltinLabelMacOS14Plus})
return map[string]uint{fleet.BuiltinLabelMacOS14Plus: 1}, nil
}
ds.ListGlobalPoliciesFunc = func(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) { return nil, nil }
ds.ListTeamPoliciesFunc = func(
ctx context.Context, teamID uint, opts fleet.ListOptions, iopts fleet.ListOptions,
) (teamPolicies []*fleet.Policy, inheritedPolicies []*fleet.Policy, err error) {
return nil, nil, nil
}
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
if savedTeams != nil {
var result []*fleet.Team
for _, t := range savedTeams {
result = append(result, *t)
}
return result, nil
}
return nil, nil
}
ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
summary := make([]*fleet.TeamSummary, 0, len(savedTeams))
for _, team := range savedTeams {
summary = append(summary, &fleet.TeamSummary{
ID: (*team).ID,
Name: (*team).Name,
Description: (*team).Description,
})
}
return summary, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opts fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
return nil, 0, 0, nil, nil
}
ds.NewMDMAppleConfigProfileFunc = func(ctx context.Context, p fleet.MDMAppleConfigProfile, vars []fleet.FleetVarName) (*fleet.MDMAppleConfigProfile, error) {
return nil, nil
}
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
job.ID = 1
return job, nil
}
ds.NewTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
team.ID = uint(len(savedTeams) + 1) //nolint:gosec // dismiss G115
savedTeams[team.Name] = &team
return team, nil
}
ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
return nil, &notFoundError{}
}
ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) {
for _, tm := range savedTeams {
if (*tm).ID == tid {
return *tm, nil
}
}
return nil, &notFoundError{}
}
ds.TeamLiteFunc = func(ctx context.Context, tid uint) (*fleet.TeamLite, error) {
if tid == 0 {
return &fleet.TeamLite{}, nil
}
for _, tm := range savedTeams {
if (*tm).ID == tid {
teamToCopy := *tm
return &fleet.TeamLite{
ID: teamToCopy.ID,
Filename: teamToCopy.Filename,
CreatedAt: teamToCopy.CreatedAt,
Name: teamToCopy.Name,
Description: teamToCopy.Description,
Config: teamToCopy.Config.ToLite(),
}, nil
}
}
return nil, &notFoundError{}
}
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
for _, tm := range savedTeams {
if (*tm).Name == name {
return *tm, nil
}
}
return nil, &notFoundError{}
}
ds.TeamByFilenameFunc = func(ctx context.Context, filename string) (*fleet.Team, error) {
for _, tm := range savedTeams {
if (*tm).Filename != nil && *(*tm).Filename == filename {
return *tm, nil
}
}
return nil, &notFoundError{}
}
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
savedTeams[team.Name] = &team
return team, nil
}
ds.SetOrUpdateMDMAppleDeclarationFunc = func(ctx context.Context, declaration *fleet.MDMAppleDeclaration) (
*fleet.MDMAppleDeclaration, error,
) {
declaration.DeclarationUUID = uuid.NewString()
return declaration, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.InsertVPPTokenFunc = func(ctx context.Context, tok *fleet.VPPTokenData) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{}, nil
}
ds.GetVPPTokenFunc = func(ctx context.Context, tokenID uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{}, err
}
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{}, nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return nil, nil
}
ds.UpdateVPPTokenTeamsFunc = func(ctx context.Context, id uint, teams []uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{{OrganizationName: "Fleet Device Management Inc."}}, nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions,
tmFilter fleet.TeamFilter,
) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
return nil
}
ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
return []*fleet.VPPTokenDB{}, nil
}
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
ds.SetSetupExperienceScriptFunc = func(ctx context.Context, script *fleet.Script) error {
return nil
}
ds.ExpandEmbeddedSecretsAndUpdatedAtFunc = func(ctx context.Context, document string) (string, *time.Time, error) {
return document, nil, nil
}
ds.MDMGetEULAMetadataFunc = func(ctx context.Context) (*fleet.MDMEULA, error) {
return nil, &notFoundError{} // No existing EULA
}
ds.BatchApplyCertificateAuthoritiesFunc = func(ctx context.Context, ops fleet.CertificateAuthoritiesBatchOperations) error {
return nil
}
ds.DeleteIconsAssociatedWithTitlesWithoutInstallersFunc = func(ctx context.Context, teamID uint) error {
return nil
}
ds.GetCertificateTemplatesByTeamIDFunc = func(ctx context.Context, teamID uint, options fleet.ListOptions) ([]*fleet.CertificateTemplateResponseSummary, *fleet.PaginationMetadata, error) {
return []*fleet.CertificateTemplateResponseSummary{}, &fleet.PaginationMetadata{}, nil
}
ds.ListCertificateAuthoritiesFunc = func(ctx context.Context) ([]*fleet.CertificateAuthoritySummary, error) {
return nil, nil
}
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
t.Setenv("TEST_TEAM_NAME", teamName)
t.Setenv("APPLE_BM_DEFAULT_TEAM", teamName)
return ds, &savedAppConfig, savedTeams
}
type AppleVPPConfigSrvConf struct {
Assets []vpp.Asset
SerialNumbers []string
}
func StartVPPApplyServer(t *testing.T, config *AppleVPPConfigSrvConf) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "associate") {
var associations vpp.AssociateAssetsRequest
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&associations); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
if len(associations.Assets) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res := vpp.ErrorResponse{
ErrorNumber: 9718,
ErrorMessage: "This request doesn't contain an asset, which is a required argument. Change the request to provide an asset.",
}
if err := json.NewEncoder(w).Encode(res); err != nil {
panic(err)
}
return
}
if len(associations.SerialNumbers) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
res := vpp.ErrorResponse{
ErrorNumber: 9719,
ErrorMessage: "Either clientUserIds or serialNumbers are required arguments. Change the request to provide assignable users and devices.",
}
if err := json.NewEncoder(w).Encode(res); err != nil {
panic(err)
}
return
}
var badAssets []vpp.Asset
for _, reqAsset := range associations.Assets {
var found bool
for _, goodAsset := range config.Assets {
if reqAsset == goodAsset {
found = true
}
}
if !found {
badAssets = append(badAssets, reqAsset)
}
}
var badSerials []string
for _, reqSerial := range associations.SerialNumbers {
var found bool
for _, goodSerial := range config.SerialNumbers {
if reqSerial == goodSerial {
found = true
}
}
if !found {
badSerials = append(badSerials, reqSerial)
}
}
if len(badAssets) != 0 || len(badSerials) != 0 {
errMsg := "error associating assets."
if len(badAssets) > 0 {
var badAdamIds []string
for _, asset := range badAssets {
badAdamIds = append(badAdamIds, asset.AdamID)
}
errMsg += fmt.Sprintf(" assets don't exist on account: %s.", strings.Join(badAdamIds, ", "))
}
if len(badSerials) > 0 {
errMsg += fmt.Sprintf(" bad serials: %s.", strings.Join(badSerials, ", "))
}
res := vpp.ErrorResponse{
ErrorInfo: vpp.ResponseErrorInfo{
Assets: badAssets,
ClientUserIds: []string{"something"},
SerialNumbers: badSerials,
},
// Not sure what error should be returned on each
// error type
ErrorNumber: 1,
ErrorMessage: errMsg,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(res); err != nil {
panic(err)
}
}
return
}
if strings.Contains(r.URL.Path, "assets") {
// Then we're responding to GetAssets
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
err := encoder.Encode(map[string][]vpp.Asset{"assets": config.Assets})
if err != nil {
panic(err)
}
return
}
resp := []byte(`{"locationName": "Fleet Location One"}`)
if strings.Contains(r.URL.RawQuery, "invalidToken") {
// This replicates the response sent back from Apple's VPP endpoints when an invalid
// token is passed. For more details see:
// https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
// https://developer.apple.com/documentation/devicemanagement/client_config
// https://developer.apple.com/documentation/devicemanagement/errorresponse
// Note that the Apple server returns 200 in this case.
resp = []byte(`{"errorNumber": 9622,"errorMessage": "Invalid authentication token"}`)
}
if strings.Contains(r.URL.RawQuery, "serverError") {
resp = []byte(`{"errorNumber": 9603,"errorMessage": "Internal server error"}`)
w.WriteHeader(http.StatusInternalServerError)
}
_, _ = w.Write(resp)
}))
dev_mode.SetOverride("FLEET_DEV_VPP_URL", srv.URL, t)
t.Cleanup(srv.Close)
}
func StartAndServeVPPServer(t *testing.T) {
config := &AppleVPPConfigSrvConf{
Assets: []vpp.Asset{
{
AdamID: "1",
PricingParam: "STDQ",
AvailableCount: 12,
},
{
AdamID: "2",
PricingParam: "STDQ",
AvailableCount: 3,
},
},
SerialNumbers: []string{"123", "456"},
}
StartVPPApplyServer(t, config)
// Set up the VPP proxy metadata server using the new format
// This replaces the old iTunes API format
vppProxySrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer test-bearer-token" || r.Header.Get("vpp-token") == "" {
w.WriteHeader(401)
_, _ = w.Write([]byte(`{"error": "unauthorized"}`))
return
}
// deviceFamilies: "mac" -> osx platform, "iphone" -> ios platform, "ipad" -> ios platform
db := map[string]string{
// macos app
"1": `{
"id": "1",
"attributes": {
"name": "App 1",
"platformAttributes": {
"osx": {
"bundleId": "a-1",
"artwork": {"url": "https://example.com/images/1/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "1.0.0"}
}
},
"deviceFamilies": ["mac"]
}
}`,
// macos, ios, ipados app
"2": `{
"id": "2",
"attributes": {
"name": "App 2",
"platformAttributes": {
"osx": {
"bundleId": "b-2",
"artwork": {"url": "https://example.com/images/2-mac/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "1.2.3"}
},
"ios": {
"bundleId": "b-2",
"artwork": {"url": "https://example.com/images/2/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "2.0.0"}
}
},
"deviceFamilies": ["mac", "iphone", "ipad"]
}
}`,
// ipados app
"3": `{
"id": "3",
"attributes": {
"name": "App 3",
"platformAttributes": {
"ios": {
"bundleId": "c-3",
"artwork": {"url": "https://example.com/images/3/{w}x{h}.{f}"},
"latestVersionInfo": {"versionDisplay": "3.0.0"}
}
},
"deviceFamilies": ["ipad"]
}
}`,
}
adamIDString := r.URL.Query().Get("ids")
adamIDs := strings.Split(adamIDString, ",")
var objs []string
for _, a := range adamIDs {
if obj, ok := db[a]; ok {
objs = append(objs, obj)
}
}
_, _ = w.Write(fmt.Appendf(nil, `{"data": [%s]}`, strings.Join(objs, ",")))
}))
vppProxyAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"fleetServerSecret": "test-bearer-token"}`))
}))
t.Cleanup(vppProxySrv.Close)
t.Cleanup(vppProxyAuthSrv.Close)
dev_mode.SetOverride("FLEET_DEV_STOKEN_AUTHENTICATED_APPS_URL", vppProxySrv.URL, t)
dev_mode.SetOverride("FLEET_DEV_VPP_PROXY_AUTH_URL", vppProxyAuthSrv.URL, t)
}
type MockPusher struct{}
func (MockPusher) Push(ctx context.Context, ids []string) (map[string]*push.Response, error) {
m := make(map[string]*push.Response, len(ids))
for _, id := range ids {
m[id] = &push.Response{Id: id}
}
return m, nil
}
type MemKeyValueStore struct {
m sync.Map
}
func NewMemKeyValueStore() *MemKeyValueStore {
return &MemKeyValueStore{}
}
func (m *MemKeyValueStore) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
m.m.Store(key, value)
return nil
}
func (m *MemKeyValueStore) Get(ctx context.Context, key string) (*string, error) {
v, ok := m.m.Load(key)
if !ok {
return nil, nil
}
vAsString := v.(string)
return &vAsString, nil
}
func AddLabelMocks(ds *mock.Store) {
var deletedLabels []string
ds.GetLabelSpecsFunc = func(ctx context.Context, filter fleet.TeamFilter) ([]*fleet.LabelSpec, error) {
return []*fleet.LabelSpec{
{
Name: "a",
Description: "A global label",
LabelMembershipType: fleet.LabelMembershipTypeManual,
Hosts: []string{"host2", "host3"},
},
{
Name: "b",
Description: "Another global label",
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
Query: "SELECT 1 from osquery_info",
},
}, nil
}
ds.ApplyLabelSpecsWithAuthorFunc = func(ctx context.Context, specs []*fleet.LabelSpec, authorID *uint) (err error) {
return nil
}
ds.DeleteLabelFunc = func(ctx context.Context, name string, filter fleet.TeamFilter) error {
deletedLabels = append(deletedLabels, name)
return nil
}
ds.LabelsByNameFunc = func(ctx context.Context, names []string, filter fleet.TeamFilter) (map[string]*fleet.Label, error) {
validLabels := map[string]*fleet.Label{
"a": {
ID: 1,
Name: "a",
},
"b": {
ID: 2,
Name: "b",
},
}
found := make(map[string]*fleet.Label)
for _, l := range names {
if label, ok := validLabels[l]; ok {
found[l] = label
}
}
return found, nil
}
}
type notFoundError struct{}
var _ fleet.NotFoundError = (*notFoundError)(nil)
func (e *notFoundError) IsNotFound() bool {
return true
}
func (e *notFoundError) Error() string {
return ""
}