From 9729befe594ce269e1ff94d891756beed1b199cf Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Fri, 3 May 2024 08:46:21 +0100 Subject: [PATCH] Merge pull request #18170 from overleaf/ae-token-access-page Convert token access page to React GitOrigin-RevId: d7434f0de395c47a95d00767727fbe9d43f9abca --- .../TokenAccess/TokenAccessController.js | 14 +- .../app/views/project/token/access-react.pug | 16 ++ .../web/frontend/extracted-translations.json | 4 + .../components/access-attempt-screen.tsx | 55 ++++++ .../components/require-accept-screen.tsx | 57 ++++++ .../components/token-access-root.tsx | 124 +++++++++++++ .../components/v1-import-data-screen.tsx | 84 +++++++++ .../web/frontend/js/pages/token-access.tsx | 13 ++ .../frontend/js/shared/components/location.js | 4 + .../frontend/js/shared/hooks/use-location.ts | 11 +- .../token-access/token-access-page.spec.tsx | 168 ++++++++++++++++++ .../components/actions-copy-project.test.jsx | 1 + .../modal-content-new-project-form.test.tsx | 1 + .../components/notifications.test.tsx | 1 + .../components/project-list-root.test.tsx | 1 + ...e-and-download-project-pdf-button.test.tsx | 1 + .../download-project-button.test.tsx | 1 + .../emails/reconfirmation-info.test.tsx | 1 + .../components/leave/modal-form.test.tsx | 1 + .../components/share-project-modal.test.jsx | 1 + .../group-subscription-memberships.test.tsx | 1 + .../dashboard/personal-subscription.test.tsx | 1 + .../dashboard/states/active/active.test.tsx | 1 + .../active/change-plan/change-plan.test.tsx | 1 + .../TokenAccess/TokenAccessControllerTests.js | 7 + 25 files changed, 568 insertions(+), 2 deletions(-) create mode 100644 services/web/app/views/project/token/access-react.pug create mode 100644 services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx create mode 100644 services/web/frontend/js/features/token-access/components/require-accept-screen.tsx create mode 100644 services/web/frontend/js/features/token-access/components/token-access-root.tsx create mode 100644 services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx create mode 100644 services/web/frontend/js/pages/token-access.tsx create mode 100644 services/web/test/frontend/components/token-access/token-access-page.spec.tsx diff --git a/services/web/app/src/Features/TokenAccess/TokenAccessController.js b/services/web/app/src/Features/TokenAccess/TokenAccessController.js index c311929793..e9d9b30077 100644 --- a/services/web/app/src/Features/TokenAccess/TokenAccessController.js +++ b/services/web/app/src/Features/TokenAccess/TokenAccessController.js @@ -12,6 +12,7 @@ const { handleAdminDomainRedirect, } = require('../Authorization/AuthorizationMiddleware') const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') const orderedPrivilegeLevels = [ PrivilegeLevels.NONE, @@ -97,7 +98,18 @@ async function tokenAccessPage(req, res, next) { } } - res.render('project/token/access', { + const { variant } = await SplitTestHandler.promises.getAssignment( + req, + res, + 'token-access-page' + ) + + const view = + variant === 'react' + ? 'project/token/access-react' + : 'project/token/access' + + res.render(view, { postUrl: makePostUrl(token), }) } catch (err) { diff --git a/services/web/app/views/project/token/access-react.pug b/services/web/app/views/project/token/access-react.pug new file mode 100644 index 0000000000..157d806f60 --- /dev/null +++ b/services/web/app/views/project/token/access-react.pug @@ -0,0 +1,16 @@ +extends ../../layout-marketing + +block entrypointVar + - entrypoint = 'pages/token-access' + +block vars + - var suppressFooter = true + - var suppressCookieBanner = true + - var suppressSkipToContent = true + +block append meta + meta(name="ol-postUrl" data-type="string" content=postUrl) + meta(name="ol-user" data-type="json" content=user) + +block content + div#token-access-page diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 5fc97510da..b47ff57c82 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -22,6 +22,7 @@ "accept_or_reject_each_changes_individually": "", "accept_terms_and_conditions": "", "accepted_invite": "", + "accepting_invite_as": "", "access_denied": "", "account_has_been_link_to_institution_account": "", "account_has_past_due_invoice_change_plan_warning": "", @@ -532,6 +533,7 @@ "history_view_all": "", "history_view_labels": "", "hit_enter_to_reply": "", + "home": "", "hotkey_add_a_comment": "", "hotkey_autocomplete_menu": "", "hotkey_beginning_of_document": "", @@ -619,6 +621,7 @@ "invite_not_accepted": "", "invited_to_group": "", "invited_to_group_have_individual_subcription": "", + "invited_to_join": "", "ip_address": "", "is_email_affiliated": "", "issued_on": "", @@ -1335,6 +1338,7 @@ "to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "", "toggle_compile_options_menu": "", "token": "", + "token_access_failure": "", "token_limit_reached": "", "token_read_only": "", "token_read_write": "", diff --git a/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx new file mode 100644 index 0000000000..0813325bd7 --- /dev/null +++ b/services/web/frontend/js/features/token-access/components/access-attempt-screen.tsx @@ -0,0 +1,55 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' + +export const AccessAttemptScreen: FC<{ + loadingScreenBrandHeight: string + inflight: boolean + accessError: string | boolean +}> = ({ loadingScreenBrandHeight, inflight, accessError }) => { + const { t } = useTranslation() + + return ( +
+
+
+
+ +

