Add more test coverage for Fleet-maintained apps (#27722)
For #27377. # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Added/updated automated tests
This commit is contained in:
@@ -2,7 +2,10 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -11,6 +14,7 @@ import (
|
||||
|
||||
ma "github.com/fleetdm/fleet/v4/ee/maintained-apps"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
@@ -253,3 +257,83 @@ func TestGetMaintainedAppAuth(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddFleetMaintainedApp(t *testing.T) {
|
||||
installerBytes := []byte("abc")
|
||||
|
||||
// this is the hash we expect to get in the DB
|
||||
h := sha256.New()
|
||||
_, err := h.Write(installerBytes)
|
||||
require.NoError(t, err)
|
||||
spoofedSHA := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
ds := new(mock.Store)
|
||||
ds.ValidateEmbeddedSecretsFunc = func(ctx context.Context, documents []string) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetMaintainedAppByIDFunc = func(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{
|
||||
ID: 1,
|
||||
Name: "Internet Exploder",
|
||||
Slug: "iexplode/windows",
|
||||
Platform: "windows",
|
||||
TitleID: nil,
|
||||
UniqueIdentifier: "Internet Exploder",
|
||||
}, nil
|
||||
}
|
||||
ds.MatchOrCreateSoftwareInstallerFunc = func(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, uint, error) {
|
||||
require.Equal(t, spoofedSHA, payload.StorageID)
|
||||
require.Empty(t, payload.BundleIdentifier)
|
||||
require.Equal(t, "Internet Exploder", payload.Title)
|
||||
require.Equal(t, "programs", payload.Source)
|
||||
require.Equal(t, "Hello World!", payload.InstallScript)
|
||||
require.Equal(t, "Hello World!", payload.UninstallScript)
|
||||
|
||||
// Can't easily inject a proper fleet.service so we bail early before NewActivity gets called and panics
|
||||
return 0, 0, errors.New("forced error to short-circuit storage and activity creation")
|
||||
}
|
||||
|
||||
// Mock server to serve the "installer"
|
||||
installerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(installerBytes)
|
||||
}))
|
||||
defer installerServer.Close()
|
||||
|
||||
// Mock server to serve the manifest
|
||||
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var versions []*ma.FMAManifestApp
|
||||
versions = append(versions, &ma.FMAManifestApp{
|
||||
Version: "6.0",
|
||||
Queries: ma.FMAQueries{
|
||||
Exists: "SELECT 1 FROM osquery_info;",
|
||||
},
|
||||
InstallerURL: installerServer.URL + "/iexplode.exe",
|
||||
InstallScriptRef: "foobaz",
|
||||
UninstallScriptRef: "foobaz",
|
||||
SHA256: noCheckHash,
|
||||
})
|
||||
|
||||
manifest := ma.FMAManifestFile{
|
||||
Versions: versions,
|
||||
Refs: map[string]string{
|
||||
"foobaz": "Hello World!",
|
||||
},
|
||||
}
|
||||
|
||||
err := json.NewEncoder(w).Encode(manifest)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
t.Cleanup(manifestServer.Close)
|
||||
os.Setenv("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL)
|
||||
defer os.Unsetenv("FLEET_DEV_MAINTAINED_APPS_BASE_URL")
|
||||
|
||||
svc := newTestService(t, ds)
|
||||
|
||||
authCtx := authz_ctx.AuthorizationContext{}
|
||||
ctx := authz_ctx.NewContext(context.Background(), &authCtx)
|
||||
ctx = viewer.NewContext(ctx, viewer.Viewer{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}})
|
||||
_, err = svc.AddFleetMaintainedApp(ctx, nil, 1, "", "", "", "", false, false, nil, nil)
|
||||
require.ErrorContains(t, err, "forced error to short-circuit storage and activity creation")
|
||||
|
||||
require.True(t, ds.MatchOrCreateSoftwareInstallerFuncInvoked)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ func TestGenerateErrors(t *testing.T) {
|
||||
PackageIDs: []string{""},
|
||||
})
|
||||
require.ErrorIs(t, err, ErrMissingTitle)
|
||||
|
||||
_, err = Generate(FMAInstallerMetadata{})
|
||||
require.ErrorIs(t, err, ErrMissingTitle)
|
||||
|
||||
_, err = FMAInstallerMetadata{}.PolicyDescription()
|
||||
require.ErrorIs(t, err, ErrMissingTitle)
|
||||
}
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
|
||||
@@ -99,6 +99,10 @@ func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
|
||||
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "Team 2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Testing search that returns no results; nothing inserted yet case
|
||||
_, _, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.ErrorIs(t, err, &fleet.NoMaintainedAppsInDatabaseError{})
|
||||
|
||||
maintained1, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
|
||||
Name: "Maintained1",
|
||||
Slug: "maintained1",
|
||||
@@ -203,6 +207,23 @@ func testListAndGetAvailableApps(t *testing.T, ds *Datastore) {
|
||||
require.False(t, meta.HasNextResults)
|
||||
require.True(t, meta.HasPreviousResults)
|
||||
|
||||
// Testing search
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{MatchQuery: "Maintained4", IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 1)
|
||||
require.EqualValues(t, 1, meta.TotalResults)
|
||||
require.Equal(t, expectedApps[3:], apps)
|
||||
require.False(t, meta.HasNextResults)
|
||||
require.False(t, meta.HasPreviousResults)
|
||||
|
||||
// Testing search that returns no results; non-error case
|
||||
apps, meta, err = ds.ListAvailableFleetMaintainedApps(ctx, &team1.ID, fleet.ListOptions{MatchQuery: "Maintained5", IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps, 0)
|
||||
require.EqualValues(t, 0, meta.TotalResults)
|
||||
require.False(t, meta.HasNextResults)
|
||||
require.False(t, meta.HasPreviousResults)
|
||||
|
||||
//
|
||||
// Test including software title ID for existing apps (installers)
|
||||
|
||||
|
||||
@@ -13,33 +13,7 @@ func init() {
|
||||
}
|
||||
|
||||
func Up_20250320200000(tx *sql.Tx) error {
|
||||
// Clean up Fleet Library App associated scripts before we drop the columns on the table
|
||||
_, err := tx.Exec(`DELETE FROM
|
||||
script_contents
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM host_script_results WHERE script_content_id = script_contents.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM scripts WHERE script_content_id = script_contents.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM software_installers si
|
||||
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id, si.uninstall_script_content_id)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fleet_library_apps fla
|
||||
WHERE script_contents.id IN (fla.install_script_content_id, fla.uninstall_script_content_id)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM setup_experience_scripts WHERE script_content_id = script_contents.id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM script_upcoming_activities WHERE script_content_id = script_contents.id
|
||||
)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean up unused scripts: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
_, err := tx.Exec(`
|
||||
ALTER TABLE software_installers
|
||||
CHANGE COLUMN fleet_library_app_id fleet_maintained_app_id INT unsigned DEFAULT NULL
|
||||
`)
|
||||
@@ -84,6 +58,28 @@ ALTER TABLE fleet_maintained_apps
|
||||
return fmt.Errorf("failed to alter fleet_maintained_apps: %w", err)
|
||||
}
|
||||
|
||||
// Clean up scripts that were only associated with FMAs
|
||||
_, err = tx.Exec(`DELETE FROM
|
||||
script_contents
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM host_script_results WHERE script_content_id = script_contents.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM scripts WHERE script_content_id = script_contents.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM software_installers si
|
||||
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id, si.uninstall_script_content_id)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM setup_experience_scripts WHERE script_content_id = script_contents.id
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM script_upcoming_activities WHERE script_content_id = script_contents.id
|
||||
)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to clean up unused scripts: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`UPDATE fleet_maintained_apps SET slug = concat(slug, '/', platform)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename FMA slugs: %w", err)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20250320200000(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Insert a scheduled and a triggered job run for maintained_apps
|
||||
execNoErr(t, db, `INSERT INTO cron_stats (name, instance, stats_type, status) VALUES (?, 'foo', ?, ?)`, fleet.CronMaintainedApps, fleet.CronStatsTypeScheduled, fleet.CronStatsStatusCompleted)
|
||||
execNoErr(t, db, `INSERT INTO cron_stats (name, instance, stats_type, status) VALUES (?, 'foo', ?, ?)`, fleet.CronMaintainedApps, fleet.CronStatsTypeTriggered, fleet.CronStatsStatusCompleted)
|
||||
|
||||
// Add the old Zoom, Zoom for IT Admins, and Box Drive FMAs
|
||||
tx, err := db.Begin()
|
||||
require.NoError(t, err)
|
||||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
|
||||
installScriptID, err := getOrInsertScript(txx, "echo install")
|
||||
require.NoError(t, err)
|
||||
uninstallScriptID, err := getOrInsertScript(txx, "echo uninstall")
|
||||
require.NoError(t, err)
|
||||
|
||||
installScriptID2, err := getOrInsertScript(txx, "echo install2")
|
||||
require.NoError(t, err)
|
||||
uninstallScriptID2, err := getOrInsertScript(txx, "echo uninstall2")
|
||||
require.NoError(t, err)
|
||||
|
||||
installScriptID3, err := getOrInsertScript(txx, "echo install different")
|
||||
require.NoError(t, err)
|
||||
|
||||
otherScriptID, err := getOrInsertScript(txx, "just a lil scripty boi")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.Commit()
|
||||
require.NoError(t, err)
|
||||
|
||||
execNoErr(
|
||||
t,
|
||||
db,
|
||||
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"Zoom",
|
||||
"zoom",
|
||||
"6.2.11.43613",
|
||||
"darwin",
|
||||
"https://cdn.zoom.us/prod/6.2.11.43613/arm64/zoomusInstallerFull.pkg",
|
||||
"dd6d28853eb6be7eaf7731aae1855c68cd6411ef6847158e6af18fffed5f8597",
|
||||
"us.zoom.xos",
|
||||
installScriptID,
|
||||
uninstallScriptID,
|
||||
)
|
||||
|
||||
execNoErr(
|
||||
t,
|
||||
db,
|
||||
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"Zoom for IT Admins",
|
||||
"zoom-for-it-admins",
|
||||
"6.2.11.43613",
|
||||
"darwin",
|
||||
"https://cdn.zoom.us/prod/6.2.11.43613/arm64/zoomusInstallerFull.pkg",
|
||||
"dd6d28853eb6be7eaf7731aae1855c68cd6411ef6847158e6af18fffed5f8597",
|
||||
"us.zoom.xos",
|
||||
installScriptID,
|
||||
uninstallScriptID,
|
||||
)
|
||||
|
||||
boxFMAID := execNoErrLastID(
|
||||
t,
|
||||
db,
|
||||
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"Box Drive",
|
||||
"box-drive",
|
||||
"2.42.212",
|
||||
"darwin",
|
||||
"https://e3.boxcdn.net/desktop/releases/mac/BoxDrive-2.42.212.pkg",
|
||||
"93550756150c434bc058c30b82352c294a21e978caf436ac99e0a5f431adfb6e",
|
||||
"com.box.desktop",
|
||||
installScriptID2,
|
||||
uninstallScriptID2,
|
||||
)
|
||||
|
||||
// add a software installer for Box to No team, same install scripts
|
||||
noTeamBox := execNoErrLastID(t, db, `
|
||||
INSERT INTO software_installers
|
||||
(filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id, fleet_library_app_id)
|
||||
VALUES
|
||||
(?,?,?,?,?,?,?,?)`, "box.pkg", "2.42.212", "darwin", installScriptID2, "sha-is-not-president", "", uninstallScriptID2, boxFMAID)
|
||||
|
||||
// add a software installer for Box to another team, different install script
|
||||
teamID := execNoErrLastID(t, db, `INSERT INTO teams (name) VALUES ("Foo")`)
|
||||
otherTeamBox := execNoErrLastID(t, db, `
|
||||
INSERT INTO software_installers
|
||||
(team_id, global_or_team_id, filename, version, platform, install_script_content_id, storage_id, package_ids, uninstall_script_content_id)
|
||||
VALUES
|
||||
(?,?,?,?,?,?,?,?,?)`, teamID, teamID, "box.pkg", "2.42.212", "darwin", installScriptID3, "sha-is-not-president", "", uninstallScriptID2)
|
||||
|
||||
// add a separate script to No team
|
||||
execNoErr(t, db, `INSERT INTO scripts (
|
||||
team_id, global_or_team_id, name, script_content_id
|
||||
) VALUES (?, ?, ?, ?)`, nil, 0, "myscript.sh", otherScriptID)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// install/uninstall scripts for Zoom should be gone
|
||||
// Box script should remain, other script should remain
|
||||
var scriptContentsIDs []int64
|
||||
err = db.Select(&scriptContentsIDs, `SELECT id FROM script_contents ORDER BY id`)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []int64{installScriptID2, uninstallScriptID2, installScriptID3, otherScriptID}, scriptContentsIDs)
|
||||
|
||||
// Should only have one Zoom plus Box
|
||||
var fmas []fleet.MaintainedApp
|
||||
err = db.Select(&fmas, `SELECT id, name, slug, unique_identifier FROM fleet_maintained_apps ORDER BY name`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, fmas, 2)
|
||||
require.Equal(t, "Box Drive", fmas[0].Name)
|
||||
require.Equal(t, "box-drive/darwin", fmas[0].Slug)
|
||||
require.Equal(t, "com.box.desktop", fmas[0].UniqueIdentifier)
|
||||
require.Equal(t, "Zoom", fmas[1].Name)
|
||||
require.Equal(t, "zoom/darwin", fmas[1].Slug)
|
||||
require.Equal(t, "us.zoom.xos", fmas[1].UniqueIdentifier)
|
||||
|
||||
var linkedFMAID *int64
|
||||
|
||||
// FMA ID for Box software installer on No team should match ID of Box FMA
|
||||
err = db.Get(&linkedFMAID, `SELECT fleet_maintained_app_id FROM software_installers WHERE id = ?`, noTeamBox)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, boxFMAID, *linkedFMAID)
|
||||
|
||||
// FMA ID for Box software installer on other team should be null
|
||||
err = db.Get(&linkedFMAID, `SELECT fleet_maintained_app_id FROM software_installers WHERE id = ?`, otherTeamBox)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, linkedFMAID)
|
||||
|
||||
// Only the triggered job record should remain in the cron_stats table
|
||||
var stats []fleet.CronStats
|
||||
err = db.Select(&stats, `SELECT name, instance, stats_type, status FROM cron_stats`)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, stats, 1)
|
||||
require.Equal(t, string(fleet.CronMaintainedApps), stats[0].Name)
|
||||
require.Equal(t, fleet.CronStatsTypeTriggered, stats[0].StatsType)
|
||||
require.Equal(t, fleet.CronStatsStatusCompleted, stats[0].Status)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package maintained_apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInstallerFilenameExtraction(t *testing.T) {
|
||||
// Mock server to serve the "installers"
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/redirect":
|
||||
w.Header().Set("Location", "/redirected%20package.exe")
|
||||
w.WriteHeader(302)
|
||||
_, _ = w.Write([]byte("redirecting"))
|
||||
case "/redirected%20package.exe":
|
||||
_, _ = w.Write([]byte("redirected fallback"))
|
||||
case "/compliant":
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="compliant.msi"`)
|
||||
_, _ = w.Write([]byte("compliant"))
|
||||
case "/not_compliant":
|
||||
w.Header().Set("Content-Disposition", `attachment; filename=not_compliant.pkg`)
|
||||
_, _ = w.Write([]byte("not_compliant"))
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// follow redirect and fall back to URL, after sanitization, when we don't have a content-disposition header
|
||||
client := fleethttp.NewClient(fleethttp.WithTimeout(time.Second))
|
||||
_, filename, err := DownloadInstaller(context.Background(), srv.URL+"/redirect", client)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "redirected package.exe", filename)
|
||||
|
||||
// handle properly formatted content-disposition header
|
||||
_, filename, err = DownloadInstaller(context.Background(), srv.URL+"/compliant", client)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "compliant.msi", filename)
|
||||
|
||||
// handle non-compliant content-disposition header
|
||||
_, filename, err = DownloadInstaller(context.Background(), srv.URL+"/not_compliant", client)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "not_compliant.pkg", filename)
|
||||
}
|
||||
Reference in New Issue
Block a user