package service import ( "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "os" "path/filepath" "runtime" "sort" "strings" "sync" "testing" "time" "github.com/WatchBeam/clock" ma "github.com/fleetdm/fleet/v4/ee/maintained-apps" "github.com/fleetdm/fleet/v4/ee/server/scim" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/ee/server/service/condaccess" "github.com/fleetdm/fleet/v4/ee/server/service/digicert" "github.com/fleetdm/fleet/v4/ee/server/service/est" "github.com/fleetdm/fleet/v4/ee/server/service/hostidentity" "github.com/fleetdm/fleet/v4/ee/server/service/hostidentity/httpsig" "github.com/fleetdm/fleet/v4/ee/server/service/scep" "github.com/fleetdm/fleet/v4/server/acl/acmeacl" "github.com/fleetdm/fleet/v4/server/acl/activityacl" activity_api "github.com/fleetdm/fleet/v4/server/activity/api" activity_bootstrap "github.com/fleetdm/fleet/v4/server/activity/bootstrap" apiendpoints "github.com/fleetdm/fleet/v4/server/api_endpoints" "github.com/fleetdm/fleet/v4/server/authz" chart_bootstrap "github.com/fleetdm/fleet/v4/server/chart/bootstrap" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/datastore/cached_mysql" "github.com/fleetdm/fleet/v4/server/datastore/filesystem" "github.com/fleetdm/fleet/v4/server/datastore/redis" "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" "github.com/fleetdm/fleet/v4/server/dev_mode" "github.com/fleetdm/fleet/v4/server/errorstore" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/logging" "github.com/fleetdm/fleet/v4/server/mail" acme_bootstrap "github.com/fleetdm/fleet/v4/server/mdm/acme/bootstrap" "github.com/fleetdm/fleet/v4/server/mdm/android" android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" nanomdm_push "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push" "github.com/fleetdm/fleet/v4/server/mdm/psso" "github.com/fleetdm/fleet/v4/server/mdm/scep/depot" fleet_mock "github.com/fleetdm/fleet/v4/server/mock" nanodep_mock "github.com/fleetdm/fleet/v4/server/mock/nanodep" "github.com/fleetdm/fleet/v4/server/platform/endpointer" common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service/async" "github.com/fleetdm/fleet/v4/server/service/middleware/auth" "github.com/fleetdm/fleet/v4/server/service/middleware/log" "github.com/fleetdm/fleet/v4/server/service/mock" "github.com/fleetdm/fleet/v4/server/service/redis_key_value" "github.com/fleetdm/fleet/v4/server/service/redis_lock" "github.com/fleetdm/fleet/v4/server/sso" "github.com/fleetdm/fleet/v4/server/test" "github.com/go-kit/kit/endpoint" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/throttled/throttled/v2" "github.com/throttled/throttled/v2/store/memstore" "google.golang.org/api/androidmanagement/v1" ) func newTestService(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) { return newTestServiceWithConfig(t, ds, config.TestConfig(), rs, lq, opts...) } func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig config.FleetConfig, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, opts ...*TestServerOpts) (fleet.Service, context.Context) { // Custom host vital reference validation is wired into all script/profile // upload paths. Provide a permissive default so tests that don't reference // $FLEET_HOST_VITAL_ don't need to stub it (the real datastore no-ops // when the document has no such tokens). Tests that assert on it can override. if mockDS, ok := ds.(*fleet_mock.Store); ok { if mockDS.ValidateReferencedCustomHostVitalsFunc == nil { mockDS.ValidateReferencedCustomHostVitalsFunc = func(ctx context.Context, documents []string) error { return nil } } // On Premium, AppConfig assembly and the osquery detail-query flow read the // Microsoft conditional access integration. Default to an empty (not set up) // integration so premium tests that don't care about it don't panic on a nil // mock. Tests that assert on it can override. if mockDS.ConditionalAccessMicrosoftGetFunc == nil { mockDS.ConditionalAccessMicrosoftGetFunc = func(ctx context.Context) (*fleet.ConditionalAccessMicrosoftIntegration, error) { return &fleet.ConditionalAccessMicrosoftIntegration{}, nil } } // Config reads hydrate the Windows enrollment default fleet from its config row. if mockDS.GetWindowsEnrollmentDefaultFleetFunc == nil { mockDS.GetWindowsEnrollmentDefaultFleetFunc = func(ctx context.Context) (*uint, string, error) { return nil, "", nil } } } lic := &fleet.LicenseInfo{Tier: fleet.TierFree} logger := slog.New(slog.DiscardHandler) writer, err := logging.NewFilesystemLogWriter(t.Context(), fleetConfig.Filesystem.StatusLogFile, logger, fleetConfig.Filesystem.EnableLogRotation, fleetConfig.Filesystem.EnableLogCompression, 500, 28, 3) require.NoError(t, err) osqlogger := &OsqueryLogger{Status: writer, Result: writer} var ( failingPolicySet fleet.FailingPolicySet = NewMemFailingPolicySet() enrollHostLimiter fleet.EnrollHostLimiter = nopEnrollHostLimiter{} depStorage nanodep_storage.AllDEPStorage = &nanodep_mock.Storage{} mailer fleet.MailService = &mockMailService{SendEmailFn: func(e fleet.Email) error { return nil }} c clock.Clock = clock.C scepConfigService = scep.NewSCEPConfigService(logger, nil) digiCertService = digicert.NewService(digicert.WithLogger(logger)) estCAService = est.NewService(est.WithLogger(logger)) conditionalAccessMicrosoftProxy ConditionalAccessMicrosoftProxy mdmStorage fleet.MDMAppleStore mdmPusher nanomdm_push.Pusher ssoStore sso.SessionStore profMatcher fleet.ProfileMatcher softwareInstallStore fleet.SoftwareInstallerStore bootstrapPackageStore fleet.MDMBootstrapPackageStore softwareTitleIconStore fleet.SoftwareTitleIconStore distributedLock fleet.Lock keyValueStore fleet.KeyValueStore androidService android.Service ) if len(opts) > 0 { if opts[0].Clock != nil { c = opts[0].Clock } } if len(opts) > 0 && opts[0].KeyValueStore != nil { keyValueStore = opts[0].KeyValueStore } // pssoNonceStore backs the PSSO single-use nonces; wired from the test Redis // pool when one is provided (integration tests), nil otherwise. var pssoNonceStore fleet.PSSONonceStore task := async.NewTask(ds, nil, c, nil) if len(opts) > 0 { if opts[0].Task != nil { task = opts[0].Task } else { opts[0].Task = task } } if len(opts) > 0 { if opts[0].Logger != nil { logger = opts[0].Logger } if opts[0].License != nil { lic = opts[0].License } if opts[0].Pool != nil { ssoStore = sso.NewSessionStore(opts[0].Pool) profMatcher = apple_mdm.NewProfileMatcher(opts[0].Pool) distributedLock = redis_lock.NewLock(opts[0].Pool) keyValueStore = redis_key_value.New(opts[0].Pool) pssoNonceStore = psso.NewRedisNonceStore(opts[0].Pool) } if opts[0].ProfileMatcher != nil { profMatcher = opts[0].ProfileMatcher } if opts[0].FailingPolicySet != nil { failingPolicySet = opts[0].FailingPolicySet } if opts[0].EnrollHostLimiter != nil { enrollHostLimiter = opts[0].EnrollHostLimiter } if opts[0].UseMailService { mailer, err = mail.NewService(config.TestConfig()) require.NoError(t, err) } if opts[0].SoftwareInstallStore != nil { softwareInstallStore = opts[0].SoftwareInstallStore } if opts[0].BootstrapPackageStore != nil { bootstrapPackageStore = opts[0].BootstrapPackageStore } if opts[0].SoftwareTitleIconStore != nil { softwareTitleIconStore = opts[0].SoftwareTitleIconStore } // allow to explicitly set MDM storage to nil mdmStorage = opts[0].MDMStorage if opts[0].DEPStorage != nil { depStorage = opts[0].DEPStorage } // allow to explicitly set mdm pusher to nil mdmPusher = opts[0].MDMPusher } ctx := license.NewContext(context.Background(), lic) cronSchedulesService := fleet.NewCronSchedules() var eh *errorstore.Handler if len(opts) > 0 { if opts[0].Pool != nil { eh = errorstore.NewHandler(ctx, opts[0].Pool, logger, time.Minute*10) ctx = ctxerr.NewContext(ctx, eh) } if opts[0].StartCronSchedules != nil { for _, fn := range opts[0].StartCronSchedules { err = cronSchedulesService.StartCronSchedule(fn(ctx, ds)) require.NoError(t, err) } } } if len(opts) > 0 && opts[0].SCEPConfigService != nil { scepConfigService = opts[0].SCEPConfigService } if len(opts) > 0 && opts[0].DigiCertService != nil { digiCertService = opts[0].DigiCertService } if len(opts) > 0 && opts[0].ConditionalAccessMicrosoftProxy != nil { conditionalAccessMicrosoftProxy = opts[0].ConditionalAccessMicrosoftProxy // The Conditional Access feature is gated on Fleet Premium; callers that // exercise it must provide a premium license via opts[0].License. require.True(t, lic.IsPremium(), "ConditionalAccessMicrosoftProxy requires a premium license via opts.License") } if len(opts) > 0 && opts[0].AndroidModule != nil { androidService = opts[0].AndroidModule } var wstepManager microsoft_mdm.CertManager if fleetConfig.MDM.WindowsWSTEPIdentityCert != "" && fleetConfig.MDM.WindowsWSTEPIdentityKey != "" { rawCert, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityCert) require.NoError(t, err) rawKey, err := os.ReadFile(fleetConfig.MDM.WindowsWSTEPIdentityKey) require.NoError(t, err) wstepManager, err = microsoft_mdm.NewCertManager(ds, rawCert, rawKey) require.NoError(t, err) } orgLogoStore, err := filesystem.NewOrgLogoStore(t.TempDir()) require.NoError(t, err) svc, err := NewService( ctx, ds, task, rs, logger, osqlogger, fleetConfig, mailer, c, ssoStore, lq, ds, failingPolicySet, &fleet.NoOpGeoIP{}, enrollHostLimiter, depStorage, mdmStorage, mdmPusher, cronSchedulesService, wstepManager, scepConfigService, digiCertService, conditionalAccessMicrosoftProxy, keyValueStore, androidService, orgLogoStore, ) if err != nil { panic(err) } if lic.IsPremium() { if softwareInstallStore == nil { // default to file-based dir := t.TempDir() store, err := filesystem.NewSoftwareInstallerStore(dir) if err != nil { panic(err) } softwareInstallStore = store } var androidModule android.Service if len(opts) > 0 { androidModule = opts[0].AndroidModule } svc, err = eeservice.NewService( svc, ds, logger, fleetConfig, mailer, c, depStorage, apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher), ssoStore, profMatcher, softwareInstallStore, bootstrapPackageStore, softwareTitleIconStore, distributedLock, keyValueStore, scepConfigService, digiCertService, androidModule, estCAService, pssoNonceStore, ) if err != nil { panic(err) } } // Set up mock activity service for unit tests. When DBConns is provided, // RunServerForTestsWithServiceWithDS will overwrite this with the real bounded context. activityMock := &fleet_mock.MockActivityService{ NewActivityFunc: func(_ context.Context, _ *activity_api.User, _ activity_api.ActivityDetails) error { return nil }, } svc.SetActivityService(activityMock) if len(opts) > 0 { opts[0].ActivityMock = activityMock } // Set up mock ACME service for unit tests. When DBConns is provided, // RunServerForTestsWithServiceWithDS will overwrite this with the real service module. svc.SetACMEService(&fleet_mock.MockACMEService{}) return svc, ctx } func newTestServiceWithClock(t *testing.T, ds fleet.Datastore, rs fleet.QueryResultStore, lq fleet.LiveQueryStore, c clock.Clock) (fleet.Service, context.Context) { testConfig := config.TestConfig() return newTestServiceWithConfig(t, ds, testConfig, rs, lq, &TestServerOpts{ Clock: c, }) } func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User { users := make(map[string]fleet.User) // Map iteration is random so we sort and iterate using the testUsers keys. var keys []string for key := range testUsers { keys = append(keys, key) } sort.Strings(keys) userID := uint(1) for _, key := range keys { u := testUsers[key] user := &fleet.User{ ID: userID, // We need to set this in case ds is a mocked Datastore. Name: "Test Name " + u.Email, Email: u.Email, GlobalRole: u.GlobalRole, } err := user.SetPassword(u.PlaintextPassword, 10, 10) require.Nil(t, err) user, err = ds.NewUser(context.Background(), user) require.Nil(t, err) users[user.Email] = *user userID++ } return users } var testUsers = map[string]struct { Email string PlaintextPassword string GlobalRole *string }{ "admin1": { PlaintextPassword: test.GoodPassword, Email: TestAdminUserEmail, GlobalRole: ptr.String(fleet.RoleAdmin), }, "user1": { PlaintextPassword: test.GoodPassword, Email: TestMaintainerUserEmail, GlobalRole: ptr.String(fleet.RoleMaintainer), }, "user2": { PlaintextPassword: test.GoodPassword, Email: TestObserverUserEmail, GlobalRole: ptr.String(fleet.RoleObserver), }, } func createEnrollSecrets(t *testing.T, count int) []*fleet.EnrollSecret { secrets := make([]*fleet.EnrollSecret, count) for i := 0; i < count; i++ { secrets[i] = &fleet.EnrollSecret{Secret: fmt.Sprintf("testSecret%d", i)} } return secrets } type mockMailService struct { SendEmailFn func(e fleet.Email) error Invoked bool } func (svc *mockMailService) SendEmail(ctx context.Context, e fleet.Email) error { svc.Invoked = true return svc.SendEmailFn(e) } func (svc *mockMailService) CanSendEmail(smtpSettings fleet.SMTPSettings) bool { return smtpSettings.SMTPConfigured } func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) { if len(opts) > 0 && opts[0].EnableCachedDS { ds = cached_mysql.New(ds) } cfg := config.TestConfig() if len(opts) > 0 && opts[0].FleetConfig != nil { cfg = *opts[0].FleetConfig } svc, ctx := NewTestService(t, ds, cfg, opts...) return RunServerForTestsWithServiceWithDS(t, ctx, ds, svc, opts...) } func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fleet.Datastore, svc fleet.Service, opts ...*TestServerOpts, ) (map[string]fleet.User, *httptest.Server) { var cfg config.FleetConfig if len(opts) > 0 && opts[0].FleetConfig != nil { cfg = *opts[0].FleetConfig } else { cfg = config.TestConfig() } users := map[string]fleet.User{} if len(opts) == 0 || (len(opts) > 0 && !opts[0].SkipCreateTestUsers) { users = createTestUsers(t, ds) } logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) if len(opts) > 0 && opts[0].Logger != nil { logger = opts[0].Logger } if len(opts) > 0 { opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, android_service.GetRoutes(svc, opts[0].AndroidModule)) } // Activity routes. If DBConns is provided, wire the real bounded context into // the main handler. Otherwise, build a path-only stub from the same registration // code and surface it to apiendpoints.Validate for catalog validation only. var extraInitFeatureRoutes []apiendpoints.FeatureRouteFunc if len(opts) > 0 && opts[0].DBConns != nil { legacyAuthorizer, err := authz.NewAuthorizer() require.NoError(t, err) activityAuthorizer := authz.NewAuthorizerAdapter(legacyAuthorizer) activityACLAdapter := activityacl.NewFleetServiceAdapter(svc, ds) activitySvc, activityRoutesFn := activity_bootstrap.New( opts[0].DBConns, activityAuthorizer, activityACLAdapter, logger, ) svc.SetActivityService(activitySvc) activityAuthMiddleware := func(next endpoint.Endpoint) endpoint.Endpoint { return auth.AuthenticatedUser(svc, next) } opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, activityRoutesFn(activityAuthMiddleware)) } else { // DBConns is not available (e.g. mock-backed tests). The activity bounded context // only dereferences its dependencies when an endpoint is actually served, so we // can pass empty conns + nil deps just to extract the route declarations. _, activityRoutesFn := activity_bootstrap.New( &common_mysql.DBConnections{}, nil, nil, logger, ) noopAuth := func(next endpoint.Endpoint) endpoint.Endpoint { return next } extraInitFeatureRoutes = append(extraInitFeatureRoutes, apiendpoints.FeatureRouteFunc(activityRoutesFn(noopAuth))) } // The chart bounded context is wired into the real server in serve.go but not into // this test handler, so build a path-only stub (regardless of DBConns) so that // apiendpoints.Validate can see the chart routes declared in api_endpoints.yml. // chart_bootstrap.New stores its deps without dereferencing them, so empty conns + // nil authorizer/viewer are fine when only the route paths are needed. { _, chartRoutesFn := chart_bootstrap.New( &common_mysql.DBConnections{}, nil, nil, logger, ) noopAuth := func(next endpoint.Endpoint) endpoint.Endpoint { return next } extraInitFeatureRoutes = append(extraInitFeatureRoutes, apiendpoints.FeatureRouteFunc(chartRoutesFn(noopAuth))) } var mdmPusher nanomdm_push.Pusher if len(opts) > 0 && opts[0].MDMPusher != nil { mdmPusher = opts[0].MDMPusher } rootMux := http.NewServeMux() memLimitStore, _ := memstore.New(0) var limitStore throttled.GCRAStore = memLimitStore var redisPool fleet.RedisPool if len(opts) > 0 && opts[0].Pool != nil { redisPool = opts[0].Pool limitStore = &redis.ThrottledStore{ Pool: opts[0].Pool, KeyPrefix: "ratelimit::", } } else { redisPool = redistest.SetupRedis(t, t.Name(), false, false, false) // We are good to initalize a redis pool here as it is only called by integration tests } // Wire real ACME service module if DBConns is provided (overrides the mock set in newTestServiceWithConfig). if len(opts) > 0 && opts[0].DBConns != nil { var acmeOpts []acme_bootstrap.ServiceOption if opts[0].ACMECertCA != nil && opts[0].ACMECertKey != nil { rootCAPool := x509.NewCertPool() rootCAPool.AddCert(opts[0].ACMECertCA) acmeOpts = append(acmeOpts, acme_bootstrap.WithTestAppleRootCAs(rootCAPool)) } acmeSigner := &acmeCSRSigner{signer: depot.NewSigner(opts[0].SCEPStorage, depot.WithValidityDays(365), depot.WithAllowRenewalDays(14))} acmeSvc, acmeRoutes := acme_bootstrap.New(opts[0].DBConns, redisPool, acmeacl.NewFleetDatastoreAdapter(ds, acmeSigner), logger, acmeOpts...) svc.SetACMEService(acmeSvc) opts[0].FeatureRoutes = append(opts[0].FeatureRoutes, acmeRoutes(log.Logged)) } if len(opts) > 0 { mdmStorage := opts[0].MDMStorage scepStorage := opts[0].SCEPStorage commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher) if mdmStorage != nil && scepStorage != nil { vppInstaller := svc.(fleet.AppleMDMVPPInstaller) checkInAndCommand := NewMDMAppleCheckinAndCommandService(ds, commander, vppInstaller, opts[0].License.IsPremium(), logger, redis_key_value.New(redisPool), svc.NewActivity) checkInAndCommand.RegisterResultsHandler("InstalledApplicationList", NewInstalledApplicationListResultsHandler(ds, commander, logger, cfg.Server.VPPVerifyTimeout, cfg.Server.VPPVerifyRequestDelay, svc.NewActivity)) checkInAndCommand.RegisterResultsHandler(fleet.DeviceLocationCmdName, NewDeviceLocationResultsHandler(ds, commander, logger)) checkInAndCommand.RegisterResultsHandler(fleet.SetRecoveryLockCmdName, NewSetRecoveryLockResultsHandler(ds, logger, svc.NewActivity)) err := RegisterAppleMDMProtocolServices( rootMux, cfg.MDM, mdmStorage, scepStorage, logger, checkInAndCommand, &MDMAppleDDMService{ ds: ds, logger: logger, }, commander, "https://test-url.com", cfg, svc, ) require.NoError(t, err) } if opts[0].EnableSCEPProxy { var timeout *time.Duration if opts[0].SCEPConfigService != nil { scepConfig, ok := opts[0].SCEPConfigService.(*scep.SCEPConfigService) if ok { // In tests, we share the same Timeout pointer between SCEPConfigService and SCEPProxy timeout = scepConfig.Timeout } } err := RegisterSCEPProxy( rootMux, ds, logger, timeout, &cfg, ) require.NoError(t, err) } } if len(opts) > 0 && opts[0].WithDEPWebview { frontendHandler := WithMDMEnrollmentMiddleware(svc, logger, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // do nothing and return 200 w.WriteHeader(http.StatusOK) })) rootMux.Handle("/", frontendHandler) } var featureRoutes []endpointer.HandlerRoutesFunc if len(opts) > 0 && len(opts[0].FeatureRoutes) > 0 { featureRoutes = opts[0].FeatureRoutes } var extra []ExtraHandlerOption extra = append(extra, WithLoginRateLimit(throttled.PerMin(1000))) if len(opts) > 0 && opts[0].HostIdentity != nil { require.NoError(t, hostidentity.RegisterSCEP(rootMux, opts[0].HostIdentity.SCEPStorage, ds, logger, &cfg)) var httpSigVerifier func(http.Handler) http.Handler httpSigVerifier, err := httpsig.Middleware(ds, opts[0].HostIdentity.RequireHTTPMessageSignature, logger.With("component", "http-sig-verifier")) require.NoError(t, err) extra = append(extra, WithHTTPSigVerifier(httpSigVerifier)) } if len(opts) > 0 && opts[0].ConditionalAccess != nil { require.NoError(t, condaccess.RegisterSCEP(ctx, rootMux, opts[0].ConditionalAccess.SCEPStorage, ds, logger, &cfg)) require.NoError(t, condaccess.RegisterIdP(rootMux, ds, logger, &cfg, limitStore)) } var carveStore fleet.CarveStore = ds // In tests, we use MySQL as storage for carves. apiHandler := MakeHandler(svc, cfg, logger, limitStore, redisPool, carveStore, featureRoutes, extra...) // SCIM endpoints are served by a prefix-mounted handler (see scim.RegisterSCIM) // that gorilla/mux can't introspect, so surface their routes to the validator // explicitly. They're always in the catalog, regardless of opts[0].EnableSCIM. extraInitFeatureRoutes = append(extraInitFeatureRoutes, scim.RegisterValidationRoutes) if err := apiendpoints.Validate(apiHandler, extraInitFeatureRoutes...); err != nil { t.Fatalf("error initializing API endpoints: %v", err) } rootMux.Handle("/api/", apiHandler) var errHandler *errorstore.Handler ctxErrHandler := ctxerr.FromContext(ctx) if ctxErrHandler != nil { errHandler = ctxErrHandler.(*errorstore.Handler) } debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds) rootMux.Handle("/debug/", debugHandler) rootMux.Handle("/enroll", ServeEndUserEnrollOTA(svc, "", ds, logger, false)) if len(opts) > 0 && opts[0].EnableSCIM { require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger, &cfg)) rootMux.Handle("/api/v1/fleet/scim/details", apiHandler) rootMux.Handle("/api/latest/fleet/scim/details", apiHandler) } server := httptest.NewUnstartedServer(rootMux) server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux) // WriteTimeout is set for security purposes. // If we don't set it, (bugy or malignant) clients making long running // requests could DDOS Fleet. require.NotZero(t, server.Config.WriteTimeout) if len(opts) > 0 && opts[0].HTTPServerConfig != nil { server.Config = opts[0].HTTPServerConfig // make sure we use the application handler we just created server.Config.Handler = rootMux } server.Start() t.Cleanup(func() { server.Close() }) return users, server } func NewTestService(t *testing.T, ds fleet.Datastore, cfg config.FleetConfig, opts ...*TestServerOpts) (fleet.Service, context.Context) { var rs fleet.QueryResultStore if len(opts) > 0 && opts[0].Rs != nil { rs = opts[0].Rs } var lq fleet.LiveQueryStore if len(opts) > 0 && opts[0].Lq != nil { lq = opts[0].Lq } return newTestServiceWithConfig(t, ds, cfg, rs, lq, opts...) } func testSESPluginConfig() config.FleetConfig { c := config.TestConfig() c.Email = config.EmailConfig{EmailBackend: "ses"} c.SES = config.SESConfig{ Region: "us-east-1", AccessKeyID: "foo", SecretAccessKey: "bar", StsAssumeRoleArn: "baz", SourceArn: "qux", SenderDomain: "email.example.com", } return c } func testKinesisPluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery.ResultLogPlugin = "kinesis" c.Osquery.StatusLogPlugin = "kinesis" c.Activity.AuditLogPlugin = "kinesis" c.Kinesis = config.KinesisConfig{ Region: "us-east-1", AccessKeyID: "foo", SecretAccessKey: "bar", StsAssumeRoleArn: "baz", StatusStream: "test-status-stream", ResultStream: "test-result-stream", AuditStream: "test-audit-stream", } return c } func testFirehosePluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery.ResultLogPlugin = "firehose" c.Osquery.StatusLogPlugin = "firehose" c.Activity.AuditLogPlugin = "firehose" c.Firehose = config.FirehoseConfig{ Region: "us-east-1", AccessKeyID: "foo", SecretAccessKey: "bar", StsAssumeRoleArn: "baz", StatusStream: "test-status-firehose", ResultStream: "test-result-firehose", AuditStream: "test-audit-firehose", } return c } func testLambdaPluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery.ResultLogPlugin = "lambda" c.Osquery.StatusLogPlugin = "lambda" c.Activity.AuditLogPlugin = "lambda" c.Lambda = config.LambdaConfig{ Region: "us-east-1", AccessKeyID: "foo", SecretAccessKey: "bar", StsAssumeRoleArn: "baz", ResultFunction: "result-func", StatusFunction: "status-func", AuditFunction: "audit-func", } return c } func testPubSubPluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery.ResultLogPlugin = "pubsub" c.Osquery.StatusLogPlugin = "pubsub" c.Activity.AuditLogPlugin = "pubsub" c.PubSub = config.PubSubConfig{ Project: "test", StatusTopic: "status-topic", ResultTopic: "result-topic", AuditTopic: "audit-topic", AddAttributes: false, } return c } func testStdoutPluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery.ResultLogPlugin = "stdout" c.Osquery.StatusLogPlugin = "stdout" c.Activity.AuditLogPlugin = "stdout" return c } func testUnrecognizedPluginConfig() config.FleetConfig { c := config.TestConfig() c.Osquery = config.OsqueryConfig{ ResultLogPlugin: "bar", StatusLogPlugin: "bar", } c.Activity.AuditLogPlugin = "bar" return c } func assertBodyContains(t *testing.T, resp *http.Response, expected string) { bodyBytes, err := io.ReadAll(resp.Body) require.Nil(t, err) bodyString := string(bodyBytes) assert.Contains(t, bodyString, expected) } func getJSON(r *http.Response, target interface{}) error { return json.NewDecoder(r.Body).Decode(target) } func assertErrorCodeAndMessage(t *testing.T, resp *http.Response, code int, message string) { err := &fleet.Error{} require.Nil(t, getJSON(resp, err)) assert.Equal(t, code, err.Code) assert.Equal(t, message, err.Message) } type nopEnrollHostLimiter struct{} func (nopEnrollHostLimiter) CanEnrollNewHost(ctx context.Context) (bool, error) { return true, nil } func (nopEnrollHostLimiter) SyncEnrolledHostIDs(ctx context.Context) error { return nil } func newMockAPNSPushProviderFactory() (*mock.APNSPushProviderFactory, *mock.APNSPushProvider) { provider := &mock.APNSPushProvider{} provider.PushFunc = mockSuccessfulPush factory := &mock.APNSPushProviderFactory{} factory.NewPushProviderFunc = func(*tls.Certificate) (push.PushProvider, error) { return provider, nil } return factory, provider } func mockSuccessfulPush(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) { res := make(map[string]*push.Response, len(pushes)) for _, p := range pushes { res[p.Token.String()] = &push.Response{ Id: uuid.New().String(), Err: nil, } } return res, nil } func mdmConfigurationRequiredEndpoints() []struct { method, path string deviceAuthenticated bool premiumOnly bool } { return []struct { method, path string deviceAuthenticated bool premiumOnly bool }{ {"POST", "/api/latest/fleet/mdm/apple/enqueue", false, false}, {"GET", "/api/latest/fleet/mdm/apple/commandresults", false, false}, {"GET", "/api/latest/fleet/mdm/apple/installers/1", false, false}, {"DELETE", "/api/latest/fleet/mdm/apple/installers/1", false, false}, {"GET", "/api/latest/fleet/mdm/apple/installers", false, false}, {"GET", "/api/latest/fleet/mdm/apple/devices", false, false}, {"GET", "/api/latest/fleet/mdm/apple/profiles", false, false}, {"GET", "/api/latest/fleet/mdm/apple/profiles/1", false, false}, {"DELETE", "/api/latest/fleet/mdm/apple/profiles/1", false, false}, {"GET", "/api/latest/fleet/mdm/apple/profiles/summary", false, false}, {"PATCH", "/api/latest/fleet/mdm/hosts/1/unenroll", false, false}, {"DELETE", "/api/latest/fleet/hosts/1/mdm", false, false}, {"GET", "/api/latest/fleet/mdm/hosts/1/profiles", false, true}, {"GET", "/api/latest/fleet/hosts/1/configuration_profiles", false, true}, {"POST", "/api/latest/fleet/mdm/hosts/1/lock", false, false}, {"POST", "/api/latest/fleet/mdm/hosts/1/wipe", false, false}, {"PATCH", "/api/latest/fleet/mdm/apple/settings", false, false}, {"GET", "/api/latest/fleet/mdm/apple", false, false}, {"GET", "/api/latest/fleet/apns", false, false}, {"GET", apple_mdm.EnrollPath + "?token=test", false, false}, {"GET", apple_mdm.InstallerPath + "?token=test", false, false}, {"GET", "/api/latest/fleet/mdm/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true}, {"GET", "/api/latest/fleet/setup_experience/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, true}, {"DELETE", "/api/latest/fleet/mdm/setup/eula/token", false, true}, {"DELETE", "/api/latest/fleet/setup_experience/eula/token", false, true}, {"GET", "/api/latest/fleet/mdm/setup/eula/metadata", false, true}, {"GET", "/api/latest/fleet/setup_experience/eula/metadata", false, true}, {"GET", "/api/latest/fleet/mdm/apple/setup/eula/0982A979-B1C9-4BDF-B584-5A37D32A1172", false, false}, {"DELETE", "/api/latest/fleet/mdm/apple/setup/eula/token", false, false}, {"GET", "/api/latest/fleet/mdm/apple/setup/eula/metadata", false, false}, {"GET", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false}, {"GET", "/api/latest/fleet/enrollment_profiles/automatic", false, false}, {"POST", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false}, {"POST", "/api/latest/fleet/enrollment_profiles/automatic", false, false}, {"DELETE", "/api/latest/fleet/mdm/apple/enrollment_profile", false, false}, {"DELETE", "/api/latest/fleet/enrollment_profiles/automatic", false, false}, {"POST", "/api/latest/fleet/device/%s/migrate_mdm", true, true}, {"POST", "/api/latest/fleet/mdm/apple/profiles/preassign", false, true}, {"POST", "/api/latest/fleet/mdm/apple/profiles/match", false, true}, {"POST", "/api/latest/fleet/mdm/commands/run", false, false}, {"POST", "/api/latest/fleet/commands/run", false, false}, {"GET", "/api/latest/fleet/mdm/commandresults", false, false}, {"GET", "/api/latest/fleet/commands/results", false, false}, {"GET", "/api/latest/fleet/mdm/commands", false, false}, {"GET", "/api/latest/fleet/commands", false, false}, {"POST", "/api/fleet/orbit/disk_encryption_key", false, false}, {"GET", "/api/latest/fleet/mdm/profiles/1", false, false}, {"GET", "/api/latest/fleet/configuration_profiles/1", false, false}, // TODO: those endpoints accept multipart/form data that gets // parsed before the MDM check, we need to refactor this // function to return more information to the caller, or find a // better way to test these endpoints. // {"POST", "/api/latest/fleet/mdm/profiles", false, false}, // {"POST", "/api/latest/fleet/configuration_profiles", false, false}, // {"POST", "/api/latest/fleet/mdm/setup/eula"}, // {"POST", "/api/latest/fleet/setup_experience/eula"}, // {"POST", "/api/latest/fleet/mdm/bootstrap", false, true}, // {"POST", "/api/latest/fleet/bootstrap", false, true}, {"GET", "/api/latest/fleet/mdm/profiles", false, false}, {"GET", "/api/latest/fleet/configuration_profiles", false, false}, {"GET", "/api/latest/fleet/mdm/manual_enrollment_profile", false, true}, {"GET", "/api/latest/fleet/enrollment_profiles/manual", false, true}, {"GET", "/api/latest/fleet/mdm/bootstrap/1/metadata", false, true}, {"GET", "/api/latest/fleet/bootstrap/1/metadata", false, true}, {"DELETE", "/api/latest/fleet/mdm/bootstrap/1", false, true}, {"DELETE", "/api/latest/fleet/bootstrap/1", false, true}, {"GET", "/api/latest/fleet/mdm/bootstrap?token=1", false, true}, {"GET", "/api/latest/fleet/bootstrap?token=1", false, true}, {"GET", "/api/latest/fleet/mdm/bootstrap/summary", false, true}, {"GET", "/api/latest/fleet/mdm/apple/bootstrap/summary", false, true}, {"GET", "/api/latest/fleet/bootstrap/summary", false, true}, {"PATCH", "/api/latest/fleet/mdm/apple/setup", false, true}, {"PATCH", "/api/latest/fleet/setup_experience", false, true}, {"POST", "/api/fleet/orbit/setup_experience/status", false, true}, {"POST", "/api/latest/fleet/software/web_apps", false, true}, {"POST", "/api/latest/fleet/hosts/1/name_template/resend", false, true}, {"GET", "/api/latest/fleet/assets", false, true}, {"GET", "/api/latest/fleet/assets/1", false, true}, // TODO: multipart/form data parsing issue, see comment above // {"POST", "/api/latest/fleet/assets", false, true}, {"DELETE", "/api/latest/fleet/assets/1", false, true}, {"POST", "/api/latest/fleet/assets/batch", false, true}, } } func windowsMDMConfigurationRequiredEndpoints() []string { return []string{ "/api/fleet/orbit/disk_encryption_key", } } func androidMDMConfigurationRequiredEndpoints() []string { return []string{ "/api/latest/fleet/software/web_apps", } } // getURLSchemas returns a list of all valid URI schemas func getURISchemas() []string { return []string{ "aaa", "aaas", "about", "acap", "acct", "acd", "acr", "adiumxtra", "adt", "afp", "afs", "aim", "amss", "android", "appdata", "apt", "ar", "ark", "at", "attachment", "aw", "barion", "bb", "beshare", "bitcoin", "bitcoincash", "blob", "bolo", "browserext", "cabal", "calculator", "callto", "cap", "cast", "casts", "chrome", "chrome-extension", "cid", "coap", "coap+tcp", "coap+ws", "coaps", "coaps+tcp", "coaps+ws", "com-eventbrite-attendee", "content", "content-type", "crid", "cstr", "cvs", "dab", "dat", "data", "dav", "dhttp", "diaspora", "dict", "did", "dis", "dlna-playcontainer", "dlna-playsingle", "dns", "dntp", "doi", "dpp", "drm", "drop", "dtmi", "dtn", "dvb", "dvx", "dweb", "ed2k", "eid", "elsi", "embedded", "ens", "ethereum", "example", "facetime", "fax", "feed", "feedready", "fido", "file", "filesystem", "finger", "first-run-pen-experience", "fish", "fm", "ftp", "fuchsia-pkg", "geo", "gg", "git", "gitoid", "gizmoproject", "go", "gopher", "graph", "grd", "gtalk", "h323", "ham", "hcap", "hcp", "http", "https", "hxxp", "hxxps", "hydrazone", "hyper", "iax", "icap", "icon", "im", "imap", "info", "iotdisco", "ipfs", "ipn", "ipns", "ipp", "ipps", "irc", "irc6", "ircs", "iris", "iris.beep", "iris.lwz", "iris.xpc", "iris.xpcs", "isostore", "itms", "jabber", "jar", "jms", "keyparc", "lastfm", "lbry", "ldap", "ldaps", "leaptofrogans", "lorawan", "lpa", "lvlt", "magnet", "mailserver", "mailto", "maps", "market", "matrix", "message", "microsoft.windows.camera", "microsoft.windows.camera.multipicker", "microsoft.windows.camera.picker", "mid", "mms", "modem", "mongodb", "moz", "ms-access", "ms-appinstaller", "ms-browser-extension", "ms-calculator", "ms-drive-to", "ms-enrollment", "ms-excel", "ms-eyecontrolspeech", "ms-gamebarservices", "ms-gamingoverlay", "ms-getoffice", "ms-help", "ms-infopath", "ms-inputapp", "ms-launchremotedesktop", "ms-lockscreencomponent-config", "ms-media-stream-id", "ms-meetnow", "ms-mixedrealitycapture", "ms-mobileplans", "ms-newsandinterests", "ms-officeapp", "ms-people", "ms-project", "ms-powerpoint", "ms-publisher", "ms-remotedesktop", "ms-remotedesktop-launch", "ms-restoretabcompanion", "ms-screenclip", "ms-screensketch", "ms-search", "ms-search-repair", "ms-secondary-screen-controller", "ms-secondary-screen-setup", "ms-settings", "ms-settings-airplanemode", "ms-settings-bluetooth", "ms-settings-camera", "ms-settings-cellular", "ms-settings-cloudstorage", "ms-settings-connectabledevices", "ms-settings-displays-topology", "ms-settings-emailandaccounts", "ms-settings-language", "ms-settings-location", "ms-settings-lock", "ms-settings-nfctransactions", "ms-settings-notifications", "ms-settings-power", "ms-settings-privacy", "ms-settings-proximity", "ms-settings-screenrotation", "ms-settings-wifi", "ms-settings-workplace", "ms-spd", "ms-stickers", "ms-sttoverlay", "ms-transit-to", "ms-useractivityset", "ms-virtualtouchpad", "ms-visio", "ms-walk-to", "ms-whiteboard", "ms-whiteboard-cmd", "ms-word", "msnim", "msrp", "msrps", "mss", "mt", "mtqp", "mumble", "mupdate", "mvn", "news", "nfs", "ni", "nih", "nntp", "notes", "num", "ocf", "oid", "onenote", "onenote-cmd", "opaquelocktoken", "openpgp4fpr", "otpauth", "p1", "pack", "palm", "paparazzi", "payment", "payto", "pkcs11", "platform", "pop", "pres", "prospero", "proxy", "pwid", "psyc", "pttp", "qb", "query", "quic-transport", "redis", "rediss", "reload", "res", "resource", "rmi", "rsync", "rtmfp", "rtmp", "rtsp", "rtsps", "rtspu", "sarif", "secondlife", "secret-token", "service", "session", "sftp", "sgn", "shc", "shttp", "sieve", "simpleledger", "simplex", "sip", "sips", "skype", "smb", "smp", "sms", "smtp", "snews", "snmp", "soap.beep", "soap.beeps", "soldat", "spiffe", "spotify", "ssb", "ssh", "starknet", "steam", "stun", "stuns", "submit", "svn", "swh", "swid", "swidpath", "tag", "taler", "teamspeak", "tel", "teliaeid", "telnet", "tftp", "things", "thismessage", "tip", "tn3270", "tool", "turn", "turns", "tv", "udp", "unreal", "upt", "urn", "ut2004", "uuid-in-package", "v-event", "vemmi", "ventrilo", "ves", "videotex", "vnc", "view-source", "vscode", "vscode-insiders", "vsls", "w3", "wais", "web3", "wcr", "webcal", "web+ap", "wifi", "wpid", "ws", "wss", "wtai", "wyciwyg", "xcon", "xcon-userid", "xfire", "xmlrpc.beep", "xmlrpc.beeps", "xmpp", "xri", "ymsgr", "z39.50", "z39.50r", "z39.50s", } } func createAndroidDeviceID(name string) string { return "enterprises/mock-enterprise-id/devices/" + name } func statusReportMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage { return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, enterpriseSpecificID, "") } func enrollmentMessageWithEnterpriseSpecificID(t *testing.T, deviceInfo androidmanagement.Device, enterpriseSpecificID string) *android.PubSubMessage { return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, enterpriseSpecificID, "") } func statusReportMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage { return messageWithAndroidIdentifiers(t, android.PubSubStatusReport, deviceInfo, "", serialNumber) } func enrollmentMessageWithSerialNumber(t *testing.T, deviceInfo androidmanagement.Device, serialNumber string) *android.PubSubMessage { return messageWithAndroidIdentifiers(t, android.PubSubEnrollment, deviceInfo, "", serialNumber) } func messageWithAndroidIdentifiers(t *testing.T, notificationType android.NotificationType, deviceInfo androidmanagement.Device, enterpriseSpecificID, serialNumber string) *android.PubSubMessage { if serialNumber == "" && enterpriseSpecificID == "" || serialNumber != "" && enterpriseSpecificID != "" { t.Fatalf("exactly one of serialNumber or enterpriseSpecificID must be provided") } deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{ Brand: "TestBrand", Model: "TestModel", Hardware: "test-hardware", } if enterpriseSpecificID != "" { deviceInfo.Ownership = android_service.DeviceOwnershipPersonallyOwned deviceInfo.HardwareInfo.EnterpriseSpecificId = enterpriseSpecificID deviceInfo.HardwareInfo.SerialNumber = enterpriseSpecificID } if serialNumber != "" { deviceInfo.Ownership = android_service.DeviceOwnershipCompanyOwned deviceInfo.HardwareInfo.SerialNumber = serialNumber } deviceInfo.SoftwareInfo = &androidmanagement.SoftwareInfo{ AndroidBuildNumber: "test-build", AndroidVersion: "1", } deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{ TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition } deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{ { EventType: "EXTERNAL_STORAGE_DETECTED", ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity CreateTime: "2024-01-15T09:00:00Z", }, { EventType: "INTERNAL_STORAGE_MEASURED", ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition CreateTime: "2024-01-15T10:00:00Z", }, { EventType: "EXTERNAL_STORAGE_MEASURED", ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage CreateTime: "2024-01-15T10:00:00Z", }, } data, err := json.Marshal(deviceInfo) require.NoError(t, err) encodedData := base64.StdEncoding.EncodeToString(data) return &android.PubSubMessage{ Attributes: map[string]string{ "notificationType": string(notificationType), }, Data: encodedData, } } type fmaTestState struct { version string installerBytes []byte sha256 string installerPath string patchQuery string // installScript is the manifest install script content. Defaults to a // placeholder when empty; set it to vary the script across builds (a real FMA // script embeds the versioned installer filename, so a rebuild changes it). installScript string } func (s *fmaTestState) ComputeSHA(b []byte) { h := sha256.New() h.Write(b) s.sha256 = hex.EncodeToString(h.Sum(nil)) } func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTestState) { if len(states) == 0 { states = make(map[string]*fmaTestState, 1) states["/zoom/windows.json"] = &fmaTestState{ version: "1.0", installerBytes: []byte("xyz"), installerPath: "/zoom.msi", } } for _, state := range states { state.ComputeSHA(state.installerBytes) } var downloadMu sync.Mutex // Mock installer server — routes by path to serve per-FMA bytes. The lookup // happens per request so a test can change a state's installerPath or bytes // between applies (recomputing sha256) without restarting the server. installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { downloadMu.Lock() defer downloadMu.Unlock() for _, state := range states { if state.installerPath == r.URL.Path { _, _ = w.Write(state.installerBytes) return } } http.NotFound(w, r) })) // Locate the repo's apps.json so the manifest server can serve it. _, thisFile, _, ok := runtime.Caller(0) if !ok { t.Fatalf("could not locate myself to serve apps.json file") } repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile))) appsJSONPath := filepath.Join(repoRoot, "ee", "maintained-apps", "outputs", "apps.json") manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/apps.json" { b, err := os.ReadFile(appsJSONPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write(b) return } var state *fmaTestState state, found := states[r.URL.Path] if !found { http.NotFound(w, r) return } versions := []*ma.FMAManifestApp{ { Version: state.version, Queries: ma.FMAQueries{ Exists: "SELECT 1 FROM osquery_info;", Patched: state.patchQuery, }, InstallerURL: installerServer.URL + state.installerPath, InstallScriptRef: "foobaz", UninstallScriptRef: "foobaz", SHA256: state.sha256, DefaultCategories: []string{"Productivity"}, }, } installScript := state.installScript if installScript == "" { installScript = "Hello World!" } manifest := ma.FMAManifestFile{ Versions: versions, Refs: map[string]string{"foobaz": installScript}, } require.NoError(t, json.NewEncoder(w).Encode(manifest)) })) t.Cleanup(func() { manifestServer.Close() installerServer.Close() }) dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t) dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", manifestServer.URL, t) require.NoError(t, maintained_apps.SyncAppsList(t.Context(), ds)) } // acmeCSRSigner adapts a depot.Signer to the acme.CSRSigner interface. type acmeCSRSigner struct { signer *depot.Signer } func (a *acmeCSRSigner) SignCSR(_ context.Context, csr *x509.CertificateRequest) (*x509.Certificate, error) { return a.signer.Signx509CSR(csr) } // mockRoundTripper is a custom http.RoundTripper that redirects requests to a mock server. type mockRoundTripper struct { mockServer string origBaseURL string next http.RoundTripper } func (rt *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if strings.Contains(req.URL.String(), rt.origBaseURL) { path := strings.TrimPrefix(req.URL.Path, "/") newURL := fmt.Sprintf("%s/%s", rt.mockServer, path) newReq, err := http.NewRequestWithContext(req.Context(), req.Method, newURL, req.Body) //nolint:gosec // test helper, URL is from mock server if err != nil { return nil, err } newReq.Header = req.Header return rt.next.RoundTrip(newReq) } return rt.next.RoundTrip(req) } // errOnly adapts RecordPolicyQueryExecutions' (stalePolicyIDs, error) return // for assertions that only care about the error. func errOnly(_ []uint, err error) error { return err }