+ {t('join_project')} + {inflight && } +

+ + {accessError && ( +
+
+
+ {accessError === 'not_found' ? ( +
+

Project not found

+
+ ) : ( +
+
+ {t('token_access_failure')} +
+

+ {t('home')} +

+
+ )} +
+
+ )} +
+ ) +} +const LoadingScreenEllipses = () => ( + + . + . + . + +) diff --git a/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx new file mode 100644 index 0000000000..1bcdcdcc4d --- /dev/null +++ b/services/web/frontend/js/features/token-access/components/require-accept-screen.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import getMeta from '@/utils/meta' + +export type RequireAcceptData = { + projectName?: string +} + +export const RequireAcceptScreen: FC<{ + requireAcceptData: RequireAcceptData + sendPostRequest: (confirmedByUser: boolean) => void +}> = ({ requireAcceptData, sendPostRequest }) => { + const { t } = useTranslation() + const user = getMeta('ol-user') + + return ( +
+
+
+
+
+
+

+ {t('invited_to_join')} +
+ {requireAcceptData.projectName || 'This project'} +

+
+ + {user && ( +
+
+

+ {t('accepting_invite_as')} {user.email} +

+
+
+ )} + +
+
+ +
+
+
+
+
+
+
+ ) +} diff --git a/services/web/frontend/js/features/token-access/components/token-access-root.tsx b/services/web/frontend/js/features/token-access/components/token-access-root.tsx new file mode 100644 index 0000000000..faef02760f --- /dev/null +++ b/services/web/frontend/js/features/token-access/components/token-access-root.tsx @@ -0,0 +1,124 @@ +import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' +import withErrorBoundary from '@/infrastructure/error-boundary' +import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback' +import { useCallback, useEffect, useRef, useState } from 'react' +import getMeta from '@/utils/meta' +import { postJSON } from '@/infrastructure/fetch-json' +import { debugConsole } from '@/utils/debugging' +import { useLocation } from '@/shared/hooks/use-location' +import { + V1ImportData, + V1ImportDataScreen, +} from '@/features/token-access/components/v1-import-data-screen' +import { AccessAttemptScreen } from '@/features/token-access/components/access-attempt-screen' +import { + RequireAcceptData, + RequireAcceptScreen, +} from '@/features/token-access/components/require-accept-screen' +import Icon from '@/shared/components/icon' + +type Mode = 'access-attempt' | 'v1Import' | 'requireAccept' + +function TokenAccessRoot() { + const [mode, setMode] = useState('access-attempt') + const [inflight, setInflight] = useState(false) + const [accessError, setAccessError] = useState(false) + const [v1ImportData, setV1ImportData] = useState() + const [requireAcceptData, setRequireAcceptData] = + useState() + const [loadingScreenBrandHeight, setLoadingScreenBrandHeight] = + useState('0px') + const location = useLocation() + + const sendPostRequest = useCallback( + (confirmedByUser = false) => { + setInflight(true) + + postJSON(getMeta('ol-postUrl'), { + body: { + confirmedByUser, + tokenHashPrefix: document.location.hash, + }, + }) + .then(async data => { + setAccessError(false) + + if (data.redirect) { + location.replace(data.redirect) + } else if (data.v1Import) { + setMode('v1Import') + setV1ImportData(data.v1Import) + } else if (data.requireAccept) { + setMode('requireAccept') + setRequireAcceptData(data.requireAccept) + } else { + debugConsole.warn( + 'invalid data from server in success response', + data + ) + setAccessError(true) + } + }) + .catch(error => { + debugConsole.warn('error response from server', error) + setAccessError(error.response?.status === 404 ? 'not_found' : 'error') + }) + .finally(() => { + setInflight(false) + }) + }, + [location] + ) + + const postedRef = useRef(false) + useEffect(() => { + if (!postedRef.current) { + postedRef.current = true + sendPostRequest() + setTimeout(() => { + setLoadingScreenBrandHeight('20%') + }, 500) + } + }, [sendPostRequest]) + + const { isReady } = useWaitForI18n() + + if (!isReady) { + return null + } + + return ( +
+
+ + + +
+ + {mode === 'access-attempt' && ( + + )} + + {mode === 'v1Import' && v1ImportData && ( + + )} + + {mode === 'requireAccept' && requireAcceptData && ( + + )} +
+ ) +} + +export default withErrorBoundary(TokenAccessRoot, GenericErrorBoundaryFallback) diff --git a/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx b/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx new file mode 100644 index 0000000000..947f041d53 --- /dev/null +++ b/services/web/frontend/js/features/token-access/components/v1-import-data-screen.tsx @@ -0,0 +1,84 @@ +import { FC } from 'react' + +export type V1ImportData = { + name?: string + status: string + projectId: string +} +export const V1ImportDataScreen: FC<{ v1ImportData: V1ImportData }> = ({ + v1ImportData, +}) => { + return ( +
+
+
+
+

+ {v1ImportData.status === 'mustLogin' + ? 'Please log in' + : 'Overleaf v1 Project'} +

+ + The new V2 editor. + + {v1ImportData.status === 'cannotImport' && ( +
+

+ Cannot Access Overleaf v1 Project +

+ +

+ Please contact the project owner or{' '} + contact support for assistance. +

+
+ )} + + {v1ImportData.status === 'mustLogin' && ( +
+

+ You will need to log in to access this project. +

+ + +
+ )} + + {v1ImportData.status === 'canDownloadZip' && ( +
+

+ {v1ImportData.name || 'This project'} has not + yet been moved into the new version of Overleaf. This project + was created anonymously and therefore cannot be automatically + imported. Please download a zip file of the project and upload + that to continue editing it. If you would like to delete this + project after you have made a copy, please contact support. +

+ + +
+ )} +
+
+
+
+ ) +} diff --git a/services/web/frontend/js/pages/token-access.tsx b/services/web/frontend/js/pages/token-access.tsx new file mode 100644 index 0000000000..003406f5c3 --- /dev/null +++ b/services/web/frontend/js/pages/token-access.tsx @@ -0,0 +1,13 @@ +import 'jquery' +import 'bootstrap' +import './../utils/meta' +import './../utils/webpack-public-path' +import './../infrastructure/error-reporter' +import './../i18n' +import ReactDOM from 'react-dom' +import TokenAccessRoot from '../features/token-access/components/token-access-root' + +const element = document.getElementById('token-access-page') +if (element) { + ReactDOM.render(, element) +} diff --git a/services/web/frontend/js/shared/components/location.js b/services/web/frontend/js/shared/components/location.js index 86dcb397f3..67ba24662d 100644 --- a/services/web/frontend/js/shared/components/location.js +++ b/services/web/frontend/js/shared/components/location.js @@ -5,6 +5,10 @@ export const location = { // eslint-disable-next-line no-restricted-syntax window.location.assign(url) }, + replace(url) { + // eslint-disable-next-line no-restricted-syntax + window.location.replace(url) + }, reload() { // eslint-disable-next-line no-restricted-syntax window.location.reload() diff --git a/services/web/frontend/js/shared/hooks/use-location.ts b/services/web/frontend/js/shared/hooks/use-location.ts index 12ee581605..3185fa8d06 100644 --- a/services/web/frontend/js/shared/hooks/use-location.ts +++ b/services/web/frontend/js/shared/hooks/use-location.ts @@ -14,11 +14,20 @@ export const useLocation = () => { [isMounted] ) + const replace = useCallback( + url => { + if (isMounted.current) { + location.replace(url) + } + }, + [isMounted] + ) + const reload = useCallback(() => { if (isMounted.current) { location.reload() } }, [isMounted]) - return useMemo(() => ({ assign, reload }), [assign, reload]) + return useMemo(() => ({ assign, replace, reload }), [assign, replace, reload]) } diff --git a/services/web/test/frontend/components/token-access/token-access-page.spec.tsx b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx new file mode 100644 index 0000000000..0a9f47e2e9 --- /dev/null +++ b/services/web/test/frontend/components/token-access/token-access-page.spec.tsx @@ -0,0 +1,168 @@ +import TokenAccessPage from '@/features/token-access/components/token-access-root' +import { location } from '@/shared/components/location' + +describe('', function () { + // this is a URL for a read-only token, but the process is the same for read-write tokens + const url = '/read/123/grant' + + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache = new Map([ + ['ol-postUrl', url], + ['ol-user', { email: 'test@example.com' }], + ]) + }) + }) + + it('handles a successful token access request', function () { + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + requireAccept: { projectName: 'Test Project' }, + }, + } + ).as('grantRequest') + + cy.mount() + + cy.wait('@grantRequest').then(interception => { + expect(interception.request.body.confirmedByUser).to.be.false + }) + + cy.get('h1').should( + 'have.text', + ['You have been invited to join', 'Test Project'].join('') + ) + + cy.contains('You are accepting this invite as test@example.com') + + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + redirect: '/project/123', + }, + } + ).as('confirmedGrantRequest') + + cy.stub(location, 'replace').as('replaceLocation') + + cy.findByRole('button', { name: 'Join Project' }).click() + + cy.wait('@confirmedGrantRequest').then(interception => { + expect(interception.request.body.confirmedByUser).to.be.true + }) + + cy.get('@replaceLocation').should( + 'have.been.calledOnceWith', + '/project/123' + ) + }) + + it('handles a project not found response', function () { + cy.intercept({ method: 'post', url, times: 1 }, { statusCode: 404 }).as( + 'grantRequest' + ) + + cy.mount() + + cy.wait('@grantRequest') + + cy.get('h3').should('have.text', 'Join Project') + cy.get('h4').should('have.text', 'Project not found') + + cy.findByRole('button', { name: 'Join Project' }).should('not.exist') + }) + + it('handles a redirect response', function () { + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + redirect: '/restricted', + }, + } + ).as('grantRequest') + + cy.stub(location, 'replace').as('replaceLocation') + + cy.mount() + + cy.wait('@grantRequest') + + cy.get('@replaceLocation').should('have.been.calledOnceWith', '/restricted') + }) + + it('handles a v1 "must login" response', function () { + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + v1Import: { status: 'mustLogin' }, + }, + } + ).as('grantRequest') + + cy.stub(location, 'replace').as('replaceLocation') + + cy.mount() + + cy.wait('@grantRequest') + + cy.get('h1').should('have.text', 'Please log in') + + cy.findByRole('link', { name: 'Log in to access project' }) + .should('have.attr', 'href') + .and('match', /^\/login\?redir=/) + }) + + it('handles a v1 "cannot import" response', function () { + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + v1Import: { status: 'cannotImport' }, + }, + } + ).as('grantRequest') + + cy.stub(location, 'replace').as('replaceLocation') + + cy.mount() + + cy.wait('@grantRequest') + + cy.get('h1').should('have.text', 'Overleaf v1 Project') + cy.get('h2').should('have.text', 'Cannot Access Overleaf v1 Project') + }) + + it('handles a v1 "can download zip" response', function () { + cy.intercept( + { method: 'post', url, times: 1 }, + { + body: { + v1Import: { + status: 'canDownloadZip', + projectId: '123', + name: 'Test Project', + }, + }, + } + ).as('grantRequest') + + cy.stub(location, 'replace').as('replaceLocation') + + cy.mount() + + cy.wait('@grantRequest') + + cy.get('h1').should('have.text', 'Overleaf v1 Project') + + cy.findByRole('link', { name: 'Download project zip file' }).should( + 'have.attr', + 'href', + '/overleaf/project/123/download/zip' + ) + }) +}) diff --git a/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx b/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx index 24710ff5b1..5ec018b489 100644 --- a/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx +++ b/services/web/test/frontend/features/editor-left-menu/components/actions-copy-project.test.jsx @@ -14,6 +14,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) }) diff --git a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx index 720eacf5a6..61504a597a 100644 --- a/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx +++ b/services/web/test/frontend/features/project-list/components/new-project-button/modal-content-new-project-form.test.tsx @@ -12,6 +12,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) }) diff --git a/services/web/test/frontend/features/project-list/components/notifications.test.tsx b/services/web/test/frontend/features/project-list/components/notifications.test.tsx index 661afab257..f6ec623ce4 100644 --- a/services/web/test/frontend/features/project-list/components/notifications.test.tsx +++ b/services/web/test/frontend/features/project-list/components/notifications.test.tsx @@ -699,6 +699,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) fetchMock.reset() diff --git a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx index b65464315c..eab21bb644 100644 --- a/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx +++ b/services/web/test/frontend/features/project-list/components/project-list-root.test.tsx @@ -57,6 +57,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) }) diff --git a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx index e736b01ab4..f330f71406 100644 --- a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx +++ b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/compile-and-download-project-pdf-button.test.tsx @@ -17,6 +17,7 @@ describe('', function () { assignStub = sinon.stub() locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) render( diff --git a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx index c75f05f622..cdfca90646 100644 --- a/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx +++ b/services/web/test/frontend/features/project-list/components/table/cells/action-buttons/download-project-button.test.tsx @@ -12,6 +12,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) render() diff --git a/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx b/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx index 2b4d74402c..2ee0653de6 100644 --- a/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/reconfirmation-info.test.tsx @@ -35,6 +35,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) }) diff --git a/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx b/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx index 2ae4f87e18..c391e78d8f 100644 --- a/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx +++ b/services/web/test/frontend/features/settings/components/leave/modal-form.test.tsx @@ -61,6 +61,7 @@ describe('', function () { assignStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: sinon.stub(), }) window.metaAttributesCache.set('ol-ExposedSettings', { isOverleaf: true }) diff --git a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx index c41efdcc52..254d9997b0 100644 --- a/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx +++ b/services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx @@ -88,6 +88,7 @@ describe('', function () { beforeEach(function () { this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: sinon.stub(), + replace: sinon.stub(), reload: sinon.stub(), }) fetchMock.get('/user/contacts', { contacts }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx index 2dc18ebbc5..91f319ee21 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/group-subscription-memberships.test.tsx @@ -82,6 +82,7 @@ describe('', function () { reloadStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: sinon.stub(), + replace: sinon.stub(), reload: reloadStub, }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx index 00a0ca4b1e..1f2fe3973d 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx @@ -54,6 +54,7 @@ describe('', function () { reloadStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: sinon.stub(), + replace: sinon.stub(), reload: reloadStub, }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx index 16fcd78e53..f91a117238 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx @@ -196,6 +196,7 @@ describe('', function () { beforeEach(function () { this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: assignStub, + replace: sinon.stub(), reload: reloadStub, }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx index 4e0542c6bc..aeaf1046ee 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active/change-plan/change-plan.test.tsx @@ -31,6 +31,7 @@ describe('', function () { reloadStub = sinon.stub() this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ assign: sinon.stub(), + replace: sinon.stub(), reload: reloadStub, }) }) diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js index fd0b9758e9..b731142949 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js +++ b/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.js @@ -66,6 +66,12 @@ describe('TokenAccessController', function () { }, } + this.SplitTestHandler = { + promises: { + getAssignment: sinon.stub().resolves({ variant: 'default' }), + }, + } + this.TokenAccessController = SandboxedModule.require(MODULE_PATH, { requires: { '@overleaf/settings': this.Settings, @@ -77,6 +83,7 @@ describe('TokenAccessController', function () { '../Authorization/AuthorizationMiddleware': this.AuthorizationMiddleware, '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler, + '../SplitTests/SplitTestHandler': this.SplitTestHandler, '../Errors/Errors': (this.Errors = { NotFoundError: sinon.stub() }), }, })