add missing patch semantics (#36004)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #35998

# Checklist for submitter

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

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results
This commit is contained in:
Jahziel Villasana-Espinoza 2025-11-20 18:55:46 -05:00 committed by GitHub
parent 9b744ea67d
commit 65234043f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 127 additions and 59 deletions

View file

@ -327,21 +327,23 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
dirty := make(map[string]bool)
payload.Categories = server.RemoveDuplicatesFromSlice(payload.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, payload.Categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}
if len(catIDs) != len(payload.Categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", payload.Categories),
if payload.Categories != nil {
payload.Categories = server.RemoveDuplicatesFromSlice(payload.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, payload.Categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}
}
payload.CategoryIDs = catIDs
dirty["Categories"] = true
if len(catIDs) != len(payload.Categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", payload.Categories),
}
}
payload.CategoryIDs = catIDs
dirty["Categories"] = true
}
// Handle in house apps separately
if software.InHouseAppCount == 1 {

View file

@ -603,7 +603,7 @@ func getVPPAppsMetadata(ctx context.Context, ids []fleet.VPPAppTeam) ([]*fleet.V
return apps, nil
}
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
if err := svc.authz.Authorize(ctx, &fleet.VPPApp{TeamID: teamID}, fleet.ActionWrite); err != nil {
return nil, err
}
@ -621,9 +621,13 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
teamName = tm.Name
}
validatedLabels, err := ValidateSoftwareLabels(ctx, svc, labelsIncludeAny, labelsExcludeAny)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels")
var validatedLabels *fleet.LabelIdentsWithScope
if labelsExcludeAny != nil || labelsIncludeAny != nil {
var err error
validatedLabels, err = ValidateSoftwareLabels(ctx, svc, labelsIncludeAny, labelsExcludeAny)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: validating software labels")
}
}
meta, err := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, teamID, titleID)
@ -631,12 +635,17 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
return nil, ctxerr.Wrap(ctx, err, "UpdateAppStoreApp: getting vpp app metadata")
}
selfServiceVal := meta.SelfService
if selfService != nil {
selfServiceVal = *selfService
}
appToWrite := &fleet.VPPApp{
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: fleet.VPPAppID{
AdamID: meta.AdamID, Platform: meta.Platform,
},
SelfService: selfService,
SelfService: selfServiceVal,
ValidatedLabels: validatedLabels,
DisplayName: displayName,
},
@ -650,20 +659,22 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
appToWrite.IconURL = *meta.IconURL
}
categories = server.RemoveDuplicatesFromSlice(categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}
if len(catIDs) != len(categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", categories),
if categories != nil {
categories = server.RemoveDuplicatesFromSlice(categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, categories)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting software category ids")
}
}
appToWrite.CategoryIDs = catIDs
if len(catIDs) != len(categories) {
return nil, &fleet.BadRequestError{
Message: "some or all of the categories provided don't exist",
InternalErr: fmt.Errorf("categories provided: %v", categories),
}
}
appToWrite.CategoryIDs = catIDs
}
// check if labels have changed
var existingLabels fleet.LabelIdentsWithScope
@ -682,8 +693,10 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
existingLabels.ByName[l.LabelName] = fleet.LabelIdent{LabelName: l.LabelName, LabelID: l.LabelID}
}
}
labelsChanged := !validatedLabels.Equal(&existingLabels)
var labelsChanged bool
if validatedLabels != nil {
labelsChanged = !validatedLabels.Equal(&existingLabels)
}
// Get the hosts that are NOT in label scope currently (before the update happens)
var hostsNotInScope map[uint]struct{}
@ -727,7 +740,7 @@ func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID
act := fleet.ActivityEditedAppStoreApp{
TeamName: &teamName,
TeamID: teamID,
SelfService: selfService,
SelfService: selfServiceVal,
SoftwareTitleID: titleID,
SoftwareTitle: meta.Name,
AppStoreID: meta.AdamID,

View file

@ -311,6 +311,8 @@ const handleEditAppStoreAppForm = (
if (formData.categories && formData.categories.length > 0) {
body.categories = formData.categories as SoftwareCategory[];
} else {
body.categories = [];
}
if (formData.targetType === "Custom") {
@ -320,6 +322,9 @@ const handleEditAppStoreAppForm = (
} else {
body.labels_exclude_any = selectedLabels;
}
} else {
body.labels_exclude_any = [];
body.labels_include_any = [];
}
};

View file

@ -717,7 +717,7 @@ type Service interface {
// AddAppStoreApp persists a VPP app onto a team and returns the resulting title ID
AddAppStoreApp(ctx context.Context, teamID *uint, appTeam VPPAppTeam) (uint, error)
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*VPPAppStoreApp, error)
UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*VPPAppStoreApp, error)
// GetInHouseAppManifest returns a manifest XML file that points at the download URL for the given in-house app.
GetInHouseAppManifest(ctx context.Context, titleID uint, teamID *uint) ([]byte, error)

