- Add missing permissions tracking with configurable limits. - Introduce `clearMissingPermissions` and `pushMissingPermissions` functions. - Update `RequestQueueProgress` layout to display missing permissions. - Improve error handling with permission extraction from response payloads. - Update tests for new functionality and expanded edge cases.
282 lines
9.3 KiB
JavaScript
282 lines
9.3 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { mount } from "@vue/test-utils";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import RequestQueueProgress from "@/components/global/RequestQueueProgress.vue";
|
|
import {
|
|
__configureRequestQueueForTests,
|
|
__resetRequestQueueForTests,
|
|
enqueueRequest,
|
|
requestQueueState
|
|
} from "@/services/requestQueue.js";
|
|
|
|
const flushMicrotasks = async () => {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
};
|
|
|
|
const flushManyMicrotasks = async (rounds = 10) => {
|
|
for (let index = 0; index < rounds; index += 1) {
|
|
await Promise.resolve();
|
|
}
|
|
};
|
|
|
|
const createDeferred = () => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
|
|
return { promise, resolve, reject };
|
|
};
|
|
|
|
describe("RequestQueueProgress", () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
__resetRequestQueueForTests();
|
|
__configureRequestQueueForTests({ maxConcurrentGet: 1, maxConcurrentOther: 1, spacingMs: 0 });
|
|
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, status: 200 })));
|
|
});
|
|
|
|
afterEach(() => {
|
|
__resetRequestQueueForTests();
|
|
vi.unstubAllGlobals();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("shows request queue progress and hides shortly after completion", async () => {
|
|
const wrapper = mount(RequestQueueProgress);
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(false);
|
|
|
|
const first = createDeferred();
|
|
const second = createDeferred();
|
|
|
|
const requestOne = enqueueRequest(() => first.promise);
|
|
const requestTwo = enqueueRequest(() => second.promise);
|
|
|
|
await flushMicrotasks();
|
|
|
|
expect(wrapper.get("[data-testid='request-queue-progress']").text()).toContain("1 active, 1 queued");
|
|
|
|
first.resolve({ status: 200 });
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.get("[data-testid='request-queue-progress']").text()).toContain("Request queue:");
|
|
|
|
second.resolve({ status: 200 });
|
|
await Promise.all([requestOne, requestTwo]);
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.get("[data-testid='request-queue-progress']").text()).toContain("Request queue complete: 2/2");
|
|
|
|
vi.advanceTimersByTime(2600);
|
|
await flushMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(false);
|
|
});
|
|
|
|
it("does not show progress below configured minimum batch size", async () => {
|
|
const wrapper = mount(RequestQueueProgress);
|
|
const onlyRequest = createDeferred();
|
|
|
|
const request = enqueueRequest(() => onlyRequest.promise);
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(false);
|
|
|
|
onlyRequest.resolve({ status: 200 });
|
|
await request;
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(false);
|
|
});
|
|
|
|
it("expands upward with active endpoint details, method icon and timer", async () => {
|
|
const wrapper = mount(RequestQueueProgress);
|
|
const first = createDeferred();
|
|
const second = createDeferred();
|
|
|
|
const requestOne = enqueueRequest(() => first.promise, { method: "POST", url: "/orders" });
|
|
const requestTwo = enqueueRequest(() => second.promise, { method: "GET", url: "/order/items" });
|
|
|
|
await flushManyMicrotasks();
|
|
|
|
await wrapper.get("[data-testid='request-queue-progress-toggle']").trigger("click");
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-errors-box']").exists()).toBe(true);
|
|
expect(wrapper.find("[data-testid='request-queue-missing-permissions-box']").exists()).toBe(true);
|
|
|
|
const details = wrapper.get("[data-testid='request-queue-progress-details']");
|
|
expect(details.text()).toContain("Active requests");
|
|
expect(details.text()).toContain("POST");
|
|
expect(details.text()).toContain("/orders");
|
|
expect(details.text()).toMatch(/ms|s/);
|
|
expect(details.find(".fa-plus-circle").exists()).toBe(true);
|
|
|
|
first.resolve({ status: 200 });
|
|
second.resolve({ status: 200 });
|
|
await Promise.all([requestOne, requestTwo]);
|
|
await flushManyMicrotasks();
|
|
});
|
|
|
|
it("stores errors with request and response payloads and respects configured error limit", async () => {
|
|
__configureRequestQueueForTests({
|
|
errorHistoryLimit: 1,
|
|
missingPermissionsLimit: 1,
|
|
retryByStatusCode: {},
|
|
payloadMaxChars: 8000
|
|
});
|
|
const wrapper = mount(RequestQueueProgress);
|
|
|
|
const requestOne = enqueueRequest(
|
|
async () => {
|
|
throw {
|
|
message: "First failure",
|
|
response: { status: 500, data: { reason: "first" } }
|
|
};
|
|
},
|
|
{
|
|
method: "POST",
|
|
url: "/first-error",
|
|
requestData: { data: { requestBody: "first" } }
|
|
}
|
|
).catch((error) => error);
|
|
|
|
const requestTwo = enqueueRequest(
|
|
async () => {
|
|
throw {
|
|
message: "Second failure",
|
|
response: {
|
|
status: 403,
|
|
data: {
|
|
success: false,
|
|
data: {
|
|
message: "Missing permission(s)",
|
|
permissions: ["department_notification_sms_get"]
|
|
},
|
|
meta: [],
|
|
includes: []
|
|
}
|
|
}
|
|
};
|
|
},
|
|
{
|
|
method: "PATCH",
|
|
url: "/second-error",
|
|
requestData: { data: { requestBody: "second" } }
|
|
}
|
|
).catch((error) => error);
|
|
|
|
await Promise.all([requestOne, requestTwo]);
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.errorRequests.length).toBe(1);
|
|
expect(requestQueueState.errorRequests[0].url).toBe("/second-error");
|
|
expect(requestQueueState.errorRequests[0].requestText).toContain("requestBody");
|
|
expect(requestQueueState.errorRequests[0].responseText).toContain("Missing permission(s)");
|
|
expect(requestQueueState.missingPermissions.length).toBe(1);
|
|
expect(requestQueueState.missingPermissions[0].permission).toBe("department_notification_sms_get");
|
|
|
|
await wrapper.get("[data-testid='request-queue-progress-toggle']").trigger("click");
|
|
await flushManyMicrotasks();
|
|
|
|
const errorsBox = wrapper.get("[data-testid='request-queue-errors-box']");
|
|
expect(errorsBox.text()).toContain("Errors (1)");
|
|
expect(errorsBox.text()).toContain("/second-error");
|
|
expect(errorsBox.text()).toContain("Request and response data");
|
|
expect(errorsBox.text()).toContain("Request");
|
|
expect(errorsBox.text()).toContain("Response");
|
|
|
|
const missingPermissionsBox = wrapper.get("[data-testid='request-queue-missing-permissions-box']");
|
|
expect(missingPermissionsBox.text()).toContain("department_notification_sms_get");
|
|
|
|
await wrapper.get("[data-testid='request-queue-clear-errors']").trigger("click");
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.errorRequests.length).toBe(0);
|
|
expect(wrapper.get("[data-testid='request-queue-errors-box']").text()).toContain("No stored errors");
|
|
expect(requestQueueState.missingPermissions.length).toBe(1);
|
|
|
|
await wrapper.get("[data-testid='request-queue-clear-permissions']").trigger("click");
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.missingPermissions.length).toBe(0);
|
|
expect(wrapper.get("[data-testid='request-queue-missing-permissions-box']").text()).toContain("No missing permissions detected");
|
|
});
|
|
|
|
it("stays visible after errors occur", async () => {
|
|
__configureRequestQueueForTests({ errorHistoryLimit: 2 });
|
|
const wrapper = mount(RequestQueueProgress);
|
|
|
|
const failed = enqueueRequest(
|
|
async () => {
|
|
throw {
|
|
message: "Failure should keep panel visible",
|
|
response: { status: 500, data: { detail: "boom" } }
|
|
};
|
|
},
|
|
{
|
|
method: "DELETE",
|
|
url: "/error-visible",
|
|
requestData: { data: { id: 123 } }
|
|
}
|
|
).catch((error) => error);
|
|
|
|
await failed;
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(true);
|
|
|
|
vi.advanceTimersByTime(30_000);
|
|
await flushManyMicrotasks();
|
|
|
|
expect(wrapper.find("[data-testid='request-queue-progress']").exists()).toBe(true);
|
|
await wrapper.get("[data-testid='request-queue-progress-toggle']").trigger("click");
|
|
await flushManyMicrotasks();
|
|
|
|
const errorsBox = wrapper.get("[data-testid='request-queue-errors-box']");
|
|
expect(errorsBox.text()).toContain("Errors");
|
|
expect(errorsBox.text()).toContain("/error-visible");
|
|
});
|
|
|
|
it("stores missing permissions when response payload is stringified JSON", async () => {
|
|
__configureRequestQueueForTests({
|
|
missingPermissionsLimit: 5,
|
|
retryByStatusCode: {},
|
|
});
|
|
|
|
const request = enqueueRequest(
|
|
async () => {
|
|
throw {
|
|
message: "Request failed with status code 403",
|
|
response: {
|
|
status: 403,
|
|
data: JSON.stringify({
|
|
success: false,
|
|
data: {
|
|
message: "Missing permission(s)",
|
|
permissions: ["department_notification_sms_get"],
|
|
},
|
|
meta: [],
|
|
includes: [],
|
|
}),
|
|
},
|
|
};
|
|
},
|
|
{
|
|
method: "GET",
|
|
url: "/notification/sms",
|
|
}
|
|
).catch((error) => error);
|
|
|
|
await request;
|
|
await flushManyMicrotasks();
|
|
|
|
expect(requestQueueState.missingPermissions.length).toBe(1);
|
|
expect(requestQueueState.missingPermissions[0].permission).toBe("department_notification_sms_get");
|
|
});
|
|
});
|