mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41391 # Details This PR updates front-end API calls to use new URLs and API params, so that the front end doesn't cause deprecation warnings to appear on the server. # 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, should not be user-visible ## Testing - [X] Added/updated automated tests - [ ] QA'd all new/changed functionality manually The biggest risk here is not that we missed a spot that still causes a deprecation warning, but that we might inadvertently make a change that breaks the front end, for instance by sending `fleet_id` to a function that drops it silently and thus sends no ID to the server. Fortunately we use TypeScript in virtually every place affected by these changes, so the code would not compile if there were mismatches between the API expectation and what we're sending. Still, spot checking as many places as possible both for deprecation-warning leaks and loss of functionality is important. ## Summary by CodeRabbit * **Refactor** * Updated API nomenclature across the application to use "fleets" instead of "teams" and "reports" instead of "queries" in endpoint paths and request/response payloads. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
372 lines
14 KiB
TypeScript
372 lines
14 KiB
TypeScript
import React from "react";
|
|
import { screen, waitFor } from "@testing-library/react";
|
|
|
|
import { UserEvent } from "@testing-library/user-event";
|
|
import { IScript } from "interfaces/script";
|
|
import { createCustomRenderer } from "test/test-utils";
|
|
import { http, HttpResponse } from "msw";
|
|
import mockServer from "test/mock-server";
|
|
import { format } from "date-fns";
|
|
|
|
import RunScriptBatchModal from "./RunScriptBatchModal";
|
|
|
|
const baseUrl = (path: string) => {
|
|
return `/api/latest/fleet${path}`;
|
|
};
|
|
|
|
const windowsScript: IScript = {
|
|
id: 123,
|
|
team_id: 1,
|
|
name: "winscript.ps1",
|
|
created_at: "2023-01-01T00:00:00Z",
|
|
updated_at: "2023-01-01T00:00:00Z",
|
|
};
|
|
|
|
const linuxScript: IScript = {
|
|
id: 456,
|
|
team_id: 1,
|
|
name: "linuxscript.sh",
|
|
created_at: "2023-01-01T00:00:00Z",
|
|
updated_at: "2023-01-01T00:00:00Z",
|
|
};
|
|
|
|
jest.mock("../RunScriptBatchPaginatedList", () => {
|
|
return {
|
|
__esModule: true,
|
|
default: ({ onRunScript }: { onRunScript: (script: IScript) => void }) => {
|
|
return (
|
|
<div>
|
|
<div onClick={() => onRunScript(windowsScript)}>windows script</div>
|
|
<div onClick={() => onRunScript(linuxScript)}>linux script</div>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
});
|
|
|
|
// Utility to validate that the script list is rendered, select a script,
|
|
// and return the run and cancel buttons.
|
|
const selectScript = async (user: UserEvent, platform: string) => {
|
|
let el;
|
|
await waitFor(async () => {
|
|
el = screen.getByText(`${platform} script`);
|
|
expect(el).toBeInTheDocument();
|
|
});
|
|
if (!el) {
|
|
throw new Error("Script element not found");
|
|
}
|
|
await user.click(el);
|
|
let runButton;
|
|
let cancelButton;
|
|
await waitFor(() => {
|
|
runButton = screen.getByRole("button", { name: "Run" });
|
|
expect(runButton).toBeInTheDocument();
|
|
cancelButton = screen.getByRole("button", { name: "Cancel" });
|
|
expect(cancelButton).toBeInTheDocument();
|
|
});
|
|
if (!runButton || !cancelButton) {
|
|
throw new Error("Run or Cancel button not found");
|
|
}
|
|
return { runButton, cancelButton };
|
|
};
|
|
|
|
// Utility to validate that the "schedule for later" and "run now" buttons are present
|
|
// and return them.
|
|
const getScheduleSelector = async () => {
|
|
let scheduleButton;
|
|
let runNowButton;
|
|
await waitFor(() => {
|
|
scheduleButton = screen.getByLabelText("Schedule for later");
|
|
expect(scheduleButton).toBeInTheDocument();
|
|
runNowButton = screen.getByLabelText("Run now");
|
|
expect(runNowButton).toBeInTheDocument();
|
|
});
|
|
if (!scheduleButton || !runNowButton) {
|
|
throw new Error("Schedule or Run Now button not found");
|
|
}
|
|
return { scheduleButton, runNowButton };
|
|
};
|
|
|
|
// Utility to validate that the scheduling UI is present and return the date and time inputs.
|
|
const getScheduleUI = async () => {
|
|
let dateInput;
|
|
let timeInput;
|
|
await waitFor(() => {
|
|
dateInput = screen.getByLabelText("Date (UTC)");
|
|
expect(dateInput).toBeInTheDocument();
|
|
timeInput = screen.getByLabelText("Time (UTC)");
|
|
expect(timeInput).toBeInTheDocument();
|
|
});
|
|
if (!dateInput || !timeInput) {
|
|
throw new Error("Date or Time input not found");
|
|
}
|
|
return { dateInput, timeInput };
|
|
};
|
|
|
|
describe("RunScriptBatchModal", () => {
|
|
// Mock the scripts endpoint to return our two test scripts.
|
|
const scriptsHandler = http.get(baseUrl("/scripts"), () => {
|
|
return HttpResponse.json({
|
|
scripts: [windowsScript, linuxScript],
|
|
});
|
|
});
|
|
|
|
// Mock the run batch endpoint to simulate running a batch script,
|
|
// and provide a mock function we can use to validate the API call.
|
|
const runBatchFn = jest.fn(async () => {
|
|
return HttpResponse.json({});
|
|
});
|
|
const runBatchHandler = http.post(baseUrl("/scripts/run/batch"), runBatchFn);
|
|
|
|
beforeEach(() => {
|
|
mockServer.use(scriptsHandler);
|
|
runBatchFn.mockReset();
|
|
mockServer.use(runBatchHandler);
|
|
});
|
|
|
|
const render = createCustomRenderer({
|
|
withBackendMock: true,
|
|
});
|
|
|
|
// Standard props to use for most tests.
|
|
const defaultProps = {
|
|
runByFilters: false,
|
|
filters: { team_id: 1, status: "" },
|
|
teamId: 1,
|
|
isFreeTier: false,
|
|
totalFilteredHostsCount: 2,
|
|
selectedHostIds: [1, 2],
|
|
onCancel: () => null,
|
|
};
|
|
|
|
it("lists the scripts available for batch running", () => {
|
|
render(<RunScriptBatchModal {...defaultProps} />);
|
|
waitFor(() => {
|
|
const windowsScriptElement = screen.getByText("windows script");
|
|
const linuxScriptElement = screen.getByText("linux script");
|
|
expect(windowsScriptElement).toBeInTheDocument();
|
|
expect(linuxScriptElement).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("after clicking run script", () => {
|
|
it("shows the correct heading for linux/macos scripts", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "linux");
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("linuxscript.sh")).toBeInTheDocument();
|
|
expect(screen.getByText(/macOS and Linux/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("shows the correct heading for windows", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "windows");
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("winscript.ps1")).toBeInTheDocument();
|
|
expect(screen.getByText(/Windows/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("does not show the scheduling UI if 'run now' is selected", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "windows");
|
|
const { runNowButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
expect(screen.queryByLabelText(/Date/)).not.toBeInTheDocument();
|
|
expect(screen.queryByLabelText(/Time/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the scheduling UI if 'schedule for later' is selected", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "windows");
|
|
const { runNowButton, scheduleButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
await user.click(scheduleButton);
|
|
await getScheduleUI();
|
|
});
|
|
|
|
describe("run now", () => {
|
|
it("should call the API with no not_before param", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
const { runButton } = await selectScript(user, "windows");
|
|
await user.click(runButton);
|
|
expect(runBatchFn.mock.calls.length).toBe(1);
|
|
// @ts-ignore
|
|
const body = await runBatchFn.mock.calls[0][0].request.json();
|
|
expect(body).toEqual({
|
|
script_id: windowsScript.id,
|
|
host_ids: defaultProps.selectedHostIds,
|
|
});
|
|
});
|
|
|
|
it("should call the API with filters if supplied", async () => {
|
|
const props = {
|
|
...defaultProps,
|
|
runByFilters: true,
|
|
filters: { query: "hi", label_id: 16, status: "" },
|
|
};
|
|
props.selectedHostIds = [];
|
|
const { user } = render(<RunScriptBatchModal {...props} />);
|
|
const { runButton } = await selectScript(user, "windows");
|
|
await user.click(runButton);
|
|
expect(runBatchFn.mock.calls.length).toBe(1);
|
|
// @ts-ignore
|
|
const body = await runBatchFn.mock.calls[0][0].request.json();
|
|
expect(body).toEqual({
|
|
script_id: windowsScript.id,
|
|
filters: {
|
|
query: "hi",
|
|
label_id: 16,
|
|
fleet_id: 1,
|
|
status: "",
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("schedule for later", () => {
|
|
it("requires a valid date", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "windows");
|
|
const { runNowButton, scheduleButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
await user.click(scheduleButton);
|
|
const { dateInput } = await getScheduleUI();
|
|
// Add a wildly invalid date
|
|
await user.type(dateInput, "u up?");
|
|
expect(dateInput).toHaveValue("u up?");
|
|
expect(
|
|
screen.getByText("Date (UTC) must have valid format")
|
|
).toBeInTheDocument();
|
|
// Add a less wild, but still invalid date
|
|
await user.clear(dateInput);
|
|
await user.type(dateInput, "2023-99-99");
|
|
expect(dateInput).toHaveValue("2023-99-99");
|
|
expect(
|
|
screen.getByText("Date (UTC) must have valid format")
|
|
).toBeInTheDocument();
|
|
// Add a valid date, but in the past
|
|
await user.clear(dateInput);
|
|
await user.type(dateInput, "2023-01-01");
|
|
expect(dateInput).toHaveValue("2023-01-01");
|
|
expect(
|
|
screen.getByText("Date (UTC) cannot be in the past")
|
|
).toBeInTheDocument();
|
|
// Add a valid date in the future
|
|
await user.clear(dateInput);
|
|
await user.type(dateInput, "2099-12-31");
|
|
expect(dateInput).toHaveValue("2099-12-31");
|
|
expect(
|
|
screen.queryByText("Date (UTC) must have valid format")
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("Date (UTC) cannot be in the past")
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("requires a valid time", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
await selectScript(user, "windows");
|
|
const { runNowButton, scheduleButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
await user.click(scheduleButton);
|
|
const { dateInput, timeInput } = await getScheduleUI();
|
|
// Add a wildly invalid time
|
|
await user.type(timeInput, "professor churro");
|
|
expect(timeInput).toHaveValue("professor churro");
|
|
expect(
|
|
screen.getByText("Time (UTC) must have valid format")
|
|
).toBeInTheDocument();
|
|
// Add a less wild, but still invalid time
|
|
await user.clear(timeInput);
|
|
await user.type(timeInput, "99:99");
|
|
expect(timeInput).toHaveValue("99:99");
|
|
expect(
|
|
screen.getByText("Time (UTC) must have valid format")
|
|
).toBeInTheDocument();
|
|
// Add a valid time in the past (no date selected)
|
|
await user.clear(timeInput);
|
|
await user.type(timeInput, "00:00");
|
|
expect(timeInput).toHaveValue("00:00");
|
|
expect(
|
|
screen.queryByText("Date (UTC) must have valid format")
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("Date cannot be in the past")
|
|
).not.toBeInTheDocument();
|
|
// Add a valid time in the past (future date selected)
|
|
await user.clear(timeInput);
|
|
await user.type(timeInput, "00:00");
|
|
await user.clear(dateInput);
|
|
await user.type(dateInput, "2099-12-31");
|
|
expect(timeInput).toHaveValue("00:00");
|
|
expect(
|
|
screen.queryByText("Date (UTC) must have valid format")
|
|
).not.toBeInTheDocument();
|
|
expect(
|
|
screen.queryByText("Date (UTC) cannot be in the past")
|
|
).not.toBeInTheDocument();
|
|
// Add a valid time in the past (today selected)
|
|
await user.clear(dateInput);
|
|
await user.type(dateInput, format(new Date(), "yyyy-MM-dd"));
|
|
await user.clear(timeInput);
|
|
await user.type(timeInput, "00:00");
|
|
expect(timeInput).toHaveValue("00:00");
|
|
expect(
|
|
screen.getByText("Time (UTC) cannot be in the past")
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("should call the API with a correct not_before param", async () => {
|
|
const { user } = render(<RunScriptBatchModal {...defaultProps} />);
|
|
const { runButton } = await selectScript(user, "windows");
|
|
const { runNowButton, scheduleButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
await user.click(scheduleButton);
|
|
expect(scheduleButton).toBeChecked();
|
|
const { dateInput, timeInput } = await getScheduleUI();
|
|
await user.type(dateInput, "2099-12-31");
|
|
await user.type(timeInput, "23:59");
|
|
await user.click(runButton);
|
|
expect(runBatchFn.mock.calls.length).toBe(1);
|
|
// @ts-ignore
|
|
const body = await runBatchFn.mock.calls[0][0].request.json();
|
|
expect(body).toEqual({
|
|
script_id: windowsScript.id,
|
|
host_ids: defaultProps.selectedHostIds,
|
|
not_before: "2099-12-31T23:59:00.000Z",
|
|
});
|
|
});
|
|
|
|
it("should call the API with a correct not_before param and filters if provided", async () => {
|
|
const props = {
|
|
...defaultProps,
|
|
runByFilters: true,
|
|
filters: { query: "hi", label_id: 16, status: "" },
|
|
};
|
|
props.selectedHostIds = [];
|
|
const { user } = render(<RunScriptBatchModal {...props} />);
|
|
const { runButton } = await selectScript(user, "windows");
|
|
const { runNowButton, scheduleButton } = await getScheduleSelector();
|
|
expect(runNowButton).toBeChecked();
|
|
await user.click(scheduleButton);
|
|
expect(scheduleButton).toBeChecked();
|
|
const { dateInput, timeInput } = await getScheduleUI();
|
|
await user.type(dateInput, "2099-12-31");
|
|
await user.type(timeInput, "23:59");
|
|
await user.click(runButton);
|
|
expect(runBatchFn.mock.calls.length).toBe(1);
|
|
// @ts-ignore
|
|
const body = await runBatchFn.mock.calls[0][0].request.json();
|
|
expect(body).toEqual({
|
|
script_id: windowsScript.id,
|
|
not_before: "2099-12-31T23:59:00.000Z",
|
|
filters: { query: "hi", label_id: 16, status: "", fleet_id: 1 },
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|