From 6b13495aabbbaf84cf4b974c0697341e64386d3b Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Wed, 8 Jul 2026 21:11:32 -0600 Subject: [PATCH 01/11] Fix [Vue Router warn]: No match found for location with path warnings in component tests --- .../failedmessages/PendingRetries.spec.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Frontend/src/components/failedmessages/PendingRetries.spec.ts b/src/Frontend/src/components/failedmessages/PendingRetries.spec.ts index a07b3ae1be..7523f179fa 100644 --- a/src/Frontend/src/components/failedmessages/PendingRetries.spec.ts +++ b/src/Frontend/src/components/failedmessages/PendingRetries.spec.ts @@ -3,6 +3,7 @@ import { render, screen } from "@testing-library/vue"; import { createTestingPinia } from "@pinia/testing"; import { createRouter, createMemoryHistory } from "vue-router"; import FailedMessagesView from "@/views/FailedMessagesView.vue"; +import routeLinks from "@/router/routeLinks"; /** * DSL for the Pending Retries Tab Visibility feature. @@ -68,38 +69,48 @@ async function renderComponent(options: RenderOptions = {}): PromiseGroups" }, }, { - path: "all", + path: routeLinks.failedMessage.failedMessages.template, name: "failed-messages-all", component: { template: "
All Messages
" }, }, { - path: "deleted-groups", + path: routeLinks.failedMessage.deletedMessagesGroup.template, name: "failed-messages-deleted-groups", component: { template: "
Deleted Groups
" }, }, { - path: "deleted", + path: routeLinks.failedMessage.deletedMessages.template, name: "failed-messages-deleted", component: { template: "
Deleted
" }, }, { - path: "pending-retries", + path: routeLinks.failedMessage.pendingRetries.template, name: "failed-messages-pending-retries", component: { template: "
Pending Retries
" }, }, + { + path: routeLinks.failedMessage.group.template, + name: "failed-messages-group", + component: { template: "
Group
" }, + }, + { + path: routeLinks.failedMessage.deletedGroup.template, + name: "failed-messages-deleted-group", + component: { template: "
Deleted Group
" }, + }, ], }, ], @@ -110,7 +121,7 @@ async function renderComponent(options: RenderOptions = {}): Promise Date: Wed, 8 Jul 2026 21:24:47 -0600 Subject: [PATCH 02/11] Fix TypeError: textRange(...).getClientRects is not a function for all tests --- src/Frontend/test/drivers/vitest/setup.ts | 27 +++++++++++++++++++ .../failedmessages/edit-and-retry.spec.ts | 26 ------------------ 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/Frontend/test/drivers/vitest/setup.ts b/src/Frontend/test/drivers/vitest/setup.ts index ce0e498c4b..b987eae445 100644 --- a/src/Frontend/test/drivers/vitest/setup.ts +++ b/src/Frontend/test/drivers/vitest/setup.ts @@ -6,6 +6,27 @@ import monitoringClient from "@/components/monitoring/monitoringClient"; import serviceControlClient from "@/components/serviceControlClient"; import VueTippy from "vue-tippy"; +function getBoundingClientRect(): DOMRect { + const rect = { + x: 0, + y: 0, + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + }; + + return { ...rect, toJSON: () => rect }; +} + +class FakeDOMRectList extends Array implements DOMRectList { + item(index: number): DOMRect | null { + return this[index]; + } +} + // Removes test warning noise failing to resolve tippy if (!config.global.plugins.includes(VueTippy)) { config.global.plugins.push(VueTippy); @@ -32,6 +53,12 @@ beforeEach(() => { }); beforeAll(() => { + document.elementFromPoint = (): null => null; + HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect; + HTMLElement.prototype.getClientRects = (): DOMRectList => new FakeDOMRectList(); + Range.prototype.getBoundingClientRect = getBoundingClientRect; + Range.prototype.getClientRects = (): DOMRectList => new FakeDOMRectList(); + mockServer.listen({ onUnhandledRequest: (_, print) => { print.warning(); diff --git a/src/Frontend/test/specs/failedmessages/edit-and-retry.spec.ts b/src/Frontend/test/specs/failedmessages/edit-and-retry.spec.ts index 30c063b2ad..44e5bee2bf 100644 --- a/src/Frontend/test/specs/failedmessages/edit-and-retry.spec.ts +++ b/src/Frontend/test/specs/failedmessages/edit-and-retry.spec.ts @@ -5,32 +5,6 @@ import { getEditAndRetryEditor } from "./questions/getEditAndRetryEditor"; import { expect } from "vitest"; describe("FEATURE: Editing failed messages", () => { - function getBoundingClientRect(): DOMRect { - const rec = { - x: 0, - y: 0, - bottom: 0, - height: 0, - left: 0, - right: 0, - top: 0, - width: 0, - }; - return { ...rec, toJSON: () => rec }; - } - - class FakeDOMRectList extends Array implements DOMRectList { - item(index: number): DOMRect | null { - return this[index]; - } - } - - document.elementFromPoint = (): null => null; - HTMLElement.prototype.getBoundingClientRect = getBoundingClientRect; - HTMLElement.prototype.getClientRects = (): DOMRectList => new FakeDOMRectList(); - Range.prototype.getBoundingClientRect = getBoundingClientRect; - Range.prototype.getClientRects = (): DOMRectList => new FakeDOMRectList(); - describe("RULE: Editing of a message should only be allowed when ServiceControl 'AllowMessageEditing' is enabled", () => { test.todo( "EXAMPLE: ServiceControl 'AllowMessageEditing' is disabled" From ba0cb6831af44329dca4c382610f950b2a23054e Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Wed, 8 Jul 2026 21:27:20 -0600 Subject: [PATCH 03/11] Fix AllowedRoutesStore test noise --- src/Frontend/src/stores/AllowedRoutesStore.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Frontend/src/stores/AllowedRoutesStore.spec.ts b/src/Frontend/src/stores/AllowedRoutesStore.spec.ts index facc6220b9..826c41b4ee 100644 --- a/src/Frontend/src/stores/AllowedRoutesStore.spec.ts +++ b/src/Frontend/src/stores/AllowedRoutesStore.spec.ts @@ -3,9 +3,17 @@ import { setActivePinia, createPinia } from "pinia"; import { ApiRoutes } from "@/composables/apiRoutes"; import { normalizeRouteKey } from "@/composables/routeMatching"; +const logger = vi.hoisted(() => ({ + warn: vi.fn(), + error: vi.fn(), +})); + const rootFetch = vi.fn(); const scFetch = vi.fn(); const monFetch = vi.fn(); +vi.mock("@/logger", () => ({ + default: logger, +})); vi.mock("@/components/serviceControlClient", () => ({ default: { fetchTypedFromServiceControl: (s: string) => rootFetch(s), @@ -30,6 +38,8 @@ const rootDoc = (myRoutesUrl?: string) => [{}, myRoutesUrl === undefined ? {} : describe("AllowedRoutesStore", () => { beforeEach(() => { setActivePinia(createPinia()); + logger.warn.mockReset(); + logger.error.mockReset(); rootFetch.mockReset(); scFetch.mockReset(); monFetch.mockReset(); @@ -80,6 +90,9 @@ describe("AllowedRoutesStore", () => { await store.refresh(); expect(store.loaded).toBe(false); expect(store.loadAttempted).toBe(true); + expect(logger.warn).toHaveBeenCalledTimes(2); + expect(logger.warn).toHaveBeenNthCalledWith(1, "Failed to fetch allowed routes", expect.any(Error)); + expect(logger.warn).toHaveBeenNthCalledWith(2, "Failed to fetch allowed routes", expect.any(Error)); }); it("Monitoring manifest entry at root (no /api prefix) matches the ApiRoutes registry path", async () => { @@ -106,6 +119,7 @@ describe("AllowedRoutesStore", () => { expect(store.loaded).toBe(true); expect(store.routes.has("GET /api/errors")).toBe(true); expect(store.routes.size).toBe(1); + expect(logger.warn).toHaveBeenCalledWith("Skipping malformed allowed-route entry", { method: "POST" }); }); it("skips the primary fetch entirely when the root document omits my_routes_url", async () => { From 6a6116aa80547a93d2deda7778f3776f68d165c9 Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Wed, 8 Jul 2026 21:30:19 -0600 Subject: [PATCH 04/11] Fix useAuth test noise --- src/Frontend/src/composables/useAuth.spec.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Frontend/src/composables/useAuth.spec.ts b/src/Frontend/src/composables/useAuth.spec.ts index 44c34ccc8e..9a816fa80d 100644 --- a/src/Frontend/src/composables/useAuth.spec.ts +++ b/src/Frontend/src/composables/useAuth.spec.ts @@ -2,11 +2,20 @@ import { describe, test, expect, vi, beforeEach } from "vitest"; import { setActivePinia, createPinia } from "pinia"; import routeLinks from "@/router/routeLinks"; +const logger = vi.hoisted(() => ({ + warn: vi.fn(), + error: vi.fn(), +})); + // Capture the OIDC event callbacks useAuth registers, plus a spy on signinRedirect, so the tests // can fire "token expired" / "silent renew error" and assert that recovery re-authenticates. const signinRedirect = vi.fn().mockResolvedValue(undefined); const captured: { expired?: () => void; renewError?: (error: unknown) => void } = {}; +vi.mock("@/logger", () => ({ + default: logger, +})); + vi.mock("oidc-client-ts", () => ({ UserManager: class { getUser = vi.fn().mockResolvedValue(null); @@ -45,6 +54,8 @@ async function initAuth() { beforeEach(() => { vi.resetModules(); + logger.warn.mockReset(); + logger.error.mockReset(); signinRedirect.mockClear(); captured.expired = undefined; captured.renewError = undefined; @@ -69,9 +80,11 @@ describe("useAuth recovers a lost session from OIDC events", () => { store.setAuthenticating(false); signinRedirect.mockClear(); - captured.renewError!(new Error("silent renew failed")); + const error = new Error("silent renew failed"); + captured.renewError!(error); expect(signinRedirect).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith("Silent renew error:", error); }); test("does not re-authenticate while an auth flow is already running", async () => { From 3ad344ed30d9b54723bf32aed349ecc606379bc8 Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Wed, 8 Jul 2026 21:46:13 -0600 Subject: [PATCH 05/11] Cleanup various fetch noise in application tests --- src/Frontend/test/preconditions/recoverability.ts | 4 ++++ .../authentication/auth-config-unavailable.spec.ts | 11 ++++++++++- .../test/specs/authentication/auth-enabled.spec.ts | 2 ++ .../audit-capability-card.spec.ts | 2 ++ .../monitoring-capability-card.spec.ts | 9 ++++++++- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Frontend/test/preconditions/recoverability.ts b/src/Frontend/test/preconditions/recoverability.ts index c9e5384dcb..32bcb3feaa 100644 --- a/src/Frontend/test/preconditions/recoverability.ts +++ b/src/Frontend/test/preconditions/recoverability.ts @@ -177,6 +177,10 @@ export const hasFailedMessage = body: withBody, }); + driver.mockEndpoint(`${serviceControlUrl}messages/${withGroupId}/body`, { + body: withBody, + }); + driver.mockEndpoint(`${serviceControlUrl}messages/search/${withMessageId}`, { body: [ { diff --git a/src/Frontend/test/specs/authentication/auth-config-unavailable.spec.ts b/src/Frontend/test/specs/authentication/auth-config-unavailable.spec.ts index e4f82113c7..aec2688aeb 100644 --- a/src/Frontend/test/specs/authentication/auth-config-unavailable.spec.ts +++ b/src/Frontend/test/specs/authentication/auth-config-unavailable.spec.ts @@ -1,4 +1,4 @@ -import { vi, expect } from "vitest"; +import { vi, expect, beforeEach } from "vitest"; import { createOidcMock } from "../../mocks/oidc-client-mock"; // Mock oidc-client-ts @@ -10,6 +10,12 @@ import { waitFor, screen } from "@testing-library/vue"; import { useAuthStore } from "@/stores/AuthStore"; describe("FEATURE: Auth Configuration Endpoint Unavailable (Scenario 15)", () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + consoleError.mockClear(); + }); + describe("RULE: App should handle ServiceControl unavailability gracefully", () => { test("EXAMPLE: App continues to load when auth config endpoint returns error", async ({ driver }) => { // Set up ServiceControl endpoints but auth config returns 500 error @@ -28,6 +34,7 @@ describe("FEATURE: Auth Configuration Endpoint Unavailable (Scenario 15)", () => // Since auth config failed, auth should be treated as disabled expect(authStore.authEnabled).toBe(false); + expect(consoleError).toHaveBeenCalledWith("Error fetching auth configuration", expect.any(Error)); // Dashboard should still be accessible (graceful degradation) await waitFor(() => { @@ -50,6 +57,7 @@ describe("FEATURE: Auth Configuration Endpoint Unavailable (Scenario 15)", () => // Auth should be treated as disabled when config unavailable expect(authStore.authEnabled).toBe(false); }); + expect(consoleError).toHaveBeenCalledWith("Error fetching auth configuration", expect.any(Error)); // App should not crash or show blank screen await waitFor(() => { @@ -78,6 +86,7 @@ describe("FEATURE: Auth Configuration Endpoint Unavailable (Scenario 15)", () => // Auth should be disabled (graceful fallback) expect(authStore.authEnabled).toBe(false); + expect(consoleError).toHaveBeenCalledWith("Error fetching auth configuration", expect.any(Error)); // Dashboard should still render await waitFor(() => { diff --git a/src/Frontend/test/specs/authentication/auth-enabled.spec.ts b/src/Frontend/test/specs/authentication/auth-enabled.spec.ts index 505d9e70e8..ce075b3cff 100644 --- a/src/Frontend/test/specs/authentication/auth-enabled.spec.ts +++ b/src/Frontend/test/specs/authentication/auth-enabled.spec.ts @@ -6,6 +6,7 @@ describe("FEATURE: Authentication Enabled (Scenario 2)", () => { describe("RULE: Authentication configuration endpoint should return valid OIDC config", () => { test("EXAMPLE: Auth config endpoint returns enabled with all required fields", async ({ driver }) => { const authConfig = await driver.setUp(precondition.scenarioAuthEnabled); + await driver.setUp(precondition.hasOidcDiscoveryMock()); // Verify all required fields are present expect(authConfig.enabled).toBe(true); @@ -23,6 +24,7 @@ describe("FEATURE: Authentication Enabled (Scenario 2)", () => { test("EXAMPLE: Auth config endpoint is accessible without authentication", async ({ driver }) => { await driver.setUp(precondition.serviceControlWithMonitoring); await driver.setUp(precondition.hasAuthenticationEnabled()); + await driver.setUp(precondition.hasOidcDiscoveryMock()); // Navigate to trigger app mount await driver.goTo("/dashboard"); diff --git a/src/Frontend/test/specs/platformcapabilities/audit-capability-card.spec.ts b/src/Frontend/test/specs/platformcapabilities/audit-capability-card.spec.ts index d24f82314d..832c7ad0f6 100644 --- a/src/Frontend/test/specs/platformcapabilities/audit-capability-card.spec.ts +++ b/src/Frontend/test/specs/platformcapabilities/audit-capability-card.spec.ts @@ -127,6 +127,7 @@ describe("FEATURE: Audit capability card", () => { test("EXAMPLE: Audit instance available with successful messages shows available status", async ({ driver }) => { // Arrange // Need to set up ServiceControl with version >= 6.6.0 for "All Messages" feature support + await driver.setUp(precondition.hasAuthenticationDisabled()); await driver.setUp(precondition.hasActiveLicense); await driver.setUp(precondition.hasLicensingSettingTest()); await driver.setUp(precondition.hasServiceControlMainInstance(precondition.serviceControlVersionSupportingAllMessages)); @@ -261,6 +262,7 @@ describe("FEATURE: Audit capability card", () => { test("EXAMPLE: ServiceControl version < 6.6.0 with successful messages still shows not configured status", async ({ driver }) => { // Arrange // Set up ServiceControl with version < 6.6.0 which does NOT support "All Messages" feature + await driver.setUp(precondition.hasAuthenticationDisabled()); await driver.setUp(precondition.hasActiveLicense); await driver.setUp(precondition.hasLicensingSettingTest()); await driver.setUp(precondition.hasServiceControlMainInstance(precondition.serviceControlVersionNotSupportingAllMessages)); diff --git a/src/Frontend/test/specs/platformcapabilities/monitoring-capability-card.spec.ts b/src/Frontend/test/specs/platformcapabilities/monitoring-capability-card.spec.ts index 5762661064..344db61a89 100644 --- a/src/Frontend/test/specs/platformcapabilities/monitoring-capability-card.spec.ts +++ b/src/Frontend/test/specs/platformcapabilities/monitoring-capability-card.spec.ts @@ -1,5 +1,5 @@ import { test, describe } from "../../drivers/vitest/driver"; -import { expect } from "vitest"; +import { expect, vi, beforeEach } from "vitest"; import * as precondition from "../../preconditions"; import { waitFor } from "@testing-library/vue"; import { @@ -15,6 +15,12 @@ import { import { disableMonitoring } from "../../drivers/vitest/setup"; describe("FEATURE: Monitoring capability card", () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + consoleError.mockClear(); + }); + describe("RULE: When monitoring instance is available with endpoints sending data, show 'Available' status", () => { test("EXAMPLE: Monitoring instance available with monitored endpoints shows available status", async ({ driver }) => { // Arrange @@ -115,6 +121,7 @@ describe("FEATURE: Monitoring capability card", () => { await waitFor(async () => { expect(await isMonitoringCardUnavailable()).toBe(true); }); + expect(consoleError).toHaveBeenCalled(); const statusBadge = await monitoringStatusBadge(); expect(statusBadge).toBeInTheDocument(); From ce08af617eb624639f908247421325581888b295 Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Thu, 9 Jul 2026 08:26:46 -0600 Subject: [PATCH 06/11] Refactor ConfigurationStore so it doesn't fetch during construction and can be mocked correctly --- src/Frontend/src/App.vue | 14 +++++++- .../src/components/audit/AuditList.spec.ts | 1 + src/Frontend/src/stores/ConfigurationStore.ts | 33 ++++++++++++++----- src/Frontend/src/stores/MessageStore.ts | 2 ++ .../src/stores/RecoverabilityStore.ts | 4 +++ 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/Frontend/src/App.vue b/src/Frontend/src/App.vue index ccaa60e86a..87c79741ba 100644 --- a/src/Frontend/src/App.vue +++ b/src/Frontend/src/App.vue @@ -9,10 +9,12 @@ import BackendChecksNotifications from "@/components/BackendChecksNotifications. import { storeToRefs } from "pinia"; import { useAuthStore } from "@/stores/AuthStore"; import { useAllowedRoutes } from "@/composables/useAllowedRoutes"; +import { useConfigurationStore } from "@/stores/ConfigurationStore"; const authStore = useAuthStore(); +const configurationStore = useConfigurationStore(); const route = useRoute(); -const { isAuthenticated, authEnabled } = storeToRefs(authStore); +const { isAuthenticated, authEnabled, loading } = storeToRefs(authStore); // Load the allowed-route manifest (my/routes) once authenticated, so the nav and other // UI can gate on it. Fail-safe: a missing/old endpoint just leaves the manifest unloaded @@ -33,6 +35,16 @@ const isAnonymousRoute = computed(() => route.meta?.allowAnonymous === true); const shouldShowApp = computed(() => !authEnabled.value || isAuthenticated.value || isAnonymousRoute.value); // Show full app layout (header, footer, notifications) only when authenticated or auth is disabled const shouldShowFullLayout = computed(() => !authEnabled.value || isAuthenticated.value); + +watch( + [loading, authEnabled, isAuthenticated], + ([isLoading, enabled, authenticated]) => { + if (!isLoading && (!enabled || authenticated)) { + configurationStore.ensureLoaded(); + } + }, + { immediate: true } +);