Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Frontend/test/preconditions/deletableFailedMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { SetupFactoryOptions } from "../driver";
import { FailedMessageStatus, type FailedMessage } from "@/resources/FailedMessage";
import { hasFailedMessage } from "./recoverability";

export interface DeletableFailedMessageTestBed {
failedMessage: FailedMessage;
deletedIds: string[];
}

export const hasDeletableFailedMessage =
(
{
withGroupId,
withMessageId,
withContentType = "application/json",
withBody = { Index: 0, Data: "" },
}: { withGroupId: string; withMessageId: string; withContentType?: string; withBody?: Record<string, string | number | boolean> | string | number | boolean | null | undefined } = { withGroupId: "", withMessageId: "" }
) =>
async ({ driver }: SetupFactoryOptions): Promise<DeletableFailedMessageTestBed> => {
const serviceControlUrl = window.defaultConfig.service_control_url;
const failedMessage = await driver.setUp(hasFailedMessage({ withGroupId, withMessageId, withContentType, withBody }));

let status: FailedMessageStatus = FailedMessageStatus.Unresolved;
const deletedIds: string[] = [];

driver.mockEndpointDynamic(`${serviceControlUrl}errors/last/:groupId`, "get", (_url, params) =>
Promise.resolve({
body: { ...failedMessage, id: String(params.groupId), status },
})
);

driver.mockEndpointDynamic(`${serviceControlUrl}errors/archive`, "patch", async (_url, _params, request) => {
const ids = ((await request.json()) as string[]) ?? [];
deletedIds.push(...ids);
status = FailedMessageStatus.Archived;
return { body: {} };
});

return { failedMessage, deletedIds };
};
63 changes: 63 additions & 0 deletions src/Frontend/test/preconditions/deletedMessageGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import dayjs from "@/utils/dayjs";
import type FailureGroupView from "@/resources/FailureGroupView";
import type { SetupFactoryOptions } from "../driver";

export type DeletedMessageGroupRow = FailureGroupView & { last_operation_completion_time?: string };

export interface DeletedMessageGroupOptions {
title?: string;
type?: string;
count?: number;
comment?: string;
firstFailedDaysAgo?: number;
lastFailedDaysAgo?: number;
lastRetriedDaysAgo?: number;
}

export const createDeletedMessageGroup = (
id: string,
{ title = `Deleted group ${id}`, type = "Endpoint Name", count = 1, comment = "", firstFailedDaysAgo = 5, lastFailedDaysAgo = 1, lastRetriedDaysAgo }: DeletedMessageGroupOptions = {}
): DeletedMessageGroupRow => {
const group: DeletedMessageGroupRow = {
id,
title,
type,
count,
comment,
first: dayjs().subtract(firstFailedDaysAgo, "day").toISOString(),
last: dayjs().subtract(lastFailedDaysAgo, "day").toISOString(),
};

if (lastRetriedDaysAgo !== undefined) {
group.last_operation_completion_time = dayjs().subtract(lastRetriedDaysAgo, "day").toISOString();
}

return group;
};

export interface DeletedMessageGroupsTestBed {
groups: DeletedMessageGroupRow[];
restoredGroupIds: string[];
}

export const hasDeletedMessageGroups =
({ groups = [] }: { groups?: DeletedMessageGroupRow[] } = {}) =>
({ driver }: SetupFactoryOptions): DeletedMessageGroupsTestBed => {
const serviceControlUrl = window.defaultConfig.service_control_url;
const table = [...groups];
const restoredGroupIds: string[] = [];

driver.mockEndpointDynamic(`${serviceControlUrl}errors/groups{/:classifier}`, "get", () => Promise.resolve({ body: [...table] }));

driver.mockEndpointDynamic(`${serviceControlUrl}recoverability/groups/:groupId/errors/unarchive`, "post", (_url, params) => {
const groupId = String(params.groupId);
const index = table.findIndex((group) => group.id === groupId);
if (index >= 0) {
table.splice(index, 1);
}
restoredGroupIds.push(groupId);
return Promise.resolve({ body: {} });
});

return { groups: table, restoredGroupIds };
};
2 changes: 2 additions & 0 deletions src/Frontend/test/preconditions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export * from "../preconditions/hasHeartbeatEndpoints";
export { serviceControlWithMonitoring } from "./serviceControlWithMonitoring";
export * from "./recoverability";
export * from "./deletedMessages";
export * from "./deletedMessageGroups";
export * from "./deletableFailedMessage";
export * from "./licensing";
export { hasLicensingReportAvailable } from "../preconditions/hasLicensingReportAvailable";
export { hasLicensingSettingTest } from "../preconditions/hasLicensingSettingTest";
Expand Down
22 changes: 22 additions & 0 deletions src/Frontend/test/preconditions/recoverability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ export const recoverabilityEditConfigDefaultHandler = ({ driver }: SetupFactoryO
});
};

export interface EditAndRetryConfigTestBed {
wasServed: () => boolean;
}

