diff --git a/frontend/cypress/e2e/external_monitorfish.spec.ts b/frontend/cypress/e2e/external_monitorfish.spec.ts index 317fc71fa7..bc96222a56 100644 --- a/frontend/cypress/e2e/external_monitorfish.spec.ts +++ b/frontend/cypress/e2e/external_monitorfish.spec.ts @@ -1,7 +1,12 @@ context('External MonitorFish', () => { it('Should redirect to /', () => { // Given - cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser') + cy.intercept('/bff/v1/authorization/current', { + body: { + isSuperUser: false + }, + statusCode: 200 + }).as('getIsSuperUser') cy.visit('/ext#@-824534.42,6082993.21,8.70') cy.wait('@getIsSuperUser') @@ -10,7 +15,12 @@ context('External MonitorFish', () => { it('Should have some features removed When not logged as super user', () => { // Given - cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser') + cy.intercept('/bff/v1/authorization/current', { + body: { + isSuperUser: false + }, + statusCode: 200 + }).as('getIsSuperUser') cy.visit('/#@-824534.42,6082993.21,8.70') cy.wait('@getIsSuperUser') cy.wait(200) diff --git a/frontend/cypress/e2e/main_window/vessel_sidebar/offline_management.spec.ts b/frontend/cypress/e2e/main_window/vessel_sidebar/offline_management.spec.ts index 2c651dc352..10718bfecf 100644 --- a/frontend/cypress/e2e/main_window/vessel_sidebar/offline_management.spec.ts +++ b/frontend/cypress/e2e/main_window/vessel_sidebar/offline_management.spec.ts @@ -78,7 +78,7 @@ context('Offline management', () => { path: '/bff/v1/vessels/find?vesselId=1&internalReferenceNumber=FAK000999999&externalReferenceNumber=DONTSINK' + '&IRCS=CALLME&vesselIdentifier=INTERNAL_REFERENCE_NUMBER&trackDepth=TWELVE_HOURS&afterDateTime=&beforeDateTime=', - times: 1 + times: 2 }, { statusCode: 400 } ).as('openVesselStubbed') @@ -117,7 +117,7 @@ context('Offline management', () => { { method: 'GET', path: '/bff/v1/vessels/logbook/find?internalReferenceNumber=FAK000999999&voyageRequest=LAST&tripNumber=', - times: 1 + times: 2 }, { statusCode: 400 } ).as('getLogbookStubbed') @@ -135,8 +135,8 @@ context('Offline management', () => { cy.intercept( { method: 'GET', - pathname: '/bff/v1/vessels/reportings', - times: 3 + pathname: '/bff/v1/vessels/reporting', + times: 2 }, { statusCode: 400 } ).as('getReportingsStubbed') diff --git a/frontend/cypress/e2e/nav_monitorfish.spec.ts b/frontend/cypress/e2e/nav_monitorfish.spec.ts index e8e9dc4021..744ce09240 100644 --- a/frontend/cypress/e2e/nav_monitorfish.spec.ts +++ b/frontend/cypress/e2e/nav_monitorfish.spec.ts @@ -1,7 +1,12 @@ context('Light MonitorFish', () => { it('Should have some features removed When not logged as super user', () => { // Given - cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser') + cy.intercept('/bff/v1/authorization/current', { + body: { + isSuperUser: false + }, + statusCode: 200 + }).as('getIsSuperUser') cy.visit('/light#@-824534.42,6082993.21,8.70') cy.wait('@getIsSuperUser') cy.wait(200) diff --git a/frontend/cypress/e2e/side_window/utils.ts b/frontend/cypress/e2e/side_window/utils.ts index a36cd48636..c6394624f4 100644 --- a/frontend/cypress/e2e/side_window/utils.ts +++ b/frontend/cypress/e2e/side_window/utils.ts @@ -1,5 +1,12 @@ +import { SideWindowMenuLabel } from '../../../src/domain/entities/sideWindow/constants' + export const openSideWindowAsUser = () => { - cy.intercept('/bff/v1/authorization/current', { statusCode: 401 }).as('getIsSuperUser') + cy.intercept('/bff/v1/authorization/current', { + body: { + isSuperUser: false + }, + statusCode: 200 + }).as('getIsSuperUser') cy.viewport(1920, 1080) cy.visit('/side_window') @@ -7,6 +14,7 @@ export const openSideWindowAsUser = () => { if (document.querySelector('[data-cy="first-loader"]')) { cy.getDataCy('first-loader').should('not.be.visible') } + cy.clickButton(SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST) } export const openSideWindowAsSuperUser = () => { @@ -16,4 +24,5 @@ export const openSideWindowAsSuperUser = () => { if (document.querySelector('[data-cy="first-loader"]')) { cy.getDataCy('first-loader').should('not.be.visible') } + cy.clickButton(SideWindowMenuLabel.PRIOR_NOTIFICATION_LIST) } diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index 92ab8cdb83..90ac1e2efc 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -95,7 +95,7 @@ Cypress.on('uncaught:exception', err => { // Run before each spec beforeEach(() => { - // We use a Cypress session to inject inject a Local Storage key + // We use a Cypress session to inject a Local Storage key // so that we can detect when the browser app is running in Cypress. // https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-preserve-cookies--localStorage-in-between-my-tests cy.session('cypress', () => { diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 0c5c49dfa1..de53cbd77b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,9 +1,11 @@ // https://redux-toolkit.js.org/rtk-query/usage/cache-behavior // https://redux-toolkit.js.org/rtk-query/usage/automated-refetching#cache-tags +import { isUnauthorizedOrForbidden } from '@api/utils' import { FrontendApiError } from '@libs/FrontendApiError' import { createApi, fetchBaseQuery, retry } from '@reduxjs/toolkit/query/react' import { setMeasurement, startSpan } from '@sentry/react' +import { normalizeRtkBaseQuery } from '@utils/normalizeRtkBaseQuery' import { sha256 } from '@utils/sha256' import ky, { HTTPError } from 'ky' @@ -11,7 +13,6 @@ import { RTK_MAX_RETRIES, RtkCacheTagType } from './constants' import { getOIDCConfig } from '../auth/getOIDCConfig' import { getOIDCUser } from '../auth/getOIDCUser' import { redirectToLoginIfUnauthorized } from '../auth/utils' -import { normalizeRtkBaseQuery } from '../utils/normalizeRtkBaseQuery' import type { BackendApi } from './BackendApi.types' import type { CustomResponseError, RTKBaseQueryArgs } from './types' @@ -243,7 +244,14 @@ export const monitorfishApiKy = ky.extend({ } ], beforeRetry: [ - async ({ request }) => { + async ({ error, request }) => { + if (error) { + // Retry is not necessary when request is unauthorized + if (isUnauthorizedOrForbidden((error as HTTPError).response?.status)) { + return ky.stop + } + } + const user = getOIDCUser() const token = user?.access_token @@ -257,8 +265,10 @@ export const monitorfishApiKy = ky.extend({ request.headers.set(CORRELATION_HEADER, hashedToken) } } + + return undefined } ] }, - retry: RTK_MAX_RETRIES + 1 + retry: RTK_MAX_RETRIES }) diff --git a/frontend/src/api/constants.ts b/frontend/src/api/constants.ts index 09aed7df23..9a57eb7503 100644 --- a/frontend/src/api/constants.ts +++ b/frontend/src/api/constants.ts @@ -3,7 +3,7 @@ import { FIVE_MINUTES, ONE_MINUTE, THIRTY_SECONDS } from '../constants' import type { RefetchConfigOptions } from '@reduxjs/toolkit' import type { StartQueryActionCreatorOptions, SubscriptionOptions } from '@reduxjs/toolkit/query' -export const RTK_MAX_RETRIES = 2 +export const RTK_MAX_RETRIES = 1 export const RTK_THIRTY_SECONDS_POLLING_QUERY_OPTIONS: SubscriptionOptions & Partial = { pollingInterval: THIRTY_SECONDS, @@ -32,7 +32,8 @@ export enum HttpStatusCode { CREATED = 201, ACCEPTED = 202, NOT_FOUND = 404, - UNAUTHORIZED = 401 + UNAUTHORIZED = 401, + FORBIDDEN = 403 } export enum RtkCacheTagType { diff --git a/frontend/src/api/utils.ts b/frontend/src/api/utils.ts new file mode 100644 index 0000000000..14a682eab7 --- /dev/null +++ b/frontend/src/api/utils.ts @@ -0,0 +1,10 @@ +import { HttpStatusCode } from '@api/constants' +import { isString } from 'lodash' + +export function isUnauthorizedOrForbidden(httpStatus: number | string | undefined) { + if (!httpStatus || isString(httpStatus)) { + return false + } + + return [HttpStatusCode.FORBIDDEN, HttpStatusCode.UNAUTHORIZED].includes(httpStatus) +} diff --git a/frontend/src/auth/getOIDCConfig.ts b/frontend/src/auth/getOIDCConfig.ts index 45ad7859f2..abcfe6337f 100644 --- a/frontend/src/auth/getOIDCConfig.ts +++ b/frontend/src/auth/getOIDCConfig.ts @@ -1,8 +1,5 @@ -import { isCypress } from '@utils/isCypress' import { WebStorageStateStore } from 'oidc-client-ts' -const IS_CYPRESS = isCypress() - export function getOIDCConfig() { const IS_OIDC_ENABLED = import.meta.env.FRONTEND_OIDC_ENABLED === 'true' const OIDC_REDIRECT_URI = import.meta.env.FRONTEND_OIDC_REDIRECT_URI @@ -30,7 +27,7 @@ export function getOIDCConfig() { return { // eslint-disable-next-line @typescript-eslint/naming-convention - IS_OIDC_ENABLED: IS_CYPRESS ? false : IS_OIDC_ENABLED, + IS_OIDC_ENABLED, oidcConfig } } diff --git a/frontend/src/auth/hooks/useGetUserAccount.tsx b/frontend/src/auth/hooks/useGetUserAccount.tsx index aa366cc4e6..a53eec01bb 100644 --- a/frontend/src/auth/hooks/useGetUserAccount.tsx +++ b/frontend/src/auth/hooks/useGetUserAccount.tsx @@ -1,5 +1,6 @@ import { useTracking } from '@hooks/useTracking' import { setUser } from '@sentry/react' +import { isCypress } from '@utils/isCypress' import { useCallback, useEffect, useMemo } from 'react' import { type AuthContextProps, useAuth } from 'react-oidc-context' @@ -7,11 +8,16 @@ import { useGetCurrentUserAuthorizationQueryOverride } from './useGetCurrentUser import type { UserAccountContextType } from '../../context/UserAccountContext' +const IS_CYPRESS = isCypress() || true + +/** + * When using Cypress, we stub `useAuth()` + */ export function useGetUserAccount(): UserAccountContextType { // `| undefined` because it's undefined if the OIDC is disabled which is the case for Cypress tests const auth = useAuth() as AuthContextProps | undefined const { trackUserId } = useTracking() - const { data: user } = useGetCurrentUserAuthorizationQueryOverride({ skip: !auth?.isAuthenticated }) + const { data: user } = useGetCurrentUserAuthorizationQueryOverride({ skip: !IS_CYPRESS && !auth?.isAuthenticated }) useEffect(() => { if (auth?.user?.profile?.email) { @@ -31,15 +37,23 @@ export function useGetUserAccount(): UserAccountContextType { auth.signoutRedirect({ id_token_hint: idTokenHint ?? '' }) }, [auth]) - const userAccount = useMemo( - () => ({ + const userAccount = useMemo(() => { + if (IS_CYPRESS) { + return { + email: 'dummy@cypress.test', + isAuthenticated: true, + isSuperUser: user?.isSuperUser ?? false, + logout + } + } + + return { email: auth?.user?.profile?.email, isAuthenticated: auth?.isAuthenticated ?? false, isSuperUser: user?.isSuperUser ?? false, logout - }), - [logout, user, auth?.isAuthenticated, auth?.user?.profile?.email] - ) + } + }, [logout, user, auth?.isAuthenticated, auth?.user?.profile?.email]) useEffect( () => diff --git a/frontend/src/auth/types.ts b/frontend/src/auth/types.ts index 09404dc4f2..bf8fbe2126 100644 --- a/frontend/src/auth/types.ts +++ b/frontend/src/auth/types.ts @@ -1,5 +1,3 @@ export type UserAuthorization = { - isLogged: boolean | undefined isSuperUser: boolean | undefined - mustReload: boolean | undefined } diff --git a/frontend/src/auth/utils.ts b/frontend/src/auth/utils.ts index cddf6ab277..d9382905fa 100644 --- a/frontend/src/auth/utils.ts +++ b/frontend/src/auth/utils.ts @@ -1,4 +1,4 @@ -import { HttpStatusCode } from '@api/constants' +import { isUnauthorizedOrForbidden } from '@api/utils' import { paths } from '../paths' import { router } from '../router' @@ -9,7 +9,7 @@ import type { CustomResponseError } from '@api/types' * Redirect to Login page if any HTTP request in Unauthorized */ export function redirectToLoginIfUnauthorized(error: CustomResponseError) { - if (!error.path.includes(paths.backendForFrontend) || error.status !== HttpStatusCode.UNAUTHORIZED) { + if (!error.path.includes(paths.backendForFrontend) || !isUnauthorizedOrForbidden(error.status)) { return } diff --git a/frontend/src/utils/isCypress.ts b/frontend/src/utils/isCypress.ts index 90e26e856f..2ddddc837c 100644 --- a/frontend/src/utils/isCypress.ts +++ b/frontend/src/utils/isCypress.ts @@ -2,7 +2,7 @@ * Detects whether the browser app is running in Cypress. * * @description - * We use a Cypress session to inject inject a Local Storage key + * We use a Cypress session to inject a Local Storage key * so that we can detect when the browser app is running in Cypress. * * @see https://docs.cypress.io/faq/questions/using-cypress-faq#How-do-I-preserve-cookies--localStorage-in-between-my-tests