View file

@ -456,7 +456,7 @@ type GetAppStoreAppsFunc func(ctx context.Context, teamID *uint) ([]*fleet.VPPAp
type AddAppStoreAppFunc func(ctx context.Context, teamID *uint, appTeam fleet.VPPAppTeam) (uint, error)
type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error)
type UpdateAppStoreAppFunc func(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error)
type GetInHouseAppManifestFunc func(ctx context.Context, titleID uint, teamID *uint) ([]byte, error)
@ -3635,7 +3635,7 @@ func (s *Service) AddAppStoreApp(ctx context.Context, teamID *uint, appTeam flee
return s.AddAppStoreAppFunc(ctx, teamID, appTeam)
}
func (s *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
func (s *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny []string, labelsExcludeAny []string, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
s.mu.Lock()
s.UpdateAppStoreAppFuncInvoked = true
s.mu.Unlock()

View file

@ -11786,7 +11786,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Len(t, resp.SoftwareTitles, 1)
nonVPPTitleID := resp.SoftwareTitles[0].ID
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false}
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false)}
// Attempt to update the non-VPP software using the VPP path. Should fail.
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", nonVPPTitleID), updateAppReq, http.StatusNotFound)
@ -13288,7 +13288,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
// Update the app to exclude any with l2. We should not enqueue an install here because mdmHost2
// has l2 (but it will re-enqueue the script for execution, immediately "activated")
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsExcludeAny: []string{l2.Name}}
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{l2.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK)
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
@ -13357,7 +13357,7 @@ func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
// Update the app to include any with l1. We should now enqueue an install as the app is in scope
// (in addition to the script execution, which will be the only one "activated").
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsIncludeAny: []string{l1.Name, l2.Name}}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsIncludeAny: []string{l1.Name, l2.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK)
s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
@ -16431,7 +16431,7 @@ func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger()
// Update the include any labels. The host has label2, so this means that the software
// moved in scope.
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsIncludeAny: []string{label2.Name}}
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsIncludeAny: []string{label2.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)
// Send back a failed result for the policy.
@ -16453,7 +16453,7 @@ func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger()
require.Equal(t, uint(1), policy1.FailingHostCount)
// Update to exclude_any: label 2. This moves the software out of scope. The policy is still failing.
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsExcludeAny: []string{label2.Name}}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label2.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)
err = s.ds.UpdateHostPolicyCounts(ctx)
@ -16495,7 +16495,7 @@ func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger()
require.Equal(t, uint(1), policy1.FailingHostCount)
// Update to exclude_any: label 2. This moves the software out of scope. The policy is still failing.
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsExcludeAny: []string{label2.Name}}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label2.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)
err = s.ds.UpdateHostPolicyCounts(ctx)
@ -16507,7 +16507,7 @@ func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger()
// Update to exclude_any: label3. Host has label1, label2, so the app is in scope again and
// status should clear.
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, LabelsExcludeAny: []string{label3.Name}}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label3.Name}}
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)
err = s.ds.UpdateHostPolicyCounts(ctx)
@ -17748,7 +17748,7 @@ func (s *integrationMDMTestSuite) TestSoftwareCategories() {
require.Contains(t, titleResponse.SoftwareTitle.AppStoreApp.Categories, cat3.Name)
// update the app and change categories
updateAppReq := &updateAppStoreAppRequest{TeamID: nil, SelfService: true}
updateAppReq := &updateAppStoreAppRequest{TeamID: nil, SelfService: ptr.Bool(true)}
updateAppReq.Categories = []string{cat1.Name, cat3.Name}
var updateAppResp updateAppStoreAppResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK, &updateAppResp)