export const hasEditAndRetryConfig =
(enabled: boolean) =>
({ driver }: SetupFactoryOptions): EditAndRetryConfigTestBed => {
const serviceControlUrl = window.defaultConfig.service_control_url;
const config = structuredClone(editConfig);
config.enabled = enabled;
let served = false;

driver.mockEndpointDynamic(`${serviceControlUrl}edit/config`, "get", () => {
served = true;
return Promise.resolve({ body: config });
});

return { wasServed: () => served };
};

export const enableEditAndRetry = ({ driver }: SetupFactoryOptions) => {
const serviceControlUrl = window.defaultConfig.service_control_url;
const config = structuredClone(editConfig);
Expand Down Expand Up @@ -280,4 +300,6 @@ export const hasFailedMessage =
driver.mockEndpoint(`${serviceControlUrl}recoverability/groups/id/${withGroupId}`, {
body: { id: withGroupId, title: "Endpoint1", type: "Endpoint Name", count: 1, first: "2024-06-27T06:14:48.912923Z", last: "2024-06-27T06:14:48.912923Z" },
});

return failedMessage;
};
20 changes: 20 additions & 0 deletions src/Frontend/test/specs/failedmessages/actions/deleteMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import userEvent from "@testing-library/user-event";
import { screen, within } from "@testing-library/vue";
import { getDeleteConfirmation } from "../questions/messageDetails";

export async function clickDeleteMessage(): Promise<void> {
await userEvent.click(screen.getByRole("button", { name: /^delete message$/i }));
}

export async function confirmDeleteMessage(): Promise<void> {
await userEvent.click(within(getDeleteConfirmation()).getByRole("button", { name: "Yes" }));
}

export async function declineDeleteMessage(): Promise<void> {
await userEvent.click(within(getDeleteConfirmation()).getByRole("button", { name: "No" }));
}

