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:
@@ -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.{" "}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user