View file

@ -104,6 +104,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
TeamID: &team.ID,
DisplayName: ptr.String("RubyUpdate1"),
SelfService: ptr.Bool(true),
Categories: []string{"Developer tools", "Browsers"},
}, http.StatusOK, "")
activityData = fmt.Sprintf(`
@ -131,7 +132,6 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
// Set display name to be empty
s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
SelfService: ptr.Bool(true),
InstallScript: ptr.String("some install script"),
PreInstallQuery: ptr.String("some pre install query"),
PostInstallScript: ptr.String("some post install script"),
@ -144,6 +144,9 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
// Entity display name is empty
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Empty(stResp.SoftwareTitle.DisplayName)
// PATCH semantics, so we shouldn't overwrite self service
s.Assert().True(stResp.SoftwareTitle.SoftwarePackage.SelfService)
s.Assert().ElementsMatch([]string{"Developer tools", "Browsers"}, stResp.SoftwareTitle.SoftwarePackage.Categories)
// List software titles display name is empty
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID))
@ -173,11 +176,31 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
LatestVersion: "1.0.0",
}
// Create a label
clr := createLabelResponse{}
s.DoJSON("POST", "/api/latest/fleet/labels", createLabelRequest{
LabelPayload: fleet.LabelPayload{
Name: "foo",
},
}, http.StatusOK, &clr)
lbl1Name := clr.Label.Name
clr = createLabelResponse{}
s.DoJSON("POST", "/api/latest/fleet/labels", createLabelRequest{
LabelPayload: fleet.LabelPayload{
Name: "bar",
},
}, http.StatusOK, &clr)
lbl2Name := clr.Label.Name
var addAppResp addAppStoreAppResponse
addAppReq := &addAppStoreAppRequest{
TeamID: &team.ID,
AppStoreID: includeAnyApp.AdamID,
SelfService: true,
TeamID: &team.ID,
AppStoreID: includeAnyApp.AdamID,
SelfService: true,
LabelsIncludeAny: []string{lbl1Name, lbl2Name},
}
// Now add it for real
@ -185,7 +208,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
macOSTitleID := addAppResp.TitleID
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: ptr.String("MacOSAppStoreAppUpdated1")}
updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), DisplayName: ptr.String("MacOSAppStoreAppUpdated1")}
var updateAppResp updateAppStoreAppResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)
@ -203,7 +226,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
}
}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: ptr.String("MacOSAppStoreAppUpdated2")}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), DisplayName: ptr.String("MacOSAppStoreAppUpdated2")}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)
s.Assert().Equal(*updateAppReq.DisplayName, updateAppResp.AppStoreApp.DisplayName)
@ -223,7 +246,11 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
existingDisplayName := *updateAppReq.DisplayName
// Omitting the field is a no-op
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: true}
updateAppReq = &updateAppStoreAppRequest{
TeamID: &team.ID,
SelfService: ptr.Bool(true),
Categories: []string{"Developer tools", "Browsers"},
}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)
s.Assert().Equal(existingDisplayName, updateAppResp.AppStoreApp.DisplayName)
@ -240,7 +267,10 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
}
}
updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: false, DisplayName: ptr.String("")}
updateAppReq = &updateAppStoreAppRequest{
TeamID: &team.ID,
DisplayName: ptr.String(""),
}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK, &updateAppResp)
s.Assert().Equal(*updateAppReq.DisplayName, updateAppResp.AppStoreApp.DisplayName)
@ -248,12 +278,24 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
stResp = getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Empty(stResp.SoftwareTitle.DisplayName)
// PATCH semantics, so we shouldn't overwrite self service or categories or labels
s.Assert().True(stResp.SoftwareTitle.AppStoreApp.SelfService)
s.Assert().ElementsMatch([]string{"Developer tools", "Browsers"}, stResp.SoftwareTitle.AppStoreApp.Categories)
s.Assert().ElementsMatch([]string{lbl1Name, lbl2Name}, func() []string {
var ret []string
for _, l := range stResp.SoftwareTitle.AppStoreApp.LabelsIncludeAny {
ret = append(ret, l.LabelName)
}
return ret
}())
// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID), "query", includeAnyApp.Name)
for _, a := range resp.SoftwareTitles {
if a.ID == macOSTitleID {
s.Assert().Empty(a.DisplayName)
// PATCH semantics, so we shouldn't overwrite self service
s.Assert().True(*a.AppStoreApp.SelfService)
}
}
@ -289,6 +331,8 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
TitleID: titleID,
TeamID: &team.ID,
DisplayName: ptr.String("InHouseAppUpdate2"),
SelfService: ptr.Bool(true),
Categories: []string{"Developer tools", "Browsers"},
}, http.StatusOK, "")
// Entity has display name
@ -306,14 +350,16 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
// Omitting the field is a no-op
s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
TeamID: &team.ID,
SelfService: ptr.Bool(true),
TitleID: titleID,
TeamID: &team.ID,
}, http.StatusOK, "")
// Entity has display name
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team.ID))
s.Assert().Equal("InHouseAppUpdate2", stResp.SoftwareTitle.DisplayName)
// PATCH semantics, so we shouldn't overwrite self service or categories
s.Assert().True(stResp.SoftwareTitle.SoftwarePackage.SelfService)
s.Assert().ElementsMatch([]string{"Developer tools", "Browsers"}, stResp.SoftwareTitle.SoftwarePackage.Categories)
// List software titles has display name
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{}, http.StatusOK, &resp, "team_id", fmt.Sprint(team.ID))
@ -321,6 +367,8 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleDisplayNames() {
for _, t := range resp.SoftwareTitles {
if t.ID == titleID {
s.Assert().Equal("InHouseAppUpdate2", t.DisplayName)
// PATCH semantics, so we shouldn't overwrite self service
s.Assert().True(*t.SoftwarePackage.SelfService)
}
}

View file

@ -966,7 +966,7 @@ func (s *integrationMDMTestSuite) TestVPPAppInstallVerification() {
// Enable self-service for vpp app
updateAppResp := updateAppStoreAppResponse{}
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", data.titleID),
&updateAppStoreAppRequest{TitleID: data.titleID, TeamID: &team.ID, SelfService: true}, http.StatusOK, &updateAppResp)
&updateAppStoreAppRequest{TitleID: data.titleID, TeamID: &team.ID, SelfService: ptr.Bool(true)}, http.StatusOK, &updateAppResp)
// Install self-service app correctly
s.DoRawWithHeaders("POST", fmt.Sprintf("/api/latest/fleet/device/%s/software/install/%d", data.host.UUID, data.titleID), nil, http.StatusAccepted, headers)
@ -1669,7 +1669,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppSelfInstall() {
}
// installed activity is now created
activityData = fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "command_uuid": %q, "install_uuid": "",
activityData = fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "command_uuid": %q, "install_uuid": "",
"software_title": "ipa_test", "software_package": "", "self_service": true, "status": "installed",
"policy_id": null, "policy_name": null}`, iosHost.ID, iosHost.DisplayName(), installCmdUUID)
s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), activityData, 0)

View file

@ -99,7 +99,7 @@ func (svc *Service) AddAppStoreApp(ctx context.Context, _ *uint, _ fleet.VPPAppT
type updateAppStoreAppRequest struct {
TitleID uint `url:"title_id"`
TeamID *uint `json:"team_id"`
SelfService bool `json:"self_service"`
SelfService *bool `json:"self_service"`
LabelsIncludeAny []string `json:"labels_include_any"`
LabelsExcludeAny []string `json:"labels_exclude_any"`
Categories []string `json:"categories"`
@ -124,7 +124,7 @@ func updateAppStoreAppEndpoint(ctx context.Context, request interface{}, svc fle
return updateAppStoreAppResponse{AppStoreApp: updatedApp}, nil
}
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
func (svc *Service) UpdateAppStoreApp(ctx context.Context, titleID uint, teamID *uint, selfService *bool, labelsIncludeAny, labelsExcludeAny, categories []string, displayName *string) (*fleet.VPPAppStoreApp, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)