From 815c7653e3e3a0267f890ec5bab4c8a87ad15903 Mon Sep 17 00:00:00 2001 From: Brock Anderson Date: Fri, 28 Jun 2024 18:10:33 -0700 Subject: [PATCH] feat: admin app - restrict access to pages based on role (#559) Co-authored-by: Sukanya Rath <98050194+sukanya-rath@users.noreply.github.com> --- admin-frontend/src/App.vue | 23 ++- admin-frontend/src/__tests__/App.spec.ts | 164 ++++++++++++------ admin-frontend/src/components/SideBar.vue | 10 +- admin-frontend/src/router.js | 19 +- .../src/store/modules/__tests__/auth.spec.ts | 20 +++ admin-frontend/src/store/modules/auth.js | 6 + backend/src/v1/services/admin-auth-service.ts | 2 +- 7 files changed, 175 insertions(+), 69 deletions(-) diff --git a/admin-frontend/src/App.vue b/admin-frontend/src/App.vue index db6c0bba8..342084810 100644 --- a/admin-frontend/src/App.vue +++ b/admin-frontend/src/App.vue @@ -27,7 +27,7 @@ diff --git a/admin-frontend/src/__tests__/App.spec.ts b/admin-frontend/src/__tests__/App.spec.ts index 90827ed09..1c0afe332 100644 --- a/admin-frontend/src/__tests__/App.spec.ts +++ b/admin-frontend/src/__tests__/App.spec.ts @@ -1,10 +1,14 @@ import { createTestingPinia } from '@pinia/testing'; import { flushPromises, mount } from '@vue/test-utils'; +import { getActivePinia, setActivePinia } from 'pinia'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createRouter, createWebHistory } from 'vue-router'; import { createVuetify } from 'vuetify'; import * as components from 'vuetify/components'; import * as directives from 'vuetify/directives'; import App from '../App.vue'; +import router from '../router'; +import { authStore } from '../store/modules/auth'; // Mock the ResizeObserver const ResizeObserverMock = vi.fn(() => ({ @@ -16,73 +20,131 @@ const ResizeObserverMock = vi.fn(() => ({ // Stub the global ResizeObserver vi.stubGlobal('ResizeObserver', ResizeObserverMock); -describe('App', () => { - let wrapper; - let pinia; - let app; +const setupComponentEnvironment = async (options: any = {}) => { + const vuetify = createVuetify({ + components, + directives, + }); - const initWrapper = async (options: any = {}) => { - const vuetify = createVuetify({ - components, - directives, - }); + const pinia = getActivePinia(); - pinia = createTestingPinia({ - initialState: { - code: {}, - config: {}, - }, - }); + const auth = authStore(); + auth.getJwtToken = vi.fn().mockResolvedValue(null); + auth.doesUserHaveRole = vi.fn().mockReturnValue(true); - wrapper = mount( - { - //SideBar depends on vuetify components, so it must be mounted within a v-layout. - //It is also an async component because one of its dependencies (v-navigation-drawer) - //is async, so it must be mounted with a Suspense component. Some - //additional info about using the Suspense component are in the vue-test-utils docs - //here: - //https://test-utils.vuejs.org/guide/advanced/async-suspense.html#Testing-asynchronous-setup - template: '', - }, - { - props: {}, - global: { - components: { - App, - }, - plugins: [vuetify, pinia], + const mockRouter = createRouter({ + history: createWebHistory(), + routes: router.getRoutes(), + }); + + const wrapper = mount( + { + //SideBar depends on vuetify components, so it must be mounted within a v-layout. + //It is also an async component because one of its dependencies (v-navigation-drawer) + //is async, so it must be mounted with a Suspense component. Some + //additional info about using the Suspense component are in the vue-test-utils docs + //here: + //https://test-utils.vuejs.org/guide/advanced/async-suspense.html#Testing-asynchronous-setup + template: '', + }, + { + props: {}, + global: { + components: { + App, }, + plugins: [vuetify, pinia as any, mockRouter], }, - ); + }, + ); - //wait for the async App component to load - await flushPromises(); + //wait for the async App component to load + await flushPromises(); - app = await wrapper.findComponent(App); + const app = await wrapper.findComponent(App); + + return { + wrapper: wrapper, + pinia: pinia, + app: app, + router: mockRouter, + auth: auth, }; +}; - beforeEach(async () => { - await initWrapper(); +beforeEach(async () => { + const pinia = createTestingPinia({ + initialState: { + code: {}, + config: {}, + }, }); + setActivePinia(pinia); +}); - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - } - }); +afterEach(() => { + vi.clearAllMocks(); +}); +describe('App', () => { describe('if the header and sidebar are supposed to be visible', () => { - it('they are both actually visible', () => { - app.vm.areHeaderAndSidebarVisible = true; - expect(app.find('header')).toBeTruthy(); - expect(app.find('SideBar')).toBeTruthy(); + it('they are both actually visible', async () => { + const componentEnv = await setupComponentEnvironment(); + componentEnv.app.vm.areHeaderAndSidebarVisible = true; + await componentEnv.app.vm.$nextTick(); + expect(componentEnv.app.find('header')).toBeTruthy(); + expect(componentEnv.app.find('SideBar')).toBeTruthy(); }); }); describe('if the header and sidebar are supposed to be hidden', () => { - it('they are both actually hidden', () => { - app.vm.areHeaderAndSidebarVisible = false; - expect(app.find('header').exists()).toBeFalsy(); - expect(app.find('SideBar').exists()).toBeFalsy(); + it('they are both actually hidden', async () => { + const componentEnv = await setupComponentEnvironment(); + componentEnv.app.vm.areHeaderAndSidebarVisible = false; + await componentEnv.app.vm.$nextTick(); + expect(componentEnv.app.find('header').exists()).toBeFalsy(); + expect(componentEnv.app.find('SideBar').exists()).toBeFalsy(); + }); + }); + + describe('when the route changes to /user-management, and the user has permission to view the breadbrumb trail', async () => { + it('breadcrumb trail is visible', async () => { + const componentEnv = await setupComponentEnvironment(); + componentEnv.auth.doesUserHaveRole.mockReturnValue(true); + componentEnv.router.push('/user-management'); + await componentEnv.router.isReady(); + await componentEnv.app.vm.$nextTick(); + expect(componentEnv.app.vm.isBreadcrumbTrailVisible).toBeTruthy(); + }); + }); + describe("when the route changes to /user-management, and the user doesn't have permission to view the breadbrumb trail", async () => { + it('breadcrumb trail is hidden', async () => { + const componentEnv = await setupComponentEnvironment(); + componentEnv.auth.doesUserHaveRole.mockReturnValue(false); + componentEnv.router.push('/user-management'); + await componentEnv.router.isReady(); + await componentEnv.app.vm.$nextTick(); + expect(componentEnv.app.vm.isBreadcrumbTrailVisible).toBeFalsy(); + }); + }); + describe('onRouteChanged', () => { + describe('when the route changes', () => { + it('various state variables are updated', async () => { + const componentEnv = await setupComponentEnvironment(); + const to = { + meta: { + requiresAuth: false, + isBreadcrumbTrailVisible: true, + pageTitle: 'sample route', + }, + }; + componentEnv.auth.doesUserHaveRole.mockReturnValue(false); + componentEnv.app.vm.onRouteChanged(to, null); + expect(componentEnv.app.vm.activeRoute).toStrictEqual(to); + expect(componentEnv.app.vm.areHeaderAndSidebarVisible).toBe( + to.meta.requiresAuth, + ); + expect(componentEnv.app.vm.isBreadcrumbTrailVisible).toBe(false); + }); }); }); }); diff --git a/admin-frontend/src/components/SideBar.vue b/admin-frontend/src/components/SideBar.vue index 865a5c39d..b94429427 100644 --- a/admin-frontend/src/components/SideBar.vue +++ b/admin-frontend/src/components/SideBar.vue @@ -30,6 +30,7 @@ to="dashboard" title="Dashboard" :class="{ active: activeRoute == 'dashboard' }" + v-if="auth.doesUserHaveRole(USER_ROLE_NAME)" >