Stabilize customer booking and i18n checks
This commit is contained in:
@@ -155,9 +155,9 @@ async function deployWorkers() {
|
||||
try {
|
||||
await deployCronWorkers({});
|
||||
await loadWorkers();
|
||||
queuedMessage.value = t(
|
||||
updatingDeployment ? "cron.messages.worker_update_queued" : "cron.messages.worker_deploy_queued"
|
||||
);
|
||||
queuedMessage.value = updatingDeployment
|
||||
? t("cron.messages.worker_update_queued")
|
||||
: t("cron.messages.worker_deploy_queued");
|
||||
} catch (error) {
|
||||
errorMessage.value = parseError(error);
|
||||
} finally {
|
||||
|
||||
@@ -62,6 +62,20 @@ const selectedIncident = ref(null);
|
||||
const incidentNote = ref("");
|
||||
const incidentFilters = ref({ status: "open", search: "" });
|
||||
const firewallDraft = ref(defaultFirewallDraft());
|
||||
const securityRuleLabels = {
|
||||
bookings_created: () => t("security.rules.bookings_created"),
|
||||
failed_login_attempts: () => t("security.rules.failed_login_attempts"),
|
||||
requests_per_customer: () => t("security.rules.requests_per_customer"),
|
||||
requests_per_ip: () => t("security.rules.requests_per_ip"),
|
||||
vehicles_created: () => t("security.rules.vehicles_created"),
|
||||
};
|
||||
const securityRuleDescriptions = {
|
||||
bookings_created: () => t("security.rule_descriptions.bookings_created"),
|
||||
failed_login_attempts: () => t("security.rule_descriptions.failed_login_attempts"),
|
||||
requests_per_customer: () => t("security.rule_descriptions.requests_per_customer"),
|
||||
requests_per_ip: () => t("security.rule_descriptions.requests_per_ip"),
|
||||
vehicles_created: () => t("security.rule_descriptions.vehicles_created"),
|
||||
};
|
||||
|
||||
const responseData = (response, fallback) => response?.data?.data ?? fallback;
|
||||
const parseError = (error) =>
|
||||
@@ -108,9 +122,7 @@ function normalizeRuleDraft(rule) {
|
||||
enabled: Boolean(rule.enabled),
|
||||
threshold_count: Number(rule.threshold_count || 1),
|
||||
window_seconds: Number(rule.window_seconds || 60),
|
||||
exemptionsText: Array.isArray(rule.exempt_permission_nodes)
|
||||
? rule.exempt_permission_nodes.join(", ")
|
||||
: "",
|
||||
exemptionsText: Array.isArray(rule.exempt_permission_nodes) ? rule.exempt_permission_nodes.join(", ") : "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,15 +148,11 @@ function formatDate(value) {
|
||||
}
|
||||
|
||||
function ruleLabel(rule) {
|
||||
const key = `security.rules.${rule.rule_key}`;
|
||||
const value = t(key);
|
||||
return value === key ? rule.label || rule.rule_key : value;
|
||||
return securityRuleLabels[rule.rule_key]?.() || rule.label || rule.rule_key;
|
||||
}
|
||||
|
||||
function ruleDescription(rule) {
|
||||
const key = `security.rule_descriptions.${rule.rule_key}`;
|
||||
const value = t(key);
|
||||
return value === key ? rule.description || "" : value;
|
||||
return securityRuleDescriptions[rule.rule_key]?.() || rule.description || "";
|
||||
}
|
||||
|
||||
function statusClass(status) {
|
||||
@@ -468,8 +476,12 @@ onMounted(() => {
|
||||
<tbody>
|
||||
<tr v-for="incident in summary?.incidents?.recent || []" :key="incident.id">
|
||||
<td>{{ incident.title }}</td>
|
||||
<td><span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span></td>
|
||||
<td><span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span></td>
|
||||
<td>
|
||||
<span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span>
|
||||
</td>
|
||||
<td>{{ formatDate(incident.last_seen_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -481,13 +493,7 @@ onMounted(() => {
|
||||
<section v-else-if="activePage === 'firewall'" class="security-section" data-testid="security-firewall">
|
||||
<div class="security-toolbar">
|
||||
<h2>{{ t("security.firewall.management") }}</h2>
|
||||
<b-button
|
||||
type="is-light"
|
||||
icon-left="sync-alt"
|
||||
icon-pack="fas"
|
||||
:loading="loading"
|
||||
@click="loadPage"
|
||||
>
|
||||
<b-button type="is-light" icon-left="sync-alt" icon-pack="fas" :loading="loading" @click="loadPage">
|
||||
{{ t("security.actions.refresh") }}
|
||||
</b-button>
|
||||
</div>
|
||||
@@ -602,7 +608,12 @@ onMounted(() => {
|
||||
<tbody>
|
||||
<tr v-for="rule in firewallRules" :key="rule.id">
|
||||
<td>
|
||||
<span class="tag" :class="rule.action === 'block' ? 'is-danger' : rule.action === 'watch' ? 'is-warning' : 'is-success'">
|
||||
<span
|
||||
class="tag"
|
||||
:class="
|
||||
rule.action === 'block' ? 'is-danger' : rule.action === 'watch' ? 'is-warning' : 'is-success'
|
||||
"
|
||||
>
|
||||
{{ rule.action }}
|
||||
</span>
|
||||
<span v-if="!rule.enabled" class="tag is-light ml-1">{{ t("security.firewall.disabled") }}</span>
|
||||
@@ -709,13 +720,7 @@ onMounted(() => {
|
||||
<section v-else class="security-section" data-testid="security-incidents">
|
||||
<div class="security-toolbar">
|
||||
<h2>{{ t("security.incidents.management") }}</h2>
|
||||
<b-button
|
||||
type="is-light"
|
||||
icon-left="sync-alt"
|
||||
icon-pack="fas"
|
||||
:loading="loading"
|
||||
@click="loadIncidents"
|
||||
>
|
||||
<b-button type="is-light" icon-left="sync-alt" icon-pack="fas" :loading="loading" @click="loadIncidents">
|
||||
{{ t("security.actions.refresh") }}
|
||||
</b-button>
|
||||
</div>
|
||||
@@ -761,8 +766,12 @@ onMounted(() => {
|
||||
@click="selectIncident(incident)"
|
||||
>
|
||||
<td>{{ incident.title }}</td>
|
||||
<td><span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span></td>
|
||||
<td><span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span></td>
|
||||
<td>
|
||||
<span class="tag" :class="statusClass(incident.status)">{{ incident.status }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag" :class="severityClass(incident.severity)">{{ incident.severity }}</span>
|
||||
</td>
|
||||
<td>{{ incident.occurrence_count }}</td>
|
||||
<td>{{ formatDate(incident.last_seen_at) }}</td>
|
||||
</tr>
|
||||
@@ -799,7 +808,9 @@ onMounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t("security.incidents.route") }}</dt>
|
||||
<dd>{{ selectedIncident.route_path || selectedIncident.route_template || t("security.empty_value") }}</dd>
|
||||
<dd>
|
||||
{{ selectedIncident.route_path || selectedIncident.route_template || t("security.empty_value") }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
|
||||
@@ -90,6 +90,35 @@ async function clickDesktopNext(page: Page) {
|
||||
throw new Error("Could not find a visible desktop next-step button in the booking flow.");
|
||||
}
|
||||
|
||||
async function isDesktopProductSelectionVisible(page: Page) {
|
||||
const productSelectionLocators = [
|
||||
page.locator('[data-testid^="pos-product-card-"]:visible').first(),
|
||||
page.getByTestId("pos-product-categories-loading"),
|
||||
page.getByTestId("pos-products-loading"),
|
||||
page.locator('[data-testid^="pos-product-category-tab-"]:visible').first(),
|
||||
page.locator('[data-testid="pos-product-category-select"]:visible').first(),
|
||||
];
|
||||
|
||||
for (const locator of productSelectionLocators) {
|
||||
if (await locator.isVisible().catch(() => false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForDesktopProductSelection(page: Page) {
|
||||
return expect
|
||||
.poll(async () => isDesktopProductSelectionVisible(page), {
|
||||
intervals: [50, 100, 250],
|
||||
timeout: 1_500,
|
||||
})
|
||||
.toBe(true)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
async function ensureVehicleInput(page: Page) {
|
||||
const reg1Input = page.locator('input[id="reg1-input"]:visible').first();
|
||||
|
||||
@@ -139,6 +168,15 @@ async function ensureVehicleTypeSelection(page: Page) {
|
||||
.toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
async function confirmVehicleRegistrationInput(page: Page, reg1Input: ReturnType<Page["locator"]>) {
|
||||
if (!(await reg1Input.isVisible().catch(() => false))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await reg1Input.press("Enter");
|
||||
return !(await reg1Input.isVisible().catch(() => false));
|
||||
}
|
||||
|
||||
export async function fillBookingField(page: Page, toggleTestId: string, inputTestId: string, value: string) {
|
||||
const input = page.locator(`[data-testid="${inputTestId}"]:visible`).first();
|
||||
const toggle = page.locator(`[data-testid="${toggleTestId}"]:visible`).first();
|
||||
@@ -188,7 +226,7 @@ export async function goToBookingProductSelectionStepWithOptions(
|
||||
|
||||
const reg1Input = await ensureVehicleInput(page);
|
||||
await reg1Input.fill(registrationNumber);
|
||||
await reg1Input.press("Enter");
|
||||
const vehicleInputClosed = await confirmVehicleRegistrationInput(page, reg1Input);
|
||||
|
||||
if (!mobileWizard) {
|
||||
await ensureVehicleTypeSelection(page);
|
||||
@@ -197,7 +235,7 @@ export async function goToBookingProductSelectionStepWithOptions(
|
||||
if (mobileWizard) {
|
||||
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
|
||||
await page.getByTestId("booking-mobile-next").click();
|
||||
} else {
|
||||
} else if (!vehicleInputClosed && !(await waitForDesktopProductSelection(page))) {
|
||||
await clickDesktopNext(page);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user