Fix software installer error team -> fleet (#41070)

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

# Details

* Updates server-side error message about software installers to use
"fleet" instead of "team".
* Update front-end code that rewrites that error text 🤦  

# Checklist for submitter

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

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
n/a

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
- [X] Saw correct error banner when trying to add a VPP app that
conflicted with an FMA
<img width="741" height="67" alt="image"
src="https://github.com/user-attachments/assets/d171097c-b165-45f8-bafb-fd6337c94cb9"
/>
- [X] Saw correct error banner when trying to add a script with the same
contents as a another script
<img width="765" height="60" alt="image"
src="https://github.com/user-attachments/assets/db02b92a-942d-448d-9062-3fca49132a94"
/>

I haven't tested all the other cases but I think these two cover them;
one uses the `CantAddSoftwareConflictMessage` constant on the server and
one uses a hard-coded message. Everything else uses the constant.

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:
Scott Gress
2026-03-05 17:28:52 -06:00
committed by GitHub
parent b2caabd343
commit 2747c96308
8 changed files with 31 additions and 31 deletions
@@ -31,8 +31,8 @@ describe("ensurePeriod", () => {
describe("formatAlreadyAvailableInstallMessage", () => {
it("returns a React fragment with the correct text and team when the string matches the regex", () => {
// Example input: "Couldn't add. MyApp already has an installer available for the Marketing team."
const msg = `${ADD_SOFTWARE_ERROR_PREFIX} MyApp already has an installer available for the Marketing team.`;
// Example input: "Couldn't add. MyApp already has an installer available for the Marketing fleet."
const msg = `${ADD_SOFTWARE_ERROR_PREFIX} MyApp already has an installer available for the Marketing fleet.`;
const result = formatAlreadyAvailableInstallMessage(msg);
// Render for querying text
@@ -40,11 +40,11 @@ describe("formatAlreadyAvailableInstallMessage", () => {
expect(container.textContent).toContain("Couldn't add.");
expect(container.textContent).toContain("MyApp");
expect(container.textContent).toContain("Marketing team");
expect(container.textContent).toContain("Marketing fleet");
});
it("returns React with correct text and team when the string matches the package exists regex", () => {
const msg = `SoftwareInstaller "MyApp" already exists with team "Marketing".`;
it("returns React with correct text and fleet when the string matches the package exists regex", () => {
const msg = `SoftwareInstaller "MyApp" already exists with fleet "Marketing".`;
const result = formatAlreadyAvailableInstallMessage(msg);
const { container } = render(<>{result}</>);
@@ -53,7 +53,7 @@ describe("formatAlreadyAvailableInstallMessage", () => {
expect(container.textContent).toContain(
"already has an installer available"
);
expect(container.textContent).toContain("Marketing");
expect(container.textContent).toContain("Marketing fleet");
});
it("returns null if the string does not match the expected pattern", () => {
@@ -62,13 +62,13 @@ describe("formatAlreadyAvailableInstallMessage", () => {
expect(result).toBeNull();
});
it("works for different app names and team names", () => {
const msg = `${ADD_SOFTWARE_ERROR_PREFIX} Zoom already has an installer available for the Engineering team.`;
it("works for different app names and fleet names", () => {
const msg = `${ADD_SOFTWARE_ERROR_PREFIX} Zoom already has an installer available for the Engineering fleet.`;
const result = formatAlreadyAvailableInstallMessage(msg);
const { container } = render(<>{result}</>);
expect(container.textContent).toContain("Zoom");
expect(container.textContent).toContain("Engineering team");
expect(container.textContent).toContain("Engineering fleet");
});
it("returns null if the input is empty", () => {
@@ -23,27 +23,27 @@ export const formatAlreadyAvailableInstallMessage = (msg: string) => {
// Remove prefix (with or without trailing space)
const cleaned = msg.replace(/^Couldn't add software\.?\s*/, "");
// New regex for "<package> already has an installer available for the <team> team."
const installerExistsRegex = /^(.+?) already.+the (.+?) team\./;
// New regex for "<package> already has an installer available for the <fleet> fleet."
const installerExistsRegex = /^(.+?) already.+the (.+?) fleet\./;
let match = cleaned.match(installerExistsRegex);
if (match) {
return (
<>
{ADD_SOFTWARE_ERROR_PREFIX} <b>{match[1]}</b> already has an installer
available for the <b>{match[2]}</b> team.{" "}
available for the <b>{match[2]}</b> fleet.{" "}
</>
);
}
// New regex for "SoftwareInstaller <package> already exists with team <team>."
// or "In-house app <package> already exists with team <team>."
const packageExistsRegex = /^(?:SoftwareInstaller|In-house app) "(.+?)" already.+ team "(.+?)"\./;
// New regex for "SoftwareInstaller <package> already exists with fleet <fleet>."
// or "In-house app <package> already exists with fleet <fleet>."
const packageExistsRegex = /^(?:SoftwareInstaller|In-house app) "(.+?)" already.+ fleet "(.+?)"\./;
match = cleaned.match(packageExistsRegex);
if (match) {
return (
<>
{ADD_SOFTWARE_ERROR_PREFIX} <b>{match[1]}</b> already has an installer
available for the <b>{match[2]}</b> team.{" "}
available for the <b>{match[2]}</b> fleet.{" "}
</>
);
}
+2 -2
View File
@@ -60,9 +60,9 @@ func (e *existsError) Error() string {
msg += " already exists"
switch {
case e.TeamID != nil:
msg += fmt.Sprintf(" with TeamID %d.", *e.TeamID)
msg += fmt.Sprintf(" with FleetID %d.", *e.TeamID)
case e.TeamName != nil:
msg += fmt.Sprintf(" with team %q.", *e.TeamName)
msg += fmt.Sprintf(" with fleet %q.", *e.TeamName)
}
return msg
}
+2 -2
View File
@@ -14,14 +14,14 @@ func TestAlreadyExistsError(t *testing.T) {
name: "WithTeamID",
fn: func(t *testing.T) {
err := alreadyExists("User", "alice").WithTeamID(42)
expectedMsg := `User "alice" already exists with TeamID 42.`
expectedMsg := `User "alice" already exists with FleetID 42.`
require.Equal(t, expectedMsg, err.Error())
},
}, {
name: "WithTeamName",
fn: func(t *testing.T) {
err := alreadyExists("User", "alice").WithTeamName("Falcon Team")
expectedMsg := `User "alice" already exists with team "Falcon Team".`
expectedMsg := `User "alice" already exists with fleet "Falcon Team".`
require.Equal(t, expectedMsg, err.Error())
},
}, {
@@ -260,7 +260,7 @@ func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload
if !(found[0].Title == payload.Title && found[0].Source == payload.Source) {
return 0, 0, fleet.NewInvalidArgumentError(
"software",
"Couldn't add software. An installer with identical contents already exists on this team.",
"Couldn't add software. An installer with identical contents already exists on this fleet.",
)
}
// If exact duplicate (same title and source), continue to let DB constraint handle it
@@ -4239,7 +4239,7 @@ func testMatchOrCreateSoftwareInstallerDuplicateHash(t *testing.T, ds *Datastore
// Binary packages with same title on same team → reject
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, mkPayload(&teamA.ID, "a.sh", "title-a"))
require.ErrorContainsf(t, err, `"title-a" already exists with team "Team A".`, "expected existsError for same-team duplicate title, got: %T: %v", err, err)
require.ErrorContainsf(t, err, `"title-a" already exists with fleet "Team A".`, "expected existsError for same-team duplicate title, got: %T: %v", err, err)
}
func testAddSoftwareTitleToMatchingSoftware(t *testing.T, ds *Datastore) {
+1 -1
View File
@@ -33,7 +33,7 @@ var (
CantDisableDiskEncryptionIfPINRequiredErrMsg = "Couldn't disable disk encryption, you need to disable the BitLocker PIN requirement first."
CantEnablePINRequiredIfDiskEncryptionEnabled = "Couldn't enable BitLocker PIN requirement, you must enable disk encryption first."
CantResendAppleDeclarationProfilesMessage = "Can't resend declaration (DDM) profiles. Unlike configuration profiles (.mobileconfig), the host automatically checks in to get the latest DDM profiles."
CantAddSoftwareConflictMessage = "Couldn't add software. %s already has an installer available for the %s team."
CantAddSoftwareConflictMessage = "Couldn't add software. %s already has an installer available for the %s fleet."
)
// ErrWithStatusCode is an interface for errors that should set a specific HTTP
@@ -1424,7 +1424,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleVPPAppSoftwarePackageConflict
Title: "DummyApp",
TeamID: &team.ID,
}
s.uploadSoftwareInstaller(t, pkgDummy, http.StatusConflict, "DummyApp already has an installer available for the Team 1 team.")
s.uploadSoftwareInstaller(t, pkgDummy, http.StatusConflict, "DummyApp already has an installer available for the Team 1 fleet.")
// Add VPP app 2 with bundle ID com.example.noversion (conflicts with NoVersion)
vppApp2 := &fleet.VPPApp{
@@ -1438,7 +1438,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleVPPAppSoftwarePackageConflict
res := s.Do("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: vppApp2.AdamID, SelfService: true}, http.StatusConflict)
txt := extractServerErrorText(res.Body)
require.Contains(t, txt, "NoVersion already has an installer available for the Team 1 team.")
require.Contains(t, txt, "NoVersion already has an installer available for the Team 1 fleet.")
// --- test with batch-set (gitops) ---
@@ -1466,7 +1466,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleVPPAppSoftwarePackageConflict
}, http.StatusAccepted, &batchResponse, "team_name", team.Name)
batchResp := waitBatchSetSoftwareInstallers(t, &s.withServer, team.Name, batchResponse.RequestUUID)
require.Equal(t, fleet.BatchSetSoftwareInstallersStatusFailed, batchResp.Status)
require.Contains(t, batchResp.Message, "DummyApp already has an installer available for the Team 1 team.")
require.Contains(t, batchResp.Message, "DummyApp already has an installer available for the Team 1 fleet.")
// batch-set the VPP apps, including one in conflict
res = s.Do("POST", "/api/latest/fleet/software/app_store_apps/batch", batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
@@ -1474,7 +1474,7 @@ func (s *integrationMDMTestSuite) TestSoftwareTitleVPPAppSoftwarePackageConflict
{AppStoreID: "2"},
}}, http.StatusConflict, "team_name", team.Name)
txt = extractServerErrorText(res.Body)
require.Contains(t, txt, "NoVersion already has an installer available for the Team 1 team.")
require.Contains(t, txt, "NoVersion already has an installer available for the Team 1 fleet.")
// listing software available to install only lists the dummy app and noversion installer
var listSw listSoftwareTitlesResponse
@@ -1985,7 +1985,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppVPPConflict() {
Platform: "ios",
}, http.StatusConflict)
txt := extractServerErrorText(res.Body)
require.Contains(t, txt, "already has an installer available for the IPA Conflict Team team.")
require.Contains(t, txt, "already has an installer available for the IPA Conflict Team fleet.")
res = s.Do("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
TeamID: &team.ID,
@@ -1993,7 +1993,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppVPPConflict() {
Platform: "ipados",
}, http.StatusConflict)
txt = extractServerErrorText(res.Body)
require.Contains(t, txt, "already has an installer available for the IPA Conflict Team team.")
require.Contains(t, txt, "already has an installer available for the IPA Conflict Team fleet.")
var addAppResp addAppStoreAppResponse
s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
@@ -2021,7 +2021,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppVPPConflict() {
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{
Filename: "ipa_test.ipa",
TeamID: &team2.ID,
}, http.StatusConflict, "already has an installer available for the IPA Conflict Team 2 team.")
}, http.StatusConflict, "already has an installer available for the IPA Conflict Team 2 fleet.")
// Test Case 3: Verify "No team" works correctly
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{
@@ -2042,7 +2042,7 @@ func (s *integrationMDMTestSuite) TestInHouseAppVPPConflict() {
Platform: "ios",
}, http.StatusConflict)
txt = extractServerErrorText(res.Body)
require.Contains(t, txt, "already has an installer available for the No team team.")
require.Contains(t, txt, "already has an installer available for the No team fleet.")
}
func (s *integrationMDMTestSuite) TestVPPAppScheduledUpdates() {