diff --git a/src/Frontend/src/App.spec.ts b/src/Frontend/src/App.spec.ts new file mode 100644 index 000000000..8af28709f --- /dev/null +++ b/src/Frontend/src/App.spec.ts @@ -0,0 +1,67 @@ +import { render } from "@testing-library/vue"; +import { describe, test, expect, vi } from "vitest"; +import { createTestingPinia } from "@pinia/testing"; +import { setActivePinia } from "pinia"; +import { ref } from "vue"; + +vi.mock("bootstrap", () => ({})); + +// Avoid pulling in a real router; App only needs useRoute (for meta) and RouterView. +const routeMeta = ref>({}); +vi.mock("vue-router", () => ({ + useRoute: () => ({ + get meta() { + return routeMeta.value; + }, + }), + RouterView: { name: "RouterView", template: "
" }, +})); + +import App from "@/App.vue"; +import { useAuthStore } from "@/stores/AuthStore"; + +function setup(opts: { authEnabled: boolean; isAuthenticated: boolean; meta?: Record }) { + routeMeta.value = opts.meta ?? {}; + const pinia = createTestingPinia({ stubActions: false }); + setActivePinia(pinia); + const store = useAuthStore(); + store.authEnabled = opts.authEnabled; + store.isAuthenticated = opts.isAuthenticated; + return render(App, { + global: { + plugins: [pinia], + stubs: { + PageHeader: { template: "
" }, + PageFooter: true, + LicenseNotifications: true, + BackendChecksNotifications: true, + }, + }, + }); +} + +// App.vue is now display-only: recovery from a lost session lives in useAuth (see useAuth.spec.ts). +describe("App layout gating", () => { + test("shows the full layout when authenticated", () => { + const { queryByTestId } = setup({ authEnabled: true, isAuthenticated: true }); + expect(queryByTestId("page-header")).not.toBeNull(); + expect(queryByTestId("router-view")).not.toBeNull(); + }); + + test("shows the full layout when authentication is disabled", () => { + const { queryByTestId } = setup({ authEnabled: false, isAuthenticated: false }); + expect(queryByTestId("page-header")).not.toBeNull(); + }); + + test("renders nothing when auth is enabled and the user is not authenticated", () => { + const { queryByTestId } = setup({ authEnabled: true, isAuthenticated: false }); + expect(queryByTestId("page-header")).toBeNull(); + expect(queryByTestId("router-view")).toBeNull(); + }); + + test("on an anonymous route, shows the page without the full layout", () => { + const { queryByTestId } = setup({ authEnabled: true, isAuthenticated: false, meta: { allowAnonymous: true } }); + expect(queryByTestId("router-view")).not.toBeNull(); + expect(queryByTestId("page-header")).toBeNull(); + }); +}); diff --git a/src/Frontend/src/App.vue b/src/Frontend/src/App.vue index de55293cb..0dbda9b8e 100644 --- a/src/Frontend/src/App.vue +++ b/src/Frontend/src/App.vue @@ -18,6 +18,8 @@ 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); +// Recovery from a lost session (expired token / failed silent renewal) is handled in the auth +// domain by useAuth's OIDC event handlers, not here.