From 1e7231aecf68bf373691c667460a87d6f0bdcefe Mon Sep 17 00:00:00 2001 From: Herbert Vicente Cotta Julio Date: Tue, 1 Oct 2024 08:28:12 -0300 Subject: [PATCH 01/18] [UXE-4954] test: add coverage for redirectToManager guard and update router guards (#1764) Unit tests for the redirectToManager guard were added, and the router guards were refactored to accept routerHandler and dependencies as parameters, improving dependency injection across the guards. --- src/router/hooks/beforeEachRoute.js | 14 +- src/router/hooks/guards/accountGuard.js | 4 +- src/router/hooks/guards/billingGuard.js | 4 +- src/router/hooks/guards/logoutGuard.js | 4 +- src/router/hooks/guards/themeGuard.js | 6 +- src/router/hooks/redirectToManager.js | 17 +- src/router/index.js | 17 +- .../router/hooks/redirectToManager.test.js | 179 ++++++++++++++++++ 8 files changed, 214 insertions(+), 31 deletions(-) create mode 100644 src/tests/router/hooks/redirectToManager.test.js diff --git a/src/router/hooks/beforeEachRoute.js b/src/router/hooks/beforeEachRoute.js index eed025ce1..e007ded62 100644 --- a/src/router/hooks/beforeEachRoute.js +++ b/src/router/hooks/beforeEachRoute.js @@ -2,16 +2,18 @@ import * as guards from '@/router/hooks/guards' import { useHelpCenterStore } from '@/stores/help-center' import { useRouter } from 'vue-router' -/** @type {import('vue-router').NavigationGuardWithThis} */ -export default async function beforeEachRoute(to, from, next) { +export default async function beforeEachRoute(routeHandler, guardDependency) { + const { to, next } = routeHandler + const { accountStore } = guardDependency + const helpCenterStore = useHelpCenterStore() helpCenterStore.close() const router = useRouter() - await guards.logoutGuard(to, next) + await guards.logoutGuard(to, next, accountStore) guards.loadingGuard(to) - await guards.accountGuard(to, next) - guards.themeGuard() - guards.billingGuard(to, next) + await guards.accountGuard(to, next, accountStore) + guards.themeGuard(accountStore) + guards.billingGuard(to, next, accountStore) guards.redirectGuard(to, next, router) return next() diff --git a/src/router/hooks/guards/accountGuard.js b/src/router/hooks/guards/accountGuard.js index 0e5ef9f3f..e41b1b459 100644 --- a/src/router/hooks/guards/accountGuard.js +++ b/src/router/hooks/guards/accountGuard.js @@ -1,11 +1,9 @@ import { getAccountInfoService, getUserInfoService } from '@/services/account-services' import { loadAccountJobRoleService } from '@/services/account-settings-services' -import { useAccountStore } from '@/stores/account' import { setRedirectRoute } from '@/helpers' /** @type {import('vue-router').NavigationGuardWithThis} */ -export async function accountGuard(to, next) { - const accountStore = useAccountStore() +export async function accountGuard(to, next, accountStore) { const isPrivateRoute = !to.meta.isPublic const userNotIsLoggedIn = !accountStore.hasActiveUserId diff --git a/src/router/hooks/guards/billingGuard.js b/src/router/hooks/guards/billingGuard.js index 5de31090f..719691fdc 100644 --- a/src/router/hooks/guards/billingGuard.js +++ b/src/router/hooks/guards/billingGuard.js @@ -1,4 +1,3 @@ -import { useAccountStore } from '@/stores/account' import { billingRoutes } from '@/router/routes/billing-routes' import { getStaticUrlsByEnvironment } from '@/helpers' @@ -10,8 +9,7 @@ const BILLING_REDIRECT_OPTIONS = { const billingUrl = getStaticUrlsByEnvironment('billing') /** @type {import('vue-router').NavigationGuardWithThis} */ -export async function billingGuard(to, next) { - const accountStore = useAccountStore() +export async function billingGuard(to, next, accountStore) { const { hasActiveUserId, redirectToExternalBillingNeeded, diff --git a/src/router/hooks/guards/logoutGuard.js b/src/router/hooks/guards/logoutGuard.js index 238085e91..728dcaa56 100644 --- a/src/router/hooks/guards/logoutGuard.js +++ b/src/router/hooks/guards/logoutGuard.js @@ -1,11 +1,9 @@ import { logoutService } from '@/services/auth-services' -import { useAccountStore } from '@/stores/account' /** @type {import('vue-router').NavigationGuardWithThis} */ -export async function logoutGuard(to, next) { +export async function logoutGuard(to, next, accountStore) { if (to.path === '/logout') { await logoutService() - const accountStore = useAccountStore() accountStore.setAccountData({}) return next() } diff --git a/src/router/hooks/guards/themeGuard.js b/src/router/hooks/guards/themeGuard.js index c57c6d2eb..cb12568fc 100644 --- a/src/router/hooks/guards/themeGuard.js +++ b/src/router/hooks/guards/themeGuard.js @@ -1,8 +1,4 @@ -import { useAccountStore } from '@/stores/account' - -export async function themeGuard() { - const accountStore = useAccountStore() - +export async function themeGuard(accountStore) { // TODO: remove the usage of localStorage when API returns the theme const theme = localStorage.getItem('theme') const fallbackTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' diff --git a/src/router/hooks/redirectToManager.js b/src/router/hooks/redirectToManager.js index 9ae386530..7c21f19fe 100644 --- a/src/router/hooks/redirectToManager.js +++ b/src/router/hooks/redirectToManager.js @@ -1,24 +1,23 @@ import { getEnvironment, getStaticUrlsByEnvironment } from '@/helpers' -import { loadContractServicePlan } from '@/services/contract-services' -import { useAccountStore } from '@/stores/account' -/** @type {import('vue-router').NavigationGuardWithThis} */ -export default async function redirectToManager(to, from, next) { - const accountStore = useAccountStore() +export default async function redirectToManager(routeHandler, guardDependency) { + const { to, from, next } = routeHandler + const { accountStore, loadContractServicePlan } = guardDependency const isPrivateRoute = !to.meta.isPublic const accountData = accountStore.accountData try { if (accountStore.hasActiveUserId && isPrivateRoute) { - if (accountStore.metricsOnlyAccessRestriction) { - return handleNavigationRestriction(to, from, next) - } - const isAzion = accountData.email.includes('@azion.com') // Azion internal access to console. if (isAzion || accountStore.hasAccessConsole) { return next() } + + if (accountStore.metricsOnlyAccessRestriction) { + return handleNavigationRestriction(to, from, next) + } + const isNotClientKind = !accountData.client_id // [Brand,Reseller,Group] are the kins without client id. if (isNotClientKind && !isAzion) { diff --git a/src/router/index.js b/src/router/index.js index 123d617fb..04bed7f02 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -41,6 +41,8 @@ import { createRouter, createWebHistory } from 'vue-router' import afterEachRouteGuard from './hooks/afterEachRoute' import beforeEachRoute from './hooks/beforeEachRoute' import redirectToManager from './hooks/redirectToManager' +import { useAccountStore } from '@/stores/account' +import { loadContractServicePlan } from '@/services/contract-services' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -85,8 +87,19 @@ const router = createRouter({ ].concat(errorRoutes) }) -router.beforeEach(beforeEachRoute) -router.beforeEach(redirectToManager) +router.beforeEach((to, from, next) => { + const accountStore = useAccountStore() + const routeHandler = { to, from, next } + + const guardDependency = { + accountStore, + loadContractServicePlan + } + + beforeEachRoute(routeHandler, guardDependency) + redirectToManager(routeHandler, guardDependency) +}) + router.afterEach(afterEachRouteGuard) export default router diff --git a/src/tests/router/hooks/redirectToManager.test.js b/src/tests/router/hooks/redirectToManager.test.js new file mode 100644 index 000000000..b64935fa3 --- /dev/null +++ b/src/tests/router/hooks/redirectToManager.test.js @@ -0,0 +1,179 @@ +import { describe, afterAll, it, expect, vi } from 'vitest' +import redirectToManager from '@/router/hooks/redirectToManager' + +vi.stubGlobal('window', { + location: { + replace: vi.fn() + } +}) + +const makeSut = (scenario) => { + const { + hasActiveUserId = false, + isPrivateRoute, + hasAccessConsole, + metricsOnlyAccessRestriction, + email = '', + clientId, + isDeveloperSupportPlan, + fromName = '' + } = scenario + + const accountStore = { + hasActiveUserId: hasActiveUserId, + hasAccessConsole: hasAccessConsole, + metricsOnlyAccessRestriction: metricsOnlyAccessRestriction, + accountData: { + email: email, + client_id: clientId + }, + setAccountData: vi.fn() + } + + const loadContractServicePlan = vi.fn().mockResolvedValue({ isDeveloperSupportPlan }) + + const guardDependency = { + accountStore, + loadContractServicePlan + } + + const routeHandler = { + to: { meta: { isPublic: !isPrivateRoute }, name: 'some-route' }, + from: { name: fromName }, + next: vi.fn() + } + + return { guardDependency, routeHandler } +} + +afterAll(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() +}) + +describe('redirectToManager', () => { + it('should call next if user has an active user id and is on a non-private route', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: false + }) + await redirectToManager(routeHandler, guardDependency) + + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + expect(routeHandler.next).toHaveBeenCalled() + }) + + it('should allow full access for users with an @azion.com account', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + email: '@azion.com' + }) + await redirectToManager(routeHandler, guardDependency) + + expect(routeHandler.next).toHaveBeenCalled() + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + }) + + it('should allow access for accounts with Developer type', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + email: '@developer.com', + clientId: '123', + isDeveloperSupportPlan: true + }) + await redirectToManager(routeHandler, guardDependency) + + expect(guardDependency.accountStore.setAccountData).toHaveBeenCalledWith({ + isDeveloperSupportPlan: true + }) + expect(routeHandler.next).toHaveBeenCalled() + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + }) + it('should redirect non-developer accounts to the RTM', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + hasAccessConsole: false, + email: '@developer.com', + clientId: '123', + isDeveloperSupportPlan: false + }) + await redirectToManager(routeHandler, guardDependency) + + expect(guardDependency.accountStore.setAccountData).toHaveBeenCalledWith({ + isDeveloperSupportPlan: false + }) + expect(windowLocationReplaceMock).toHaveBeenCalled() + }) + + it('should redirect to the RTM when accountData does not have a client_id', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + email: '@developer.com', + clientId: null + }) + + await redirectToManager(routeHandler, guardDependency) + + expect(windowLocationReplaceMock).toHaveBeenCalled() + expect(routeHandler.next).toHaveBeenCalled() + }) + + it('should handle navigation restriction for metrics only access', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + metricsOnlyAccessRestriction: true + }) + + await redirectToManager(routeHandler, guardDependency) + + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + expect(routeHandler.next).toHaveBeenCalled({ name: 'real-time-metrics' }) + }) + + it('should handle navigation restriction for metrics only access and from.name is not metrics', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + metricsOnlyAccessRestriction: true, + fromName: 'real-time-metrics' + }) + + await redirectToManager(routeHandler, guardDependency) + + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + expect(routeHandler.next).toHaveBeenCalledWith(false) + }) + + it('should allow access for accounts with console access', async () => { + const windowLocationReplaceMock = vi.spyOn(window.location, 'replace') + + const { routeHandler, guardDependency } = makeSut({ + hasActiveUserId: true, + isPrivateRoute: true, + hasAccessConsole: true + }) + + await redirectToManager(routeHandler, guardDependency) + + expect(windowLocationReplaceMock).not.toHaveBeenCalled() + expect(routeHandler.next).toHaveBeenCalled() + }) +}) From 04418dde06c96a8071b1231623c25efaed851aa0 Mon Sep 17 00:00:00 2001 From: Lucas Mendes <92529166+lucasmendes21@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:22:36 -0300 Subject: [PATCH 02/18] [UXE-5134] feat: implements tracking events in edge services (#1773) --- src/views/EdgeServices/CreateView.vue | 26 ++++++++++++++++++++++++++ src/views/EdgeServices/EditView.vue | 27 +++++++++++++++++++++++++++ src/views/EdgeServices/ListView.vue | 20 +++++++++++++++++++- 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/views/EdgeServices/CreateView.vue b/src/views/EdgeServices/CreateView.vue index 5862f4d52..d3926cdc4 100644 --- a/src/views/EdgeServices/CreateView.vue +++ b/src/views/EdgeServices/CreateView.vue @@ -5,6 +5,12 @@ import PageHeadingBlock from '@/templates/page-heading-block' import FormFieldsEdgeService from './FormFields/FormFieldsEdgeService' import * as yup from 'yup' + import { inject } from 'vue' + import { handleTrackerError } from '@/utils/errorHandlingTracker' + + /**@type {import('@/plugins/analytics/AnalyticsTrackerAdapter').AnalyticsTrackerAdapter} */ + const tracker = inject('tracker') + defineOptions({ name: 'create-edge-service' }) const props = defineProps({ @@ -31,6 +37,24 @@ code: '', active: true } + + const handleTrackSuccessCreated = () => { + tracker.product.productCreated({ + productName: 'Edge Service' + }) + } + + const handleTrackFailCreated = (error) => { + const { fieldName, message } = handleTrackerError(error) + tracker.product + .failedToCreate({ + productName: 'Edge Service', + errorType: 'api', + fieldName: fieldName.trim(), + errorMessage: message + }) + .track() + }