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, ¬FoundError{} } ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { for _, tm := range savedTeams { if (*tm).ID == tid { return *tm, nil } } return nil, ¬FoundError{} } 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, ¬FoundError{} } ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) { for _, tm := range savedTeams { if (*tm).Name == name { return *tm, nil } } return nil, ¬FoundError{} } 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, ¬FoundError{} } 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, ¬FoundError{} // 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 "" }