fleet/server/datastore/mysql/in_house_apps_test.go
Jonathan Katz 5d62977e1f
Add and Delete display names via GitOps (#36575)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #36509 and #36513
-  batch set in-house apps will now add or delete display names with
- batch set software installers will now delete display names (could
already add)

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
2025-12-08 10:01:07 -05:00

1733 lines
65 KiB
Go

package mysql
import (
"context"
"database/sql"
"fmt"
"sort"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
nanomdm_mysql "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/storage/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)
func TestInHouseApps(t *testing.T) {
ds := CreateMySQLDS(t)
cases := []struct {
name string
fn func(t *testing.T, ds *Datastore)
}{
{"TestInHouseAppsCrud", testInHouseAppsCrud},
{"MultipleTeams", testInHouseAppsMultipleTeams},
{"BatchSetInHouseInstallers", testBatchSetInHouseInstallers},
{"BatchSetInHouseInstallersScopedViaLabels", testBatchSetInHouseInstallersScopedViaLabels},
{"EditDeleteInHouseInstallersActivateNextActivity", testEditDeleteInHouseInstallersActivateNextActivity},
{"Categories", testInHouseAppsCategories},
{"SoftwareTitleDisplayName", testSoftwareTitleDisplayNameInHouse},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
defer TruncateTables(t, ds)
c.fn(t, ds)
})
}
}
func testInHouseAppsCrud(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
host3 := test.NewHost(t, ds, "host3", "3", "host3key", "host3uuid", time.Now())
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID, host2.ID, host3.ID}))
require.NoError(t, err)
nanoEnroll(t, ds, host1, false)
nanoEnroll(t, ds, host2, false)
nanoEnroll(t, ds, host3, false)
payload := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "foo",
Filename: "foo.ipa",
BundleIdentifier: "com.foo",
StorageID: "testingtesting123",
Platform: "ios",
Extension: "ipa",
Version: "1.2.3",
}
payloadV2 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "foo_v3_different_name", // Not used in code, needed for test
Filename: "foo_v3_different_name.ipa",
BundleIdentifier: "com.foo",
StorageID: "testingtesting456",
Platform: "ios",
Extension: "ipa",
Version: "3.0.0",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
// -------------------------
// Upload software installer
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.Error(t, err, "ValidatedLabels must not be nil")
payload.ValidatedLabels = &fleet.LabelIdentsWithScope{}
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.NoError(t, err)
require.NotZero(t, installerID)
require.NotZero(t, titleID)
// both ios and ipados apps are created, both installer and title
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
var countI, countS uint
var ihaFilename, titleName string
errI := sqlx.GetContext(ctx, q, &countI, `SELECT COUNT(*) FROM in_house_apps`)
require.NoError(t, errI)
require.Equal(t, uint(2), countI)
errS := sqlx.GetContext(ctx, q, &countS, `SELECT COUNT(*) FROM software_titles`)
require.NoError(t, errS)
require.Equal(t, uint(2), countS)
errI = sqlx.GetContext(ctx, q, &ihaFilename, `SELECT filename FROM in_house_apps WHERE id = ?`, installerID)
require.NoError(t, errI)
require.Equal(t, "foo.ipa", ihaFilename)
errS = sqlx.GetContext(ctx, q, &titleName, `SELECT name FROM software_titles WHERE id = ?`, titleID)
require.NoError(t, errS)
require.Equal(t, "foo", titleName)
return nil
})
// Try to upload in house app again, expect duplicate error
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.ErrorContains(t, err, fmt.Sprintf(`In-house app %q already exists`, payload.Filename))
// Try to upload in house app with different version or name but same bundle id
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payloadV2)
require.ErrorContains(t, err, fmt.Sprintf(`In-house app %q already exists`, payloadV2.Filename))
// Get new in house app
installer, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
require.NoError(t, err)
require.Equal(t, payload.Title, installer.SoftwareTitle)
require.Equal(t, payload.Version, installer.Version)
require.WithinDuration(t, time.Now(), installer.UploadedAt, time.Minute)
require.False(t, payload.SelfService)
// Install on multiple users with pending, success, failure
createInHouseAppInstallRequest(t, ds, host1.ID, installerID, titleID, user1)
cmdUUID2 := createInHouseAppInstallRequest(t, ds, host2.ID, installerID, titleID, user1)
createInHouseAppInstallResultVerified(t, ds, host2, cmdUUID2, "Acknowledged")
cmdUUID3 := createInHouseAppInstallRequest(t, ds, host3.ID, installerID, titleID, user1)
createInHouseAppInstallResult(t, ds, host3, cmdUUID3, "Error")
// Get summary and expect failed, installed, pending
summary, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1, Failed: 1}, *summary)
// -------------------------
// Update software installer
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "include-any-1", Query: "select 1"})
require.NoError(t, err)
validatedLabels := fleet.LabelIdentsWithScope{
LabelScope: "include_any",
ByName: map[string]fleet.LabelIdent{
"include-any-1": {
LabelID: label.ID,
LabelName: label.Name,
},
}}
updatePayload := fleet.UpdateSoftwareInstallerPayload{
TeamID: &team.ID,
TitleID: titleID,
InstallerID: installerID,
Filename: "ipa_test.ipa",
StorageID: "new_storage_id",
ValidatedLabels: &validatedLabels,
SelfService: ptr.Bool(true),
}
err = ds.SaveInHouseAppUpdates(ctx, &updatePayload)
require.NoError(t, err)
// Installer updates correctly
var expectedLabels []fleet.SoftwareScopeLabel
expectedLabels = append(expectedLabels, fleet.SoftwareScopeLabel{LabelID: label.ID, LabelName: label.Name, Exclude: false, TitleID: titleID})
newInstaller, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
require.NoError(t, err)
require.Equal(t, "new_storage_id", newInstaller.StorageID)
require.Equal(t, expectedLabels, newInstaller.LabelsIncludeAny)
require.True(t, newInstaller.SelfService)
// Summary is unchanged?
summary2, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
require.NoError(t, err)
require.Equal(t, summary, summary2)
// -------------------------
// Delete software installer
err = ds.DeleteInHouseApp(ctx, installerID)
require.NoError(t, err)
// TODO: test RemovePendingInHouseAppInstalls independently
_, err = ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID)
require.Error(t, err)
status, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, installerID)
require.NoError(t, err)
require.Zero(t, *status)
// Check that entire tables are empty for this test
checkEmpty := func(table string) {
var count int
err := sqlx.GetContext(ctx, ds.reader(ctx), &count, fmt.Sprintf(`SELECT COUNT(*) FROM %s`, table))
require.NoError(t, err)
require.Zero(t, count, "expected %s to be empty", table)
}
checkEmpty("in_house_app_labels")
checkEmpty("host_in_house_software_installs")
checkEmpty("in_house_app_upcoming_activities")
checkEmpty("upcoming_activities")
// ipadOS installer should remain
var ipadID uint
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &ipadID, `SELECT id FROM in_house_apps LIMIT 1`))
// Try to upload installer again, expect duplicate error
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.Error(t, err)
// Delete ipadOS installer
require.NoError(t, ds.DeleteInHouseApp(ctx, ipadID))
var count int
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(*) FROM in_house_apps`)
require.NoError(t, err)
require.Zero(t, count, "expected in_house_apps to be empty")
// create one that's self-service at creation time
payload2 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "foo2",
Filename: "foo2.ipa",
BundleIdentifier: "com.foo2",
StorageID: "testingtesting1234",
Platform: "ios",
Extension: "ipa",
Version: "1.2.3",
SelfService: true,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
_, titleID2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
require.NoError(t, err)
require.NotZero(t, installerID)
require.NotZero(t, titleID)
installer2, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleID2)
require.NoError(t, err)
require.Equal(t, payload2.Title, installer2.SoftwareTitle)
require.True(t, installer2.SelfService)
// Test that app name is correct
payloadWithTitle := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "New Title",
Filename: "foo_with_title.ipa",
BundleIdentifier: "com.foo_with_title",
StorageID: "testing5",
Extension: "ipa",
Version: "1.0.0",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
_, titleIDWithTitle, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadWithTitle)
require.NoError(t, err)
installerWithTitle, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleIDWithTitle)
require.NoError(t, err)
require.Equal(t, payloadWithTitle.Title, installerWithTitle.SoftwareTitle)
// Different bundle id with same name should create new software title
payloadSameName := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "New Title",
Filename: "different_filename.ipa",
BundleIdentifier: "com.different.bundle",
StorageID: "testing5",
Extension: "ipa",
Version: "1.0.0",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
_, titleSameName, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadSameName)
require.NoError(t, err)
require.NotEqual(t, titleIDWithTitle, titleSameName)
}
func testInHouseAppsMultipleTeams(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID}))
require.NoError(t, err)
team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team2.ID, []uint{host2.ID}))
require.NoError(t, err)
nanoEnroll(t, ds, host1, false)
payload1 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user1.ID,
Title: "foo",
BundleIdentifier: "com.foo",
StorageID: "testingtesting123",
Platform: "ios",
Extension: "ipa",
Version: "1.2.3",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
payload2 := payload1
payload2.TeamID = &team2.ID
payloadNoTeam := payload1
payloadNoTeam.TeamID = nil
// Add installers for both teams
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload1)
require.NoError(t, err)
installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payloadNoTeam)
require.NoError(t, err)
var count int
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
require.NoError(t, err)
require.Equal(t, 2, count)
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
require.NoError(t, err)
require.Equal(t, 6, count)
// Team 2: Delete 1 installer from 1 team
err = ds.DeleteInHouseApp(ctx, installerID2)
require.NoError(t, err)
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
require.NoError(t, err)
require.Equal(t, 5, count)
// Team 2: Try to add installer again
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
require.Error(t, err)
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
require.NoError(t, err)
require.Equal(t, 5, count)
// Test that software titles for IHA don't get cleaned up
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.CleanupSoftwareTitles(ctx))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
require.NoError(t, err)
require.Equal(t, 2, count)
}
func testInHouseAppsCategories(t *testing.T, ds *Datastore) {
ctx := context.Background()
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
require.NoError(t, ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host1.ID})))
nanoEnroll(t, ds, host1, false)
payload1 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user1.ID,
BundleIdentifier: "com.foo",
Filename: "foo.ipa",
StorageID: "id1234",
Extension: "ipa",
SelfService: false,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
CategoryIDs: []uint{1, 2},
}
// Add installers for both teams
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload1)
require.NoError(t, err)
var count int
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, installerID)
require.NoError(t, err)
require.Equal(t, 2, count)
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM software_titles`)
require.NoError(t, err)
require.Equal(t, 2, count)
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_apps`)
require.NoError(t, err)
require.Equal(t, 2, count)
// Test with empty categories
payload2 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user1.ID,
BundleIdentifier: "com.bar",
Filename: "bar.ipa",
StorageID: "id5678",
Extension: "ipa",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
CategoryIDs: nil, // empty slice should work the same
}
secondInstallerID, secondTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload2)
require.NoError(t, err)
// Check that this has no categories
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
require.NoError(t, err)
require.Equal(t, 0, count)
// Update software categories
updatePayload := fleet.UpdateSoftwareInstallerPayload{
TeamID: payload2.TeamID,
TitleID: secondTitleID,
InstallerID: secondInstallerID,
Filename: payload2.Filename,
StorageID: payload2.StorageID,
ValidatedLabels: payload2.ValidatedLabels,
CategoryIDs: []uint{1, 2},
SelfService: ptr.Bool(true),
}
err = ds.SaveInHouseAppUpdates(ctx, &updatePayload)
require.NoError(t, err)
// Categories
err = sqlx.GetContext(ctx, ds.reader(ctx), &count, `SELECT COUNT(id) FROM in_house_app_software_categories WHERE in_house_app_id = ?`, secondInstallerID)
require.NoError(t, err)
require.Equal(t, 2, count)
}
func createInHouseAppInstallRequest(t *testing.T, ds *Datastore, hostID uint, appID uint, titleID uint, user *fleet.User) string {
ctx := context.Background()
ctx = viewer.NewContext(ctx, viewer.Viewer{User: user})
cmdUUID := uuid.NewString()
err := ds.InsertHostInHouseAppInstall(ctx, hostID, appID, titleID, cmdUUID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
return cmdUUID
}
func createInHouseAppInstallResult(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
ctx := context.Background()
ctx = context.WithValue(ctx, fleet.ActivityWebhookContextKey, true)
nanoDB, err := nanomdm_mysql.New(nanomdm_mysql.WithDB(ds.primary.DB))
require.NoError(t, err)
nanoCtx := &mdm.Request{EnrollID: &mdm.EnrollID{ID: host.UUID}, Context: ctx}
cmdRes := &mdm.CommandResults{
CommandUUID: cmdUUID,
Status: status,
Raw: []byte(`<?xml version="1.0" encoding="UTF-8"?>`),
}
err = nanoDB.StoreCommandReport(nanoCtx, cmdRes)
require.NoError(t, err)
// inserting the activity is what marks the upcoming activity as completed
// (and activates the next one).
err = ds.NewActivity(ctx, nil, fleet.ActivityInstalledAppStoreApp{
HostID: host.ID,
CommandUUID: cmdUUID,
}, []byte(`{}`), time.Now())
require.NoError(t, err)
}
func createInHouseAppInstallResultVerified(t *testing.T, ds *Datastore, host *fleet.Host, cmdUUID string, status string) {
createInHouseAppInstallResult(t, ds, host, cmdUUID, status)
ctx := t.Context()
timestampCol := "verification_at"
if status != "Acknowledged" {
timestampCol = "verification_failed_at"
}
ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, fmt.Sprintf(`UPDATE host_in_house_software_installs SET
%s = NOW(6), verification_command_uuid = ? WHERE host_id = ? AND command_uuid = ?`, timestampCol),
uuid.NewString(), host.ID, cmdUUID)
return err
})
}
func testBatchSetInHouseInstallers(t *testing.T, ds *Datastore) {
ctx := context.Background()
t.Cleanup(func() { ds.testActivateSpecificNextActivities = nil })
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
require.NoError(t, err)
// create a couple hosts
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now(), test.WithPlatform("ios"))
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID, host2.ID}))
require.NoError(t, err)
nanoEnroll(t, ds, host1, false)
nanoEnroll(t, ds, host2, false)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
assertTitlesAndApps := func(wantTitles []fleet.SoftwareTitleListResult, wantApps []fleet.SoftwareInstaller) {
tmFilter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
titles, _, _, err := ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{TeamID: &team.ID},
tmFilter,
)
require.NoError(t, err)
require.Len(t, titles, len(wantTitles))
sort.Slice(wantTitles, func(i, j int) bool {
l, r := wantTitles[i], wantTitles[j]
return l.Name < r.Name || l.Name == r.Name && l.Source < r.Source
})
sort.Slice(titles, func(i, j int) bool {
l, r := titles[i], titles[j]
return l.Name < r.Name || l.Name == r.Name && l.Source < r.Source
})
titleIDs := make([]uint, len(wantTitles))
for i, want := range wantTitles {
got := titles[i]
require.Equal(t, want.Name, got.Name)
require.Equal(t, want.Source, got.Source)
require.Equal(t, want.DisplayName, got.DisplayName)
require.Equal(t, want.BundleIdentifier == nil, got.BundleIdentifier == nil)
if want.BundleIdentifier != nil {
require.Equal(t, *want.BundleIdentifier, *got.BundleIdentifier)
}
titleIDs[i] = got.ID
}
sort.Slice(wantApps, func(i, j int) bool {
l, r := wantApps[i], wantApps[j]
return l.Name < r.Name || l.Name == r.Name && l.Platform < r.Platform
})
require.Len(t, wantApps, len(titleIDs))
for i, want := range wantApps {
got, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, titleIDs[i])
require.NoError(t, err)
require.Equal(t, want.Name, got.Name)
require.Equal(t, want.Platform, got.Platform)
require.Equal(t, want.Version, got.Version)
require.Equal(t, want.StorageID, got.StorageID)
require.Equal(t, want.DisplayName, got.DisplayName)
require.Equal(t, want.SelfService, got.SelfService)
require.Equal(t, want.BundleIdentifier, got.BundleIdentifier)
require.ElementsMatch(t, want.Categories, got.Categories)
}
}
// batch set with everything empty
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, nil)
require.NoError(t, err)
apps, err := ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Empty(t, apps)
assertTitlesAndApps(nil, nil)
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Empty(t, apps)
assertTitlesAndApps(nil, nil)
ipa1 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "ipa1",
Filename: "ipa1.ipa",
BundleIdentifier: "com.ipa1",
StorageID: "ipa1",
Extension: "ipa",
Version: "1.0.0",
SelfService: true,
DisplayName: "Yeehaw 1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
// the batch-upload handler would've generated both iOS and iPadOS entries
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
require.NotNil(t, apps[0].TeamID)
require.Equal(t, team.ID, *apps[0].TeamID)
require.NotNil(t, apps[0].TitleID)
require.Equal(t, "https://example.com/1", apps[0].URL)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
})
// add a new installer + ipa1 installer
ipa2 := fleet.UploadSoftwareInstallerPayload{
TeamID: &team.ID,
UserID: user1.ID,
Title: "ipa2",
Filename: "ipa2.ipa",
BundleIdentifier: "com.ipa2",
StorageID: "ipa2",
Extension: "ipa",
Version: "2.0.0",
SelfService: false,
DisplayName: "Yeehaw 2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
// the batch-upload handler would've generated both iOS and iPadOS entries
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ios_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ipados_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
sort.Slice(apps, func(i, j int) bool {
l, r := apps[i], apps[j]
return l.URL < r.URL
})
require.Len(t, apps, 4)
require.NotNil(t, apps[0].TeamID)
require.Equal(t, team.ID, *apps[0].TeamID)
require.NotNil(t, apps[0].TitleID)
require.Equal(t, "https://example.com/1", apps[0].URL)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
})
// rerun with no change
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ios_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ipados_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 4)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, DisplayName: ipa2.DisplayName},
})
// change ipa2 self-service and add categories
ipa2.SelfService = !ipa2.SelfService
ipa2.Categories = []string{"Communication", "Productivity"}
catIDs, err := ds.GetSoftwareCategoryIDs(ctx, ipa2.Categories)
require.NoError(t, err)
ipa2.CategoryIDs = catIDs
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ios_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ipados_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 4)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
})
// remove ipa1
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ios_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ipados_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
var iosInHouseID2, iosTitleID2 uint
for _, app := range apps {
require.NotNil(t, app.TitleID)
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, *app.TitleID)
require.NoError(t, err)
if meta.Platform == "ios" {
iosInHouseID2 = meta.InstallerID
iosTitleID2 = *app.TitleID
}
}
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
})
// add pending and completed installs for ipa2
completedCmd := createInHouseAppInstallRequest(t, ds, host1.ID, iosInHouseID2, iosTitleID2, user1)
createInHouseAppInstallRequest(t, ds, host2.ID, iosInHouseID2, iosTitleID2, user1)
createInHouseAppInstallResultVerified(t, ds, host1, completedCmd, "Acknowledged")
summary, err := ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
// batch-set without changes, should not affect installs
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ios_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
{
StorageID: ipa2.StorageID,
Filename: ipa2.Filename,
Title: ipa2.Title,
Source: "ipados_apps",
Version: ipa2.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/2",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa2.BundleIdentifier,
SelfService: ipa2.SelfService,
DisplayName: ipa2.DisplayName,
CategoryIDs: ipa2.CategoryIDs,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa2.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
{Name: ipa2.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa2"), DisplayName: ipa2.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa2.Filename, Platform: "ios", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
{Name: ipa2.Filename, Platform: "ipados", Version: ipa2.Version, StorageID: ipa2.StorageID, SelfService: ipa2.SelfService, BundleIdentifier: ipa2.BundleIdentifier, Categories: ipa2.Categories, DisplayName: ipa2.DisplayName},
})
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
ipa2TitleID := apps[0].TitleID
name, err := ds.getSoftwareTitleDisplayName(ctx, team.ID, *ipa2TitleID)
require.NoError(t, err)
require.Equal(t, ipa2.DisplayName, name)
// remove ipa2 and add ipa1
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
DisplayName: ipa1.DisplayName,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
var iosInHouseID1, iosTitleID1 uint
for _, app := range apps {
require.NotNil(t, app.TitleID)
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, *app.TitleID)
require.NoError(t, err)
if meta.Platform == "ios" {
iosInHouseID1 = meta.InstallerID
iosTitleID1 = *app.TitleID
}
}
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1"), DisplayName: ipa1.DisplayName},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier, DisplayName: ipa1.DisplayName},
})
// stats don't report anything about ipa2 anymore
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID2)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 0, Pending: 0}, *summary)
pendingHost1, _, err := ds.ListHostUpcomingActivities(ctx, host1.ID, fleet.ListOptions{PerPage: 10})
require.NoError(t, err)
require.Empty(t, pendingHost1)
pendingHost2, _, err := ds.ListHostUpcomingActivities(ctx, host2.ID, fleet.ListOptions{PerPage: 10})
require.NoError(t, err)
require.Empty(t, pendingHost2)
// display names for ipa2 are removed
_, err = ds.getSoftwareTitleDisplayName(ctx, team.ID, *ipa2TitleID)
require.ErrorContains(t, err, "not found")
// add pending and completed installs for ipa1
completedCmd = createInHouseAppInstallRequest(t, ds, host1.ID, iosInHouseID1, iosTitleID1, user1)
createInHouseAppInstallRequest(t, ds, host2.ID, iosInHouseID1, iosTitleID1, user1)
createInHouseAppInstallResultVerified(t, ds, host1, completedCmd, "Acknowledged")
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID1)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 1, Pending: 1}, *summary)
// update the storage ID of ipa1 (so it is a different installer binary)
ipa1.StorageID = "ipa1-new"
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ios_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
},
{
StorageID: ipa1.StorageID,
Filename: ipa1.Filename,
Title: ipa1.Title,
Source: "ipados_apps",
Version: ipa1.Version,
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: ipa1.BundleIdentifier,
SelfService: ipa1.SelfService,
},
})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, apps, 2)
assertTitlesAndApps([]fleet.SoftwareTitleListResult{
{Name: ipa1.Title, Source: "ios_apps", BundleIdentifier: ptr.String("com.ipa1")},
{Name: ipa1.Title, Source: "ipados_apps", BundleIdentifier: ptr.String("com.ipa1")},
}, []fleet.SoftwareInstaller{
{Name: ipa1.Filename, Platform: "ios", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier},
{Name: ipa1.Filename, Platform: "ipados", Version: ipa1.Version, StorageID: ipa1.StorageID, SelfService: ipa1.SelfService, BundleIdentifier: ipa1.BundleIdentifier},
})
// stats don't report anything about ipa1 anymore (as if it was deleted)
summary, err = ds.GetSummaryHostInHouseAppInstalls(ctx, &team.ID, iosInHouseID1)
require.NoError(t, err)
require.Equal(t, fleet.VPPAppStatusSummary{Installed: 0, Pending: 0}, *summary)
pendingHost1, _, err = ds.ListHostUpcomingActivities(ctx, host1.ID, fleet.ListOptions{PerPage: 10})
require.NoError(t, err)
require.Empty(t, pendingHost1)
pendingHost2, _, err = ds.ListHostUpcomingActivities(ctx, host2.ID, fleet.ListOptions{PerPage: 10})
require.NoError(t, err)
require.Empty(t, pendingHost2)
// remove everything
err = ds.BatchSetInHouseAppsInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
require.NoError(t, err)
apps, err = ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Empty(t, apps)
assertTitlesAndApps(nil, nil)
}
func testBatchSetInHouseInstallersScopedViaLabels(t *testing.T, ds *Datastore) {
ctx := context.Background()
// create a host to have a pending install request
host := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
nanoEnroll(t, ds, host, false)
// create a couple teams and a user
tm1, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "1"})
require.NoError(t, err)
tm2, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
require.NoError(t, err)
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
// create some installer payloads to be used by test cases
installers := make([]*fleet.UploadSoftwareInstallerPayload, 3)
for i := range installers {
installers[i] = &fleet.UploadSoftwareInstallerPayload{
UserID: user.ID,
Title: "ipa" + fmt.Sprint(i),
Filename: fmt.Sprintf("ipa%d.ipa", i),
BundleIdentifier: "com.ipa" + fmt.Sprint(i),
StorageID: "ipa" + fmt.Sprint(i),
Extension: "ipa",
Version: "1.0.0",
URL: "https://example.com/" + fmt.Sprint(i),
Source: "ios_apps",
Platform: "ios",
}
}
// create some labels to be used by test cases
labels := make([]*fleet.Label, 4)
for i := range labels {
lbl, err := ds.NewLabel(ctx, &fleet.Label{Name: "label" + fmt.Sprint(i)})
require.NoError(t, err)
labels[i] = lbl
}
type testPayload struct {
Installer *fleet.UploadSoftwareInstallerPayload
Labels []*fleet.Label
Exclude bool
ShouldCancelPending *bool // nil if the installer is new (could not have pending), otherwise true/false if it was edited
}
// test scenarios - note that subtests must NOT be used as the sequence of
// tests matters - they cannot be run in isolation.
cases := []struct {
desc string
team *fleet.Team
payload []testPayload
}{
{
desc: "empty payload",
payload: nil,
},
{
desc: "no team, installer0, no label",
payload: []testPayload{
{Installer: installers[0]},
},
},
{
desc: "team 1, installer0, include label0",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[0]}},
},
},
{
desc: "no team, installer0 no change, add installer1 with exclude label1",
payload: []testPayload{
{Installer: installers[0], ShouldCancelPending: ptr.Bool(false)},
{Installer: installers[1], Labels: []*fleet.Label{labels[1]}, Exclude: true},
},
},
{
desc: "no team, installer0 no change, installer1 change to include label1",
payload: []testPayload{
{Installer: installers[0], ShouldCancelPending: ptr.Bool(false)},
{Installer: installers[1], Labels: []*fleet.Label{labels[1]}, Exclude: false, ShouldCancelPending: ptr.Bool(true)},
},
},
{
desc: "team 1, installer0, include label0 and add label1",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[1]}, ShouldCancelPending: ptr.Bool(true)},
},
},
{
desc: "team 1, installer0, remove label0 and keep label1",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[1]}, ShouldCancelPending: ptr.Bool(true)},
},
},
{
desc: "team 1, installer0, switch to label0 and label2",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[2]}, ShouldCancelPending: ptr.Bool(true)},
},
},
{
desc: "team 2, 3 installers, mix of labels",
team: tm2,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[0]}, Exclude: false},
{Installer: installers[1], Labels: []*fleet.Label{labels[0], labels[1], labels[2]}, Exclude: true},
{Installer: installers[2], Labels: []*fleet.Label{labels[1], labels[2]}, Exclude: false},
},
},
{
desc: "team 1, installer0 no change and add installer2",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[0], labels[2]}, ShouldCancelPending: ptr.Bool(false)},
{Installer: installers[2]},
},
},
{
desc: "team 1, installer0 switch to labels 1 and 3, installer2 no change",
team: tm1,
payload: []testPayload{
{Installer: installers[0], Labels: []*fleet.Label{labels[1], labels[3]}, ShouldCancelPending: ptr.Bool(true)},
{Installer: installers[2], ShouldCancelPending: ptr.Bool(false)},
},
},
{
desc: "team 2, remove installer0, labels of install1 and no change installer2",
team: tm2,
payload: []testPayload{
{Installer: installers[1], ShouldCancelPending: ptr.Bool(true)},
{Installer: installers[2], Labels: []*fleet.Label{labels[1], labels[2]}, Exclude: false, ShouldCancelPending: ptr.Bool(false)},
},
},
{
desc: "no team, remove all",
payload: []testPayload{},
},
}
for _, c := range cases {
t.Log("Running test case ", c.desc)
var teamID *uint
var globalOrTeamID uint
if c.team != nil {
teamID = &c.team.ID
globalOrTeamID = c.team.ID
}
// cleanup any existing install requests for the host
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
if _, err := q.ExecContext(ctx, `DELETE FROM upcoming_activities WHERE host_id = ?`, host.ID); err != nil {
return err
}
_, err := q.ExecContext(ctx, `DELETE FROM host_in_house_software_installs WHERE host_id = ?`, host.ID)
return err
})
installerIDs := make([]uint, len(c.payload))
if len(c.payload) > 0 {
// create pending install requests for each updated installer, to see if
// it cancels it or not as expected.
err := ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(teamID, []uint{host.ID}))
require.NoError(t, err)
for i, payload := range c.payload {
if payload.ShouldCancelPending != nil {
// the installer must exist
var ihaID, titleID uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
err := sqlx.GetContext(ctx, q, &titleID, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND extension_for = ''`,
payload.Installer.Title, payload.Installer.Source)
if err != nil {
return err
}
err = sqlx.GetContext(ctx, q, &ihaID, `SELECT id FROM in_house_apps WHERE global_or_team_id = ?
AND title_id = ?`, globalOrTeamID, titleID)
return err
})
createInHouseAppInstallRequest(t, ds, host.ID, ihaID, titleID, user)
installerIDs[i] = ihaID
}
}
}
// create the payload by copying the test one, so that the original installers
// structs are not modified
payload := make([]*fleet.UploadSoftwareInstallerPayload, len(c.payload))
for i, p := range c.payload {
installer := *p.Installer
installer.ValidatedLabels = &fleet.LabelIdentsWithScope{LabelScope: fleet.LabelScopeIncludeAny}
if p.Exclude {
installer.ValidatedLabels.LabelScope = fleet.LabelScopeExcludeAny
}
byName := make(map[string]fleet.LabelIdent, len(p.Labels))
for _, lbl := range p.Labels {
byName[lbl.Name] = fleet.LabelIdent{LabelName: lbl.Name, LabelID: lbl.ID}
}
installer.ValidatedLabels.ByName = byName
payload[i] = &installer
}
err = ds.BatchSetInHouseAppsInstallers(ctx, teamID, payload)
require.NoError(t, err)
installers, err := ds.GetSoftwareInstallers(ctx, globalOrTeamID)
require.NoError(t, err)
require.Len(t, installers, len(c.payload))
// get the metadata for each installer to assert the batch did set the
// expected ones.
installersByFilename := make(map[string]*fleet.SoftwareInstaller, len(installers))
for _, ins := range installers {
meta, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, teamID, *ins.TitleID)
require.NoError(t, err)
installersByFilename[meta.Name] = meta
}
// validate that the inserted software is as expected
for i, payload := range c.payload {
meta, ok := installersByFilename[payload.Installer.Filename]
require.True(t, ok, "installer %s was not created", payload.Installer.Filename)
require.Equal(t, meta.SoftwareTitle, payload.Installer.Title)
wantLabelIDs := make([]uint, len(payload.Labels))
for j, lbl := range payload.Labels {
wantLabelIDs[j] = lbl.ID
}
if payload.Exclude {
require.Empty(t, meta.LabelsIncludeAny)
gotLabelIDs := make([]uint, len(meta.LabelsExcludeAny))
for i, lbl := range meta.LabelsExcludeAny {
gotLabelIDs[i] = lbl.LabelID
}
require.ElementsMatch(t, wantLabelIDs, gotLabelIDs)
} else {
require.Empty(t, meta.LabelsExcludeAny)
gotLabelIDs := make([]uint, len(meta.LabelsIncludeAny))
for j, lbl := range meta.LabelsIncludeAny {
gotLabelIDs[j] = lbl.LabelID
}
require.ElementsMatch(t, wantLabelIDs, gotLabelIDs)
}
// check if it deleted pending installs or not
if payload.ShouldCancelPending != nil {
var exists bool
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
err := sqlx.GetContext(ctx, q, &exists, `SELECT 1
FROM upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ua.id = ihua.upcoming_activity_id AND ua.activity_type = 'in_house_app_install'
WHERE ua.host_id = ? AND ihua.in_house_app_id = ?`, host.ID, installerIDs[i])
if err == sql.ErrNoRows {
err = nil
}
return err
})
if *payload.ShouldCancelPending {
require.False(t, exists, "pending install for installer %s was not cancelled but it should have been", payload.Installer.Filename)
} else {
require.True(t, exists, "pending install for installer %s was cancelled but it should not have been", payload.Installer.Filename)
}
}
}
}
}
func testEditDeleteInHouseInstallersActivateNextActivity(t *testing.T, ds *Datastore) {
ctx := t.Context()
user := test.NewUser(t, ds, "Alice", "alice@example.com", true)
// create a label
label, err := ds.NewLabel(ctx, &fleet.Label{Name: "A"})
require.NoError(t, err)
// create a few installers
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: "ipa0",
Filename: "ipa0.ipa",
Title: "ipa0",
Source: "ios_apps",
Version: "1.0.0",
UserID: user.ID,
Platform: "ios",
URL: "https://example.com/0",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: "com.ipa0",
SelfService: false,
},
{
StorageID: "ipa1",
Filename: "ipa1.ipa",
Title: "ipa1",
Source: "ios_apps",
Version: "1.0.0",
UserID: user.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: "com.ipa1",
SelfService: false,
},
})
require.NoError(t, err)
installers, err := ds.GetSoftwareInstallers(ctx, 0)
require.NoError(t, err)
require.Len(t, installers, 2)
sort.Slice(installers, func(i, j int) bool {
return installers[i].URL < installers[j].URL
})
ipa0, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, nil, *installers[0].TitleID)
require.NoError(t, err)
ipa1, err := ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, nil, *installers[1].TitleID)
require.NoError(t, err)
// create a few hosts
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now(), test.WithPlatform("ios"))
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now(), test.WithPlatform("ios"))
host3 := test.NewHost(t, ds, "host3", "3", "host3key", "host3uuid", time.Now(), test.WithPlatform("ios"))
nanoEnroll(t, ds, host1, false)
nanoEnroll(t, ds, host2, false)
nanoEnroll(t, ds, host3, false)
// enqueue software installs on each host
host1Ipa0 := createInHouseAppInstallRequest(t, ds, host1.ID, ipa0.InstallerID, *installers[0].TitleID, user)
host1Ipa1 := createInHouseAppInstallRequest(t, ds, host1.ID, ipa1.InstallerID, *installers[1].TitleID, user)
// add a script exec as last activity for host1
host1Script, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{
HostID: host1.ID, ScriptContents: "echo", UserID: &user.ID, SyncRequest: true,
})
require.NoError(t, err)
host2Ipa0 := createInHouseAppInstallRequest(t, ds, host2.ID, ipa0.InstallerID, *installers[0].TitleID, user)
host2Ipa1 := createInHouseAppInstallRequest(t, ds, host2.ID, ipa1.InstallerID, *installers[1].TitleID, user)
// add a script exec as first activity for host3
host3Script, err := ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{
HostID: host3.ID, ScriptContents: "echo", UserID: &user.ID, SyncRequest: true,
})
require.NoError(t, err)
host3Ipa1 := createInHouseAppInstallRequest(t, ds, host3.ID, ipa1.InstallerID, *installers[1].TitleID, user)
checkUpcomingActivities(t, ds, host1, host1Ipa0, host1Ipa1, host1Script.ExecutionID)
checkUpcomingActivities(t, ds, host2, host2Ipa0, host2Ipa1)
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID, host3Ipa1)
// update installer ipa0 metadata (label condition)
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: "ipa0",
Filename: "ipa0.ipa",
Title: "ipa0",
Source: "ios_apps",
Version: "1.0.0",
UserID: user.ID,
Platform: "ios",
URL: "https://example.com/0",
ValidatedLabels: &fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label.Name: {LabelID: label.ID, LabelName: label.Name}},
},
BundleIdentifier: "com.ipa0",
SelfService: false,
},
{
StorageID: "ipa1",
Filename: "ipa1.ipa",
Title: "ipa1",
Source: "ios_apps",
Version: "1.0.0",
UserID: user.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: "com.ipa1",
SelfService: false,
},
})
require.NoError(t, err)
// installer ipa0 activities were deleted, next activity was activated
checkUpcomingActivities(t, ds, host1, host1Ipa1, host1Script.ExecutionID)
checkUpcomingActivities(t, ds, host2, host2Ipa1)
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID, host3Ipa1)
// delete ipa1
err = ds.BatchSetInHouseAppsInstallers(ctx, nil, []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: "ipa0",
Filename: "ipa0.ipa",
Title: "ipa0",
Source: "ios_apps",
Version: "1.0.0",
UserID: user.ID,
Platform: "ios",
URL: "https://example.com/0",
ValidatedLabels: &fleet.LabelIdentsWithScope{
LabelScope: fleet.LabelScopeIncludeAny,
ByName: map[string]fleet.LabelIdent{label.Name: {LabelID: label.ID, LabelName: label.Name}},
},
BundleIdentifier: "com.ipa0",
SelfService: false,
},
})
require.NoError(t, err)
// installer ipa1 activities were deleted, next activity was activated for host1 and host2
checkUpcomingActivities(t, ds, host1, host1Script.ExecutionID)
checkUpcomingActivities(t, ds, host2)
checkUpcomingActivities(t, ds, host3, host3Script.ExecutionID)
}
func testSoftwareTitleDisplayNameInHouse(t *testing.T, ds *Datastore) {
ctx := context.Background()
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
payload := fleet.UploadSoftwareInstallerPayload{
UserID: user1.ID,
Title: "foo",
BundleIdentifier: "com.foo",
Filename: "foo.ipa",
StorageID: "testingtesting123",
Platform: "ios",
Extension: "ipa",
Version: "1.2.3",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.NoError(t, err)
err = ds.SaveInHouseAppUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
InstallerID: installerID,
Filename: "new_foo_file.ipa",
StorageID: "new_storage_id",
Version: "2.0.0",
SelfService: ptr.Bool(true),
DisplayName: ptr.String("super foo"),
})
require.NoError(t, err)
// Delete in-house app, display name should be deleted
require.NoError(t, ds.DeleteInHouseApp(ctx, installerID))
_, err = ds.getSoftwareTitleDisplayName(ctx, 0, titleID)
require.ErrorContains(t, err, "not found")
// Delete ipadOS installer
var ipadID uint
require.NoError(t, sqlx.GetContext(ctx, ds.reader(ctx), &ipadID, `SELECT id FROM in_house_apps LIMIT 1`))
require.NoError(t, ds.DeleteInHouseApp(ctx, ipadID))
// Add ipa, regular installer, vpp with names
installerID, titleID, err = ds.MatchOrCreateSoftwareInstaller(ctx, &payload)
require.NoError(t, err)
err = ds.SaveInHouseAppUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
InstallerID: installerID,
Filename: "foo.ipa",
Version: "1.0.0",
DisplayName: ptr.String("super foo"),
})
require.NoError(t, err)
payloadPkg := fleet.UploadSoftwareInstallerPayload{
UserID: user1.ID,
Title: "bar",
Filename: "bar.pkg",
InstallScript: "echo install",
BundleIdentifier: "com.bar",
Version: "1.2.3",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
}
_, pkgTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &payloadPkg)
require.NoError(t, err)
err = ds.SaveInstallerUpdates(ctx, &fleet.UpdateSoftwareInstallerPayload{
DisplayName: ptr.String("update1"),
TitleID: pkgTitleID,
InstallerFile: &fleet.TempFileReader{},
InstallScript: new(string),
PreInstallQuery: new(string),
PostInstallScript: new(string),
SelfService: ptr.Bool(false),
UninstallScript: new(string),
})
require.NoError(t, err)
test.CreateInsertGlobalVPPToken(t, ds)
_, err = ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "adam_vpp_1", Platform: "darwin"}, DisplayName: ptr.String("VPP 1")},
Name: "vpp1",
BundleIdentifier: "com.app.vpp1",
}, nil)
require.NoError(t, err)
getAllDisplayNames := func() []string {
var names []string
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
err := sqlx.SelectContext(ctx, q, &names, `SELECT display_name FROM software_title_display_names`)
require.NoError(t, err)
return nil
})
return names
}
names := getAllDisplayNames()
require.Len(t, names, 3)
// Batch set in-house apps, previous names should be deleted
err = ds.BatchSetInHouseAppsInstallers(ctx, ptr.Uint(0), []*fleet.UploadSoftwareInstallerPayload{
{
StorageID: "ipa1storageid",
Filename: "ipa1.ipa",
Title: "App Name",
Source: "ios_apps",
Version: "1.0.0",
UserID: user1.ID,
Platform: "ios",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: "com.ipa1",
SelfService: true,
DisplayName: "Yeehaw",
},
{
StorageID: "ipa1storageid",
Filename: "ipa1.ipa",
Title: "App Name",
Source: "ipados_apps",
Version: "1.0.0",
UserID: user1.ID,
Platform: "ipados",
URL: "https://example.com/1",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
BundleIdentifier: "com.ipa1",
SelfService: true,
DisplayName: "Yeehaw",
},
})
require.NoError(t, err)
names = getAllDisplayNames()
require.Len(t, names, 4) // installer and vpp display names aren't deleted
require.NotContains(t, names, "super foo") // previous in-house display names are deleted
// Delete all in-house apps
err = ds.BatchSetInHouseAppsInstallers(ctx, ptr.Uint(0), []*fleet.UploadSoftwareInstallerPayload{})
require.NoError(t, err)
names = getAllDisplayNames()
require.Len(t, names, 2)
}