export async function deleteMessage(): Promise<void> {
await clickDeleteMessage();
await confirmDeleteMessage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import userEvent from "@testing-library/user-event";
import { within } from "@testing-library/vue";
import { getRestoreGroupConfirmation } from "../questions/deletedMessageGroupsView";
import { getDeletedMessageGroupRow } from "../questions/deletedMessageGroupRows";

export async function clickRestoreGroup(groupTitle: string): Promise<void> {
const row = getDeletedMessageGroupRow(groupTitle);
if (!row) {
throw new Error(`Cannot restore: no deleted message group row titled "${groupTitle}" is listed`);
}
await userEvent.click(within(row.element).getByRole("button", { name: /restore group/i }));
}

export async function confirmRestoreGroup(): Promise<void> {
await userEvent.click(within(getRestoreGroupConfirmation()).getByRole("button", { name: "Yes" }));
}

export async function declineRestoreGroup(): Promise<void> {
await userEvent.click(within(getRestoreGroupConfirmation()).getByRole("button", { name: "No" }));
}

export async function restoreGroup(groupTitle: string): Promise<void> {
await clickRestoreGroup(groupTitle);
await confirmRestoreGroup();
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,109 @@
import { expect } from "vitest";
import { waitFor } from "@testing-library/vue";
import { test, describe } from "../../drivers/vitest/driver";
import type { Driver } from "../../driver";
import * as precondition from "../../preconditions";
import type { DeletedMessageGroupsTestBed } from "../../preconditions/deletedMessageGroups";
import routeLinks from "@/router/routeLinks";
import { getDeletedMessageGroupRowCount, getDeletedMessageGroupRow } from "./questions/deletedMessageGroupRows";
import { isEmptyGroupsMessageVisible, isDismissButtonVisible, isRestoreCompletedVisible, isRestoreGroupConfirmationVisible } from "./questions/deletedMessageGroupsView";
import { clickRestoreGroup, confirmRestoreGroup } from "./actions/restoreDeletedMessageGroups";

const DELETED_MESSAGE_GROUPS = routeLinks.failedMessage.deletedMessagesGroup.link;

async function givenDeletedMessageGroupsAreShown(driver: Driver, titles: string[]): Promise<DeletedMessageGroupsTestBed> {
await driver.setUp(precondition.serviceControlWithMonitoring);
const groups = titles.map((title, index) => precondition.createDeletedMessageGroup(`group-${index + 1}`, { title, count: 3, firstFailedDaysAgo: 5, lastFailedDaysAgo: 2, lastRetriedDaysAgo: 3 }));
const bed = await driver.setUp(precondition.hasDeletedMessageGroups({ groups }));

await driver.goTo(DELETED_MESSAGE_GROUPS);
await waitFor(() => expect(getDeletedMessageGroupRowCount()).toBe(groups.length), { timeout: 5000 });

return bed;
}

describe("FEATURE: Deleted Message Groups", () => {
describe("RULE: Deleted Message Groups view should shows all current deleted messages, grouped by the selected grouping", () => {
test.todo("EXAMPLE: A message should be show when there are no deleted messages");

/* SCENARIO
Given there are no Deleted Messages
Then the "Deleted Message Groups" tab will display a message indicating the fact
*/

test.todo("EXAMPLE: The number of deleted messages in a group should be shown");
test.todo("EXAMPLE: The time period from the first failed message should be shown");
test.todo("EXAMPLE: The time period of the last failed message should be shown");
test.todo("EXAMPLE: The time period of when the group was last retried should be shown");
test.todo("EXAMPLE: A deleted message group that has not been retried should show N/A for the last retry time");

/* SCENARIO
Given there are 1 or more groups shown on the "Deleted Message Groups" tab
Then the group row will display the current grouping name in bold
and the group will display the number of messages in the group
and the group will display a time period indicating how long ago the first failure happened
and the group will display a time period indicating how long ago the last failure happened
and the group will display a time period indicating how long ago the group was last retried, or N/A if never retried
*/
test("EXAMPLE: A message should be shown when there are no deleted messages", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasDeletedMessageGroups({ groups: [] }));
await driver.goTo(DELETED_MESSAGE_GROUPS);

await waitFor(() => expect(isEmptyGroupsMessageVisible()).toBe(true));
expect(getDeletedMessageGroupRowCount()).toBe(0);
});

test("EXAMPLE: The name of a deleted message group should be shown in bold", async ({ driver }) => {
await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

const row = getDeletedMessageGroupRow("Payments failures");
expect(row?.title).toBe("Payments failures");
expect(row?.titleIsBold).toBe(true);
});

test("EXAMPLE: The number of deleted messages in a group should be shown", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasDeletedMessageGroups({ groups: [precondition.createDeletedMessageGroup("group-1", { title: "Payments failures", count: 7 })] }));
await driver.goTo(DELETED_MESSAGE_GROUPS);
await waitFor(() => expect(getDeletedMessageGroupRowCount()).toBe(1));

expect(getDeletedMessageGroupRow("Payments failures")?.messageCount).toBe("7 messages");
});

test("EXAMPLE: The time period from the first failed message should be shown", async ({ driver }) => {
await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

expect(getDeletedMessageGroupRow("Payments failures")?.firstFailed).toBe("5 days ago");
});

test("EXAMPLE: The time period of the last failed message should be shown", async ({ driver }) => {
await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

expect(getDeletedMessageGroupRow("Payments failures")?.lastFailed).toBe("2 days ago");
});

test("EXAMPLE: The time period of when the group was last retried should be shown", async ({ driver }) => {
await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

expect(getDeletedMessageGroupRow("Payments failures")?.lastRetried).toBe("3 days ago");
});

test("EXAMPLE: A deleted message group that has not been retried should show N/A for the last retry time", async ({ driver }) => {
await driver.setUp(precondition.serviceControlWithMonitoring);
await driver.setUp(precondition.hasDeletedMessageGroups({ groups: [precondition.createDeletedMessageGroup("group-1", { title: "Payments failures" })] }));
await driver.goTo(DELETED_MESSAGE_GROUPS);
await waitFor(() => expect(getDeletedMessageGroupRowCount()).toBe(1));

expect(getDeletedMessageGroupRow("Payments failures")?.lastRetried).toMatch(/n\/a/i);
});
});
describe("RULE: All messages in a Deleted Message Group should be able to be restored in a single action", () => {
test.todo("EXAMPLE: A restore button should be shown when there are deleted messages in a group");
/* SCENARIO
Given there are 1 or more groups shown on the "Deleted Message Groups" tab
Then "Restore group" is shown as an available action on the group
*/

/* SCENARIO
Given there are 1 or more groups shown on the "Deleted Message Groups" tab
When the user clicks the "Restore group" action
and clicks "Yes" on the action confirmation modal
Then all the messages in the selected group are returned to the Failed Messages list
and the list refreshes with progress/confirmation of the successful restoration
*/
});
describe("RULE: Ability to select a given group should be hinted ", () => {
test.todo("EXAMPLE: A group should indicate that it is active and selectable when the cursor is hovered over");
test("EXAMPLE: A restore button should be shown when there are deleted messages in a group", async ({ driver }) => {
await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

const row = getDeletedMessageGroupRow("Payments failures");
expect(row?.restoreGroupText).toBe("Restore group");
expect(row?.restoreGroupIsDisabled).toBe(false);
});

test("EXAMPLE: Restoring a group restores all its messages and the list refreshes with confirmation", async ({ driver }) => {
const bed = await givenDeletedMessageGroupsAreShown(driver, ["Payments failures"]);

await clickRestoreGroup("Payments failures");
expect(isRestoreGroupConfirmationVisible()).toBe(true);
await confirmRestoreGroup();

await waitFor(() => expect(bed.restoredGroupIds).toEqual(["group-1"]));

/* SCENARIO
Mouse hovering a group
await waitFor(
() => {
expect(isRestoreCompletedVisible()).toBe(true);
expect(isDismissButtonVisible()).toBe(true);
},
{ timeout: 5000 }
);

Given there are 1 or more groups show in the "Deleted Message Groups" tab
and the user hovers over a Deleted Message Group row
Then the row indicates that it is active (hover state)
and that it is selectable (cursor/underlining)
*/
expect(getDeletedMessageGroupRow("Payments failures")?.restoreGroupText).toBeNull();
});
});
});
Loading
Loading