Enhance request tracking and error management in RequestQueue:
- 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.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { API_URL, REQUEST_QUEUE_CONFIG } from "@/config.js";
|
||||
import { requestQueueState } from "@/services/requestQueue.js";
|
||||
import {
|
||||
clearErrorRequests,
|
||||
clearMissingPermissions,
|
||||
requestQueueState,
|
||||
} from "@/services/requestQueue.js";
|
||||
|
||||
const METHOD_ICON_CLASS = Object.freeze({
|
||||
GET: "fa-download",
|
||||
@@ -24,9 +28,13 @@ let pingTimer = null;
|
||||
|
||||
const hasOutstandingRequests = computed(() => requestQueueState.pending + requestQueueState.active > 0);
|
||||
const processedRequests = computed(() => requestQueueState.batchCompleted + requestQueueState.batchFailed);
|
||||
const missingPermissions = computed(() => requestQueueState.missingPermissions || []);
|
||||
const hasStoredErrors = computed(() => (requestQueueState.errorRequests || []).length > 0);
|
||||
const hasStoredPermissions = computed(() => missingPermissions.value.length > 0);
|
||||
const hasStoredInspectorItems = computed(() => hasStoredErrors.value || hasStoredPermissions.value);
|
||||
const shouldRenderForBatch = computed(() =>
|
||||
hasStoredErrors.value || requestQueueState.batchTotal >= REQUEST_QUEUE_CONFIG.progress.minBatchSizeToShow
|
||||
hasStoredInspectorItems.value
|
||||
|| requestQueueState.batchTotal >= REQUEST_QUEUE_CONFIG.progress.minBatchSizeToShow
|
||||
);
|
||||
const activeRequests = computed(() => requestQueueState.activeRequests || []);
|
||||
const recentRequests = computed(() => requestQueueState.recentRequests || []);
|
||||
@@ -131,14 +139,22 @@ const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
};
|
||||
|
||||
watch([hasOutstandingRequests, shouldRenderForBatch, hasStoredErrors], ([hasOutstanding, shouldRender, hasErrors]) => {
|
||||
const handleClearErrors = () => {
|
||||
clearErrorRequests();
|
||||
};
|
||||
|
||||
const handleClearMissingPermissions = () => {
|
||||
clearMissingPermissions();
|
||||
};
|
||||
|
||||
watch([hasOutstandingRequests, shouldRenderForBatch, hasStoredInspectorItems], ([hasOutstanding, shouldRender, hasInspectorItems]) => {
|
||||
if (!shouldRender) {
|
||||
clearHideTimer();
|
||||
isVisible.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasOutstanding || hasErrors) {
|
||||
if (hasOutstanding || hasInspectorItems) {
|
||||
clearHideTimer();
|
||||
isVisible.value = true;
|
||||
return;
|
||||
@@ -187,7 +203,18 @@ onBeforeUnmount(() => {
|
||||
<div v-if="isVisible" class="request-queue-progress-shell" data-testid="request-queue-progress">
|
||||
<div v-if="isExpanded" class="request-queue-progress__expanded-layout">
|
||||
<aside class="request-queue-progress__side" data-testid="request-queue-errors-box">
|
||||
<div class="request-queue-progress__section-title">Errors ({{ errorRequests.length }})</div>
|
||||
<div class="request-queue-progress__side-header">
|
||||
<div class="request-queue-progress__section-title">Errors ({{ errorRequests.length }})</div>
|
||||
<button
|
||||
class="request-queue-progress__clear-button"
|
||||
data-testid="request-queue-clear-errors"
|
||||
type="button"
|
||||
:disabled="errorRequests.length === 0"
|
||||
@click="handleClearErrors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<ul v-if="errorRequests.length > 0" class="request-queue-progress__list">
|
||||
<li v-for="request in errorRequests" :key="request.id" class="request-queue-progress__error-item">
|
||||
<div class="request-queue-progress__error-summary">
|
||||
@@ -197,7 +224,7 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
<span class="request-queue-progress__endpoint" :title="request.url">{{ request.url }}</span>
|
||||
<span class="request-queue-progress__error-meta">
|
||||
{{ request.statusCode || "n/a" }} • {{ formatDuration(request.requestDurationMs) }}
|
||||
{{ request.statusCode || "n/a" }} - {{ formatDuration(request.requestDurationMs) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -227,39 +254,73 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div class="request-queue-progress__section">
|
||||
<div class="request-queue-progress__section-title">Active requests ({{ activeRequests.length }})</div>
|
||||
<ul v-if="activeRequests.length > 0" class="request-queue-progress__list">
|
||||
<li v-for="request in activeRequests" :key="request.id" class="request-queue-progress__list-item">
|
||||
<span class="request-queue-progress__method">
|
||||
<i class="fas" :class="getMethodIconClass(request.method)" aria-hidden="true"></i>
|
||||
{{ request.method }}
|
||||
</span>
|
||||
<span class="request-queue-progress__endpoint" :title="request.url">{{ request.url }}</span>
|
||||
<span class="request-queue-progress__time">{{ formatDuration(getActiveRequestElapsedMs(request)) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="request-queue-progress__empty">No active requests</div>
|
||||
<div class="request-queue-progress__section-content request-queue-progress__section-content--active">
|
||||
<ul v-if="activeRequests.length > 0" class="request-queue-progress__list">
|
||||
<li v-for="request in activeRequests" :key="request.id" class="request-queue-progress__list-item">
|
||||
<span class="request-queue-progress__method">
|
||||
<i class="fas" :class="getMethodIconClass(request.method)" aria-hidden="true"></i>
|
||||
{{ request.method }}
|
||||
</span>
|
||||
<span class="request-queue-progress__endpoint" :title="request.url">{{ request.url }}</span>
|
||||
<span class="request-queue-progress__time">{{ formatDuration(getActiveRequestElapsedMs(request)) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="request-queue-progress__empty">No active requests</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request-queue-progress__section">
|
||||
<div class="request-queue-progress__section-title">Recent requests</div>
|
||||
<ul v-if="recentRequests.length > 0" class="request-queue-progress__list">
|
||||
<li v-for="request in recentRequests" :key="request.id" class="request-queue-progress__list-item">
|
||||
<span class="request-queue-progress__method">
|
||||
<i class="fas" :class="getMethodIconClass(request.method)" aria-hidden="true"></i>
|
||||
{{ request.method }}
|
||||
</span>
|
||||
<span class="request-queue-progress__endpoint" :title="request.url">{{ request.url }}</span>
|
||||
<span class="request-queue-progress__time">
|
||||
{{ formatDuration(request.requestDurationMs) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="request-queue-progress__empty">No recent requests</div>
|
||||
<div class="request-queue-progress__section-content request-queue-progress__section-content--recent">
|
||||
<ul v-if="recentRequests.length > 0" class="request-queue-progress__list">
|
||||
<li v-for="request in recentRequests" :key="request.id" class="request-queue-progress__list-item">
|
||||
<span class="request-queue-progress__method">
|
||||
<i class="fas" :class="getMethodIconClass(request.method)" aria-hidden="true"></i>
|
||||
{{ request.method }}
|
||||
</span>
|
||||
<span class="request-queue-progress__endpoint" :title="request.url">{{ request.url }}</span>
|
||||
<span class="request-queue-progress__time">
|
||||
{{ formatDuration(request.requestDurationMs) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="request-queue-progress__empty">No recent requests</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="request-queue-progress__side" data-testid="request-queue-missing-permissions-box">
|
||||
<div class="request-queue-progress__section-title">Missing permissions</div>
|
||||
<div class="request-queue-progress__side-header">
|
||||
<div class="request-queue-progress__section-title">Missing permissions</div>
|
||||
<button
|
||||
class="request-queue-progress__clear-button"
|
||||
data-testid="request-queue-clear-permissions"
|
||||
type="button"
|
||||
:disabled="missingPermissions.length === 0"
|
||||
@click="handleClearMissingPermissions"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="request-queue-progress__section-content request-queue-progress__section-content--permissions">
|
||||
<ul v-if="missingPermissions.length > 0" class="request-queue-progress__list">
|
||||
<li
|
||||
v-for="item in missingPermissions"
|
||||
:key="`${item.permission}:${item.lastSeenAt}`"
|
||||
class="request-queue-progress__list-item request-queue-progress__list-item--permission"
|
||||
>
|
||||
<span class="request-queue-progress__permission">{{ item.permission }}</span>
|
||||
<span class="request-queue-progress__permission-meta">
|
||||
<i class="fas" :class="getMethodIconClass(item.method)" aria-hidden="true"></i>
|
||||
{{ item.method }}
|
||||
</span>
|
||||
<span class="request-queue-progress__permission-meta">
|
||||
{{ item.statusCode || "n/a" }} x{{ item.count }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="request-queue-progress__empty">No missing permissions detected</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -347,6 +408,7 @@ onBeforeUnmount(() => {
|
||||
grid-template-columns: minmax(250px, 1fr) minmax(520px, 720px) minmax(250px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.request-queue-progress__details {
|
||||
@@ -355,6 +417,8 @@ onBeforeUnmount(() => {
|
||||
color: #ffffff;
|
||||
padding: 10px 12px;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.28);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.request-queue-progress__side {
|
||||
@@ -365,6 +429,18 @@ onBeforeUnmount(() => {
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.28);
|
||||
}
|
||||
|
||||
.request-queue-progress__side-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.request-queue-progress__side-header .request-queue-progress__section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.request-queue-progress__details-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -381,12 +457,51 @@ onBeforeUnmount(() => {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.request-queue-progress__section-content {
|
||||
min-height: 48px;
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.request-queue-progress__section-content--active {
|
||||
min-height: 54px;
|
||||
max-height: 112px;
|
||||
}
|
||||
|
||||
.request-queue-progress__section-content--recent {
|
||||
min-height: 70px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.request-queue-progress__section-content--permissions {
|
||||
min-height: 54px;
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
.request-queue-progress__section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.request-queue-progress__clear-button {
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
border-radius: 999px;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.request-queue-progress__clear-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.request-queue-progress__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
@@ -480,6 +595,9 @@ onBeforeUnmount(() => {
|
||||
.request-queue-progress__time {
|
||||
white-space: nowrap;
|
||||
opacity: 0.88;
|
||||
min-width: 72px;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.request-queue-progress__empty {
|
||||
@@ -487,6 +605,25 @@ onBeforeUnmount(() => {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.request-queue-progress__list-item--permission {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.request-queue-progress__permission {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.request-queue-progress__permission-meta {
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
opacity: 0.82;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.queue-progress-fade-enter-active,
|
||||
.queue-progress-fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
@@ -497,3 +634,4 @@ onBeforeUnmount(() => {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export const REQUEST_QUEUE_CONFIG = Object.freeze({
|
||||
timerRefreshMs: 250,
|
||||
recentRequestsLimit: 8,
|
||||
errorHistoryLimit: 10,
|
||||
missingPermissionsLimit: 20,
|
||||
payloadMaxChars: 4000,
|
||||
}),
|
||||
// Queue progress widget behavior
|
||||
|
||||
@@ -12,6 +12,7 @@ const queueConfig = {
|
||||
retryDelayJitterMs: REQUEST_QUEUE_CONFIG.retryDelay.jitterMs,
|
||||
recentRequestsLimit: REQUEST_QUEUE_CONFIG.inspector.recentRequestsLimit,
|
||||
errorHistoryLimit: REQUEST_QUEUE_CONFIG.inspector.errorHistoryLimit,
|
||||
missingPermissionsLimit: REQUEST_QUEUE_CONFIG.inspector.missingPermissionsLimit,
|
||||
payloadMaxChars: REQUEST_QUEUE_CONFIG.inspector.payloadMaxChars,
|
||||
};
|
||||
|
||||
@@ -25,6 +26,7 @@ const requestQueueStateMutable = reactive({
|
||||
activeRequests: [],
|
||||
recentRequests: [],
|
||||
errorRequests: [],
|
||||
missingPermissions: [],
|
||||
});
|
||||
|
||||
const requestQueue = [];
|
||||
@@ -107,6 +109,65 @@ const toSafeText = (value) => {
|
||||
return `${text.slice(0, maxChars)}\n... [truncated]`;
|
||||
};
|
||||
|
||||
const parseJsonIfString = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
if (!trimmedValue.startsWith("{") && !trimmedValue.startsWith("[")) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(trimmedValue);
|
||||
} catch (error) {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeResponseData = (value) => {
|
||||
const parsed = parseJsonIfString(value);
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const parsedData = parseJsonIfString(parsed.data);
|
||||
if (parsedData === parsed.data) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return {
|
||||
...parsed,
|
||||
data: parsedData,
|
||||
};
|
||||
};
|
||||
|
||||
const extractMissingPermissions = (error) => {
|
||||
const responseData = normalizeResponseData(error?.response?.data);
|
||||
const messageCandidates = [
|
||||
responseData?.data?.message,
|
||||
responseData?.message,
|
||||
error?.message,
|
||||
];
|
||||
const hasMissingPermissionMessage = messageCandidates.some((message) =>
|
||||
typeof message === "string" && message.toLowerCase().includes("missing permission")
|
||||
);
|
||||
|
||||
if (!hasMissingPermissionMessage) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const permissions = responseData?.data?.permissions ?? responseData?.permissions ?? [];
|
||||
if (!Array.isArray(permissions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return permissions
|
||||
.map((permission) => String(permission || "").trim())
|
||||
.filter((permission) => permission.length > 0);
|
||||
};
|
||||
|
||||
const getMethodConcurrencyLimit = (method) => {
|
||||
const normalizedMethod = normalizeMethod(method);
|
||||
const configuredLimit = queueConfig.concurrencyByMethod[normalizedMethod]
|
||||
@@ -252,6 +313,42 @@ const pushErrorRequest = (entry) => {
|
||||
requestQueueStateMutable.errorRequests = next.slice(0, limit);
|
||||
};
|
||||
|
||||
const pushMissingPermissions = (permissionEntries) => {
|
||||
if (!Array.isArray(permissionEntries) || permissionEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const existing = [...requestQueueStateMutable.missingPermissions];
|
||||
permissionEntries.forEach(({ permission, method, url, statusCode }) => {
|
||||
const index = existing.findIndex((item) => item.permission === permission);
|
||||
if (index >= 0) {
|
||||
existing[index] = {
|
||||
...existing[index],
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
count: Number(existing[index].count || 0) + 1,
|
||||
lastSeenAt: now,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
existing.push({
|
||||
permission,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
count: 1,
|
||||
lastSeenAt: now,
|
||||
});
|
||||
});
|
||||
|
||||
existing.sort((a, b) => Number(b.lastSeenAt || 0) - Number(a.lastSeenAt || 0));
|
||||
const limit = Math.max(1, Number(queueConfig.missingPermissionsLimit) || 20);
|
||||
requestQueueStateMutable.missingPermissions = existing.slice(0, limit);
|
||||
};
|
||||
|
||||
const runJob = (job) => {
|
||||
const method = normalizeMethod(job.method);
|
||||
const startedAt = Date.now();
|
||||
@@ -324,6 +421,16 @@ const runJob = (job) => {
|
||||
requestText: toSafeText(requestSnapshot),
|
||||
responseText: toSafeText(responseSnapshot),
|
||||
});
|
||||
|
||||
const missingPermissions = extractMissingPermissions(error);
|
||||
pushMissingPermissions(
|
||||
missingPermissions.map((permission) => ({
|
||||
permission,
|
||||
method,
|
||||
url: job.url,
|
||||
statusCode,
|
||||
}))
|
||||
);
|
||||
job.reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -389,6 +496,14 @@ export const enqueueRequest = (requestFactory, options = {}) => {
|
||||
|
||||
export const requestQueueState = readonly(requestQueueStateMutable);
|
||||
|
||||
export const clearErrorRequests = () => {
|
||||
requestQueueStateMutable.errorRequests = [];
|
||||
};
|
||||
|
||||
export const clearMissingPermissions = () => {
|
||||
requestQueueStateMutable.missingPermissions = [];
|
||||
};
|
||||
|
||||
export const __resetRequestQueueForTests = () => {
|
||||
requestQueue.length = 0;
|
||||
activeWorkers = 0;
|
||||
@@ -406,6 +521,7 @@ export const __resetRequestQueueForTests = () => {
|
||||
requestQueueStateMutable.activeRequests = [];
|
||||
requestQueueStateMutable.recentRequests = [];
|
||||
requestQueueStateMutable.errorRequests = [];
|
||||
requestQueueStateMutable.missingPermissions = [];
|
||||
|
||||
queueConfig.concurrencyByMethod = cloneObject(REQUEST_QUEUE_CONFIG.concurrency);
|
||||
queueConfig.spacingMs = 0;
|
||||
@@ -415,6 +531,7 @@ export const __resetRequestQueueForTests = () => {
|
||||
queueConfig.retryDelayJitterMs = 0;
|
||||
queueConfig.recentRequestsLimit = REQUEST_QUEUE_CONFIG.inspector.recentRequestsLimit;
|
||||
queueConfig.errorHistoryLimit = REQUEST_QUEUE_CONFIG.inspector.errorHistoryLimit;
|
||||
queueConfig.missingPermissionsLimit = REQUEST_QUEUE_CONFIG.inspector.missingPermissionsLimit;
|
||||
queueConfig.payloadMaxChars = REQUEST_QUEUE_CONFIG.inspector.payloadMaxChars;
|
||||
};
|
||||
|
||||
@@ -429,6 +546,7 @@ export const __configureRequestQueueForTests = ({
|
||||
retryDelayJitterMs,
|
||||
recentRequestsLimit,
|
||||
errorHistoryLimit,
|
||||
missingPermissionsLimit,
|
||||
payloadMaxChars,
|
||||
} = {}) => {
|
||||
if (concurrencyByMethod && typeof concurrencyByMethod === "object") {
|
||||
@@ -479,6 +597,10 @@ export const __configureRequestQueueForTests = ({
|
||||
queueConfig.errorHistoryLimit = Math.max(1, Math.floor(errorHistoryLimit));
|
||||
}
|
||||
|
||||
if (typeof missingPermissionsLimit === "number" && missingPermissionsLimit > 0) {
|
||||
queueConfig.missingPermissionsLimit = Math.max(1, Math.floor(missingPermissionsLimit));
|
||||
}
|
||||
|
||||
if (typeof payloadMaxChars === "number" && payloadMaxChars >= 200) {
|
||||
queueConfig.payloadMaxChars = Math.floor(payloadMaxChars);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ describe("RequestQueueProgress", () => {
|
||||
});
|
||||
|
||||
it("stores errors with request and response payloads and respects configured error limit", async () => {
|
||||
__configureRequestQueueForTests({ errorHistoryLimit: 1, payloadMaxChars: 8000 });
|
||||
__configureRequestQueueForTests({
|
||||
errorHistoryLimit: 1,
|
||||
missingPermissionsLimit: 1,
|
||||
retryByStatusCode: {},
|
||||
payloadMaxChars: 8000
|
||||
});
|
||||
const wrapper = mount(RequestQueueProgress);
|
||||
|
||||
const requestOne = enqueueRequest(
|
||||
@@ -144,7 +149,18 @@ describe("RequestQueueProgress", () => {
|
||||
async () => {
|
||||
throw {
|
||||
message: "Second failure",
|
||||
response: { status: 429, data: { reason: "second" } }
|
||||
response: {
|
||||
status: 403,
|
||||
data: {
|
||||
success: false,
|
||||
data: {
|
||||
message: "Missing permission(s)",
|
||||
permissions: ["department_notification_sms_get"]
|
||||
},
|
||||
meta: [],
|
||||
includes: []
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
@@ -160,7 +176,9 @@ describe("RequestQueueProgress", () => {
|
||||
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("second");
|
||||
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();
|
||||
@@ -171,6 +189,22 @@ describe("RequestQueueProgress", () => {
|
||||
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 () => {
|
||||
@@ -207,4 +241,41 @@ describe("RequestQueueProgress", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user