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
4 changes: 2 additions & 2 deletions src/Frontend/src/components/failedmessages/PendingRetries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function retryAllMessages() {
}

function retryAllClicked() {
if (selectedQueue.value === "empty") {
if (selectedQueue.value === null) {
showCantRetryAll.value = true;
} else {
showRetryAllConfirm.value = true;
Expand Down Expand Up @@ -154,7 +154,7 @@ watch(isRefreshing, () => {
<div class="input-group mb-3">
<label class="input-group-text"><FAIcon :icon="faFilter" size="sm" class="icon" /> <span class="hidden-xs">Filter</span></label>
<select class="form-select" id="inputGroupSelect01" onchange="this.dataset.chosen = true" @change="store.refresh()" v-model="selectedQueue">
<option selected disabled hidden class="placeholder" value="empty">Select a queue...</option>
<option selected disabled hidden class="placeholder" :value="null">Select a queue...</option>
<option v-for="(endpoint, index) in endpoints" :key="index" :value="endpoint">
{{ endpoint }}
</option>
Expand Down
25 changes: 19 additions & 6 deletions src/Frontend/src/stores/RecoverabilityStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const useRecoverabilityStore = defineStore("RecoverabilityStore", () => {
const endDate = ref(new Date());
const dateRange = computed(() => `${startDate.value.toISOString()}...${endDate.value.toISOString()}`);
const selectedPeriod = ref<DeletedPeriodOption | RetryPeriodOption>("Deleted in the last 7 days");
const selectedQueue = ref("empty");
const selectedQueue = ref<string | null>(null);
const endpoints = ref<string[]>([]);

const configurationStore = useConfigurationStore();
Expand Down Expand Up @@ -80,7 +80,7 @@ export const useRecoverabilityStore = defineStore("RecoverabilityStore", () => {
case FailedMessageStatus.Archived:
return `&modified=${dateRange.value}`;
case FailedMessageStatus.RetryIssued: {
const searchPhrase = selectedQueue.value === "empty" ? "" : selectedQueue.value;
const searchPhrase = selectedQueue.value ?? "";
return `&queueaddress=${searchPhrase}&modified=${dateRange.value}`;
}
default:
Expand Down Expand Up @@ -240,25 +240,38 @@ export const useRecoverabilityStore = defineStore("RecoverabilityStore", () => {

async function retryAll() {
updateDateRangeForPeriod();
const queue = selectedQueue.value;
let url = "pendingretries/retry";
const data: { from: string; to: string; queueaddress?: string } = {
from: startDate.value.toISOString(),
to: endDate.value.toISOString(),
};
if (selectedQueue.value !== "empty") {
if (queue !== null) {
url = "pendingretries/queues/retry";
data.queueaddress = selectedQueue.value;
data.queueaddress = queue;
}

await serviceControlClient.postToServiceControl(url, data);
}

async function resolveAll() {
await serviceControlClient.patchToServiceControl("pendingretries/resolve", { from: new Date(0).toISOString(), to: new Date().toISOString() });
updateDateRangeForPeriod();
const queue = selectedQueue.value;
let url = "pendingretries/resolve";
const data: { from: string; to: string; queueaddress?: string } = {
from: startDate.value.toISOString(),
to: endDate.value.toISOString(),
};
if (queue !== null) {
url = "pendingretries/queues/resolve";
data.queueaddress = queue;
}

await serviceControlClient.patchToServiceControl(url, data);
}

async function clearSelectedQueue() {
selectedQueue.value = "empty";
selectedQueue.value = null;
await refresh();
}

Expand Down
23 changes: 22 additions & 1 deletion src/Frontend/test/preconditions/pendingRetries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ export interface RetryAllRequest {
queueaddress?: string;
}

export interface ResolveAllRequest {
from: string;
to: string;
queueaddress?: string;
}

export interface PendingRetriesTestBed {
pendingRetryMessages: FailedMessage[];
retryAllRequests: RetryAllRequest[];
resolveAllRequests: ResolveAllRequest[];
}

export const hasPendingRetryMessages =
Expand All @@ -58,6 +65,7 @@ export const hasPendingRetryMessages =
const serviceControlUrl = window.defaultConfig.service_control_url;
const table = [...messages];
const retryAllRequests: RetryAllRequest[] = [];
const resolveAllRequests: ResolveAllRequest[] = [];

const withinModifiedWindow = (url: URL) => {
const range = url.searchParams.get("modified");
Expand Down Expand Up @@ -110,5 +118,18 @@ export const hasPendingRetryMessages =
return { body: {} };
});

return { pendingRetryMessages: table, retryAllRequests };
driver.mockEndpointDynamic(`${serviceControlUrl}pendingretries/queues/resolve`, "patch", async (_url, _params, request) => {
resolveAllRequests.push((await request.json()) as ResolveAllRequest);
return { body: {} };
});

driver.mockEndpointDynamic(`${serviceControlUrl}pendingretries/resolve`, "patch", async (_url, _params, request) => {
const body = (await request.json()) as Record<string, unknown>;
if (!("uniquemessageids" in body)) {
resolveAllRequests.push(body as unknown as ResolveAllRequest);
}
return { body: {} };
});

return { pendingRetryMessages: table, retryAllRequests, resolveAllRequests };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import userEvent from "@testing-library/user-event";
import { screen, within } from "@testing-library/vue";
import { getResolveAllConfirmation } from "../questions/pendingRetriesView";

export async function clickResolveAll(): Promise<void> {
await userEvent.click(screen.getByRole("button", { name: /mark all as resolved/i }));
}

export async function confirmResolveAll(): Promise<void> {
await userEvent.click(within(getResolveAllConfirmation()).getByRole("button", { name: "Yes" }));
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { normalise } from "./domText";

const ROW_SELECTOR = ".row.box.repeat-item.failed-message";
const RETRY_ALL_CONFIRMATION_NAME = /confirm retry of all messages/i;
const RESOLVE_ALL_CONFIRMATION_NAME = /resolve all messages/i;
const SELECT_QUEUE_FIRST_NAME = /select a queue first/i;

export function getPendingRetryRowCount(): number {
Expand All @@ -21,6 +22,14 @@ export function isRetryAllConfirmationVisible(): boolean {
return screen.queryByRole("dialog", { name: RETRY_ALL_CONFIRMATION_NAME }) !== null;
}

export function getResolveAllConfirmation(): HTMLElement {
return screen.getByRole("dialog", { name: RESOLVE_ALL_CONFIRMATION_NAME }) as HTMLElement;
}

export function isResolveAllConfirmationVisible(): boolean {
return screen.queryByRole("dialog", { name: RESOLVE_ALL_CONFIRMATION_NAME }) !== null;
}

export function isSelectQueueFirstNoticeVisible(): boolean {
return screen.queryByRole("dialog", { name: SELECT_QUEUE_FIRST_NAME }) !== null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect } from "vitest";
import { waitFor } from "@testing-library/vue";
import { test, describe } from "../../drivers/vitest/driver";
import * as precondition from "../../preconditions";
import type { Driver } from "../../driver";
import type { PendingRetriesTestBed, ResolveAllRequest } from "../../preconditions/pendingRetries";
import routeLinks from "@/router/routeLinks";
import { getPendingRetryRowCount } from "./questions/pendingRetriesView";
import { selectQueueFilter, selectRetryPeriod } from "./actions/retryAllPendingRetries";
import { clickResolveAll, confirmResolveAll } from "./actions/resolveAllPendingRetries";

const PENDING_RETRIES = routeLinks.failedMessage.pendingRetries.link;
const SALES_QUEUE = "Sales.Service";
const HOUR_IN_MS = 60 * 60 * 1000;

async function givenPendingRetriesAreShown(driver: Driver): Promise<PendingRetriesTestBed> {
await driver.setUp(precondition.serviceControlWithMonitoring);
const bed = await driver.setUp(
precondition.hasPendingRetryMessages({
messages: [precondition.createPendingRetryMessage("msg-1", { endpoint: SALES_QUEUE }), precondition.createPendingRetryMessage("msg-2", { endpoint: SALES_QUEUE })],
})
);
await driver.goTo(PENDING_RETRIES);
await waitFor(() => expect(getPendingRetryRowCount()).toBe(2), { timeout: 5000 });
return bed;
}

function requestedRange(request: ResolveAllRequest) {
return { from: new Date(request.from).getTime(), to: new Date(request.to).getTime() };
}

describe("FEATURE: Resolving all pending retries", () => {
describe("RULE: Resolving all should cover the displayed period", () => {
test("EXAMPLE: With the default period, the whole period up to now is requested", async ({ driver }) => {
const bed = await givenPendingRetriesAreShown(driver);

await clickResolveAll();
await confirmResolveAll();

await waitFor(() => expect(bed.resolveAllRequests).toHaveLength(1));
const request = bed.resolveAllRequests[0];
expect(request.queueaddress).toBeUndefined();

const { from, to } = requestedRange(request);
expect(to).toBeGreaterThan(from);
expect(Math.abs(Date.now() - to)).toBeLessThan(30 * 1000);
expect(Math.abs(to - from - 365 * 24 * HOUR_IN_MS)).toBeLessThan(2 * HOUR_IN_MS);
});

test("EXAMPLE: With 'Retried in the last 2 Hours' selected, only the last two hours are requested", async ({ driver }) => {
const bed = await givenPendingRetriesAreShown(driver);
await selectRetryPeriod("Retried in the last 2 Hours");

await clickResolveAll();
await confirmResolveAll();

await waitFor(() => expect(bed.resolveAllRequests).toHaveLength(1));
const { from, to } = requestedRange(bed.resolveAllRequests[0]);
expect(Math.abs(Date.now() - to)).toBeLessThan(30 * 1000);
expect(Math.abs(to - from - 2 * HOUR_IN_MS)).toBeLessThan(60 * 1000);
});
});

describe("RULE: Resolving all with a queue selected should only resolve that queue", () => {
test("EXAMPLE: With a queue selected, resolving all targets that queue with the displayed period", async ({ driver }) => {
const bed = await givenPendingRetriesAreShown(driver);
await selectQueueFilter(SALES_QUEUE);
await selectRetryPeriod("Retried in the last 1 Day");

await clickResolveAll();
await confirmResolveAll();

await waitFor(() => expect(bed.resolveAllRequests).toHaveLength(1));
const request = bed.resolveAllRequests[0];
expect(request.queueaddress).toBe(SALES_QUEUE);

const { from, to } = requestedRange(request);
expect(Math.abs(Date.now() - to)).toBeLessThan(30 * 1000);
expect(Math.abs(to - from - 24 * HOUR_IN_MS)).toBeLessThan(60 * 1000);
});
});
});
Loading