From 1c6ee3f930cdd35fc82450178123a40b73cb8175 Mon Sep 17 00:00:00 2001 From: CloudBuild Date: Sat, 24 May 2025 01:33:31 +0000 Subject: [PATCH 001/595] auto update translation GitOrigin-RevId: 4e2e3d1e7ca70f76f13f905753ba1ca2c945b72f --- services/web/locales/da.json | 1 - services/web/locales/de.json | 1 - services/web/locales/es.json | 1 - services/web/locales/fr.json | 1 - services/web/locales/it.json | 1 - services/web/locales/ja.json | 1 - services/web/locales/ko.json | 1 - services/web/locales/nl.json | 1 - services/web/locales/no.json | 1 - services/web/locales/pt.json | 1 - services/web/locales/ru.json | 1 - services/web/locales/sv.json | 1 - services/web/locales/tr.json | 1 - services/web/locales/zh-CN.json | 4 ---- 14 files changed, 17 deletions(-) diff --git a/services/web/locales/da.json b/services/web/locales/da.json index 86911ff083..3fc8910d50 100644 --- a/services/web/locales/da.json +++ b/services/web/locales/da.json @@ -5,7 +5,6 @@ "3_4_width": "¾ bredde", "About": "Om", "Account": "Konto", - "Account Settings": "Kontoindstillinger", "Documentation": "Dokumentation", "Projects": "Projekter", "Security": "Sikkerhed", diff --git a/services/web/locales/de.json b/services/web/locales/de.json index 928c95499e..c336215ee8 100644 --- a/services/web/locales/de.json +++ b/services/web/locales/de.json @@ -4,7 +4,6 @@ "3_4_width": "¾ Breite", "About": "Über uns", "Account": "Konto", - "Account Settings": "Kontoeinstellungen", "Documentation": "Dokumentation", "Projects": "Projekte", "Security": "Sicherheit", diff --git a/services/web/locales/es.json b/services/web/locales/es.json index 0565d9d541..2c9f5b1fbe 100644 --- a/services/web/locales/es.json +++ b/services/web/locales/es.json @@ -4,7 +4,6 @@ "3_4_width": "¾ ancho", "About": "Quiénes somos", "Account": "Cuenta", - "Account Settings": "Opciones de la cuenta", "Documentation": "Documentación", "Projects": "Proyectos", "Security": "Seguridad", diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json index b60de8ed5a..a47a785740 100644 --- a/services/web/locales/fr.json +++ b/services/web/locales/fr.json @@ -4,7 +4,6 @@ "3_4_width": "¾ largeur", "About": "À propos", "Account": "Compte", - "Account Settings": "Paramètres du compte", "Documentation": "Documentation", "Projects": "Projets", "Security": "Sécurité", diff --git a/services/web/locales/it.json b/services/web/locales/it.json index 2c5c880ad7..d8e0a1fa62 100644 --- a/services/web/locales/it.json +++ b/services/web/locales/it.json @@ -1,7 +1,6 @@ { "About": "About", "Account": "Account", - "Account Settings": "Impostazioni Account", "Documentation": "Documentazione", "Projects": "Progetti", "Security": "Sicurezza", diff --git a/services/web/locales/ja.json b/services/web/locales/ja.json index a6ddefe117..b96b4f5b75 100644 --- a/services/web/locales/ja.json +++ b/services/web/locales/ja.json @@ -1,7 +1,6 @@ { "About": "概要", "Account": "アカウント", - "Account Settings": "アカウントの設定", "Documentation": "ドキュメンテーション", "Projects": "プロジェクト", "Security": "セキュリティ", diff --git a/services/web/locales/ko.json b/services/web/locales/ko.json index cb320bd996..d3fdd36242 100644 --- a/services/web/locales/ko.json +++ b/services/web/locales/ko.json @@ -1,7 +1,6 @@ { "About": "소개", "Account": "계정", - "Account Settings": "계정 설정", "Documentation": "참고 문서", "Projects": "프로젝트", "Security": "보안", diff --git a/services/web/locales/nl.json b/services/web/locales/nl.json index 14e0d7b703..f3a60aa417 100644 --- a/services/web/locales/nl.json +++ b/services/web/locales/nl.json @@ -1,7 +1,6 @@ { "About": "Over", "Account": "Account", - "Account Settings": "Accountinstellingen", "Documentation": "Documentatie", "Projects": "Projecten", "Security": "Beveiliging", diff --git a/services/web/locales/no.json b/services/web/locales/no.json index 306970b1b3..73e98199ad 100644 --- a/services/web/locales/no.json +++ b/services/web/locales/no.json @@ -1,7 +1,6 @@ { "About": "Om", "Account": "Konto", - "Account Settings": "Kontoinnstillinger", "Documentation": "Dokumentasjon", "Projects": "Prosjekter", "Security": "Sikkerhet", diff --git a/services/web/locales/pt.json b/services/web/locales/pt.json index 8d2455c369..27add8a65b 100644 --- a/services/web/locales/pt.json +++ b/services/web/locales/pt.json @@ -1,7 +1,6 @@ { "About": "Sobre", "Account": "Conta", - "Account Settings": "Configurações da Conta", "Documentation": "Documentação", "Projects": "Projetos", "Security": "Segurança", diff --git a/services/web/locales/ru.json b/services/web/locales/ru.json index ed77938c53..13975aadf2 100644 --- a/services/web/locales/ru.json +++ b/services/web/locales/ru.json @@ -6,7 +6,6 @@ "3_4_width": "¾ ширины", "About": "О сайте", "Account": "Аккаунт", - "Account Settings": "Настройки аккаунта", "Documentation": "Документация", "Projects": "Проекты", "Security": "Безопасность", diff --git a/services/web/locales/sv.json b/services/web/locales/sv.json index da03604b4b..9304c00da6 100644 --- a/services/web/locales/sv.json +++ b/services/web/locales/sv.json @@ -1,7 +1,6 @@ { "About": "Om", "Account": "Konto", - "Account Settings": "Kontoinställningar", "Documentation": "Dokumentation", "Projects": "Projekt", "Security": "Säkerhet", diff --git a/services/web/locales/tr.json b/services/web/locales/tr.json index 163264953a..322497e9ee 100644 --- a/services/web/locales/tr.json +++ b/services/web/locales/tr.json @@ -1,7 +1,6 @@ { "About": "Hakkında", "Account": "Hesap", - "Account Settings": "Hesap Ayarları", "Documentation": "Dökümantasyon", "Projects": "Projeler", "Security": "Güvenlik", diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json index 6611d0f31d..e69edbbd6d 100644 --- a/services/web/locales/zh-CN.json +++ b/services/web/locales/zh-CN.json @@ -6,7 +6,6 @@ "3_4_width": "¾ 宽度", "About": "关于", "Account": "账户", - "Account Settings": "账户设置", "Documentation": "文档", "Projects": "项目", "Security": "安全性", @@ -1862,7 +1861,6 @@ "saml_response": "SAML 响应:", "save": "保存", "save_20_percent": "节省 20%", - "save_20_percent_when_you_switch_to_annual": "切换到年度计划可节省 20%", "save_or_cancel-cancel": "取消", "save_or_cancel-or": "或者", "save_or_cancel-save": "保存", @@ -2127,7 +2125,6 @@ "sure_you_want_to_delete": "您确定要永久删除以下文件吗?", "sure_you_want_to_leave_group": "您确定要退出该群吗?", "sv": "瑞典语", - "switch_back_to_monthly_pay_20_more": "切换回按月付费(增加 20%)", "switch_compile_mode_for_faster_draft_compilation": "切换编译模式以加快草稿编译速度", "switch_to_editor": "切换到编辑器", "switch_to_new_editor": "切换到新编辑器", @@ -2586,7 +2583,6 @@ "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z": "您是由 <1>__adminEmail__ 管理的 <1>__groupName__ 团队的、<0>__planName__ 计划的 <1>管理员", "you_are_a_manager_of_x_plan_as_member_of_group_subscription_y_administered_by_z_you": "您是<1>您 (__adminEmail__) 管理的<0>__planName__团体订阅<1>__groupName__的<1>管理员。", "you_are_currently_logged_in_as": "您当前以 __email__ 身份登录。", - "you_are_now_saving_20_percent": "您现在节省 20%", "you_are_on_a_paid_plan_contact_support_to_find_out_more": "您使用的是 __appName__ 付费计划。 <0>联系支持人员以了解更多信息。", "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "您作为 <1>__institutionName__ 的<1>确认成员加入了我们的<0>__planName__计划", "you_are_on_x_plan_as_member_of_group_subscription_y_administered_by_z": "您作为<1>__groupName__群组订阅的<1>成员加入了我们的<0>__planName__计划,该群组订阅由<1>__adminEmail__管理", From 43563158d377350909cfa5d9db8690d2d64d4148 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Tue, 27 May 2025 09:00:05 +0100 Subject: [PATCH 002/595] Merge pull request #25779 from overleaf/dp-recompile-button Update Recompile button to match figma designs GitOrigin-RevId: c3614fe2e621a64eb35dd4989b86c68a89bea342 --- .../components/pdf-compile-button.tsx | 4 +- .../bootstrap-5/pages/editor/pdf.scss | 49 +++++++++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx index b2b78d9e19..d693fe071f 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx @@ -75,11 +75,13 @@ function PdfCompileButton() { 'btn-striped-animated': hasChanges, }, 'no-left-border', - 'dropdown-button-toggle' + 'dropdown-button-toggle', + 'compile-dropdown-toggle' ) const buttonClassName = classNames( 'align-items-center py-0 no-left-radius px-3', + 'compile-button', { 'btn-striped-animated': hasChanges, } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss index df5c9e2b77..31000b0478 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/pdf.scss @@ -12,6 +12,40 @@ } } +.ide-redesign-main { + --pdf-bg: var(--bg-dark-secondary); + + .pdf-viewer { + .pdfjs-viewer { + .page { + box-shadow: + 0 5px 5px 0 #23282f0d, + 0 3px 14px 0 #23282f08, + 0 8px 10px 0 #23282f14; + } + } + } + + .toolbar-pdf-left { + .compile-button-group { + height: 24px; + border-radius: 12px; + margin-left: var(--spacing-02); + } + + .dropdown > .compile-button { + border-top-left-radius: 12px; + border-bottom-left-radius: 12px; + font-size: var(--font-size-02); + } + + .dropdown > .compile-dropdown-toggle { + width: 26px; + padding: var(--spacing-01); + } + } +} + .pdf .toolbar.toolbar-pdf { @include toolbar-sm-height; @include toolbar-alt-bg; @@ -158,21 +192,6 @@ top: var(--toolbar-small-height); } -.ide-redesign-main { - --pdf-bg: var(--bg-dark-secondary); - - .pdf-viewer { - .pdfjs-viewer { - .page { - box-shadow: - 0 5px 5px 0 #23282f0d, - 0 3px 14px 0 #23282f08, - 0 8px 10px 0 #23282f14; - } - } - } -} - .pdf-viewer { isolation: isolate; From 9000a3b70c5872de944e033b773f5d5b91c6cca9 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Tue, 27 May 2025 09:00:16 +0100 Subject: [PATCH 003/595] Merge pull request #25923 from overleaf/dp-view-dropdown Update UI of view dropdown GitOrigin-RevId: 2d689a73886e0821eaa21e6666092e9414528e55 --- .../features/ide-redesign/components/toolbar/menu-bar.tsx | 7 ++++++- .../js/shared/components/menu-bar/menu-bar-option.tsx | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx index e046eafde3..ed0ebd77f8 100644 --- a/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx @@ -1,6 +1,7 @@ import { DropdownDivider, DropdownHeader, + DropdownItem, } from '@/features/ui/components/bootstrap-5/dropdown-menu' import { MenuBar } from '@/shared/components/menu-bar/menu-bar' import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown' @@ -209,13 +210,17 @@ export const ToolbarMenuBar = () => { className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued" > + Editor settings + } onClick={toggleMathPreview} /> + setSelected(null)} onClick={onClick} disabled={disabled} + leadingIcon={leadingIcon} trailingIcon={trailingIcon} href={href} rel={rel} From f7b6246d41887b7d29ab569ffd03092c7a24f80f Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Tue, 27 May 2025 10:02:13 +0200 Subject: [PATCH 004/595] [web] Use 6-digits verification in project-list notifications (bis) (#25847) * Pull email context outside of `ResendConfirmationCodeModal` * Use `loading` prop of button instead of deprecated Icon * Swap notification order to clarify priority (no change in behaviour) * Replace confirmation link action by confirmationCodeModal, and simplify code * Change to secondary button variant in the Notification * Display errors within the modal * Increase ratelimit for resend-confirmation * Copy changes * Add stories on email confirmation notifications * Fix other Notification stories * Update tests * Update services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> * Remove placeholder on 6-digit code input --------- Co-authored-by: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> GitOrigin-RevId: dad8bfd79505a2e7d065fd48791fd57c8a31e071 --- services/web/app/src/router.mjs | 2 +- .../web/frontend/extracted-translations.json | 8 +- .../notifications/groups/confirm-email.tsx | 251 +++++++----------- .../components/emails/confirm-email-form.tsx | 31 ++- .../settings/components/emails/email.tsx | 15 +- .../emails/resend-confirmation-code-modal.tsx | 56 ++-- .../frontend/stories/hooks/use-fetch-mock.tsx | 1 + .../project-list/notifications.stories.tsx | 9 + .../notifications/confirm-email.stories.tsx | 66 +++++ services/web/locales/en.json | 8 +- .../components/notifications.test.tsx | 78 +++--- .../components/emails/emails-section.test.tsx | 22 +- 12 files changed, 282 insertions(+), 265 deletions(-) create mode 100644 services/web/frontend/stories/project-list/notifications/confirm-email.stories.tsx diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index f87297c35c..a7e8d5e05f 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -182,7 +182,7 @@ const rateLimiters = { duration: 60, }), sendConfirmation: new RateLimiter('send-confirmation', { - points: 1, + points: 2, duration: 60, }), sendChatMessage: new RateLimiter('send-chat-message', { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index c64817b94c..9862e47817 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -519,7 +519,6 @@ "enabling": "", "end_of_document": "", "ensure_recover_account": "", - "enter_6_digit_code": "", "enter_any_size_including_units_or_valid_latex_command": "", "enter_image_url": "", "enter_the_code": "", @@ -1224,8 +1223,8 @@ "please_check_your_inbox_to_confirm": "", "please_compile_pdf_before_download": "", "please_compile_pdf_before_word_count": "", - "please_confirm_primary_email": "", - "please_confirm_secondary_email": "", + "please_confirm_primary_email_or_edit": "", + "please_confirm_secondary_email_or_edit": "", "please_confirm_your_email_before_making_it_default": "", "please_contact_support_to_makes_change_to_your_plan": "", "please_enter_confirmation_code": "", @@ -1375,7 +1374,6 @@ "remote_service_error": "", "remove": "", "remove_access": "", - "remove_email_address": "", "remove_from_group": "", "remove_link": "", "remove_manager": "", @@ -1408,7 +1406,6 @@ "resend_link_sso": "", "resend_managed_user_invite": "", "resending_confirmation_code": "", - "resending_confirmation_email": "", "resize": "", "resolve_comment": "", "resolve_comment_error_message": "", @@ -1520,6 +1517,7 @@ "select_user": "", "selected": "", "selection_deleted": "", + "send_confirmation_code": "", "send_first_message": "", "send_message": "", "send_request": "", diff --git a/services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx b/services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx index ca73d87a0c..364e60fd3a 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx +++ b/services/web/frontend/js/features/project-list/components/notifications/groups/confirm-email.tsx @@ -1,16 +1,10 @@ import { Trans, useTranslation } from 'react-i18next' import Notification from '../notification' import getMeta from '../../../../../utils/meta' -import useAsync from '../../../../../shared/hooks/use-async' import { useProjectListContext } from '../../../context/project-list-context' -import { - postJSON, - getUserFacingMessage, -} from '../../../../../infrastructure/fetch-json' import { UserEmailData } from '../../../../../../../types/user-email' -import { debugConsole } from '@/utils/debugging' -import OLButton from '@/features/ui/components/ol/ol-button' -import LoadingSpinner from '@/shared/components/loading-spinner' +import ResendConfirmationCodeModal from '@/features/settings/components/emails/resend-confirmation-code-modal' +import { ReactNode, useState } from 'react' const ssoAvailable = ({ samlProviderId, affiliation }: UserEmailData) => { const { hasSamlFeature, hasSamlBeta } = getMeta('ol-ExposedSettings') @@ -114,12 +108,17 @@ function getEmailDeletionDate(emailData: UserEmailData, signUpDate: string) { function ConfirmEmailNotification({ userEmail, signUpDate, + setIsLoading, + isLoading, }: { userEmail: UserEmailData signUpDate: string + setIsLoading: (loading: boolean) => void + isLoading: boolean }) { const { t } = useTranslation() - const { isLoading, isSuccess, isError, error, runAsync } = useAsync() + const [isSuccess, setIsSuccess] = useState(false) + const emailAddress = userEmail.email // We consider secondary emails added on or after 22.03.2024 to be trusted for account recovery // https://github.com/overleaf/internal/pull/17572 @@ -127,6 +126,7 @@ function ConfirmEmailNotification({ const emailDeletionDate = getEmailDeletionDate(userEmail, signUpDate) const isPrimary = userEmail.default + const isEmailConfirmed = !!userEmail.lastConfirmedAt const isEmailTrusted = userEmail.lastConfirmedAt && new Date(userEmail.lastConfirmedAt) >= emailTrustCutoffDate @@ -134,163 +134,97 @@ function ConfirmEmailNotification({ const shouldShowCommonsNotification = emailHasLicenceAfterConfirming(userEmail) && isOnFreeOrIndividualPlan() - const handleResendConfirmationEmail = ({ email }: UserEmailData) => { - runAsync( - postJSON('/user/emails/resend_confirmation', { - body: { email }, - }) - ).catch(debugConsole.error) - } - if (isSuccess) { return null } - if (!userEmail.lastConfirmedAt && !shouldShowCommonsNotification) { - return ( - - {isLoading ? ( -
- -
- ) : isError ? ( -
{getUserFacingMessage(error)}
- ) : ( - <> -

- {isPrimary - ? t('please_confirm_primary_email', { - emailAddress: userEmail.email, - }) - : t('please_confirm_secondary_email', { - emailAddress: userEmail.email, - })} -

- {emailDeletionDate && ( -

- {t('email_remove_by_date', { date: emailDeletionDate })} -

- )} - - )} - - } - action={ - <> - handleResendConfirmationEmail(userEmail)} - > - {t('resend_confirmation_email')} - - - {isPrimary - ? t('change_primary_email') - : t('remove_email_address')} - - - } - /> - ) - } + const confirmationCodeModal = ( + setIsSuccess(true)} + setGroupLoading={setIsLoading} + groupLoading={isLoading} + triggerVariant="secondary" + /> + ) - if (!isEmailTrusted && !isPrimary && !shouldShowCommonsNotification) { - return ( - - {isLoading ? ( -
- -
- ) : isError ? ( -
{getUserFacingMessage(error)}
- ) : ( - <> -

- {t('confirm_secondary_email')} -

-

- {t('reconfirm_secondary_email', { - emailAddress: userEmail.email, - })} -

-

{t('ensure_recover_account')}

- - )} - - } - action={ - <> - handleResendConfirmationEmail(userEmail)} - > - {t('resend_confirmation_email')} - - - {t('remove_email_address')} - - - } - /> - ) - } + let notificationType: 'info' | 'warning' | undefined + let notificationBody: ReactNode | undefined - // Only show the notification if a) a commons license is available and b) the - // user is on a free or individual plan. Users on a group or Commons plan - // already have premium features. if (shouldShowCommonsNotification) { + notificationType = 'info' + notificationBody = ( + <> + ]} // eslint-disable-line react/jsx-key + /> +
+ ]} // eslint-disable-line react/jsx-key + /> + + ) + } else if (!isEmailConfirmed) { + notificationType = 'warning' + notificationBody = ( + <> +

+ {isPrimary ? ( + , + ]} + /> + ) : ( + , + ]} + /> + )} +

+ {emailDeletionDate && ( +

{t('email_remove_by_date', { date: emailDeletionDate })}

+ )} + + ) + } else if (!isEmailTrusted && !isPrimary) { + notificationType = 'warning' + notificationBody = ( + <> +

+ {t('confirm_secondary_email')} +

+

{t('reconfirm_secondary_email', { emailAddress })}

+

{t('ensure_recover_account')}

+ + ) + } + + if (notificationType) { return ( - {isLoading ? ( - - ) : isError ? ( -
{getUserFacingMessage(error)}
- ) : ( - <> - ]} // eslint-disable-line react/jsx-key - /> -
- ]} // eslint-disable-line react/jsx-key - /> - - )} - - } - action={ - handleResendConfirmationEmail(userEmail)} - > - {t('resend_email')} - - } + type={notificationType} + content={notificationBody} + action={confirmationCodeModal} /> ) } @@ -302,6 +236,7 @@ function ConfirmEmail() { const { totalProjectsCount } = useProjectListContext() const userEmails = getMeta('ol-userEmails') || [] const signUpDate = getMeta('ol-user')?.signUpDate + const [isLoading, setIsLoading] = useState(false) if (!totalProjectsCount || !userEmails.length || !signUpDate) { return null @@ -315,6 +250,8 @@ function ConfirmEmail() { key={`confirm-email-${userEmail.email}`} userEmail={userEmail} signUpDate={signUpDate} + isLoading={isLoading} + setIsLoading={setIsLoading} /> ) : null })} diff --git a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx index 66aaee9cce..d82a43315c 100644 --- a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx +++ b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx @@ -2,7 +2,7 @@ import { postJSON } from '@/infrastructure/fetch-json' import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' import Notification from '@/shared/components/notification' import getMeta from '@/utils/meta' -import { FormEvent, MouseEventHandler, useState } from 'react' +import { FormEvent, MouseEventHandler, ReactNode, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import LoadingSpinner from '@/shared/components/loading-spinner' import MaterialIcon from '@/shared/components/material-icon' @@ -27,6 +27,7 @@ type ConfirmEmailFormProps = { interstitial: boolean isModal?: boolean onCancel?: () => void + outerError?: string } export function ConfirmEmailForm({ @@ -40,15 +41,17 @@ export function ConfirmEmailForm({ interstitial, isModal, onCancel, + outerError, }: ConfirmEmailFormProps) { const { t } = useTranslation() const [confirmationCode, setConfirmationCode] = useState('') const [feedback, setFeedback] = useState(null) const [isConfirming, setIsConfirming] = useState(false) const [isResending, setIsResending] = useState(false) + const [hasResent, setHasResent] = useState(false) const [successRedirectPath, setSuccessRedirectPath] = useState('') const { isReady } = useWaitForI18n() - + const outerErrorDisplay = (!hasResent && outerError) || null const errorHandler = (err: any, actionType?: string) => { let errorName = err?.data?.message?.key || 'generic_something_went_wrong' @@ -131,6 +134,7 @@ export function ConfirmEmailForm({ }) .finally(() => { setIsResending(false) + setHasResent(true) }) sendMB('email-verification-click', { @@ -158,8 +162,15 @@ export function ConfirmEmailForm({ ) } - let intro =
{t('confirm_your_email')}
- if (isModal) intro =
{t('we_sent_code')}
+ let intro: ReactNode | null = ( +
{t('confirm_your_email')}
+ ) + if (isModal) + intro = outerErrorDisplay ? ( +
+ ) : ( +

{outerErrorDisplay ? null : t('we_sent_code')}

+ ) if (interstitial) intro = (

{t('confirm_your_email')}

@@ -172,12 +183,14 @@ export function ConfirmEmailForm({ className="confirm-email-form" >
- {feedback?.type === 'alert' && ( + {(feedback?.type === 'alert' || outerErrorDisplay) && ( } + type={outerErrorDisplay ? 'error' : feedback!.style} + content={ + outerErrorDisplay || + } /> )} @@ -191,7 +204,6 @@ export function ConfirmEmailForm({
{feedback?.type === 'input' && ( @@ -214,7 +227,7 @@ export function ConfirmEmailForm({
{t('unconfirmed')}.
{!ssoAvailable && ( - + )}
)} diff --git a/services/web/frontend/js/features/settings/components/emails/resend-confirmation-code-modal.tsx b/services/web/frontend/js/features/settings/components/emails/resend-confirmation-code-modal.tsx index 0c7b1394fe..b17337924e 100644 --- a/services/web/frontend/js/features/settings/components/emails/resend-confirmation-code-modal.tsx +++ b/services/web/frontend/js/features/settings/components/emails/resend-confirmation-code-modal.tsx @@ -1,10 +1,8 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Icon from '../../../../shared/components/icon' import { FetchError, postJSON } from '@/infrastructure/fetch-json' import useAsync from '../../../../shared/hooks/use-async' import { UserEmailData } from '../../../../../../types/user-email' -import { useUserEmailsContext } from '../../context/user-email-context' import OLButton from '@/features/ui/components/ol/ol-button' import OLModal, { OLModalBody, @@ -16,39 +14,33 @@ import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm- type ResendConfirmationEmailButtonProps = { email: UserEmailData['email'] + groupLoading: boolean + setGroupLoading: (loading: boolean) => void + onSuccess: () => void + triggerVariant: 'link' | 'secondary' } function ResendConfirmationCodeModal({ email, + groupLoading, + setGroupLoading, + onSuccess, + triggerVariant, }: ResendConfirmationEmailButtonProps) { const { t } = useTranslation() const { error, isLoading, isError, runAsync } = useAsync() - const { - state, - setLoading: setUserEmailsContextLoading, - getEmails, - } = useUserEmailsContext() const [modalVisible, setModalVisible] = useState(false) - // Update global isLoading prop useEffect(() => { - setUserEmailsContextLoading(isLoading) - }, [setUserEmailsContextLoading, isLoading]) + setGroupLoading(isLoading) + }, [isLoading, setGroupLoading]) const handleResendConfirmationEmail = async () => { await runAsync( postJSON('/user/emails/send-confirmation-code', { body: { email } }) ) - .then(() => setModalVisible(true)) .catch(() => {}) - } - - if (isLoading) { - return ( - <> - {t('sending')}… - - ) + .finally(() => setModalVisible(true)) } const rateLimited = @@ -77,9 +69,16 @@ function ResendConfirmationCodeModal({ confirmationEndpoint="/user/emails/confirm-code" email={email} onSuccessfulConfirmation={() => { - getEmails() + onSuccess() setModalVisible(false) }} + outerError={ + isError + ? rateLimited + ? t('too_many_requests') + : t('generic_something_went_wrong') + : undefined + } /> @@ -94,21 +93,14 @@ function ResendConfirmationCodeModal({ )} - {t('resend_confirmation_code')} + {t('send_confirmation_code')} -
- {isError && ( -
- {rateLimited - ? t('too_many_requests') - : t('generic_something_went_wrong')} -
- )} ) } diff --git a/services/web/frontend/stories/hooks/use-fetch-mock.tsx b/services/web/frontend/stories/hooks/use-fetch-mock.tsx index 7f00118aac..304d9e0273 100644 --- a/services/web/frontend/stories/hooks/use-fetch-mock.tsx +++ b/services/web/frontend/stories/hooks/use-fetch-mock.tsx @@ -10,6 +10,7 @@ export default function useFetchMock( fetchMock.mockGlobal() useLayoutEffect(() => { + fetchMock.mockGlobal() callback(fetchMock) return () => { fetchMock.removeRoutes() diff --git a/services/web/frontend/stories/project-list/notifications.stories.tsx b/services/web/frontend/stories/project-list/notifications.stories.tsx index aaabe2ba5b..90fa82bfa5 100644 --- a/services/web/frontend/stories/project-list/notifications.stories.tsx +++ b/services/web/frontend/stories/project-list/notifications.stories.tsx @@ -14,6 +14,8 @@ import { setReconfirmationMeta, } from './helpers/emails' import { useMeta } from '../hooks/use-meta' +import { SplitTestProvider } from '@/shared/context/split-test-context' +import React, { ComponentType } from 'react' export const ProjectInvite = (args: any) => { useFetchMock(commonSetupMocks) @@ -343,4 +345,11 @@ export const ReconfirmedAffiliationSuccess = (args: any) => { export default { title: 'Project List / Notifications', component: UserNotifications, + decorators: [ + (Story: ComponentType) => ( + + + + ), + ], } diff --git a/services/web/frontend/stories/project-list/notifications/confirm-email.stories.tsx b/services/web/frontend/stories/project-list/notifications/confirm-email.stories.tsx new file mode 100644 index 0000000000..5bbcbcf8fb --- /dev/null +++ b/services/web/frontend/stories/project-list/notifications/confirm-email.stories.tsx @@ -0,0 +1,66 @@ +import { SplitTestProvider } from '@/shared/context/split-test-context' +import UserNotifications from '../../../js/features/project-list/components/notifications/user-notifications' +import { ProjectListProvider } from '../../../js/features/project-list/context/project-list-context' +import { useMeta } from '../../hooks/use-meta' +import useFetchMock from '../../hooks/use-fetch-mock' +import ConfirmEmailNotification from '@/features/project-list/components/notifications/groups/confirm-email' + +export const ConfirmEmail = (args: any) => { + useMeta({ + 'ol-userEmails': [ + { + email: 'erika.mustermann+unconfirmed-primary@example.com', + default: true, + }, + { email: 'erika.mustermann+unconfirmed@example.com' }, + { + email: 'erika.mustermann+untrusted@example.com', + lastConfirmedAt: '2019-01-01', + confirmedAt: '2019-01-01', + }, + { + email: 'erika.mustermann+mit@example.com', + affiliation: { + institution: { + id: 123, + name: 'Massachusetts Institute of Technology', + confirmed: true, + commonsAccount: true, + }, + }, + }, + ], + 'ol-user': { signUpDate: '2021-01-01' }, + 'ol-usersBestSubscription': { type: 'free' }, + 'ol-prefetchedProjectsBlob': { totalSize: 20 }, + }) + useFetchMock(fetchMock => { + fetchMock.post('/user/emails/send-confirmation-code', args.statusCode, { + delay: 500, + }) + fetchMock.post('/user/emails/confirm-code', args.statusCode, { + delay: 500, + }) + fetchMock.post('/user/emails/resend-confirmation-code', args.statusCode, { + delay: 500, + }) + }) + return ( + + + + + + ) +} + +export default { + title: 'Project List / Notifications', + component: ConfirmEmailNotification, + args: { + statusCode: 200, + }, + argTypes: { + statusCode: { type: 'select', options: [200, 400, 429] }, + }, +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4729f54756..910621f51a 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -669,7 +669,6 @@ "enabling": "Enabling", "end_of_document": "End of document", "ensure_recover_account": "This will ensure that it can be used to recover your __appName__ account in case you lose access to your primary email address.", - "enter_6_digit_code": "Enter 6-digit code", "enter_any_size_including_units_or_valid_latex_command": "Enter any size (including units) or valid LaTeX command", "enter_image_url": "Enter image URL", "enter_the_code": "Enter the 6-digit code sent to __email__.", @@ -1627,8 +1626,8 @@ "please_compile_pdf_before_download": "Please compile your project before downloading the PDF", "please_compile_pdf_before_word_count": "Please compile your project before performing a word count", "please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ", - "please_confirm_primary_email": "Please confirm your primary email address __emailAddress__ by clicking on the link in the confirmation email.", - "please_confirm_secondary_email": "Please confirm your secondary email address __emailAddress__ by clicking on the link in the confirmation email.", + "please_confirm_primary_email_or_edit": "Please confirm your primary email address __emailAddress__. To edit it, go to <0>Account settings.", + "please_confirm_secondary_email_or_edit": "Please confirm your secondary email address __emailAddress__. To edit it, go to <0>Account settings.", "please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.", "please_contact_support_to_makes_change_to_your_plan": "Please <0>contact Support to make changes to your plan", "please_contact_us_if_you_think_this_is_in_error": "Please <0>contact us if you think this is in error.", @@ -1810,7 +1809,6 @@ "remote_service_error": "The remote service produced an error", "remove": "Remove", "remove_access": "Remove access", - "remove_email_address": "Remove email address", "remove_from_group": "Remove from group", "remove_link": "Remove link", "remove_manager": "Remove manager", @@ -1853,7 +1851,6 @@ "resend_link_sso": "Resend SSO invite", "resend_managed_user_invite": "Resend managed user invite", "resending_confirmation_code": "Resending confirmation code", - "resending_confirmation_email": "Resending confirmation email", "reset_password": "Reset Password", "reset_password_link": "Click this link to reset your password", "reset_password_sentence_case": "Reset password", @@ -1987,6 +1984,7 @@ "selected_by_overleaf_staff": "Selected by Overleaf staff", "selection_deleted": "Selection deleted", "send": "Send", + "send_confirmation_code": "Send confirmation code", "send_first_message": "Send your first message to your collaborators", "send_message": "Send message", "send_request": "Send request", 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 7197ddb365..78c732ebe3 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 @@ -5,7 +5,6 @@ import { render, screen, waitForElementToBeRemoved, - within, } from '@testing-library/react' import fetchMock from 'fetch-mock' import { merge, cloneDeep } from 'lodash' @@ -672,32 +671,37 @@ describe('', function () { renderWithinProjectListProvider(ConfirmEmail) await fetchMock.callHistory.flush(true) - fetchMock.post('/user/emails/resend_confirmation', 200) + fetchMock.post('/user/emails/send-confirmation-code', 200) const email = userEmails[0].email - const notificationBody = await screen.findByTestId( - 'pro-notification-body' - ) + const alert = await screen.findByRole('alert') if (isPrimary) { - expect(notificationBody.textContent).to.contain( - `Please confirm your primary email address ${email} by clicking on the link in the confirmation email.` + expect(alert.textContent).to.contain( + `Please confirm your primary email address ${email}. To edit it, go to ` ) } else { - expect(notificationBody.textContent).to.contain( - `Please confirm your secondary email address ${email} by clicking on the link in the confirmation email.` + expect(alert.textContent).to.contain( + `Please confirm your secondary email address ${email}. To edit it, go to ` ) } - const resendButton = screen.getByRole('button', { name: /resend/i }) - fireEvent.click(resendButton) + expect( + screen + .getByRole('button', { name: 'Send confirmation code' }) + .classList.contains('button-loading') + ).to.be.false - await waitForElementToBeRemoved(() => - screen.queryByRole('button', { name: /resend/i }) - ) + expect(screen.queryByRole('dialog')).to.be.null + + const sendCodeButton = await screen.findByRole('button', { + name: 'Send confirmation code', + }) + fireEvent.click(sendCodeButton) + + await screen.findByRole('dialog') expect(fetchMock.callHistory.called()).to.be.true - expect(screen.queryByRole('alert')).to.be.null }) } @@ -716,25 +720,22 @@ describe('', function () { renderWithinProjectListProvider(ConfirmEmail) await fetchMock.callHistory.flush(true) - fetchMock.post('/user/emails/resend_confirmation', 200) + fetchMock.post('/user/emails/send-confirmation-code', 200) const email = untrustedUserData.email - const notificationBody = await screen.findByTestId( - 'not-trusted-notification-body' - ) - expect(notificationBody.textContent).to.contain( + const alert = await screen.findByRole('alert') + expect(alert.textContent).to.contain( `To enhance the security of your Overleaf account, please reconfirm your secondary email address ${email}.` ) - const resendButton = screen.getByRole('button', { name: /resend/i }) + const resendButton = screen.getByRole('button', { + name: 'Send confirmation code', + }) fireEvent.click(resendButton) - await waitForElementToBeRemoved(() => - screen.getByRole('button', { name: /resend/i }) - ) + await screen.findByRole('dialog') expect(fetchMock.callHistory.called()).to.be.true - expect(screen.queryByRole('alert')).to.be.null }) it('fails to send', async function () { @@ -742,20 +743,15 @@ describe('', function () { renderWithinProjectListProvider(ConfirmEmail) await fetchMock.callHistory.flush(true) - fetchMock.post('/user/emails/resend_confirmation', 500) + fetchMock.post('/user/emails/send-confirmation-code', 500) const resendButtons = await screen.findAllByRole('button', { - name: /resend/i, + name: 'Send confirmation code', }) const resendButton = resendButtons[0] fireEvent.click(resendButton) - const notificationBody = screen.getByTestId('pro-notification-body') - await waitForElementToBeRemoved(() => - within(notificationBody).getByTestId( - 'loading-resending-confirmation-email' - ) - ) + await screen.findByRole('dialog') expect(fetchMock.callHistory.called()).to.be.true screen.getByText(/something went wrong/i) @@ -773,11 +769,10 @@ describe('', function () { const alert = await screen.findByRole('alert') const email = unconfirmedCommonsUserData.email - const notificationBody = within(alert).getByTestId('notification-body') - expect(notificationBody.textContent).to.contain( + expect(alert.textContent).to.contain( 'You are one step away from accessing Overleaf Professional features' ) - expect(notificationBody.textContent).to.contain( + expect(alert.textContent).to.contain( `Overleaf has an Overleaf subscription. Click the confirmation link sent to ${email} to upgrade to Overleaf Professional` ) }) @@ -794,17 +789,14 @@ describe('', function () { const alert = await screen.findByRole('alert') const email = unconfirmedCommonsUserData.email - const notificationBody = within(alert).getByTestId( - 'pro-notification-body' - ) const isPrimary = unconfirmedCommonsUserData.default if (isPrimary) { - expect(notificationBody.textContent).to.contain( - `Please confirm your primary email address ${email} by clicking on the link in the confirmation email` + expect(alert.textContent).to.contain( + `Please confirm your primary email address ${email}.` ) } else { - expect(notificationBody.textContent).to.contain( - `Please confirm your secondary email address ${email} by clicking on the link in the confirmation email` + expect(alert.textContent).to.contain( + `Please confirm your secondary email address ${email}.` ) } }) diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx index e784f6aaac..55c833df1c 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx @@ -99,7 +99,7 @@ describe('', function () { fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData]) render() - await screen.findByRole('button', { name: /resend confirmation code/i }) + await screen.findByRole('button', { name: 'Send confirmation code' }) }) it('renders professional label', async function () { @@ -121,24 +121,24 @@ describe('', function () { fetchMock.post('/user/emails/send-confirmation-code', 200) const button = screen.getByRole('button', { - name: /resend confirmation code/i, + name: 'Send confirmation code', }) fireEvent.click(button) expect( screen.queryByRole('button', { - name: /resend confirmation code/i, + name: 'Send confirmation code', }) ).to.be.null - await waitForElementToBeRemoved(() => screen.getByText(/sending/i)) + await screen.findByRole('dialog') expect( screen.queryByText(/an error has occurred while performing your request/i) ).to.be.null await screen.findAllByRole('button', { - name: /resend confirmation code/i, + name: 'Resend confirmation code', }) }) @@ -151,17 +151,17 @@ describe('', function () { fetchMock.post('/user/emails/send-confirmation-code', 503) const button = screen.getByRole('button', { - name: /resend confirmation code/i, + name: 'Send confirmation code', }) fireEvent.click(button) - expect(screen.queryByRole('button', { name: /resend confirmation code/i })) - .to.be.null + expect(screen.queryByRole('button', { name: 'Send confirmation code' })).to + .be.null - await waitForElementToBeRemoved(() => screen.getByText(/sending/i)) + await screen.findByRole('dialog') - screen.getByText(/sorry, something went wrong/i) - screen.getByRole('button', { name: /resend confirmation code/i }) + await screen.findByText(/sorry, something went wrong/i) + screen.getByRole('button', { name: 'Resend confirmation code' }) }) it('sorts emails with primary first, then confirmed, then unconfirmed', async function () { From 344405cdcb54f891de6ac86909efe345fa8ff0b7 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Tue, 27 May 2025 10:03:06 +0200 Subject: [PATCH 005/595] Revert case-insensitivity in e2e tests (#25828) * Revert case-insensitivity in e2e tests * Use `{ exact: false }` to filter createProject type * Update server-ce/test/helpers/project.ts Co-authored-by: Jakob Ackermann --------- Co-authored-by: Jakob Ackermann GitOrigin-RevId: b8b2f8439a55e9527358b13d9292779dc3509e9d --- server-ce/test/git-bridge.spec.ts | 2 +- server-ce/test/helpers/project.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server-ce/test/git-bridge.spec.ts b/server-ce/test/git-bridge.spec.ts index 447f28bfd2..1f114574ac 100644 --- a/server-ce/test/git-bridge.spec.ts +++ b/server-ce/test/git-bridge.spec.ts @@ -107,7 +107,7 @@ describe('git-bridge', function () { cy.get('code').contains(`git clone ${gitURL(id.toString())}`) }) cy.findByText('Generate token').should('not.exist') - cy.findByText(/generate a new one in Account settings/i) + cy.findByText(/generate a new one in Account settings/) cy.findByText('Go to settings') .should('have.attr', 'target', '_blank') .and('have.attr', 'href', '/user/settings') diff --git a/server-ce/test/helpers/project.ts b/server-ce/test/helpers/project.ts index 8fb6aa2404..abcce3f9b2 100644 --- a/server-ce/test/helpers/project.ts +++ b/server-ce/test/helpers/project.ts @@ -37,7 +37,8 @@ export function createProject( } cy.findAllByRole('button').contains(newProjectButtonMatcher).click() // FIXME: This should only look in the left menu - cy.findAllByText(new RegExp(type, 'i')).first().click() + // The upgrading tests create projects in older versions of Server Pro which used different casing of the project type. Use case-insensitive match. + cy.findAllByText(type, { exact: false }).first().click() cy.findByRole('dialog').within(() => { cy.get('input').type(name) cy.findByText('Create').click() From 4315777638691776154ca80b077c05a4d54087cd Mon Sep 17 00:00:00 2001 From: Miguel Serrano Date: Tue, 27 May 2025 11:44:39 +0200 Subject: [PATCH 006/595] Merge pull request #25916 from overleaf/msm-git-bridge-bump-jgit [git-bridge] bump `jgit` to `6.10.1` GitOrigin-RevId: a1ffaa68a2eaca278c48acaf8e9d72b06c0cf29a --- services/git-bridge/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/git-bridge/pom.xml b/services/git-bridge/pom.xml index 7b2c5b8e55..840eb57721 100644 --- a/services/git-bridge/pom.xml +++ b/services/git-bridge/pom.xml @@ -19,7 +19,7 @@ 9.4.57.v20241219 2.9.0 3.0.1 - 6.6.1.202309021850-r + 6.10.1.202505221210-r 3.41.2.2 2.9.9 1.37.0 From 13fa735da05d2bd6925c82b0c2c22afb0d1926d9 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Tue, 27 May 2025 08:03:08 -0400 Subject: [PATCH 007/595] Merge pull request #25869 from overleaf/em-split-editor-facade Split EditorFacade functionality for history OT GitOrigin-RevId: 1e415e1d058c0de0b27271a9a5d7208b4a8a689b --- .../features/ide-react/editor/share-js-doc.ts | 2 +- .../source-editor/extensions/realtime.ts | 73 ++++++++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts index 96e866afec..a773684dcb 100644 --- a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts +++ b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts @@ -365,7 +365,7 @@ export class ShareJsDoc extends EventEmitter { attachToCM6(cm6: EditorFacade) { this.attachToEditor(cm6, () => { - cm6.attachShareJs(this._doc, getMeta('ol-maxDocLength')) + cm6.attachShareJs(this._doc, getMeta('ol-maxDocLength'), this.type) }) } diff --git a/services/web/frontend/js/features/source-editor/extensions/realtime.ts b/services/web/frontend/js/features/source-editor/extensions/realtime.ts index 36d9956a76..44c28cb9ad 100644 --- a/services/web/frontend/js/features/source-editor/extensions/realtime.ts +++ b/services/web/frontend/js/features/source-editor/extensions/realtime.ts @@ -5,6 +5,7 @@ import RangesTracker from '@overleaf/ranges-tracker' import { ShareDoc } from '../../../../../types/share-doc' import { debugConsole } from '@/utils/debugging' import { DocumentContainer } from '@/features/ide-react/editor/document-container' +import { OTType } from '@/features/ide-react/editor/share-js-doc' /* * Integrate CodeMirror 6 with the real-time system, via ShareJS. @@ -76,15 +77,22 @@ export const realtime = ( return Prec.highest([realtimePlugin, ensureRealtimePlugin]) } +type OTAdapter = { + handleUpdateFromCM( + transactions: readonly Transaction[], + ranges?: RangesTracker + ): void + attachShareJs(): void +} + export class EditorFacade extends EventEmitter { - public shareDoc: ShareDoc | null + private otAdapter: OTAdapter | null public events: EventEmitter - private maxDocLength?: number constructor(public view: EditorView) { super() this.view = view - this.shareDoc = null + this.otAdapter = null this.events = new EventEmitter() } @@ -118,23 +126,56 @@ export class EditorFacade extends EventEmitter { this.cmChange({ from: position, to: position + text.length }, origin) } + attachShareJs(shareDoc: ShareDoc, maxDocLength?: number, type?: OTType) { + this.otAdapter = + type === 'history-ot' + ? new HistoryOTAdapter(this, shareDoc, maxDocLength) + : new ShareLatexOTAdapter(this, shareDoc, maxDocLength) + this.otAdapter.attachShareJs() + } + + detachShareJs() { + this.otAdapter = null + } + + handleUpdateFromCM( + transactions: readonly Transaction[], + ranges?: RangesTracker + ) { + if (this.otAdapter == null) { + throw new Error('Trying to process updates with no otAdapter') + } + + this.otAdapter.handleUpdateFromCM(transactions, ranges) + } +} + +class ShareLatexOTAdapter { + constructor( + public editor: EditorFacade, + private shareDoc: ShareDoc, + private maxDocLength?: number + ) { + this.editor = editor + this.shareDoc = shareDoc + this.maxDocLength = maxDocLength + } + // Connect to ShareJS, passing changes to the CodeMirror view // as new transactions. // This is a broad immitation of helper functions supplied in // the sharejs library. (See vendor/libs/sharejs, in particular // the 'attach_ace' helper) - attachShareJs(shareDoc: ShareDoc, maxDocLength?: number) { - this.shareDoc = shareDoc - this.maxDocLength = maxDocLength - + attachShareJs() { + const shareDoc = this.shareDoc const check = () => { // run in a timeout so it checks the editor content once this update has been applied window.setTimeout(() => { - const editorText = this.getValue() + const editorText = this.editor.getValue() const otText = shareDoc.getText() if (editorText !== otText) { - shareDoc.emit('error', 'Text does not match in CodeMirror 6') + this.shareDoc.emit('error', 'Text does not match in CodeMirror 6') debugConsole.error('Text does not match!') debugConsole.error('editor: ' + editorText) debugConsole.error('ot: ' + otText) @@ -143,12 +184,12 @@ export class EditorFacade extends EventEmitter { } const onInsert = (pos: number, text: string) => { - this.cmInsert(pos, text, 'remote') + this.editor.cmInsert(pos, text, 'remote') check() } const onDelete = (pos: number, text: string) => { - this.cmDelete(pos, text, 'remote') + this.editor.cmDelete(pos, text, 'remote') check() } @@ -161,7 +202,7 @@ export class EditorFacade extends EventEmitter { shareDoc.removeListener('insert', onInsert) shareDoc.removeListener('delete', onDelete) delete shareDoc.detach_cm6 - this.shareDoc = null + this.editor.detachShareJs() } } @@ -175,10 +216,6 @@ export class EditorFacade extends EventEmitter { const trackedDeletesLength = ranges != null ? ranges.getTrackedDeletesLength() : 0 - if (!shareDoc) { - throw new Error('Trying to process updates with no shareDoc') - } - for (const transaction of transactions) { if (transaction.docChanged) { const origin = chooseOrigin(transaction) @@ -234,7 +271,7 @@ export class EditorFacade extends EventEmitter { removed, } - this.emit('change', this, changeDescription) + this.editor.emit('change', this, changeDescription) } ) } @@ -242,6 +279,8 @@ export class EditorFacade extends EventEmitter { } } +class HistoryOTAdapter extends ShareLatexOTAdapter {} + export const trackChangesAnnotation = Annotation.define() const chooseOrigin = (transaction: Transaction) => { From 25adb7e3039a77d5216611949c74dff862c4a663 Mon Sep 17 00:00:00 2001 From: Eric Mc Sween <5454374+emcsween@users.noreply.github.com> Date: Tue, 27 May 2025 08:25:30 -0400 Subject: [PATCH 008/595] Merge pull request #25949 from overleaf/revert-25869-em-split-editor-facade Revert "Split EditorFacade functionality for history OT" GitOrigin-RevId: a55328e08776fa0f59071fca955ba73ef130984d --- .../features/ide-react/editor/share-js-doc.ts | 2 +- .../source-editor/extensions/realtime.ts | 73 +++++-------------- 2 files changed, 18 insertions(+), 57 deletions(-) diff --git a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts index a773684dcb..96e866afec 100644 --- a/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts +++ b/services/web/frontend/js/features/ide-react/editor/share-js-doc.ts @@ -365,7 +365,7 @@ export class ShareJsDoc extends EventEmitter { attachToCM6(cm6: EditorFacade) { this.attachToEditor(cm6, () => { - cm6.attachShareJs(this._doc, getMeta('ol-maxDocLength'), this.type) + cm6.attachShareJs(this._doc, getMeta('ol-maxDocLength')) }) } diff --git a/services/web/frontend/js/features/source-editor/extensions/realtime.ts b/services/web/frontend/js/features/source-editor/extensions/realtime.ts index 44c28cb9ad..36d9956a76 100644 --- a/services/web/frontend/js/features/source-editor/extensions/realtime.ts +++ b/services/web/frontend/js/features/source-editor/extensions/realtime.ts @@ -5,7 +5,6 @@ import RangesTracker from '@overleaf/ranges-tracker' import { ShareDoc } from '../../../../../types/share-doc' import { debugConsole } from '@/utils/debugging' import { DocumentContainer } from '@/features/ide-react/editor/document-container' -import { OTType } from '@/features/ide-react/editor/share-js-doc' /* * Integrate CodeMirror 6 with the real-time system, via ShareJS. @@ -77,22 +76,15 @@ export const realtime = ( return Prec.highest([realtimePlugin, ensureRealtimePlugin]) } -type OTAdapter = { - handleUpdateFromCM( - transactions: readonly Transaction[], - ranges?: RangesTracker - ): void - attachShareJs(): void -} - export class EditorFacade extends EventEmitter { - private otAdapter: OTAdapter | null + public shareDoc: ShareDoc | null public events: EventEmitter + private maxDocLength?: number constructor(public view: EditorView) { super() this.view = view - this.otAdapter = null + this.shareDoc = null this.events = new EventEmitter() } @@ -126,56 +118,23 @@ export class EditorFacade extends EventEmitter { this.cmChange({ from: position, to: position + text.length }, origin) } - attachShareJs(shareDoc: ShareDoc, maxDocLength?: number, type?: OTType) { - this.otAdapter = - type === 'history-ot' - ? new HistoryOTAdapter(this, shareDoc, maxDocLength) - : new ShareLatexOTAdapter(this, shareDoc, maxDocLength) - this.otAdapter.attachShareJs() - } - - detachShareJs() { - this.otAdapter = null - } - - handleUpdateFromCM( - transactions: readonly Transaction[], - ranges?: RangesTracker - ) { - if (this.otAdapter == null) { - throw new Error('Trying to process updates with no otAdapter') - } - - this.otAdapter.handleUpdateFromCM(transactions, ranges) - } -} - -class ShareLatexOTAdapter { - constructor( - public editor: EditorFacade, - private shareDoc: ShareDoc, - private maxDocLength?: number - ) { - this.editor = editor - this.shareDoc = shareDoc - this.maxDocLength = maxDocLength - } - // Connect to ShareJS, passing changes to the CodeMirror view // as new transactions. // This is a broad immitation of helper functions supplied in // the sharejs library. (See vendor/libs/sharejs, in particular // the 'attach_ace' helper) - attachShareJs() { - const shareDoc = this.shareDoc + attachShareJs(shareDoc: ShareDoc, maxDocLength?: number) { + this.shareDoc = shareDoc + this.maxDocLength = maxDocLength + const check = () => { // run in a timeout so it checks the editor content once this update has been applied window.setTimeout(() => { - const editorText = this.editor.getValue() + const editorText = this.getValue() const otText = shareDoc.getText() if (editorText !== otText) { - this.shareDoc.emit('error', 'Text does not match in CodeMirror 6') + shareDoc.emit('error', 'Text does not match in CodeMirror 6') debugConsole.error('Text does not match!') debugConsole.error('editor: ' + editorText) debugConsole.error('ot: ' + otText) @@ -184,12 +143,12 @@ class ShareLatexOTAdapter { } const onInsert = (pos: number, text: string) => { - this.editor.cmInsert(pos, text, 'remote') + this.cmInsert(pos, text, 'remote') check() } const onDelete = (pos: number, text: string) => { - this.editor.cmDelete(pos, text, 'remote') + this.cmDelete(pos, text, 'remote') check() } @@ -202,7 +161,7 @@ class ShareLatexOTAdapter { shareDoc.removeListener('insert', onInsert) shareDoc.removeListener('delete', onDelete) delete shareDoc.detach_cm6 - this.editor.detachShareJs() + this.shareDoc = null } } @@ -216,6 +175,10 @@ class ShareLatexOTAdapter { const trackedDeletesLength = ranges != null ? ranges.getTrackedDeletesLength() : 0 + if (!shareDoc) { + throw new Error('Trying to process updates with no shareDoc') + } + for (const transaction of transactions) { if (transaction.docChanged) { const origin = chooseOrigin(transaction) @@ -271,7 +234,7 @@ class ShareLatexOTAdapter { removed, } - this.editor.emit('change', this, changeDescription) + this.emit('change', this, changeDescription) } ) } @@ -279,8 +242,6 @@ class ShareLatexOTAdapter { } } -class HistoryOTAdapter extends ShareLatexOTAdapter {} - export const trackChangesAnnotation = Annotation.define() const chooseOrigin = (transaction: Transaction) => { From 93a1996491376978fde5d5e1f7f4efa3ad070276 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Tue, 27 May 2025 14:54:48 +0200 Subject: [PATCH 009/595] Show add-on list for non-personal subscription (#25901) GitOrigin-RevId: ba23158f51a7183fabc61c16b19809f58cf15323 --- .../components/dashboard/subscription-dashboard.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx index 972e268597..8cb07181cf 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx @@ -14,6 +14,7 @@ import OLPageContentCard from '@/features/ui/components/ol/ol-page-content-card' import OLRow from '@/features/ui/components/ol/ol-row' import OLCol from '@/features/ui/components/ol/ol-col' import OLNotification from '@/features/ui/components/ol/ol-notification' +import WritefullManagedBundleAddOn from './states/active/change-plan/modals/writefull-bundle-management-modal' function SubscriptionDashboard() { const { t } = useTranslation() @@ -24,6 +25,7 @@ function SubscriptionDashboard() { personalSubscription, } = useSubscriptionDashboardContext() + const hasAiAssistViaWritefull = getMeta('ol-hasAiAssistViaWritefull') const fromPlansPage = getMeta('ol-fromPlansPage') return ( @@ -50,6 +52,12 @@ function SubscriptionDashboard() { + {!personalSubscription && hasAiAssistViaWritefull && ( +
+

{t('add_ons')}

+ +
+ )} {hasValidActiveSubscription && ( )} From 881db9b4725741f624f70e01fa16c54b404bd2fa Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 27 May 2025 09:38:53 -0500 Subject: [PATCH 010/595] Merge pull request #25011 from overleaf/jel-group-audit-logs-part-2 [web] Update group audit log when user enrolls in managed users GitOrigin-RevId: 15d79854007ac3334a2bb66bcf73230bf42c68ce --- .../app/src/Features/Subscription/TeamInvitesController.mjs | 3 ++- .../web/app/src/Features/Subscription/TeamInvitesHandler.js | 5 +++-- services/web/test/acceptance/src/helpers/Subscription.mjs | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs index ca508755e6..2da67c3010 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs @@ -197,7 +197,8 @@ async function acceptInvite(req, res, next) { const subscription = await TeamInvitesHandler.promises.acceptInvite( token, - userId + userId, + { initiatorId: userId, ipAddress: req.ip } ) const groupSSOActive = ( await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription) diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js index 45a0495353..7312266ddf 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -64,7 +64,7 @@ async function importInvite(subscription, inviterName, email, token, sentAt) { return subscription.save() } -async function acceptInvite(token, userId) { +async function acceptInvite(token, userId, auditLog) { const { invite, subscription } = await getInvite(token) if (!invite) { throw new Errors.NotFoundError('invite not found') @@ -76,7 +76,8 @@ async function acceptInvite(token, userId) { await Modules.promises.hooks.fire( 'enrollInManagedSubscription', userId, - subscription + subscription, + auditLog ) } if (subscription.ssoConfig) { diff --git a/services/web/test/acceptance/src/helpers/Subscription.mjs b/services/web/test/acceptance/src/helpers/Subscription.mjs index db5c9c5898..a9adc113ae 100644 --- a/services/web/test/acceptance/src/helpers/Subscription.mjs +++ b/services/web/test/acceptance/src/helpers/Subscription.mjs @@ -126,7 +126,11 @@ class PromisifiedSubscription { return await Modules.promises.hooks.fire( 'enrollInManagedSubscription', user._id, - subscription + subscription, + { + initiatorId: user._id, + ipAddress: '0:0:0:0', + } ) } From dcd520d7ebb4d78c2abf2fa4c82e895dd01d6a9e Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 27 May 2025 09:39:02 -0500 Subject: [PATCH 011/595] Merge pull request #25360 from overleaf/jel-group-audit-log-join [web] Update group audit log when user joins GitOrigin-RevId: 81c0d5003cdde384cb5ff90b57f6aa8b8dae0ee2 --- .../Subscription/SubscriptionUpdater.js | 34 ++++++++++++++--- .../Subscription/TeamInvitesController.mjs | 7 +++- .../Subscription/TeamInvitesHandler.js | 10 ++++- .../Subscription/SubscriptionUpdaterTests.js | 38 +++++++++++++++++++ 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js index 482d81ff41..7b57e32619 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -12,6 +12,8 @@ const Features = require('../../infrastructure/Features') const UserAuditLogHandler = require('../User/UserAuditLogHandler') const AccountMappingHelper = require('../Analytics/AccountMappingHelper') const { SSOConfig } = require('../../models/SSOConfig') +const mongoose = require('../../infrastructure/Mongoose') +const Modules = require('../../infrastructure/Modules') /** * @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription @@ -65,7 +67,9 @@ async function syncSubscription( ) } -async function addUserToGroup(subscriptionId, userId) { +async function addUserToGroup(subscriptionId, userId, auditLog) { + const session = await mongoose.startSession() + await UserAuditLogHandler.promises.addEntry( userId, 'join-group-subscription', @@ -73,10 +77,30 @@ async function addUserToGroup(subscriptionId, userId) { undefined, { subscriptionId } ) - await Subscription.updateOne( - { _id: subscriptionId }, - { $addToSet: { member_ids: userId } } - ).exec() + + try { + await session.withTransaction(async () => { + await Subscription.updateOne( + { _id: subscriptionId }, + { $addToSet: { member_ids: userId } }, + { session } + ).exec() + + await Modules.promises.hooks.fire( + 'addGroupAuditLogEntry', + { + initiatorId: auditLog?.initiatorId, + ipAddress: auditLog?.ipAddress, + groupId: subscriptionId, + operation: 'join-group', + }, + session + ) + }) + } finally { + await session.endSession() + } + await FeaturesUpdater.promises.refreshFeatures(userId, 'add-to-group') await _sendUserGroupPlanCodeUserProperty(userId) await _sendSubscriptionEvent( diff --git a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs index 2da67c3010..b2c9840de4 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesController.mjs +++ b/services/web/app/src/Features/Subscription/TeamInvitesController.mjs @@ -36,10 +36,15 @@ async function createInvite(req, res, next) { } try { + const auditLog = { + initiatorId: teamManagerId, + ipAddress: req.ip, + } const invitedUserData = await TeamInvitesHandler.promises.createInvite( teamManagerId, subscription, - email + email, + auditLog ) return res.json({ user: invitedUserData }) } catch (err) { diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js index 7312266ddf..a89f0612f2 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js +++ b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js @@ -70,7 +70,11 @@ async function acceptInvite(token, userId, auditLog) { throw new Errors.NotFoundError('invite not found') } - await SubscriptionUpdater.promises.addUserToGroup(subscription._id, userId) + await SubscriptionUpdater.promises.addUserToGroup( + subscription._id, + userId, + auditLog + ) if (subscription.managedUsersEnabled) { await Modules.promises.hooks.fire( @@ -147,9 +151,11 @@ async function _createInvite(subscription, email, inviter) { emailData => emailData.email === email ) if (isInvitingSelf) { + const auditLog = { initiatorId: inviter._id } await SubscriptionUpdater.promises.addUserToGroup( subscription._id, - inviter._id + inviter._id, + auditLog ) // legacy: remove any invite that might have been created in the past diff --git a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js index 09644bc7b1..a122f0e4b2 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionUpdaterTests.js @@ -120,6 +120,18 @@ describe('SubscriptionUpdater', function () { }, }, ], + mongo: { + options: { + appname: 'web', + maxPoolSize: 100, + serverSelectionTimeoutMS: 60000, + socketTimeoutMS: 60000, + monitorCommands: true, + family: 4, + }, + url: 'mongodb://mongo/test-overleaf', + hasSecondaries: false, + }, } this.UserFeaturesUpdater = { @@ -181,6 +193,13 @@ describe('SubscriptionUpdater', function () { }), '../../infrastructure/Features': this.Features, '../User/UserAuditLogHandler': this.UserAuditLogHandler, + '../../infrastructure/Modules': (this.Modules = { + promises: { + hooks: { + fire: sinon.stub().resolves(), + }, + }, + }), }, }) }) @@ -486,6 +505,7 @@ describe('SubscriptionUpdater', function () { this.SubscriptionModel.updateOne .calledWith(searchOps, insertOperation) .should.equal(true) + expect(this.SubscriptionModel.updateOne.lastCall.args[2].session).to.exist sinon.assert.calledWith( this.AnalyticsManager.recordEventForUserInBackground, this.otherUserId, @@ -571,6 +591,24 @@ describe('SubscriptionUpdater', function () { } ) }) + + it('should add an entry to the group audit log when joining a group', async function () { + await this.SubscriptionUpdater.promises.addUserToGroup( + this.subscription._id, + this.otherUserId, + { ipAddress: '0:0:0:0', initiatorId: 'user123' } + ) + + expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + 'addGroupAuditLogEntry', + { + groupId: this.subscription._id, + initiatorId: 'user123', + ipAddress: '0:0:0:0', + operation: 'join-group', + } + ) + }) }) describe('removeUserFromGroup', function () { From ce67a27c974545db1b48a360a84853d4b9db32af Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Tue, 27 May 2025 09:39:21 -0500 Subject: [PATCH 012/595] Merge pull request #25556 from overleaf/jel-group-audit-log-remove-from-group [web] Log when user leaves or is removed from group GitOrigin-RevId: 8a5042b21cbf4eb622d5ca35cc095d94fe5a8539 --- .../SubscriptionGroupController.mjs | 8 ++- .../Subscription/SubscriptionGroupHandler.js | 9 ++- .../Subscription/SubscriptionUpdater.js | 72 ++++++++++++------- .../SubscriptionGroupControllerTests.mjs | 11 ++- .../SubscriptionGroupHandlerTests.js | 10 ++- .../types/group-management/group-audit-log.ts | 7 ++ 6 files changed, 83 insertions(+), 34 deletions(-) create mode 100644 services/web/types/group-management/group-audit-log.ts diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index ce1207cded..90ecd51091 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -108,10 +108,16 @@ async function _removeUserFromGroup( }) } + const groupAuditLog = { + initiatorId: loggedInUserId, + ipAddress: req.ip, + } + try { await SubscriptionGroupHandler.promises.removeUserFromGroup( subscriptionId, - userToRemoveId + userToRemoveId, + groupAuditLog ) } catch (error) { logger.err( diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js index 5772946b8a..c717b2eec6 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js @@ -22,10 +22,11 @@ const { const EmailHelper = require('../Helpers/EmailHelper') const { InvalidEmailError } = require('../Errors/Errors') -async function removeUserFromGroup(subscriptionId, userIdToRemove) { +async function removeUserFromGroup(subscriptionId, userIdToRemove, auditLog) { await SubscriptionUpdater.promises.removeUserFromGroup( subscriptionId, - userIdToRemove + userIdToRemove, + auditLog ) } @@ -463,7 +464,9 @@ async function updateGroupMembersBulk( ) } for (const user of membersToRemove) { - await removeUserFromGroup(subscription._id, user._id) + await removeUserFromGroup(subscription._id, user._id, { + initiatorId: inviterId, + }) } } diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js index 7b57e32619..15f61b6160 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -18,8 +18,31 @@ const Modules = require('../../infrastructure/Modules') /** * @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription * @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider + * @typedef {import('../../../../types/group-management/group-audit-log').GroupAuditLog} GroupAuditLog */ +/** + * + * @param {GroupAuditLog} auditLog + */ +async function subscriptionUpdateWithAuditLog(dbFilter, dbUpdate, auditLog) { + const session = await mongoose.startSession() + + try { + await session.withTransaction(async () => { + await Subscription.updateOne(dbFilter, dbUpdate, { session }).exec() + + await Modules.promises.hooks.fire( + 'addGroupAuditLogEntry', + auditLog, + session + ) + }) + } finally { + await session.endSession() + } +} + /** * Change the admin of the given subscription. * @@ -68,8 +91,6 @@ async function syncSubscription( } async function addUserToGroup(subscriptionId, userId, auditLog) { - const session = await mongoose.startSession() - await UserAuditLogHandler.promises.addEntry( userId, 'join-group-subscription', @@ -78,28 +99,16 @@ async function addUserToGroup(subscriptionId, userId, auditLog) { { subscriptionId } ) - try { - await session.withTransaction(async () => { - await Subscription.updateOne( - { _id: subscriptionId }, - { $addToSet: { member_ids: userId } }, - { session } - ).exec() - - await Modules.promises.hooks.fire( - 'addGroupAuditLogEntry', - { - initiatorId: auditLog?.initiatorId, - ipAddress: auditLog?.ipAddress, - groupId: subscriptionId, - operation: 'join-group', - }, - session - ) - }) - } finally { - await session.endSession() - } + await subscriptionUpdateWithAuditLog( + { _id: subscriptionId }, + { $addToSet: { member_ids: userId } }, + { + initiatorId: auditLog?.initiatorId, + ipAddress: auditLog?.ipAddress, + groupId: subscriptionId, + operation: 'join-group', + } + ) await FeaturesUpdater.promises.refreshFeatures(userId, 'add-to-group') await _sendUserGroupPlanCodeUserProperty(userId) @@ -110,7 +119,7 @@ async function addUserToGroup(subscriptionId, userId, auditLog) { ) } -async function removeUserFromGroup(subscriptionId, userId) { +async function removeUserFromGroup(subscriptionId, userId, auditLog) { await UserAuditLogHandler.promises.addEntry( userId, 'leave-group-subscription', @@ -118,6 +127,19 @@ async function removeUserFromGroup(subscriptionId, userId) { undefined, { subscriptionId } ) + + await subscriptionUpdateWithAuditLog( + { _id: subscriptionId }, + { $pull: { member_ids: userId } }, + { + initiatorId: auditLog?.initiatorId, + ipAddress: auditLog?.ipAddress, + groupId: subscriptionId, + operation: 'leave-group', + info: { userIdRemoved: userId }, + } + ) + await Subscription.updateOne( { _id: subscriptionId }, { $pull: { member_ids: userId } } diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs index 4376e752e7..c1ce6733ca 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs @@ -185,7 +185,10 @@ describe('SubscriptionGroupController', function () { const res = { sendStatus: () => { this.SubscriptionGroupHandler.promises.removeUserFromGroup - .calledWith(this.subscriptionId, userIdToRemove) + .calledWith(this.subscriptionId, userIdToRemove, { + initiatorId: this.req.session.user._id, + ipAddress: this.req.ip, + }) .should.equal(true) done() }, @@ -277,7 +280,11 @@ describe('SubscriptionGroupController', function () { sinon.assert.calledWith( this.SubscriptionGroupHandler.promises.removeUserFromGroup, this.subscriptionId, - memberUserIdToremove + memberUserIdToremove, + { + initiatorId: this.req.session.user._id, + ipAddress: this.req.ip, + } ) done() }, diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js index 0c47db3e14..1c314458da 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js @@ -233,13 +233,15 @@ describe('SubscriptionGroupHandler', function () { describe('removeUserFromGroup', function () { it('should call the subscription updater to remove the user', async function () { + const auditLog = { ipAddress: '0:0:0:0', initiatorId: this.user._id } await this.Handler.promises.removeUserFromGroup( this.adminUser_id, - this.user._id + this.user._id, + auditLog ) this.SubscriptionUpdater.promises.removeUserFromGroup - .calledWith(this.adminUser_id, this.user._id) + .calledWith(this.adminUser_id, this.user._id, auditLog) .should.equal(true) }) }) @@ -1149,7 +1151,9 @@ describe('SubscriptionGroupHandler', function () { expect( this.SubscriptionUpdater.promises.removeUserFromGroup - ).to.have.been.calledWith(this.subscription._id, members[2]._id) + ).to.have.been.calledWith(this.subscription._id, members[2]._id, { + initiatorId: inviterId, + }) expect( this.TeamInvitesHandler.promises.createInvite.callCount diff --git a/services/web/types/group-management/group-audit-log.ts b/services/web/types/group-management/group-audit-log.ts new file mode 100644 index 0000000000..c96c12e7cd --- /dev/null +++ b/services/web/types/group-management/group-audit-log.ts @@ -0,0 +1,7 @@ +export type GroupAuditLog = { + groupId: string + operation: string + ipAddress?: string + initiatorId?: string + info?: object +} From 0d3025b8cfd29c69255bde2d725aa786bd722d09 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Mon, 28 Apr 2025 11:25:12 +0100 Subject: [PATCH 013/595] Add vitest and configuration GitOrigin-RevId: 1262f9f32a0db6a29d3feedd8158b8dd04e48b6a --- package-lock.json | 737 ++++++++++++++++++++ services/web/.eslintrc.js | 24 + services/web/Makefile | 10 + services/web/bin/test_unit_run_dir | 45 ++ services/web/docker-compose.ci.yml | 1 + services/web/package.json | 6 +- services/web/test/unit/bootstrap.js | 25 +- services/web/test/unit/common_bootstrap.js | 24 + services/web/test/unit/vitest_bootstrap.mjs | 29 + services/web/vitest.config.js | 12 + 10 files changed, 888 insertions(+), 25 deletions(-) create mode 100755 services/web/bin/test_unit_run_dir create mode 100644 services/web/test/unit/common_bootstrap.js create mode 100644 services/web/test/unit/vitest_bootstrap.mjs create mode 100644 services/web/vitest.config.js diff --git a/package-lock.json b/package-lock.json index 4a14efb544..146ba3255d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44850,6 +44850,7 @@ "@uppy/react": "^3.2.1", "@uppy/utils": "^5.7.0", "@uppy/xhr-upload": "^3.6.0", + "@vitest/eslint-plugin": "1.1.44", "5to6-codemod": "^1.8.0", "abort-controller": "^3.0.0", "acorn": "^7.1.1", @@ -44956,6 +44957,7 @@ "tty-browserify": "^0.0.1", "typescript": "^5.0.4", "uuid": "^9.0.1", + "vitest": "^3.1.2", "w3c-keyname": "^2.2.8", "webpack": "^5.98.0", "webpack-assets-manifest": "^5.2.1", @@ -44965,6 +44967,26 @@ "yup": "^0.32.11" } }, + "services/web/node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, "services/web/node_modules/@google-cloud/bigquery": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@google-cloud/bigquery/-/bigquery-6.0.3.tgz", @@ -45161,6 +45183,18 @@ "dev": true, "license": "MIT" }, + "services/web/node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/ms": "*" + } + }, "services/web/node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -45179,6 +45213,143 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, + "services/web/node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "services/web/node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "services/web/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "services/web/node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "services/web/node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "services/web/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "services/web/node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "services/web/node_modules/@uppy/core": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/@uppy/core/-/core-3.8.0.tgz", @@ -45332,6 +45503,130 @@ "@uppy/core": "^3.8.0" } }, + "services/web/node_modules/@vitest/eslint-plugin": { + "version": "1.1.44", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.44.tgz", + "integrity": "sha512-m4XeohMT+Dj2RZfxnbiFR+Cv5dEC0H7C6TlxRQT7GK2556solm99kxgzJp/trKrZvanZcOFyw7aABykUTfWyrg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": ">= 8.24.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "services/web/node_modules/@vitest/expect": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/@vitest/expect/node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "services/web/node_modules/@vitest/pretty-format": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/@vitest/runner": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.1.4", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/@vitest/snapshot": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/@vitest/spy": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/@vitest/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.1.4", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "services/web/node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -45368,6 +45663,16 @@ "ajv": "^8.8.2" } }, + "services/web/node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "services/web/node_modules/base-x": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", @@ -45405,6 +45710,16 @@ "ieee754": "^1.2.1" } }, + "services/web/node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "services/web/node_modules/csv": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.5.tgz", @@ -45451,6 +45766,16 @@ } } }, + "services/web/node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "services/web/node_modules/duplexify": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", @@ -45462,6 +45787,20 @@ "stream-shift": "^1.0.0" } }, + "services/web/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "services/web/node_modules/esmock": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.6.7.tgz", @@ -45479,6 +45818,21 @@ "node": ">=0.8.x" } }, + "services/web/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "services/web/node_modules/fetch-mock": { "version": "12.5.2", "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.5.2.tgz", @@ -45598,6 +45952,18 @@ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", "dev": true }, + "services/web/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "services/web/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -45609,6 +45975,13 @@ "integrity": "sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg==", "dev": true }, + "services/web/node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, "services/web/node_modules/lru-cache": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", @@ -45735,6 +46108,29 @@ "isarray": "0.0.1" } }, + "services/web/node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "services/web/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "services/web/node_modules/retry-request": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-5.0.2.tgz", @@ -45778,6 +46174,20 @@ "url": "https://opencollective.com/webpack" } }, + "services/web/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "services/web/node_modules/sinon": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.5.0.tgz", @@ -45941,6 +46351,30 @@ } } }, + "services/web/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "services/web/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "services/web/node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -45953,6 +46387,294 @@ "uuid": "dist/bin/uuid" } }, + "services/web/node_modules/vite-node": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "services/web/node_modules/vite-node/node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "services/web/node_modules/vitest": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.13", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.1.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "services/web/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "services/web/node_modules/vitest/node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "services/web/node_modules/vitest/node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "services/web/node_modules/xml-crypto": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-2.1.6.tgz", @@ -45980,6 +46702,21 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "services/web/node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "tools/saas-e2e": { "name": "@overleaf/saas-e2e", "devDependencies": { diff --git a/services/web/.eslintrc.js b/services/web/.eslintrc.js index 3c672de7e7..b505080b98 100644 --- a/services/web/.eslintrc.js +++ b/services/web/.eslintrc.js @@ -64,6 +64,10 @@ module.exports = { { // Test specific rules files: ['**/test/**/*.*'], + excludedFiles: [ + '**/test/unit/src/**/*.test.mjs', + 'test/unit/vitest_bootstrap.mjs', + ], // exclude vitest files plugins: ['mocha', 'chai-expect', 'chai-friendly'], env: { mocha: true, @@ -95,6 +99,26 @@ module.exports = { '@typescript-eslint/no-unused-expressions': 'off', }, }, + { + files: [ + '**/test/unit/src/**/*.test.mjs', + 'test/unit/vitest_bootstrap.mjs', + ], + env: { + jest: true, // best match for vitest API etc. + }, + plugins: ['@vitest', 'chai-expect', 'chai-friendly'], // still using chai for now + rules: { + // Swap the no-unused-expressions rule with a more chai-friendly one + 'no-unused-expressions': 'off', + 'chai-friendly/no-unused-expressions': 'error', + + // chai-specific rules + 'chai-expect/missing-assertion': 'error', + 'chai-expect/terminating-properties': 'error', + '@typescript-eslint/no-unused-expressions': 'off', + }, + }, { // ES specific rules files: [ diff --git a/services/web/Makefile b/services/web/Makefile index c6916048d6..58323058b8 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -83,6 +83,16 @@ test_unit_app: $(DOCKER_COMPOSE) run --name unit_test_$(BUILD_DIR_NAME) --rm test_unit $(DOCKER_COMPOSE) down -v -t 0 +test_unit_esm: export COMPOSE_PROJECT_NAME=unit_test_esm_$(BUILD_DIR_NAME) +test_unit_esm: + $(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:esm + $(DOCKER_COMPOSE) down -v -t 0 + +test_unit_esm_watch: export COMPOSE_PROJECT_NAME=unit_test_esm_watch_$(BUILD_DIR_NAME) +test_unit_esm_watch: + $(DOCKER_COMPOSE) run --rm test_unit npm run test:unit:esm:watch + $(DOCKER_COMPOSE) down -v -t 0 + TEST_SUITES = $(sort $(filter-out \ $(wildcard test/unit/src/helpers/*), \ $(wildcard test/unit/src/*/*))) diff --git a/services/web/bin/test_unit_run_dir b/services/web/bin/test_unit_run_dir new file mode 100755 index 0000000000..20f580cf06 --- /dev/null +++ b/services/web/bin/test_unit_run_dir @@ -0,0 +1,45 @@ +#!/bin/bash + +TARGET_DIR=$1 + + +declare -a vitest_args=("$TARGET_DIR") + +if [[ -n "$MOCHA_GREP" ]]; then + vitest_args+=("--testNamePattern" "$MOCHA_GREP") +fi + +if [[ -n "$VITEST_NO_CACHE" ]]; then + echo "Disabling cache for vitest." + vitest_args+=("--no-cache") +fi + +echo "Running unit tests in directory: $TARGET_DIR" + +npm run test:unit:esm -- "${vitest_args[@]}" + +vitest_status=$? + +if find "$TARGET_DIR" -type f -name "*.js" -print -quit | grep -q '.'; then + mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=js "$TARGET_DIR" + mocha_status=$? +else + echo "No mocha tests found in $TARGET_DIR, skipping mocha step." + mocha_status=0 +fi + +if [ $mocha_status -eq 0 ] && [ $vitest_status -eq 0 ]; then + exit 0 +fi + +# Report status briefly at the end for failures + +if [ $mocha_status -ne 0 ]; then + echo "Mocha tests failed with status: $mocha_status" +fi + +if [ $vitest_status -ne 0 ]; then + echo "Vitest tests failed with status: $vitest_status" +fi + +exit 1 diff --git a/services/web/docker-compose.ci.yml b/services/web/docker-compose.ci.yml index 164cc22c5a..5cffe19810 100644 --- a/services/web/docker-compose.ci.yml +++ b/services/web/docker-compose.ci.yml @@ -21,6 +21,7 @@ services: OVERLEAF_CONFIG: NODE_ENV: test NODE_OPTIONS: "--unhandled-rejections=strict" + VITEST_NO_CACHE: true depends_on: - mongo diff --git a/services/web/package.json b/services/web/package.json index 5080813d55..ee5f81d4f8 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -9,10 +9,12 @@ "scripts": { "test:acceptance:run_dir": "mocha --recursive --timeout 25000 --grep=$MOCHA_GREP --require test/acceptance/bootstrap.js", "test:acceptance:app": "npm run test:acceptance:run_dir -- test/acceptance/src", - "test:unit:run_dir": "mocha --recursive --timeout 25000 --exit --grep=$MOCHA_GREP --require test/unit/bootstrap.js", + "test:unit:run_dir": "bin/test_unit_run_dir", "test:unit:all": "npm run test:unit:run_dir -- test/unit/src modules/*/test/unit/src", "test:unit:all:silent": "npm run test:unit:all -- --reporter dot", "test:unit:app": "npm run test:unit:run_dir -- test/unit/src", + "test:unit:esm": "vitest run", + "test:unit:esm:watch": "vitest", "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' --ignore '**/helpers/**/*.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend", "test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend", "start": "node app.mjs", @@ -250,6 +252,7 @@ "@uppy/react": "^3.2.1", "@uppy/utils": "^5.7.0", "@uppy/xhr-upload": "^3.6.0", + "@vitest/eslint-plugin": "1.1.44", "5to6-codemod": "^1.8.0", "abort-controller": "^3.0.0", "acorn": "^7.1.1", @@ -356,6 +359,7 @@ "tty-browserify": "^0.0.1", "typescript": "^5.0.4", "uuid": "^9.0.1", + "vitest": "^3.1.2", "w3c-keyname": "^2.2.8", "webpack": "^5.98.0", "webpack-assets-manifest": "^5.2.1", diff --git a/services/web/test/unit/bootstrap.js b/services/web/test/unit/bootstrap.js index ee4a022c15..f3d3f382f2 100644 --- a/services/web/test/unit/bootstrap.js +++ b/services/web/test/unit/bootstrap.js @@ -1,29 +1,6 @@ const Path = require('path') -const chai = require('chai') const sinon = require('sinon') - -/* - * Chai configuration - */ - -// add chai.should() -chai.should() - -// Load sinon-chai assertions so expect(stubFn).to.have.been.calledWith('abc') -// has a nicer failure messages -chai.use(require('sinon-chai')) - -// Load promise support for chai -chai.use(require('chai-as-promised')) - -// Do not truncate assertion errors -chai.config.truncateThreshold = 0 - -// add support for mongoose in sinon -require('sinon-mongoose') - -// ensure every ObjectId has the id string as a property for correct comparisons -require('mongodb-legacy').ObjectId.cacheHexString = true +require('./common_bootstrap') /* * Global stubs diff --git a/services/web/test/unit/common_bootstrap.js b/services/web/test/unit/common_bootstrap.js new file mode 100644 index 0000000000..d74fee60b2 --- /dev/null +++ b/services/web/test/unit/common_bootstrap.js @@ -0,0 +1,24 @@ +const chai = require('chai') + +/* + * Chai configuration + */ + +// add chai.should() +chai.should() + +// Load sinon-chai assertions so expect(stubFn).to.have.been.calledWith('abc') +// has a nicer failure messages +chai.use(require('sinon-chai')) + +// Load promise support for chai +chai.use(require('chai-as-promised')) + +// Do not truncate assertion errors +chai.config.truncateThreshold = 0 + +// add support for mongoose in sinon +require('sinon-mongoose') + +// ensure every ObjectId has the id string as a property for correct comparisons +require('mongodb-legacy').ObjectId.cacheHexString = true diff --git a/services/web/test/unit/vitest_bootstrap.mjs b/services/web/test/unit/vitest_bootstrap.mjs new file mode 100644 index 0000000000..fc4d883b1a --- /dev/null +++ b/services/web/test/unit/vitest_bootstrap.mjs @@ -0,0 +1,29 @@ +import { vi } from 'vitest' +import './common_bootstrap.js' +import sinon from 'sinon' +import logger from '@overleaf/logger' + +vi.mock('@overleaf/logger', async () => { + const sinon = (await import('sinon')).default + return { + default: { + debug: sinon.stub(), + info: sinon.stub(), + log: sinon.stub(), + warn: sinon.stub(), + err: sinon.stub(), + error: sinon.stub(), + fatal: sinon.stub(), + }, + } +}) + +beforeEach(ctx => { + ctx.logger = logger +}) + +afterEach(() => { + vi.restoreAllMocks() + vi.resetModules() + sinon.restore() +}) diff --git a/services/web/vitest.config.js b/services/web/vitest.config.js new file mode 100644 index 0000000000..3b84690447 --- /dev/null +++ b/services/web/vitest.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('vitest/config') + +module.exports = defineConfig({ + test: { + include: [ + 'modules/*/test/unit/**/*.test.mjs', + 'test/unit/src/**/*.test.mjs', + ], + setupFiles: ['./test/unit/vitest_bootstrap.mjs'], + globals: true, + }, +}) From 51dcc88f276bd7c506d6bbaef949d26de028ef1c Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Mon, 28 Apr 2025 11:27:57 +0100 Subject: [PATCH 014/595] Rename test files for vitest GitOrigin-RevId: f8792c0ce5eeb4843a534d3ff83e011d25fb65e0 --- ...{LaunchpadControllerTests.mjs => LaunchpadController.test.mjs} | 0 ...ctivateControllerTests.mjs => UserActivateController.test.mjs} | 0 ...{AnalyticsControllerTests.mjs => AnalyticsController.test.mjs} | 0 ...iddlewareTests.mjs => AnalyticsUTMTrackingMiddleware.test.mjs} | 0 ...aProgramControllerTests.mjs => BetaProgramController.test.mjs} | 0 .../{BetaProgramHandlerTests.mjs => BetaProgramHandler.test.mjs} | 0 ...ratorsControllerTests.mjs => CollaboratorsController.test.mjs} | 0 ...ControllerTests.mjs => CollaboratorsInviteController.test.mjs} | 0 ...InviteHandlerTests.mjs => CollaboratorsInviteHandler.test.mjs} | 0 .../{ContactControllerTests.mjs => ContactController.test.mjs} | 0 .../{CooldownMiddlewareTests.mjs => CooldownMiddleware.test.mjs} | 0 ...aterControllerTests.mjs => DocumentUpdaterController.test.mjs} | 0 .../{DocumentControllerTests.mjs => DocumentController.test.mjs} | 0 ...adsControllerTests.mjs => ProjectDownloadsController.test.mjs} | 0 ...ZipStreamManagerTests.mjs => ProjectZipStreamManager.test.mjs} | 0 .../{ExportsControllerTests.mjs => ExportsController.test.mjs} | 0 .../Exports/{ExportsHandlerTests.mjs => ExportsHandler.test.mjs} | 0 ...{FileStoreControllerTests.mjs => FileStoreController.test.mjs} | 0 ...kedFilesControllerTests.mjs => LinkedFilesController.test.mjs} | 0 .../Metadata/{MetaControllerTests.mjs => MetaController.test.mjs} | 0 .../src/Metadata/{MetaHandlerTests.mjs => MetaHandler.test.mjs} | 0 ...ationsControllerTests.mjs => NotificationsController.test.mjs} | 0 ...dResetControllerTests.mjs => PasswordResetController.test.mjs} | 0 ...asswordResetHandlerTests.mjs => PasswordResetHandler.test.mjs} | 0 .../{DocLinesComparitorTests.mjs => DocLinesComparitor.test.mjs} | 0 ...rojectApiControllerTests.mjs => ProjectApiController.test.mjs} | 0 ...jectListControllerTests.mjs => ProjectListController.test.mjs} | 0 .../Referal/{ReferalConnectTests.mjs => ReferalConnect.test.mjs} | 0 .../{ReferalControllerTests.mjs => ReferalController.test.mjs} | 0 .../Referal/{ReferalHandlerTests.mjs => ReferalHandler.test.mjs} | 0 ...eferencesControllerTests.mjs => ReferencesController.test.mjs} | 0 .../{ReferencesHandlerTests.mjs => ReferencesHandler.test.mjs} | 0 ...upControllerTests.mjs => SubscriptionGroupController.test.mjs} | 0 ...mInvitesControllerTests.mjs => TeamInvitesController.test.mjs} | 0 .../src/Tags/{TagsControllerTests.mjs => TagsController.test.mjs} | 0 .../{TpdsControllerTests.mjs => TpdsController.test.mjs} | 0 .../{TpdsUpdateHandlerTests.mjs => TpdsUpdateHandler.test.mjs} | 0 ...enAccessControllerTests.mjs => TokenAccessController.test.mjs} | 0 ...UploadControllerTests.mjs => ProjectUploadController.test.mjs} | 0 ...{UserPagesControllerTests.mjs => UserPagesController.test.mjs} | 0 ...rshipControllerTests.mjs => UserMembershipController.test.mjs} | 0 .../{ServeStaticWrapperTests.mjs => ServeStaticWrapper.test.mjs} | 0 42 files changed, 0 insertions(+), 0 deletions(-) rename services/web/modules/launchpad/test/unit/src/{LaunchpadControllerTests.mjs => LaunchpadController.test.mjs} (100%) rename services/web/modules/user-activate/test/unit/src/{UserActivateControllerTests.mjs => UserActivateController.test.mjs} (100%) rename services/web/test/unit/src/Analytics/{AnalyticsControllerTests.mjs => AnalyticsController.test.mjs} (100%) rename services/web/test/unit/src/Analytics/{AnalyticsUTMTrackingMiddlewareTests.mjs => AnalyticsUTMTrackingMiddleware.test.mjs} (100%) rename services/web/test/unit/src/BetaProgram/{BetaProgramControllerTests.mjs => BetaProgramController.test.mjs} (100%) rename services/web/test/unit/src/BetaProgram/{BetaProgramHandlerTests.mjs => BetaProgramHandler.test.mjs} (100%) rename services/web/test/unit/src/Collaborators/{CollaboratorsControllerTests.mjs => CollaboratorsController.test.mjs} (100%) rename services/web/test/unit/src/Collaborators/{CollaboratorsInviteControllerTests.mjs => CollaboratorsInviteController.test.mjs} (100%) rename services/web/test/unit/src/Collaborators/{CollaboratorsInviteHandlerTests.mjs => CollaboratorsInviteHandler.test.mjs} (100%) rename services/web/test/unit/src/Contact/{ContactControllerTests.mjs => ContactController.test.mjs} (100%) rename services/web/test/unit/src/Cooldown/{CooldownMiddlewareTests.mjs => CooldownMiddleware.test.mjs} (100%) rename services/web/test/unit/src/DocumentUpdater/{DocumentUpdaterControllerTests.mjs => DocumentUpdaterController.test.mjs} (100%) rename services/web/test/unit/src/Documents/{DocumentControllerTests.mjs => DocumentController.test.mjs} (100%) rename services/web/test/unit/src/Downloads/{ProjectDownloadsControllerTests.mjs => ProjectDownloadsController.test.mjs} (100%) rename services/web/test/unit/src/Downloads/{ProjectZipStreamManagerTests.mjs => ProjectZipStreamManager.test.mjs} (100%) rename services/web/test/unit/src/Exports/{ExportsControllerTests.mjs => ExportsController.test.mjs} (100%) rename services/web/test/unit/src/Exports/{ExportsHandlerTests.mjs => ExportsHandler.test.mjs} (100%) rename services/web/test/unit/src/FileStore/{FileStoreControllerTests.mjs => FileStoreController.test.mjs} (100%) rename services/web/test/unit/src/LinkedFiles/{LinkedFilesControllerTests.mjs => LinkedFilesController.test.mjs} (100%) rename services/web/test/unit/src/Metadata/{MetaControllerTests.mjs => MetaController.test.mjs} (100%) rename services/web/test/unit/src/Metadata/{MetaHandlerTests.mjs => MetaHandler.test.mjs} (100%) rename services/web/test/unit/src/Notifications/{NotificationsControllerTests.mjs => NotificationsController.test.mjs} (100%) rename services/web/test/unit/src/PasswordReset/{PasswordResetControllerTests.mjs => PasswordResetController.test.mjs} (100%) rename services/web/test/unit/src/PasswordReset/{PasswordResetHandlerTests.mjs => PasswordResetHandler.test.mjs} (100%) rename services/web/test/unit/src/Project/{DocLinesComparitorTests.mjs => DocLinesComparitor.test.mjs} (100%) rename services/web/test/unit/src/Project/{ProjectApiControllerTests.mjs => ProjectApiController.test.mjs} (100%) rename services/web/test/unit/src/Project/{ProjectListControllerTests.mjs => ProjectListController.test.mjs} (100%) rename services/web/test/unit/src/Referal/{ReferalConnectTests.mjs => ReferalConnect.test.mjs} (100%) rename services/web/test/unit/src/Referal/{ReferalControllerTests.mjs => ReferalController.test.mjs} (100%) rename services/web/test/unit/src/Referal/{ReferalHandlerTests.mjs => ReferalHandler.test.mjs} (100%) rename services/web/test/unit/src/References/{ReferencesControllerTests.mjs => ReferencesController.test.mjs} (100%) rename services/web/test/unit/src/References/{ReferencesHandlerTests.mjs => ReferencesHandler.test.mjs} (100%) rename services/web/test/unit/src/Subscription/{SubscriptionGroupControllerTests.mjs => SubscriptionGroupController.test.mjs} (100%) rename services/web/test/unit/src/Subscription/{TeamInvitesControllerTests.mjs => TeamInvitesController.test.mjs} (100%) rename services/web/test/unit/src/Tags/{TagsControllerTests.mjs => TagsController.test.mjs} (100%) rename services/web/test/unit/src/ThirdPartyDataStore/{TpdsControllerTests.mjs => TpdsController.test.mjs} (100%) rename services/web/test/unit/src/ThirdPartyDataStore/{TpdsUpdateHandlerTests.mjs => TpdsUpdateHandler.test.mjs} (100%) rename services/web/test/unit/src/TokenAccess/{TokenAccessControllerTests.mjs => TokenAccessController.test.mjs} (100%) rename services/web/test/unit/src/Uploads/{ProjectUploadControllerTests.mjs => ProjectUploadController.test.mjs} (100%) rename services/web/test/unit/src/User/{UserPagesControllerTests.mjs => UserPagesController.test.mjs} (100%) rename services/web/test/unit/src/UserMembership/{UserMembershipControllerTests.mjs => UserMembershipController.test.mjs} (100%) rename services/web/test/unit/src/infrastructure/{ServeStaticWrapperTests.mjs => ServeStaticWrapper.test.mjs} (100%) diff --git a/services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.mjs b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs similarity index 100% rename from services/web/modules/launchpad/test/unit/src/LaunchpadControllerTests.mjs rename to services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs diff --git a/services/web/modules/user-activate/test/unit/src/UserActivateControllerTests.mjs b/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs similarity index 100% rename from services/web/modules/user-activate/test/unit/src/UserActivateControllerTests.mjs rename to services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs diff --git a/services/web/test/unit/src/Analytics/AnalyticsControllerTests.mjs b/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Analytics/AnalyticsControllerTests.mjs rename to services/web/test/unit/src/Analytics/AnalyticsController.test.mjs diff --git a/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddlewareTests.mjs b/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs similarity index 100% rename from services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddlewareTests.mjs rename to services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramControllerTests.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs similarity index 100% rename from services/web/test/unit/src/BetaProgram/BetaProgramControllerTests.mjs rename to services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/BetaProgram/BetaProgramHandlerTests.mjs rename to services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Collaborators/CollaboratorsControllerTests.mjs rename to services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs similarity index 100% rename from services/web/test/unit/src/Collaborators/CollaboratorsInviteControllerTests.mjs rename to services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/Collaborators/CollaboratorsInviteHandlerTests.mjs rename to services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs diff --git a/services/web/test/unit/src/Contact/ContactControllerTests.mjs b/services/web/test/unit/src/Contact/ContactController.test.mjs similarity index 100% rename from services/web/test/unit/src/Contact/ContactControllerTests.mjs rename to services/web/test/unit/src/Contact/ContactController.test.mjs diff --git a/services/web/test/unit/src/Cooldown/CooldownMiddlewareTests.mjs b/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs similarity index 100% rename from services/web/test/unit/src/Cooldown/CooldownMiddlewareTests.mjs rename to services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterControllerTests.mjs b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs similarity index 100% rename from services/web/test/unit/src/DocumentUpdater/DocumentUpdaterControllerTests.mjs rename to services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs diff --git a/services/web/test/unit/src/Documents/DocumentControllerTests.mjs b/services/web/test/unit/src/Documents/DocumentController.test.mjs similarity index 100% rename from services/web/test/unit/src/Documents/DocumentControllerTests.mjs rename to services/web/test/unit/src/Documents/DocumentController.test.mjs diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsControllerTests.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Downloads/ProjectDownloadsControllerTests.mjs rename to services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs diff --git a/services/web/test/unit/src/Downloads/ProjectZipStreamManagerTests.mjs b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs similarity index 100% rename from services/web/test/unit/src/Downloads/ProjectZipStreamManagerTests.mjs rename to services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs diff --git a/services/web/test/unit/src/Exports/ExportsControllerTests.mjs b/services/web/test/unit/src/Exports/ExportsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Exports/ExportsControllerTests.mjs rename to services/web/test/unit/src/Exports/ExportsController.test.mjs diff --git a/services/web/test/unit/src/Exports/ExportsHandlerTests.mjs b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/Exports/ExportsHandlerTests.mjs rename to services/web/test/unit/src/Exports/ExportsHandler.test.mjs diff --git a/services/web/test/unit/src/FileStore/FileStoreControllerTests.mjs b/services/web/test/unit/src/FileStore/FileStoreController.test.mjs similarity index 100% rename from services/web/test/unit/src/FileStore/FileStoreControllerTests.mjs rename to services/web/test/unit/src/FileStore/FileStoreController.test.mjs diff --git a/services/web/test/unit/src/LinkedFiles/LinkedFilesControllerTests.mjs b/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs similarity index 100% rename from services/web/test/unit/src/LinkedFiles/LinkedFilesControllerTests.mjs rename to services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs diff --git a/services/web/test/unit/src/Metadata/MetaControllerTests.mjs b/services/web/test/unit/src/Metadata/MetaController.test.mjs similarity index 100% rename from services/web/test/unit/src/Metadata/MetaControllerTests.mjs rename to services/web/test/unit/src/Metadata/MetaController.test.mjs diff --git a/services/web/test/unit/src/Metadata/MetaHandlerTests.mjs b/services/web/test/unit/src/Metadata/MetaHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/Metadata/MetaHandlerTests.mjs rename to services/web/test/unit/src/Metadata/MetaHandler.test.mjs diff --git a/services/web/test/unit/src/Notifications/NotificationsControllerTests.mjs b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Notifications/NotificationsControllerTests.mjs rename to services/web/test/unit/src/Notifications/NotificationsController.test.mjs diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs similarity index 100% rename from services/web/test/unit/src/PasswordReset/PasswordResetControllerTests.mjs rename to services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetHandlerTests.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/PasswordReset/PasswordResetHandlerTests.mjs rename to services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs diff --git a/services/web/test/unit/src/Project/DocLinesComparitorTests.mjs b/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs similarity index 100% rename from services/web/test/unit/src/Project/DocLinesComparitorTests.mjs rename to services/web/test/unit/src/Project/DocLinesComparitor.test.mjs diff --git a/services/web/test/unit/src/Project/ProjectApiControllerTests.mjs b/services/web/test/unit/src/Project/ProjectApiController.test.mjs similarity index 100% rename from services/web/test/unit/src/Project/ProjectApiControllerTests.mjs rename to services/web/test/unit/src/Project/ProjectApiController.test.mjs diff --git a/services/web/test/unit/src/Project/ProjectListControllerTests.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs similarity index 100% rename from services/web/test/unit/src/Project/ProjectListControllerTests.mjs rename to services/web/test/unit/src/Project/ProjectListController.test.mjs diff --git a/services/web/test/unit/src/Referal/ReferalConnectTests.mjs b/services/web/test/unit/src/Referal/ReferalConnect.test.mjs similarity index 100% rename from services/web/test/unit/src/Referal/ReferalConnectTests.mjs rename to services/web/test/unit/src/Referal/ReferalConnect.test.mjs diff --git a/services/web/test/unit/src/Referal/ReferalControllerTests.mjs b/services/web/test/unit/src/Referal/ReferalController.test.mjs similarity index 100% rename from services/web/test/unit/src/Referal/ReferalControllerTests.mjs rename to services/web/test/unit/src/Referal/ReferalController.test.mjs diff --git a/services/web/test/unit/src/Referal/ReferalHandlerTests.mjs b/services/web/test/unit/src/Referal/ReferalHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/Referal/ReferalHandlerTests.mjs rename to services/web/test/unit/src/Referal/ReferalHandler.test.mjs diff --git a/services/web/test/unit/src/References/ReferencesControllerTests.mjs b/services/web/test/unit/src/References/ReferencesController.test.mjs similarity index 100% rename from services/web/test/unit/src/References/ReferencesControllerTests.mjs rename to services/web/test/unit/src/References/ReferencesController.test.mjs diff --git a/services/web/test/unit/src/References/ReferencesHandlerTests.mjs b/services/web/test/unit/src/References/ReferencesHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/References/ReferencesHandlerTests.mjs rename to services/web/test/unit/src/References/ReferencesHandler.test.mjs diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs similarity index 100% rename from services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs rename to services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs diff --git a/services/web/test/unit/src/Subscription/TeamInvitesControllerTests.mjs b/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs similarity index 100% rename from services/web/test/unit/src/Subscription/TeamInvitesControllerTests.mjs rename to services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs diff --git a/services/web/test/unit/src/Tags/TagsControllerTests.mjs b/services/web/test/unit/src/Tags/TagsController.test.mjs similarity index 100% rename from services/web/test/unit/src/Tags/TagsControllerTests.mjs rename to services/web/test/unit/src/Tags/TagsController.test.mjs diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs similarity index 100% rename from services/web/test/unit/src/ThirdPartyDataStore/TpdsControllerTests.mjs rename to services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs similarity index 100% rename from services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandlerTests.mjs rename to services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs similarity index 100% rename from services/web/test/unit/src/TokenAccess/TokenAccessControllerTests.mjs rename to services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs diff --git a/services/web/test/unit/src/Uploads/ProjectUploadControllerTests.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs similarity index 100% rename from services/web/test/unit/src/Uploads/ProjectUploadControllerTests.mjs rename to services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs diff --git a/services/web/test/unit/src/User/UserPagesControllerTests.mjs b/services/web/test/unit/src/User/UserPagesController.test.mjs similarity index 100% rename from services/web/test/unit/src/User/UserPagesControllerTests.mjs rename to services/web/test/unit/src/User/UserPagesController.test.mjs diff --git a/services/web/test/unit/src/UserMembership/UserMembershipControllerTests.mjs b/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs similarity index 100% rename from services/web/test/unit/src/UserMembership/UserMembershipControllerTests.mjs rename to services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs diff --git a/services/web/test/unit/src/infrastructure/ServeStaticWrapperTests.mjs b/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs similarity index 100% rename from services/web/test/unit/src/infrastructure/ServeStaticWrapperTests.mjs rename to services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs From 873068a1875f50c8f64c80385022fa3d311c571f Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Wed, 30 Apr 2025 09:08:59 +0100 Subject: [PATCH 015/595] Update test files with vitest compat changes GitOrigin-RevId: 494f906089d250268a5ff8c8a2150ff2692c37e2 --- .../unit/src/LaunchpadController.test.mjs | 1152 +++++------ .../unit/src/UserActivateController.test.mjs | 178 +- .../Analytics/AnalyticsController.test.mjs | 125 +- .../AnalyticsUTMTrackingMiddleware.test.mjs | 149 +- .../BetaProgramController.test.mjs | 253 ++- .../BetaProgram/BetaProgramHandler.test.mjs | 169 +- .../CollaboratorsController.test.mjs | 597 +++--- .../CollaboratorsInviteController.test.mjs | 1787 +++++++++-------- .../CollaboratorsInviteHandler.test.mjs | 852 ++++---- .../src/Contact/ContactController.test.mjs | 169 +- .../src/Cooldown/CooldownMiddleware.test.mjs | 140 +- .../DocumentUpdaterController.test.mjs | 100 +- .../src/Documents/DocumentController.test.mjs | 223 +- .../ProjectDownloadsController.test.mjs | 161 +- .../ProjectZipStreamManager.test.mjs | 514 ++--- .../src/Exports/ExportsController.test.mjs | 278 +-- .../unit/src/Exports/ExportsHandler.test.mjs | 933 ++++----- .../FileStore/FileStoreController.test.mjs | 229 ++- .../LinkedFilesController.test.mjs | 260 ++- .../unit/src/Metadata/MetaController.test.mjs | 77 +- .../unit/src/Metadata/MetaHandler.test.mjs | 95 +- .../NotificationsController.test.mjs | 80 +- .../PasswordResetController.test.mjs | 752 ++++--- .../PasswordResetHandler.test.mjs | 720 ++++--- .../src/Project/DocLinesComparitor.test.mjs | 42 +- .../src/Project/ProjectApiController.test.mjs | 76 +- .../Project/ProjectListController.test.mjs | 873 ++++---- .../unit/src/Referal/ReferalConnect.test.mjs | 203 +- .../src/Referal/ReferalController.test.mjs | 12 +- .../unit/src/Referal/ReferalHandler.test.mjs | 54 +- .../References/ReferencesController.test.mjs | 249 +-- .../src/References/ReferencesHandler.test.mjs | 440 ++-- .../SubscriptionGroupController.test.mjs | 1231 ++++++------ .../TeamInvitesController.test.mjs | 308 +-- .../unit/src/Tags/TagsController.test.mjs | 431 ++-- .../TpdsController.test.mjs | 723 ++++--- .../TpdsUpdateHandler.test.mjs | 389 ++-- .../TokenAccessController.test.mjs | 1340 ++++++------ .../Uploads/ProjectUploadController.test.mjs | 337 ++-- .../src/User/UserPagesController.test.mjs | 684 ++++--- .../UserMembershipController.test.mjs | 478 +++-- .../ServeStaticWrapper.test.mjs | 43 +- 42 files changed, 9712 insertions(+), 8194 deletions(-) diff --git a/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs index e1ca25a75a..89bc165305 100644 --- a/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs +++ b/services/web/modules/launchpad/test/unit/src/LaunchpadController.test.mjs @@ -1,187 +1,215 @@ -import { fileURLToPath } from 'node:url' +import { vi } from 'vitest' import * as path from 'node:path' -import { strict as esmock } from 'esmock' import { expect } from 'chai' import sinon from 'sinon' -import Settings from '@overleaf/settings' import MockResponse from '../../../../../test/unit/src/helpers/MockResponse.js' -const __dirname = fileURLToPath(new URL('.', import.meta.url)) const modulePath = path.join( - __dirname, + import.meta.dirname, '../../../app/src/LaunchpadController.mjs' ) describe('LaunchpadController', function () { // esmock doesn't work well with CommonJS dependencies, global imports for // @overleaf/settings aren't working until that module is migrated to ESM. In the - // meantime, the workaroung is to set and restore settings values - let oldSettingsAdminPrivilegeAvailable + // meantime, the workaround is to set and restore settings values - beforeEach(async function () { - this.user = { + beforeEach(async function (ctx) { + ctx.user = { _id: '323123', first_name: 'fn', last_name: 'ln', save: sinon.stub().callsArgWith(0), } - oldSettingsAdminPrivilegeAvailable = Settings.adminPrivilegeAvailable - Settings.adminPrivilegeAvailable = true + ctx.User = {} - this.User = {} - this.LaunchpadController = await esmock(modulePath, { - '@overleaf/metrics': (this.Metrics = {}), - '../../../../../app/src/Features/User/UserRegistrationHandler.js': - (this.UserRegistrationHandler = { + ctx.Settings = { + adminPrivilegeAvailable: true, + } + + vi.doMock('@overleaf/settings', () => ({ default: ctx.Settings })) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.Metrics = {}), + })) + + vi.doMock( + '../../../../../app/src/Features/User/UserRegistrationHandler.js', + () => ({ + default: (ctx.UserRegistrationHandler = { promises: {}, }), - '../../../../../app/src/Features/Email/EmailHandler.js': - (this.EmailHandler = { promises: {} }), - '../../../../../app/src/Features/User/UserGetter.js': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../../app/src/Features/Email/EmailHandler.js', () => ({ + default: (ctx.EmailHandler = { promises: {} }), + })) + + vi.doMock('../../../../../app/src/Features/User/UserGetter.js', () => ({ + default: (ctx.UserGetter = { promises: {}, }), - '../../../../../app/src/models/User.js': { User: this.User }, - '../../../../../app/src/Features/Authentication/AuthenticationController.js': - (this.AuthenticationController = {}), - '../../../../../app/src/Features/Authentication/AuthenticationManager.js': - (this.AuthenticationManager = {}), - '../../../../../app/src/Features/Authentication/SessionManager.js': - (this.SessionManager = { + })) + + vi.doMock('../../../../../app/src/models/User.js', () => ({ + User: ctx.User, + })) + + vi.doMock( + '../../../../../app/src/Features/Authentication/AuthenticationController.js', + () => ({ + default: (ctx.AuthenticationController = {}), + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Authentication/AuthenticationManager.js', + () => ({ + default: (ctx.AuthenticationManager = {}), + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: (ctx.SessionManager = { getSessionUser: sinon.stub(), }), - }) + }) + ) - this.email = 'bob@smith.com' + ctx.LaunchpadController = (await import(modulePath)).default - this.req = { + ctx.email = 'bob@smith.com' + + ctx.req = { query: {}, body: {}, session: {}, } - this.res = new MockResponse() - this.res.locals = { + ctx.res = new MockResponse() + ctx.res.locals = { translate(key) { return key }, } - this.next = sinon.stub() - }) - - afterEach(function () { - Settings.adminPrivilegeAvailable = oldSettingsAdminPrivilegeAvailable + ctx.next = sinon.stub() }) describe('launchpadPage', function () { - beforeEach(function () { - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists - this.AuthenticationController.setRedirectInSession = sinon.stub() + beforeEach(function (ctx) { + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists + ctx.AuthenticationController.setRedirectInSession = sinon.stub() }) describe('when the user is not logged in', function () { - beforeEach(function () { - this.SessionManager.getSessionUser = sinon.stub().returns(null) + beforeEach(function (ctx) { + ctx.SessionManager.getSessionUser = sinon.stub().returns(null) }) describe('when there are no admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should render the launchpad page', function () { - const viewPath = path.join(__dirname, '../../../app/views/launchpad') - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith(viewPath, { - adminUserExists: false, - authMethod: 'local', - }) - .should.equal(true) + it('should render the launchpad page', function (ctx) { + const viewPath = path.join( + import.meta.dirname, + '../../../app/views/launchpad' + ) + ctx.res.render.callCount.should.equal(1) + expect(ctx.res.render).to.have.been.calledWith(viewPath, { + adminUserExists: false, + authMethod: 'local', + }) }) }) describe('when there is at least one admin', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should redirect to login page', function () { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( + it('should redirect to login page', function (ctx) { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( 1 ) - this.res.redirect.calledWith('/login').should.equal(true) + ctx.res.redirect.calledWith('/login').should.equal(true) }) - it('should not render the launchpad page', function () { - this.res.render.callCount.should.equal(0) + it('should not render the launchpad page', function (ctx) { + ctx.res.render.callCount.should.equal(0) }) }) }) describe('when the user is logged in', function () { - beforeEach(function () { - this.user = { + beforeEach(function (ctx) { + ctx.user = { _id: 'abcd', email: 'abcd@example.com', } - this.SessionManager.getSessionUser.returns(this.user) - this._atLeastOneAdminExists.resolves(true) + ctx.SessionManager.getSessionUser.returns(ctx.user) + ctx._atLeastOneAdminExists.resolves(true) }) describe('when the user is an admin', function () { - beforeEach(async function () { - this.UserGetter.promises.getUser = sinon + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .resolves({ isAdmin: true }) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should render the launchpad page', function () { - const viewPath = path.join(__dirname, '../../../app/views/launchpad') - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith(viewPath, { - wsUrl: undefined, - adminUserExists: true, - authMethod: 'local', - }) - .should.equal(true) + it('should render the launchpad page', function (ctx) { + const viewPath = path.join( + import.meta.dirname, + '../../../app/views/launchpad' + ) + ctx.res.render.callCount.should.equal(1) + expect(ctx.res.render).to.have.been.calledWith(viewPath, { + wsUrl: undefined, + adminUserExists: true, + authMethod: 'local', + }) }) }) describe('when the user is not an admin', function () { - beforeEach(async function () { - this.UserGetter.promises.getUser = sinon + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .resolves({ isAdmin: false }) - await this.LaunchpadController.launchpadPage( - this.req, - this.res, - this.next + await ctx.LaunchpadController.launchpadPage( + ctx.req, + ctx.res, + ctx.next ) }) - it('should redirect to restricted page', function () { - this.res.redirect.callCount.should.equal(1) - this.res.redirect.calledWith('/restricted').should.equal(true) + it('should redirect to restricted page', function (ctx) { + ctx.res.redirect.callCount.should.equal(1) + ctx.res.redirect.calledWith('/restricted').should.equal(true) }) }) }) @@ -189,100 +217,92 @@ describe('LaunchpadController', function () { describe('_atLeastOneAdminExists', function () { describe('when there are no admins', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon.stub().resolves(null) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(null) }) - it('should callback with false', async function () { - const exists = await this.LaunchpadController._atLeastOneAdminExists() + it('should callback with false', async function (ctx) { + const exists = await ctx.LaunchpadController._atLeastOneAdminExists() expect(exists).to.equal(false) }) }) describe('when there are some admins', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon - .stub() - .resolves({ _id: 'abcd' }) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves({ _id: 'abcd' }) }) - it('should callback with true', async function () { - const exists = await this.LaunchpadController._atLeastOneAdminExists() + it('should callback with true', async function (ctx) { + const exists = await ctx.LaunchpadController._atLeastOneAdminExists() expect(exists).to.equal(true) }) }) describe('when getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.LaunchpadController._atLeastOneAdminExists()).rejected + it('should produce an error', async function (ctx) { + await expect(ctx.LaunchpadController._atLeastOneAdminExists()).rejected }) }) }) describe('sendTestEmail', function () { - beforeEach(function () { - this.EmailHandler.promises.sendEmail = sinon.stub().resolves() - this.req.body.email = 'someone@example.com' + beforeEach(function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon.stub().resolves() + ctx.req.body.email = 'someone@example.com' }) - it('should produce a 200 response', async function () { - await this.LaunchpadController.sendTestEmail( - this.req, - this.res, - this.next - ) - this.res.json.calledWith({ message: 'email_sent' }).should.equal(true) + it('should produce a 200 response', async function (ctx) { + await ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.res.json.calledWith({ message: 'email_sent' }).should.equal(true) }) - it('should not call next with an error', function () { - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(0) }) - it('should have called sendEmail', async function () { - await this.LaunchpadController.sendTestEmail( - this.req, - this.res, - this.next - ) - this.EmailHandler.promises.sendEmail.callCount.should.equal(1) - this.EmailHandler.promises.sendEmail + it('should have called sendEmail', async function (ctx) { + await ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.EmailHandler.promises.sendEmail.callCount.should.equal(1) + ctx.EmailHandler.promises.sendEmail .calledWith('testEmail') .should.equal(true) }) describe('when sendEmail produces an error', function () { - beforeEach(function () { - this.EmailHandler.promises.sendEmail = sinon + beforeEach(function (ctx) { + ctx.EmailHandler.promises.sendEmail = sinon .stub() .rejects(new Error('woops')) }) - it('should call next with an error', function (done) { - this.next = sinon.stub().callsFake(err => { - expect(err).to.be.instanceof(Error) - this.next.callCount.should.equal(1) - done() + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(err => { + expect(err).to.be.instanceof(Error) + ctx.next.callCount.should.equal(1) + resolve() + }) + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) }) - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) }) }) describe('when no email address is supplied', function () { - beforeEach(function () { - this.req.body.email = undefined + beforeEach(function (ctx) { + ctx.req.body.email = undefined }) - it('should produce a 400 response', function () { - this.LaunchpadController.sendTestEmail(this.req, this.res, this.next) - this.res.status.calledWith(400).should.equal(true) - this.res.json + it('should produce a 400 response', function (ctx) { + ctx.LaunchpadController.sendTestEmail(ctx.req, ctx.res, ctx.next) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json .calledWith({ message: 'no email address supplied', }) @@ -292,67 +312,63 @@ describe('LaunchpadController', function () { }) describe('registerAdmin', function () { - beforeEach(function () { - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists + beforeEach(function (ctx) { + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists }) describe('when all goes well', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json).to.have.been.calledWith({ redir: '/launchpad' }) + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json).to.have.been.calledWith({ redir: '/launchpad' }) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne.callCount.should.equal(1) - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne.callCount.should.equal(1) + ctx.User.updateOne .calledWithMatch( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -362,390 +378,345 @@ describe('LaunchpadController', function () { }) describe('when no email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when no password is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = undefined - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = undefined + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when an invalid email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'invalid password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'invalid password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon .stub() .returns(new Error('bad email')) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(400).should.equal(true) - this.res.json.calledWith({ + it('should send a 400 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json.calledWith({ message: { type: 'error', text: 'bad email' }, }) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when an invalid password is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'invalid password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'invalid password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon .stub() .returns(new Error('bad password')) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 400 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(400).should.equal(true) - this.res.json.calledWith({ + it('should send a 400 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(400).should.equal(true) + ctx.res.json.calledWith({ message: { type: 'error', text: 'bad password' }, }) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when there are already existing admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should send a 403 response', function () { - this.res.status.callCount.should.equal(1) - this.res.status.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.status.callCount.should.equal(1) + ctx.res.status.calledWith(403).should.equal(true) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when checking admins produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.rejects(new Error('woops')) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.rejects(new Error('woops')) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when registerNewUser produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() .rejects(new Error('woops')) - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should not call update', function () { - this.User.updateOne.callCount.should.equal(0) + it('should not call update', function (ctx) { + ctx.User.updateOne.callCount.should.equal(0) }) }) describe('when user update produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon.stub().returns({ + .resolves(ctx.user) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub().rejects(new Error('woops')), }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) }) describe('when overleaf', function () { - let oldSettingsOverleaf - - beforeEach(async function () { - oldSettingsOverleaf = Settings.overleaf - Settings.overleaf = { one: 1 } - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.password = 'a_really_bad_password' - this.req.body.email = this.email - this.req.body.password = this.password - this.user = { + beforeEach(async function (ctx) { + ctx.Settings.overleaf = { one: 1 } + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.password = 'a_really_bad_password' + ctx.req.body.email = ctx.email + ctx.req.body.password = ctx.password + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.AuthenticationManager.validateEmail = sinon.stub().returns(null) - this.AuthenticationManager.validatePassword = sinon.stub().returns(null) - this.UserGetter.promises.getUser = sinon - .stub() - .resolves({ _id: '1234' }) - await this.LaunchpadController.registerAdmin( - this.req, - this.res, - this.next - ) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.AuthenticationManager.validateEmail = sinon.stub().returns(null) + ctx.AuthenticationManager.validatePassword = sinon.stub().returns(null) + ctx.UserGetter.promises.getUser = sinon.stub().resolves({ _id: '1234' }) + await ctx.LaunchpadController.registerAdmin(ctx.req, ctx.res, ctx.next) }) - afterEach(async function () { - Settings.overleaf = oldSettingsOverleaf + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json).to.have.been.calledWith({ redir: '/launchpad' }) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json).to.have.been.calledWith({ redir: '/launchpad' }) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) - }) - - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser - .calledWith({ email: this.email, password: this.password }) + ctx.UserRegistrationHandler.promises.registerNewUser + .calledWith({ email: ctx.email, password: ctx.password }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne .calledWith( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -756,76 +727,69 @@ describe('LaunchpadController', function () { }) describe('registerExternalAuthAdmin', function () { - let oldSettingsLDAP - - beforeEach(function () { - oldSettingsLDAP = Settings.ldap - Settings.ldap = { one: 1 } - this.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() - this._atLeastOneAdminExists = - this.LaunchpadController._mocks._atLeastOneAdminExists - }) - - afterEach(function () { - Settings.ldap = oldSettingsLDAP + beforeEach(function (ctx) { + ctx.Settings.ldap = { one: 1 } + ctx.LaunchpadController._mocks._atLeastOneAdminExists = sinon.stub() + ctx._atLeastOneAdminExists = + ctx.LaunchpadController._mocks._atLeastOneAdminExists }) describe('when all goes well', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon + .resolves(ctx.user) + ctx.User.updateOne = sinon .stub() .returns({ exec: sinon.stub().resolves() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send back a json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.lastCall.args[0].email).to.equal(this.email) + it('should send back a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.lastCall.args[0].email).to.equal(ctx.email) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) }) - it('should have updated the user to make them an admin', function () { - this.User.updateOne.callCount.should.equal(1) - this.User.updateOne + it('should have updated the user to make them an admin', function (ctx) { + ctx.User.updateOne.callCount.should.equal(1) + ctx.User.updateOne .calledWith( - { _id: this.user._id }, + { _id: ctx.user._id }, { $set: { isAdmin: true, emails: [ - { email: this.user.email, reversedHostname: 'moc.elpmaxe' }, + { email: ctx.user.email, reversedHostname: 'moc.elpmaxe' }, ], }, } @@ -833,240 +797,240 @@ describe('LaunchpadController', function () { .should.equal(true) }) - it('should have set a redirect in session', function () { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( + it('should have set a redirect in session', function (ctx) { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( 1 ) - this.AuthenticationController.setRedirectInSession - .calledWith(this.req, '/launchpad') + ctx.AuthenticationController.setRedirectInSession + .calledWith(ctx.req, '/launchpad') .should.equal(true) }) }) describe('when the authMethod is invalid', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin( + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin( 'NOTAVALIDAUTHMETHOD' - )(this.req, this.res, this.next) + )(ctx.req, ctx.res, ctx.next) }) - it('should send a 403 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(403).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when no email is supplied', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = undefined - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = undefined + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 400 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(400).should.equal(true) + it('should send a 400 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(400).should.equal(true) }) - it('should not check for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(0) + it('should not check for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(0) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when there are already existing admins', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(true) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(true) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 403 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(403).should.equal(true) + it('should send a 403 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(403).should.equal(true) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when checking admins produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.rejects(new Error('woops')) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.rejects(new Error('woops')) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon.stub() - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.UserRegistrationHandler.promises.registerNewUser = sinon.stub() + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should not call registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should not call registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 0 ) }) }) describe('when registerNewUser produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() .rejects(new Error('woops')) - this.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub() }) + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) }) - it('should not call update', function () { - this.User.updateOne.callCount.should.equal(0) + it('should not call update', function (ctx) { + ctx.User.updateOne.callCount.should.equal(0) }) }) describe('when user update produces an error', function () { - beforeEach(async function () { - this._atLeastOneAdminExists.resolves(false) - this.email = 'someone@example.com' - this.req.body.email = this.email - this.user = { + beforeEach(async function (ctx) { + ctx._atLeastOneAdminExists.resolves(false) + ctx.email = 'someone@example.com' + ctx.req.body.email = ctx.email + ctx.user = { _id: 'abcdef', - email: this.email, + email: ctx.email, } - this.UserRegistrationHandler.promises.registerNewUser = sinon + ctx.UserRegistrationHandler.promises.registerNewUser = sinon .stub() - .resolves(this.user) - this.User.updateOne = sinon.stub().returns({ + .resolves(ctx.user) + ctx.User.updateOne = sinon.stub().returns({ exec: sinon.stub().rejects(new Error('woops')), }) - this.AuthenticationController.setRedirectInSession = sinon.stub() - await this.LaunchpadController.registerExternalAuthAdmin('ldap')( - this.req, - this.res, - this.next + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + await ctx.LaunchpadController.registerExternalAuthAdmin('ldap')( + ctx.req, + ctx.res, + ctx.next ) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should have checked for existing admins', function () { - this._atLeastOneAdminExists.callCount.should.equal(1) + it('should have checked for existing admins', function (ctx) { + ctx._atLeastOneAdminExists.callCount.should.equal(1) }) - it('should have called registerNewUser', function () { - this.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( + it('should have called registerNewUser', function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUser.callCount.should.equal( 1 ) - this.UserRegistrationHandler.promises.registerNewUser + ctx.UserRegistrationHandler.promises.registerNewUser .calledWith({ - email: this.email, + email: ctx.email, password: 'password_here', - first_name: this.email, + first_name: ctx.email, last_name: '', }) .should.equal(true) diff --git a/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs b/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs index 7c4382a720..9019e525d7 100644 --- a/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs +++ b/services/web/modules/user-activate/test/unit/src/UserActivateController.test.mjs @@ -1,132 +1,162 @@ +import { vi } from 'vitest' import Path from 'node:path' -import { fileURLToPath } from 'node:url' -import { strict as esmock } from 'esmock' import sinon from 'sinon' -const __dirname = Path.dirname(fileURLToPath(import.meta.url)) - const MODULE_PATH = '../../../app/src/UserActivateController.mjs' -const VIEW_PATH = Path.join(__dirname, '../../../app/views/user/activate') +const VIEW_PATH = Path.join( + import.meta.dirname, + '../../../app/views/user/activate' +) describe('UserActivateController', function () { - beforeEach(async function () { - this.user = { - _id: (this.user_id = 'kwjewkl'), + beforeEach(async function (ctx) { + ctx.user = { + _id: (ctx.user_id = 'kwjewkl'), features: {}, email: 'joe@example.com', } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub(), }, } - this.UserRegistrationHandler = { promises: {} } - this.ErrorController = { notFound: sinon.stub() } - this.SplitTestHandler = { + ctx.UserRegistrationHandler = { promises: {} } + ctx.ErrorController = { notFound: sinon.stub() } + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, } - this.UserActivateController = await esmock(MODULE_PATH, { - '../../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../../app/src/Features/User/UserRegistrationHandler.js': - this.UserRegistrationHandler, - '../../../../../app/src/Features/Errors/ErrorController.js': - this.ErrorController, - '../../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) - this.req = { + + vi.doMock('../../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../../app/src/Features/User/UserRegistrationHandler.js', + () => ({ + default: ctx.UserRegistrationHandler, + }) + ) + + vi.doMock( + '../../../../../app/src/Features/Errors/ErrorController.js', + () => ({ + default: ctx.ErrorController, + }) + ) + + vi.doMock( + '../../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.UserActivateController = (await import(MODULE_PATH)).default + ctx.req = { body: {}, query: {}, session: { - user: this.user, + user: ctx.user, }, } - this.res = { + ctx.res = { json: sinon.stub(), } }) describe('activateAccountPage', function () { - beforeEach(function () { - this.UserGetter.promises.getUser = sinon.stub().resolves(this.user) - this.req.query.user_id = this.user_id - this.req.query.token = this.token = 'mock-token-123' + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user) + ctx.req.query.user_id = ctx.user_id + ctx.req.query.token = ctx.token = 'mock-token-123' }) - it('should 404 without a user_id', async function (done) { - delete this.req.query.user_id - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a user_id', async function (ctx) { + delete ctx.req.query.user_id + return new Promise(resolve => { + ctx.ErrorController.notFound = () => resolve() + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 404 without a token', function (done) { - delete this.req.query.token - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a token', function (ctx) { + return new Promise(resolve => { + delete ctx.req.query.token + ctx.ErrorController.notFound = resolve + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 404 without a valid user_id', function (done) { - this.UserGetter.promises.getUser = sinon.stub().resolves(null) - this.ErrorController.notFound = () => done() - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 404 without a valid user_id', function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser = sinon.stub().resolves(null) + ctx.ErrorController.notFound = resolve + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should 403 for complex user_id', function (done) { - this.ErrorController.forbidden = () => done() - this.req.query.user_id = { first_name: 'X' } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should 403 for complex user_id', function (ctx) { + return new Promise(resolve => { + ctx.ErrorController.forbidden = resolve + ctx.req.query.user_id = { first_name: 'X' } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('should redirect activated users to login', function (done) { - this.user.loginCount = 1 - this.res.redirect = url => { - sinon.assert.calledWith(this.UserGetter.promises.getUser, this.user_id) - url.should.equal('/login') - return done() - } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('should redirect activated users to login', function (ctx) { + return new Promise(resolve => { + ctx.user.loginCount = 1 + ctx.res.redirect = url => { + sinon.assert.calledWith(ctx.UserGetter.promises.getUser, ctx.user_id) + url.should.equal('/login') + resolve() + } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) - it('render the activation page if the user has not logged in before', function (done) { - this.user.loginCount = 0 - this.res.render = (page, opts) => { - page.should.equal(VIEW_PATH) - opts.email.should.equal(this.user.email) - opts.token.should.equal(this.token) - return done() - } - this.UserActivateController.activateAccountPage(this.req, this.res) + it('render the activation page if the user has not logged in before', function (ctx) { + return new Promise(resolve => { + ctx.user.loginCount = 0 + ctx.res.render = (page, opts) => { + page.should.equal(VIEW_PATH) + opts.email.should.equal(ctx.user.email) + opts.token.should.equal(ctx.token) + resolve() + } + ctx.UserActivateController.activateAccountPage(ctx.req, ctx.res) + }) }) }) describe('register', function () { - beforeEach(async function () { - this.UserRegistrationHandler.promises.registerNewUserAndSendActivationEmail = + beforeEach(async function (ctx) { + ctx.UserRegistrationHandler.promises.registerNewUserAndSendActivationEmail = sinon.stub().resolves({ - user: this.user, - setNewPasswordUrl: (this.url = 'mock/url'), + user: ctx.user, + setNewPasswordUrl: (ctx.url = 'mock/url'), }) - this.req.body.email = this.user.email = this.email = 'email@example.com' - await this.UserActivateController.register(this.req, this.res) + ctx.req.body.email = ctx.user.email = ctx.email = 'email@example.com' + await ctx.UserActivateController.register(ctx.req, ctx.res) }) - it('should register the user and send them an email', function () { + it('should register the user and send them an email', function (ctx) { sinon.assert.calledWith( - this.UserRegistrationHandler.promises + ctx.UserRegistrationHandler.promises .registerNewUserAndSendActivationEmail, - this.email + ctx.email ) }) - it('should return the user and activation url', function () { - this.res.json + it('should return the user and activation url', function (ctx) { + ctx.res.json .calledWith({ - email: this.email, - setNewPasswordUrl: this.url, + email: ctx.email, + setNewPasswordUrl: ctx.url, }) .should.equal(true) }) diff --git a/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs b/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs index cba0e935db..4019f2bce9 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs +++ b/services/web/test/unit/src/Analytics/AnalyticsController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockResponse from '../helpers/MockResponse.js' const modulePath = new URL( @@ -7,37 +7,52 @@ const modulePath = new URL( ).pathname describe('AnalyticsController', function () { - beforeEach(async function () { - this.SessionManager = { getLoggedInUserId: sinon.stub() } + beforeEach(async function (ctx) { + ctx.SessionManager = { getLoggedInUserId: sinon.stub() } - this.AnalyticsManager = { + ctx.AnalyticsManager = { updateEditingSession: sinon.stub(), recordEventForSession: sinon.stub(), } - this.Features = { + ctx.Features = { hasFeature: sinon.stub().returns(true), } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - this.AnalyticsManager, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '../../../../app/src/infrastructure/Features.js': this.Features, - '../../../../app/src/infrastructure/GeoIpLookup.js': (this.GeoIpLookup = { + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features.js', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/infrastructure/GeoIpLookup.js', () => ({ + default: (ctx.GeoIpLookup = { promises: { getDetails: sinon.stub().resolves(), }, }), - }) + })) - this.res = new MockResponse() + ctx.controller = (await import(modulePath)).default + + ctx.res = new MockResponse() }) describe('updateEditingSession', function () { - beforeEach(function () { - this.req = { + beforeEach(function (ctx) { + ctx.req = { params: { projectId: 'a project id', }, @@ -48,34 +63,36 @@ describe('AnalyticsController', function () { }, }, } - this.GeoIpLookup.promises.getDetails = sinon + ctx.GeoIpLookup.promises.getDetails = sinon .stub() .resolves({ country_code: 'XY' }) }) - it('delegates to the AnalyticsManager', function (done) { - this.SessionManager.getLoggedInUserId.returns('1234') - this.res.callback = () => { - sinon.assert.calledWith( - this.AnalyticsManager.updateEditingSession, - '1234', - 'a project id', - 'XY', - { editorType: 'abc' } - ) - done() - } - this.controller.updateEditingSession(this.req, this.res) + it('delegates to the AnalyticsManager', function (ctx) { + return new Promise(resolve => { + ctx.SessionManager.getLoggedInUserId.returns('1234') + ctx.res.callback = () => { + sinon.assert.calledWith( + ctx.AnalyticsManager.updateEditingSession, + '1234', + 'a project id', + 'XY', + { editorType: 'abc' } + ) + resolve() + } + ctx.controller.updateEditingSession(ctx.req, ctx.res) + }) }) }) describe('recordEvent', function () { - beforeEach(function () { + beforeEach(function (ctx) { const body = { foo: 'stuff', _csrf: 'atoken123', } - this.req = { + ctx.req = { params: { event: 'i_did_something', }, @@ -84,30 +101,34 @@ describe('AnalyticsController', function () { session: {}, } - this.expectedData = Object.assign({}, body) - delete this.expectedData._csrf + ctx.expectedData = Object.assign({}, body) + delete ctx.expectedData._csrf }) - it('should use the session', function (done) { - this.controller.recordEvent(this.req, this.res) - sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, - this.req.params.event, - this.expectedData - ) - done() + it('should use the session', function (ctx) { + return new Promise(resolve => { + ctx.controller.recordEvent(ctx.req, ctx.res) + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, + ctx.req.params.event, + ctx.expectedData + ) + resolve() + }) }) - it('should remove the CSRF token before sending to the manager', function (done) { - this.controller.recordEvent(this.req, this.res) - sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, - this.req.params.event, - this.expectedData - ) - done() + it('should remove the CSRF token before sending to the manager', function (ctx) { + return new Promise(resolve => { + ctx.controller.recordEvent(ctx.req, ctx.res) + sinon.assert.calledWith( + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, + ctx.req.params.event, + ctx.expectedData + ) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs b/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs index 461a2a70d1..fff5224b48 100644 --- a/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs +++ b/services/web/test/unit/src/Analytics/AnalyticsUTMTrackingMiddleware.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' @@ -10,83 +10,90 @@ const MODULE_PATH = new URL( ).pathname describe('AnalyticsUTMTrackingMiddleware', function () { - beforeEach(async function () { - this.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55' - this.userId = '61795fcb013504bb7b663092' + beforeEach(async function (ctx) { + ctx.analyticsId = 'ecdb935a-52f3-4f91-aebc-7a70d2ffbb55' + ctx.userId = '61795fcb013504bb7b663092' - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub().returns() - this.req.session = { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub().returns() + ctx.req.session = { user: { - _id: this.userId, - analyticsId: this.analyticsId, + _id: ctx.userId, + analyticsId: ctx.analyticsId, }, } - this.AnalyticsUTMTrackingMiddleware = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - (this.AnalyticsManager = { + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: (ctx.AnalyticsManager = { recordEventForSession: sinon.stub().resolves(), setUserPropertyForSessionInBackground: sinon.stub(), }), - '@overleaf/settings': { + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: { siteUrl: 'https://www.overleaf.com', }, - }) + })) - this.middleware = this.AnalyticsUTMTrackingMiddleware.recordUTMTags() + ctx.AnalyticsUTMTrackingMiddleware = (await import(MODULE_PATH)).default + + ctx.middleware = ctx.AnalyticsUTMTrackingMiddleware.recordUTMTags() }) describe('without UTM tags in query', function () { - beforeEach(function () { - this.req.url = '/project' - this.middleware(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.url = '/project' + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is not redirected', function () { - assert.isFalse(this.res.redirected) + it('user is not redirected', function (ctx) { + assert.isFalse(ctx.res.redirected) }) - it('next middleware is executed', function () { - sinon.assert.calledOnce(this.next) + it('next middleware is executed', function (ctx) { + sinon.assert.calledOnce(ctx.next) }) - it('no event or user property is recorded', function () { - sinon.assert.notCalled(this.AnalyticsManager.recordEventForSession) + it('no event or user property is recorded', function (ctx) { + sinon.assert.notCalled(ctx.AnalyticsManager.recordEventForSession) sinon.assert.notCalled( - this.AnalyticsManager.setUserPropertyForSessionInBackground + ctx.AnalyticsManager.setUserPropertyForSessionInBackground ) }) }) describe('with all UTM tags in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_source=Organic&utm_medium=Facebook&utm_campaign=Some%20Campaign&utm_content=foo-bar&utm_term=overridden' - this.req.query = { + ctx.req.query = { utm_source: 'Organic', utm_medium: 'Facebook', utm_campaign: 'Some Campaign', utm_content: 'foo-bar', utm_term: 'overridden', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -99,10 +106,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'Organic;Facebook;Some Campaign;foo-bar' ) @@ -110,30 +117,30 @@ describe('AnalyticsUTMTrackingMiddleware', function () { }) describe('with some UTM tags in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_medium=Facebook&utm_campaign=Some%20Campaign&utm_term=foo' - this.req.query = { + ctx.req.query = { utm_medium: 'Facebook', utm_campaign: 'Some Campaign', utm_term: 'foo', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -144,10 +151,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'N/A;Facebook;Some Campaign;foo' ) @@ -155,30 +162,30 @@ describe('AnalyticsUTMTrackingMiddleware', function () { }) describe('with some UTM tags and additional parameters in query', function () { - beforeEach(function () { - this.req.url = + beforeEach(function (ctx) { + ctx.req.url = '/project?utm_medium=Facebook&utm_campaign=Some%20Campaign&other_param=some-value' - this.req.query = { + ctx.req.query = { utm_medium: 'Facebook', utm_campaign: 'Some Campaign', other_param: 'some-value', } - this.middleware(this.req, this.res, this.next) + ctx.middleware(ctx.req, ctx.res, ctx.next) }) - it('user is redirected', function () { - assert.isTrue(this.res.redirected) - assert.equal('/project?other_param=some-value', this.res.redirectedTo) + it('user is redirected', function (ctx) { + assert.isTrue(ctx.res.redirected) + assert.equal('/project?other_param=some-value', ctx.res.redirectedTo) }) - it('next middleware is not executed', function () { - sinon.assert.notCalled(this.next) + it('next middleware is not executed', function (ctx) { + sinon.assert.notCalled(ctx.next) }) - it('page-view event is recorded for session', function () { + it('page-view event is recorded for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.recordEventForSession, - this.req.session, + ctx.AnalyticsManager.recordEventForSession, + ctx.req.session, 'page-view', { path: '/project', @@ -188,10 +195,10 @@ describe('AnalyticsUTMTrackingMiddleware', function () { ) }) - it('utm-tags user property is set for session', function () { + it('utm-tags user property is set for session', function (ctx) { sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForSessionInBackground, - this.req.session, + ctx.AnalyticsManager.setUserPropertyForSessionInBackground, + ctx.req.session, 'utm-tags', 'N/A;Facebook;Some Campaign;N/A' ) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs index 78747b8880..e2160cca08 100644 --- a/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs +++ b/services/web/test/unit/src/BetaProgram/BetaProgramController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import path from 'node:path' import sinon from 'sinon' import { expect } from 'chai' @@ -13,185 +13,228 @@ const modulePath = path.join( ) describe('BetaProgramController', function () { - beforeEach(async function () { - this.user = { - _id: (this.user_id = 'a_simple_id'), + beforeEach(async function (ctx) { + ctx.user = { + _id: (ctx.user_id = 'a_simple_id'), email: 'user@example.com', features: {}, betaProgram: false, } - this.req = { + ctx.req = { query: {}, session: { - user: this.user, + user: ctx.user, }, } - this.SplitTestSessionHandler = { + ctx.SplitTestSessionHandler = { promises: { sessionMaintenance: sinon.stub(), }, } - this.BetaProgramController = await esmock.strict(modulePath, { - '../../../../app/src/Features/SplitTests/SplitTestSessionHandler': - this.SplitTestSessionHandler, - '../../../../app/src/Features/BetaProgram/BetaProgramHandler': - (this.BetaProgramHandler = { + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestSessionHandler', + () => ({ + default: ctx.SplitTestSessionHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/BetaProgram/BetaProgramHandler', + () => ({ + default: (ctx.BetaProgramHandler = { promises: { optIn: sinon.stub().resolves(), optOut: sinon.stub().resolves(), }, }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: { getUser: sinon.stub().resolves(), }, }), - '@overleaf/settings': (this.settings = { + })) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { languages: {}, }), - '../../../../app/src/Features/Authentication/AuthenticationController': - (this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.user._id), + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: (ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), }), - }) - this.res = new MockResponse() - this.next = sinon.stub() + }) + ) + + ctx.BetaProgramController = (await import(modulePath)).default + ctx.res = new MockResponse() + ctx.next = sinon.stub() }) describe('optIn', function () { - it("should redirect to '/beta/participate'", function (done) { - this.res.callback = () => { - this.res.redirectedTo.should.equal('/beta/participate') - done() - } - this.BetaProgramController.optIn(this.req, this.res, done) + it("should redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.redirectedTo.should.equal('/beta/participate') + resolve() + } + ctx.BetaProgramController.optIn(ctx.req, ctx.res, resolve) + }) }) - it('should not call next with an error', function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(0) }) - it('should call BetaProgramHandler.optIn', function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.BetaProgramHandler.promises.optIn.callCount.should.equal(1) + it('should call BetaProgramHandler.optIn', function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.BetaProgramHandler.promises.optIn.callCount.should.equal(1) }) - it('should invoke the session maintenance', function (done) { - this.res.callback = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req - ) - done() - } - this.BetaProgramController.optIn(this.req, this.res, done) + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req + ) + resolve() + } + ctx.BetaProgramController.optIn(ctx.req, ctx.res, resolve) + }) }) describe('when BetaProgramHandler.opIn produces an error', function () { - beforeEach(function () { - this.BetaProgramHandler.promises.optIn.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.BetaProgramHandler.promises.optIn.throws(new Error('woops')) }) - it("should not redirect to '/beta/participate'", function () { - this.BetaProgramController.optIn(this.req, this.res, this.next) - this.res.redirect.callCount.should.equal(0) + it("should not redirect to '/beta/participate'", function (ctx) { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, ctx.next) + ctx.res.redirect.callCount.should.equal(0) }) - it('should produce an error', function (done) { - this.BetaProgramController.optIn(this.req, this.res, err => { - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optIn(ctx.req, ctx.res, err => { + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) }) describe('optOut', function () { - it("should redirect to '/beta/participate'", function (done) { - this.res.callback = () => { - expect(this.res.redirectedTo).to.equal('/beta/participate') - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it("should redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.redirectedTo).to.equal('/beta/participate') + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should not call next with an error', function (done) { - this.res.callback = () => { - this.next.callCount.should.equal(0) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should not call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.next.callCount.should.equal(0) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should call BetaProgramHandler.optOut', function (done) { - this.res.callback = () => { - this.BetaProgramHandler.promises.optOut.callCount.should.equal(1) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should call BetaProgramHandler.optOut', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.BetaProgramHandler.promises.optOut.callCount.should.equal(1) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) - it('should invoke the session maintenance', function (done) { - this.res.callback = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req, - null - ) - done() - } - this.BetaProgramController.optOut(this.req, this.res, done) + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req, + null + ) + resolve() + } + ctx.BetaProgramController.optOut(ctx.req, ctx.res, resolve) + }) }) describe('when BetaProgramHandler.optOut produces an error', function () { - beforeEach(function () { - this.BetaProgramHandler.promises.optOut.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.BetaProgramHandler.promises.optOut.throws(new Error('woops')) }) - it("should not redirect to '/beta/participate'", function (done) { - this.BetaProgramController.optOut(this.req, this.res, error => { - expect(error).to.exist - expect(this.res.redirected).to.equal(false) - done() + it("should not redirect to '/beta/participate'", function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optOut(ctx.req, ctx.res, error => { + expect(error).to.exist + expect(ctx.res.redirected).to.equal(false) + resolve() + }) }) }) - it('should produce an error', function (done) { - this.BetaProgramController.optOut(this.req, this.res, error => { - expect(error).to.exist - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optOut(ctx.req, ctx.res, error => { + expect(error).to.exist + resolve() + }) }) }) }) }) describe('optInPage', function () { - beforeEach(function () { - this.UserGetter.promises.getUser.resolves(this.user) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser.resolves(ctx.user) }) - it('should render the opt-in page', function (done) { - this.res.callback = () => { - expect(this.res.renderedTemplate).to.equal('beta_program/opt_in') - done() - } - this.BetaProgramController.optInPage(this.req, this.res, done) + it('should render the opt-in page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedTemplate).to.equal('beta_program/opt_in') + resolve() + } + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, resolve) + }) }) describe('when UserGetter.getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUser.throws(new Error('woops')) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUser.throws(new Error('woops')) }) - it('should not render the opt-in page', function () { - this.BetaProgramController.optInPage(this.req, this.res, this.next) - this.res.render.callCount.should.equal(0) + it('should not render the opt-in page', function (ctx) { + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, ctx.next) + ctx.res.render.callCount.should.equal(0) }) - it('should produce an error', function (done) { - this.BetaProgramController.optInPage(this.req, this.res, error => { - expect(error).to.exist - expect(error).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.BetaProgramController.optInPage(ctx.req, ctx.res, error => { + expect(error).to.exist + expect(error).to.be.instanceof(Error) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs index 2b72271fd5..14438a8ed7 100644 --- a/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs +++ b/services/web/test/unit/src/BetaProgram/BetaProgramHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import path from 'node:path' import sinon from 'sinon' @@ -13,128 +13,155 @@ const modulePath = path.join( ) describe('BetaProgramHandler', function () { - beforeEach(async function () { - this.user_id = 'some_id' - this.user = { - _id: this.user_id, + beforeEach(async function (ctx) { + ctx.user_id = 'some_id' + ctx.user = { + _id: ctx.user_id, email: 'user@example.com', features: {}, betaProgram: false, save: sinon.stub().callsArgWith(0, null), } - this.handler = await esmock.strict(modulePath, { - '@overleaf/metrics': { + + vi.doMock('@overleaf/metrics', () => ({ + default: { inc: sinon.stub(), }, - '../../../../app/src/Features/User/UserUpdater': (this.UserUpdater = { + })) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: (ctx.UserUpdater = { promises: { updateUser: sinon.stub().resolves(), }, }), - '../../../../app/src/Features/Analytics/AnalyticsManager': - (this.AnalyticsManager = { + })) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: (ctx.AnalyticsManager = { setUserPropertyForUserInBackground: sinon.stub(), }), - }) + }) + ) + + ctx.handler = (await import(modulePath)).default }) describe('optIn', function () { - beforeEach(function () { - this.user.betaProgram = false - this.call = callback => { - this.handler.optIn(this.user_id, callback) + beforeEach(function (ctx) { + ctx.user.betaProgram = false + ctx.call = callback => { + ctx.handler.optIn(ctx.user_id, callback) } }) - it('should call userUpdater', function (done) { - this.call(err => { - expect(err).to.not.exist - this.UserUpdater.promises.updateUser.callCount.should.equal(1) - done() + it('should call userUpdater', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + ctx.UserUpdater.promises.updateUser.callCount.should.equal(1) + resolve() + }) }) }) - it('should set beta-program user property to true', function (done) { - this.call(err => { - expect(err).to.not.exist - sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForUserInBackground, - this.user_id, - 'beta-program', - true - ) - done() + it('should set beta-program user property to true', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + sinon.assert.calledWith( + ctx.AnalyticsManager.setUserPropertyForUserInBackground, + ctx.user_id, + 'beta-program', + true + ) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(err => { - expect(err).to.not.exist - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + resolve() + }) }) }) describe('when userUpdater produces an error', function () { - beforeEach(function () { - this.UserUpdater.promises.updateUser.rejects() + beforeEach(function (ctx) { + ctx.UserUpdater.promises.updateUser.rejects() }) - it('should produce an error', function (done) { - this.call(err => { - expect(err).to.exist - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) }) describe('optOut', function () { - beforeEach(function () { - this.user.betaProgram = true - this.call = callback => { - this.handler.optOut(this.user_id, callback) + beforeEach(function (ctx) { + ctx.user.betaProgram = true + ctx.call = callback => { + ctx.handler.optOut(ctx.user_id, callback) } }) - it('should call userUpdater', function (done) { - this.call(err => { - expect(err).to.not.exist - this.UserUpdater.promises.updateUser.callCount.should.equal(1) - done() + it('should call userUpdater', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + ctx.UserUpdater.promises.updateUser.callCount.should.equal(1) + resolve() + }) }) }) - it('should set beta-program user property to false', function (done) { - this.call(err => { - expect(err).to.not.exist - sinon.assert.calledWith( - this.AnalyticsManager.setUserPropertyForUserInBackground, - this.user_id, - 'beta-program', - false - ) - done() + it('should set beta-program user property to false', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + sinon.assert.calledWith( + ctx.AnalyticsManager.setUserPropertyForUserInBackground, + ctx.user_id, + 'beta-program', + false + ) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(err => { - expect(err).to.not.exist - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.not.exist + resolve() + }) }) }) describe('when userUpdater produces an error', function () { - beforeEach(function () { - this.UserUpdater.promises.updateUser.rejects() + beforeEach(function (ctx) { + ctx.UserUpdater.promises.updateUser.rejects() }) - it('should produce an error', function (done) { - this.call(err => { - expect(err).to.exist - expect(err).to.be.instanceof(Error) - done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(err => { + expect(err).to.exist + expect(err).to.be.instanceof(Error) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs index 27460da148..9bb9c4b3c0 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import mongodb from 'mongodb-legacy' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockRequest from '../helpers/MockRequest.js' @@ -11,425 +11,510 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsController.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('CollaboratorsController', function () { - beforeEach(async function () { - this.res = new MockResponse() - this.req = new MockRequest() + beforeEach(async function (ctx) { + ctx.res = new MockResponse() + ctx.req = new MockRequest() - this.user = { _id: new ObjectId() } - this.projectId = new ObjectId() - this.callback = sinon.stub() + ctx.user = { _id: new ObjectId() } + ctx.projectId = new ObjectId() + ctx.callback = sinon.stub() - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { removeUserFromProject: sinon.stub().resolves(), setCollaboratorPrivilegeLevel: sinon.stub().resolves(), }, createTokenHashPrefix: sinon.stub().returns('abc123'), } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { getAllInvitedMembers: sinon.stub(), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.HttpErrorHandler = { + ctx.HttpErrorHandler = { forbidden: sinon.stub(), notFound: sinon.stub(), } - this.TagsHandler = { + ctx.TagsHandler = { promises: { removeProjectFromAllTags: sinon.stub().resolves(), }, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.user), - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.user), + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.OwnershipTransferHandler = { + ctx.OwnershipTransferHandler = { promises: { transferOwnership: sinon.stub().resolves(), }, } - this.TokenAccessHandler = { + ctx.TokenAccessHandler = { getRequestToken: sinon.stub().returns('access-token'), } - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { addEntryInBackground: sinon.stub(), } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves({ owner_ref: this.user._id }), + getProject: sinon.stub().resolves({ owner_ref: ctx.user._id }), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAddXEditCollaborators: sinon.stub().resolves(), canChangeCollaboratorPrivilegeLevel: sinon.stub().resolves(true), }, } - this.CollaboratorsController = await esmock.strict(MODULE_PATH, { - 'mongodb-legacy': { ObjectId }, - '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js': - this.CollaboratorsHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js': - this.CollaboratorsGetter, - '../../../../app/src/Features/Collaborators/OwnershipTransferHandler.js': - this.OwnershipTransferHandler, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Errors/HttpErrorHandler.js': - this.HttpErrorHandler, - '../../../../app/src/Features/Tags/TagsHandler.js': this.TagsHandler, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '../../../../app/src/Features/TokenAccess/TokenAccessHandler.js': - this.TokenAccessHandler, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/SplitTests/SplitTestHandler.js': - this.SplitTestHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/OwnershipTransferHandler.js', + () => ({ + default: ctx.OwnershipTransferHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Errors/HttpErrorHandler.js', + () => ({ + default: ctx.HttpErrorHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler.js', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler.js', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.js', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + ctx.CollaboratorsController = (await import(MODULE_PATH)).default }) describe('removeUserFromProject', function () { - beforeEach(function (done) { - this.req.params = { - Project_id: this.projectId, - user_id: this.user._id, - } - this.res.sendStatus = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { + Project_id: ctx.projectId, + user_id: ctx.user._id, + } + ctx.res.sendStatus = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsController.removeUserFromProject(ctx.req, ctx.res) }) - this.CollaboratorsController.removeUserFromProject(this.req, this.res) }) - it('should from the user from the project', function () { + it('should from the user from the project', function (ctx) { expect( - this.CollaboratorsHandler.promises.removeUserFromProject - ).to.have.been.calledWith(this.projectId, this.user._id) + ctx.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(ctx.projectId, ctx.user._id) }) - it('should emit a userRemovedFromProject event to the proejct', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should emit a userRemovedFromProject event to the proejct', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'userRemovedFromProject', - this.user._id + ctx.user._id ) }) - it('should send the back a success response', function () { - this.res.sendStatus.calledWith(204).should.equal(true) + it('should send the back a success response', function (ctx) { + ctx.res.sendStatus.calledWith(204).should.equal(true) }) - it('should have called emitToRoom', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should have called emitToRoom', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'project:membership:changed' ) }) - it('should write a project audit log', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should write a project audit log', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'remove-collaborator', - this.user._id, - this.req.ip, - { userId: this.user._id } + ctx.user._id, + ctx.req.ip, + { userId: ctx.user._id } ) }) }) describe('removeSelfFromProject', function () { - beforeEach(function (done) { - this.req.params = { Project_id: this.projectId } - this.res.sendStatus = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { Project_id: ctx.projectId } + ctx.res.sendStatus = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsController.removeSelfFromProject(ctx.req, ctx.res) }) - this.CollaboratorsController.removeSelfFromProject(this.req, this.res) }) - it('should remove the logged in user from the project', function () { + it('should remove the logged in user from the project', function (ctx) { expect( - this.CollaboratorsHandler.promises.removeUserFromProject - ).to.have.been.calledWith(this.projectId, this.user._id) + ctx.CollaboratorsHandler.promises.removeUserFromProject + ).to.have.been.calledWith(ctx.projectId, ctx.user._id) }) - it('should emit a userRemovedFromProject event to the proejct', function () { - expect(this.EditorRealTimeController.emitToRoom).to.have.been.calledWith( - this.projectId, + it('should emit a userRemovedFromProject event to the proejct', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.projectId, 'userRemovedFromProject', - this.user._id + ctx.user._id ) }) - it('should remove the project from all tags', function () { + it('should remove the project from all tags', function (ctx) { expect( - this.TagsHandler.promises.removeProjectFromAllTags - ).to.have.been.calledWith(this.user._id, this.projectId) + ctx.TagsHandler.promises.removeProjectFromAllTags + ).to.have.been.calledWith(ctx.user._id, ctx.projectId) }) - it('should return a success code', function () { - this.res.sendStatus.calledWith(204).should.equal(true) + it('should return a success code', function (ctx) { + ctx.res.sendStatus.calledWith(204).should.equal(true) }) - it('should write a project audit log', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should write a project audit log', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'leave-project', - this.user._id, - this.req.ip + ctx.user._id, + ctx.req.ip ) }) }) describe('getAllMembers', function () { - beforeEach(function (done) { - this.req.params = { Project_id: this.projectId } - this.res.json = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { Project_id: ctx.projectId } + ctx.res.json = sinon.spy(() => { + resolve() + }) + ctx.next = sinon.stub() + ctx.members = [{ a: 1 }] + ctx.CollaboratorsGetter.promises.getAllInvitedMembers.resolves( + ctx.members + ) + ctx.CollaboratorsController.getAllMembers(ctx.req, ctx.res, ctx.next) }) - this.next = sinon.stub() - this.members = [{ a: 1 }] - this.CollaboratorsGetter.promises.getAllInvitedMembers.resolves( - this.members - ) - this.CollaboratorsController.getAllMembers(this.req, this.res, this.next) }) - it('should not produce an error', function () { - this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should produce a json response', function () { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith({ members: this.members }).should.equal(true) + it('should produce a json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith({ members: ctx.members }).should.equal(true) }) - it('should call CollaboratorsGetter.getAllInvitedMembers', function () { - expect(this.CollaboratorsGetter.promises.getAllInvitedMembers).to.have - .been.calledOnce + it('should call CollaboratorsGetter.getAllInvitedMembers', function (ctx) { + expect(ctx.CollaboratorsGetter.promises.getAllInvitedMembers).to.have.been + .calledOnce }) describe('when CollaboratorsGetter.getAllInvitedMembers produces an error', function () { - beforeEach(function (done) { - this.res.json = sinon.stub() - this.next = sinon.spy(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.json = sinon.stub() + ctx.next = sinon.spy(() => { + resolve() + }) + ctx.CollaboratorsGetter.promises.getAllInvitedMembers.rejects( + new Error('woops') + ) + ctx.CollaboratorsController.getAllMembers(ctx.req, ctx.res, ctx.next) }) - this.CollaboratorsGetter.promises.getAllInvitedMembers.rejects( - new Error('woops') - ) - this.CollaboratorsController.getAllMembers( - this.req, - this.res, - this.next - ) }) - it('should produce an error', function () { - expect(this.next).to.have.been.calledOnce - expect(this.next).to.have.been.calledWithMatch( + it('should produce an error', function (ctx) { + expect(ctx.next).to.have.been.calledOnce + expect(ctx.next).to.have.been.calledWithMatch( sinon.match.instanceOf(Error) ) }) - it('should not produce a json response', function () { - this.res.json.callCount.should.equal(0) + it('should not produce a json response', function (ctx) { + ctx.res.json.callCount.should.equal(0) }) }) }) describe('setCollaboratorInfo', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - user_id: this.user._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + user_id: ctx.user._id, } - this.req.body = { privilegeLevel: 'readOnly' } + ctx.req.body = { privilegeLevel: 'readOnly' } }) - it('should set the collaborator privilege level', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith(this.projectId, this.user._id, 'readOnly') - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) - }) - - it('should return a 404 when the project or collaborator is not found', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - done() + it('should set the collaborator privilege level', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.have.been.calledWith(ctx.projectId, ctx.user._id, 'readOnly') + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) }) - - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( - new Errors.NotFoundError() - ) - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) - it('should pass the error to the next handler when setting the privilege level fails', function (done) { - this.next = sinon.spy(err => { - expect(err).instanceOf(Error) - done() - }) + it('should return a 404 when the project or collaborator is not found', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + resolve() + }) - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( - new Error() - ) - this.CollaboratorsController.setCollaboratorInfo( - this.req, - this.res, - this.next - ) + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( + new Errors.NotFoundError() + ) + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) + }) + + it('should pass the error to the next handler when setting the privilege level fails', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.spy(err => { + expect(err).instanceOf(Error) + resolve() + }) + + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel.rejects( + new Error() + ) + ctx.CollaboratorsController.setCollaboratorInfo( + ctx.req, + ctx.res, + ctx.next + ) + }) }) describe('when setting privilege level to readAndWrite', function () { - beforeEach(function () { - this.req.body = { privilegeLevel: 'readAndWrite' } + beforeEach(function (ctx) { + ctx.req.body = { privilegeLevel: 'readAndWrite' } }) describe('when owner can add new edit collaborators', function () { - it('should set privilege level after checking collaborators can be added', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect( - this.LimitationsManager.promises - .canChangeCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.projectId, - this.user._id, - 'readAndWrite' - ) - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) + it('should set privilege level after checking collaborators can be added', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect( + ctx.LimitationsManager.promises + .canChangeCollaboratorPrivilegeLevel + ).to.have.been.calledWith( + ctx.projectId, + ctx.user._id, + 'readAndWrite' + ) + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) }) }) describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canChangeCollaboratorPrivilegeLevel.resolves( false ) }) - it('should return a 403 if trying to set a new edit collaborator', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect( - this.LimitationsManager.promises - .canChangeCollaboratorPrivilegeLevel - ).to.have.been.calledWith( - this.projectId, - this.user._id, - 'readAndWrite' - ) - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.not.have.been.called - done() + it('should return a 403 if trying to set a new edit collaborator', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.forbidden = sinon.spy((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect( + ctx.LimitationsManager.promises + .canChangeCollaboratorPrivilegeLevel + ).to.have.been.calledWith( + ctx.projectId, + ctx.user._id, + 'readAndWrite' + ) + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.not.have.been.called + resolve() + }) + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) }) - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) }) }) }) describe('when setting privilege level to readOnly', function () { - beforeEach(function () { - this.req.body = { privilegeLevel: 'readOnly' } + beforeEach(function (ctx) { + ctx.req.body = { privilegeLevel: 'readOnly' } }) describe('when owner cannot add edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves( false ) }) - it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - expect(this.LimitationsManager.promises.canAddXEditCollaborators).to - .not.have.been.called - expect( - this.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel - ).to.have.been.calledWith(this.projectId, this.user._id, 'readOnly') - done() - } - this.CollaboratorsController.setCollaboratorInfo(this.req, this.res) + it('should always allow setting a collaborator to viewer even if user cant add edit collaborators', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + expect(ctx.LimitationsManager.promises.canAddXEditCollaborators) + .to.not.have.been.called + expect( + ctx.CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel + ).to.have.been.calledWith(ctx.projectId, ctx.user._id, 'readOnly') + resolve() + } + ctx.CollaboratorsController.setCollaboratorInfo(ctx.req, ctx.res) + }) }) }) }) }) describe('transferOwnership', function () { - beforeEach(function () { - this.req.body = { user_id: this.user._id.toString() } + beforeEach(function (ctx) { + ctx.req.body = { user_id: ctx.user._id.toString() } }) - it('returns 204 on success', function (done) { - this.res.sendStatus = status => { - expect(status).to.equal(204) - done() - } - this.CollaboratorsController.transferOwnership(this.req, this.res) - }) - - it('returns 404 if the project does not exist', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect(message).to.match(/project not found/) - done() + it('returns 204 on success', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = status => { + expect(status).to.equal(204) + resolve() + } + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) }) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.ProjectNotFoundError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) }) - it('returns 404 if the user does not exist', function (done) { - this.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - expect(message).to.match(/user not found/) - done() + it('returns 404 if the project does not exist', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect(message).to.match(/project not found/) + resolve() + }) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.ProjectNotFoundError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) }) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.UserNotFoundError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) }) - it('invokes HTTP forbidden error handler if the user is not a collaborator', function (done) { - this.HttpErrorHandler.forbidden = sinon.spy(() => done()) - this.OwnershipTransferHandler.promises.transferOwnership.rejects( - new Errors.UserNotCollaboratorError() - ) - this.CollaboratorsController.transferOwnership(this.req, this.res) + it('returns 404 if the user does not exist', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.notFound = sinon.spy((req, res, message) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + expect(message).to.match(/user not found/) + resolve() + }) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.UserNotFoundError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) + }) + }) + + it('invokes HTTP forbidden error handler if the user is not a collaborator', function (ctx) { + return new Promise(resolve => { + ctx.HttpErrorHandler.forbidden = sinon.spy(() => resolve()) + ctx.OwnershipTransferHandler.promises.transferOwnership.rejects( + new Errors.UserNotCollaboratorError() + ) + ctx.CollaboratorsController.transferOwnership(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs index 3e7d4c3daa..d948e69ed4 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import mongodb from 'mongodb-legacy' @@ -12,419 +12,488 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsInviteController.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('CollaboratorsInviteController', function () { - beforeEach(async function () { - this.projectId = 'project-id-123' - this.token = 'some-opaque-token' - this.tokenHmac = 'some-hmac-token' - this.targetEmail = 'user@example.com' - this.privileges = 'readAndWrite' - this.projectOwner = { + beforeEach(async function (ctx) { + ctx.projectId = 'project-id-123' + ctx.token = 'some-opaque-token' + ctx.tokenHmac = 'some-hmac-token' + ctx.targetEmail = 'user@example.com' + ctx.privileges = 'readAndWrite' + ctx.projectOwner = { _id: 'project-owner-id', email: 'project-owner@example.com', } - this.currentUser = { + ctx.currentUser = { _id: 'current-user-id', email: 'current-user@example.com', } - this.invite = { + ctx.invite = { _id: new ObjectId(), - token: this.token, - tokenHmac: this.tokenHmac, - sendingUserId: this.currentUser._id, - projectId: this.projectId, - email: this.targetEmail, - privileges: this.privileges, + token: ctx.token, + tokenHmac: ctx.tokenHmac, + sendingUserId: ctx.currentUser._id, + projectId: ctx.projectId, + email: ctx.targetEmail, + privileges: ctx.privileges, createdAt: new Date(), } - this.inviteReducedData = _.pick(this.invite, ['_id', 'email', 'privileges']) - this.project = { - _id: this.projectId, - owner_ref: this.projectOwner._id, + ctx.inviteReducedData = _.pick(ctx.invite, ['_id', 'email', 'privileges']) + ctx.project = { + _id: ctx.projectId, + owner_ref: ctx.projectOwner._id, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.currentUser), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.currentUser), } - this.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() } + ctx.AnalyticsManger = { recordEventForUserInBackground: sinon.stub() } - this.rateLimiter = { + ctx.rateLimiter = { consume: sinon.stub().resolves(), } - this.RateLimiter = { - RateLimiter: sinon.stub().returns(this.rateLimiter), + ctx.RateLimiter = { + RateLimiter: sinon.stub().returns(ctx.rateLimiter), } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { allowedNumberOfCollaboratorsForUser: sinon.stub(), canAddXEditCollaborators: sinon.stub().resolves(true), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { getUserByAnyEmail: sinon.stub(), getUser: sinon.stub(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { getProject: sinon.stub(), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { isUserInvitedMemberOfProject: sinon.stub(), }, } - this.CollaboratorsInviteHandler = { + ctx.CollaboratorsInviteHandler = { promises: { - inviteToProject: sinon.stub().resolves(this.inviteReducedData), - generateNewInvite: sinon.stub().resolves(this.invite), - revokeInvite: sinon.stub().resolves(this.invite), + inviteToProject: sinon.stub().resolves(ctx.inviteReducedData), + generateNewInvite: sinon.stub().resolves(ctx.invite), + revokeInvite: sinon.stub().resolves(ctx.invite), acceptInvite: sinon.stub(), }, } - this.CollaboratorsInviteGetter = { + ctx.CollaboratorsInviteGetter = { promises: { getAllInvites: sinon.stub(), - getInviteByToken: sinon.stub().resolves(this.invite), + getInviteByToken: sinon.stub().resolves(ctx.invite), }, } - this.EditorRealTimeController = { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.settings = {} + ctx.settings = {} - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, addEntryInBackground: sinon.stub(), } - this.AuthenticationController = { + ctx.AuthenticationController = { setRedirectInSession: sinon.stub(), } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.CollaboratorsInviteController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - '../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js': - this.CollaboratorsGetter, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs': - this.CollaboratorsInviteHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js': - this.CollaboratorsInviteGetter, - '../../../../app/src/Features/Editor/EditorRealTimeController.js': - this.EditorRealTimeController, - '../../../../app/src/Features/Analytics/AnalyticsManager.js': - this.AnalyticsManger, - '../../../../app/src/Features/Authentication/SessionManager.js': - this.SessionManager, - '@overleaf/settings': this.settings, - '../../../../app/src/infrastructure/RateLimiter': this.RateLimiter, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) - this.res = new MockResponse() - this.req = new MockRequest() - this.next = sinon.stub() + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter.js', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs', + () => ({ + default: ctx.CollaboratorsInviteHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter.js', + () => ({ + default: ctx.CollaboratorsInviteGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController.js', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager.js', + () => ({ + default: ctx.AnalyticsManger, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager.js', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/infrastructure/RateLimiter', + () => ctx.RateLimiter + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.CollaboratorsInviteController = (await import(MODULE_PATH)).default + + ctx.res = new MockResponse() + ctx.req = new MockRequest() + ctx.next = sinon.stub() }) describe('getAllInvites', function () { - beforeEach(function () { - this.fakeInvites = [ + beforeEach(function (ctx) { + ctx.fakeInvites = [ { _id: new ObjectId(), one: 1 }, { _id: new ObjectId(), two: 2 }, ] - this.req.params = { Project_id: this.projectId } + ctx.req.params = { Project_id: ctx.projectId } }) describe('when all goes well', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvites - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.getAllInvites( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvites + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.getAllInvites( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce an error', function () { - this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should produce a list of invite objects', function () { - this.res.json.callCount.should.equal(1) - this.res.json - .calledWith({ invites: this.fakeInvites }) - .should.equal(true) + it('should produce a list of invite objects', function (ctx) { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith({ invites: ctx.fakeInvites }).should.equal(true) }) - it('should have called CollaboratorsInviteHandler.getAllInvites', function () { - this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( + it('should have called CollaboratorsInviteHandler.getAllInvites', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getAllInvites - .calledWith(this.projectId) + ctx.CollaboratorsInviteGetter.promises.getAllInvites + .calledWith(ctx.projectId) .should.equal(true) }) }) describe('when CollaboratorsInviteHandler.getAllInvites produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getAllInvites.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.getAllInvites( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.getAllInvites( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - this.next.firstCall.args[0].should.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.firstCall.args[0].should.be.instanceof(Error) }) }) }) describe('inviteToProject', function () { - beforeEach(function () { - this.req.params = { Project_id: this.projectId } - this.req.body = { - email: this.targetEmail, - privileges: this.privileges, + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.projectId } + ctx.req.body = { + email: ctx.targetEmail, + privileges: ctx.privileges, } - this.ProjectGetter.promises.getProject.resolves({ - owner_ref: this.project.owner_ref, + ctx.ProjectGetter.promises.getProject.resolves({ + owner_ref: ctx.project.owner_ref, }) }) describe('when all goes well', function (done) { - beforeEach(async function () { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(async function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) - await this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res + await ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res ) }) - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, + it('should produce json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ + invite: ctx.inviteReducedData, }) }) - it('should have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXEditCollaborators - .calledWith(this.projectId) + ctx.LimitationsManager.promises.canAddXEditCollaborators + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('adds a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'send-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when the user is not allowed to add more edit collaborators', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAddXEditCollaborators.resolves( - false - ) + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false) }) describe('readAndWrite collaborator', function () { - beforeEach(function (done) { - this.privileges = 'readAndWrite' - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.privileges = 'readAndWrite' + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce json response without an invite', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should produce json response without an invite', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, }) }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should not have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 0 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.currentUser, this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.currentUser, ctx.targetEmail) .should.equal(false) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('readOnly collaborator (always allowed)', function () { - beforeEach(function (done) { - this.req.body = { - email: this.targetEmail, - privileges: (this.privileges = 'readOnly'), - } - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) - }) - - it('should produce json response', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ - invite: this.inviteReducedData, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + email: ctx.targetEmail, + privileges: (ctx.privileges = 'readOnly'), + } + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) }) }) - it('should not have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should produce json response', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ + invite: ctx.inviteReducedData, + }) + }) + + it('should not have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 0 ) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('adds a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('adds a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'send-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) @@ -432,808 +501,834 @@ describe('CollaboratorsInviteController', function () { }) describe('when inviteToProject produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.CollaboratorsInviteHandler.promises.inviteToProject.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.CollaboratorsInviteHandler.promises.inviteToProject.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next).to.have.been.calledWith(sinon.match.instanceOf(Error)) }) - it('should have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 1 ) - this.LimitationsManager.promises.canAddXEditCollaborators - .calledWith(this.projectId) + ctx.LimitationsManager.promises.canAddXEditCollaborators + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.currentUser, - this.targetEmail, - this.privileges + ctx.projectId, + ctx.currentUser, + ctx.targetEmail, + ctx.privileges ) .should.equal(true) }) }) describe('when _checkShouldInviteEmail disallows the invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .resolves(false) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.res.callback = () => done() - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .resolves(false) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce json response with no invite, and an error property', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should produce json response with no invite, and an error property', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, error: 'cannot_invite_non_user', }) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when _checkShouldInviteEmail produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon - .stub() - .rejects(new Error('woops')) - this.CollaboratorsInviteController._checkRateLimit = sinon - .stub() - .resolves(true) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon + .stub() + .rejects(new Error('woops')) + ctx.CollaboratorsInviteController._checkRateLimit = sinon + .stub() + .resolves(true) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 1 ) - this.CollaboratorsInviteController._checkShouldInviteEmail - .calledWith(this.targetEmail) + ctx.CollaboratorsInviteController._checkShouldInviteEmail + .calledWith(ctx.targetEmail) .should.equal(true) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when the user invites themselves to the project', function () { - beforeEach(function () { - this.req.body.email = this.currentUser.email - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(function (ctx) { + ctx.req.body.email = ctx.currentUser.email + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) - this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next + ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should reject action, return json response with error code', function () { - this.res.json.callCount.should.equal(1) - expect(this.res.json.firstCall.args[0]).to.deep.equal({ + it('should reject action, return json response with error code', function (ctx) { + ctx.res.json.callCount.should.equal(1) + expect(ctx.res.json.firstCall.args[0]).to.deep.equal({ invite: null, error: 'cannot_invite_self', }) }) - it('should not have called canAddXEditCollaborators', function () { - this.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( + it('should not have called canAddXEditCollaborators', function (ctx) { + ctx.LimitationsManager.promises.canAddXEditCollaborators.callCount.should.equal( 0 ) }) - it('should not have called _checkShouldInviteEmail', function () { - this.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( + it('should not have called _checkShouldInviteEmail', function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail.callCount.should.equal( 0 ) }) - it('should not have called inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) - it('should not have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + it('should not have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) }) }) describe('when _checkRateLimit returns false', function () { - beforeEach(async function () { - this.CollaboratorsInviteController._checkShouldInviteEmail = sinon + beforeEach(async function (ctx) { + ctx.CollaboratorsInviteController._checkShouldInviteEmail = sinon .stub() .resolves(true) - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(false) - await this.CollaboratorsInviteController.inviteToProject( - this.req, - this.res, - this.next + await ctx.CollaboratorsInviteController.inviteToProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should send a 429 response', function () { - this.res.sendStatus.calledWith(429).should.equal(true) + it('should send a 429 response', function (ctx) { + ctx.res.sendStatus.calledWith(429).should.equal(true) }) - it('should not call inviteToProject', function () { - this.CollaboratorsInviteHandler.promises.inviteToProject.called.should.equal( + it('should not call inviteToProject', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.inviteToProject.called.should.equal( false ) }) - it('should not call emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.called.should.equal(false) + it('should not call emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.called.should.equal(false) }) }) }) describe('viewInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - token: this.token, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + token: ctx.token, } - this.fakeProject = { - _id: this.projectId, + ctx.fakeProject = { + _id: ctx.projectId, name: 'some project', - owner_ref: this.invite.sendingUserId, + owner_ref: ctx.invite.sendingUserId, collaberator_refs: [], readOnly_refs: [], } - this.owner = { - _id: this.fakeProject.owner_ref, + ctx.owner = { + _id: ctx.fakeProject.owner_ref, first_name: 'John', last_name: 'Doe', email: 'john@example.com', } - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( false ) - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( - this.invite + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves( + ctx.invite ) - this.ProjectGetter.promises.getProject.resolves(this.fakeProject) - this.UserGetter.promises.getUser.resolves(this.owner) + ctx.ProjectGetter.promises.getProject.resolves(ctx.fakeProject) + ctx.UserGetter.promises.getUser.resolves(ctx.owner) }) describe('when the token is valid', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render.calledWith('project/invite/show').should.equal(true) + it('should render the view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/show').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getInviteByToken - .calledWith(this.fakeProject._id, this.invite.token) + ctx.CollaboratorsInviteGetter.promises.getInviteByToken + .calledWith(ctx.fakeProject._id, ctx.invite.token) .should.equal(true) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) - this.ProjectGetter.promises.getProject - .calledWith(this.projectId) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.projectId) .should.equal(true) }) }) describe('when not logged in', function () { - beforeEach(function (done) { - this.SessionManager.getSessionUser.returns(null) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.SessionManager.getSessionUser.returns(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not check member status', function () { - expect(this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject) - .to.not.have.been.called + it('should not check member status', function (ctx) { + expect(ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject).to + .not.have.been.called }) - it('should set redirect back to invite', function () { + it('should set redirect back to invite', function (ctx) { expect( - this.AuthenticationController.setRedirectInSession - ).to.have.been.calledWith(this.req) + ctx.AuthenticationController.setRedirectInSession + ).to.have.been.calledWith(ctx.req) }) - it('should redirect to the register page', function () { - expect(this.res.render).to.not.have.been.called - expect(this.res.redirect).to.have.been.calledOnce - expect(this.res.redirect).to.have.been.calledWith('/register') + it('should redirect to the register page', function (ctx) { + expect(ctx.res.render).to.not.have.been.called + expect(ctx.res.redirect).to.have.been.calledOnce + expect(ctx.res.redirect).to.have.been.calledWith('/register') }) }) describe('when user is already a member of the project', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - true - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + true + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should redirect to the project page', function () { - this.res.redirect.callCount.should.equal(1) - this.res.redirect - .calledWith(`/project/${this.projectId}`) + it('should redirect to the project page', function (ctx) { + ctx.res.redirect.callCount.should.equal(1) + ctx.res.redirect + .calledWith(`/project/${ctx.projectId}`) .should.equal(true) }) - it('should not call next with an error', function () { - this.next.callCount.should.equal(0) + it('should not call next with an error', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should not call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 0 ) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when isUserInvitedMemberOfProject produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should call next with an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should not call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 0 ) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when the getInviteByToken produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when the getInviteByToken does not produce an invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should not call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(0) + it('should not call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(0) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when User.getUser produces an error', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUser.rejects(new Error('woops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser.rejects(new Error('woops')) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when User.getUser does not find a user', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUser.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUser.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should not call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) }) describe('when getProject produces an error', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.rejects(new Error('woops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.rejects(new Error('woops')) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce an error', function () { - this.next.callCount.should.equal(1) - expect(this.next.firstCall.args[0]).to.be.instanceof(Error) + it('should produce an error', function (ctx) { + ctx.next.callCount.should.equal(1) + expect(ctx.next.firstCall.args[0]).to.be.instanceof(Error) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call User.getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call User.getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) }) }) describe('when Project.getUser does not find a user', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.resolves(null) - this.res.callback = () => done() - this.CollaboratorsInviteController.viewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.viewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should render the not-valid view template', function () { - this.res.render.callCount.should.equal(1) - this.res.render - .calledWith('project/invite/not-valid') - .should.equal(true) + it('should render the not-valid view template', function (ctx) { + ctx.res.render.callCount.should.equal(1) + ctx.res.render.calledWith('project/invite/not-valid').should.equal(true) }) - it('should not call next', function () { - this.next.callCount.should.equal(0) + it('should not call next', function (ctx) { + ctx.next.callCount.should.equal(0) }) - it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function () { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( + it('should call CollaboratorsGetter.isUserInvitedMemberOfProject', function (ctx) { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.callCount.should.equal( 1 ) - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject - .calledWith(this.currentUser._id, this.projectId) + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject + .calledWith(ctx.currentUser._id, ctx.projectId) .should.equal(true) }) - it('should call getInviteByToken', function () { - this.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( + it('should call getInviteByToken', function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.callCount.should.equal( 1 ) }) - it('should call getUser', function () { - this.UserGetter.promises.getUser.callCount.should.equal(1) - this.UserGetter.promises.getUser - .calledWith({ _id: this.fakeProject.owner_ref }) + it('should call getUser', function (ctx) { + ctx.UserGetter.promises.getUser.callCount.should.equal(1) + ctx.UserGetter.promises.getUser + .calledWith({ _id: ctx.fakeProject.owner_ref }) .should.equal(true) }) - it('should call ProjectGetter.getProject', function () { - this.ProjectGetter.promises.getProject.callCount.should.equal(1) + it('should call ProjectGetter.getProject', function (ctx) { + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) }) }) }) describe('generateNewInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - invite_id: this.invite._id.toString(), + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + invite_id: ctx.invite._id.toString(), } - this.CollaboratorsInviteController._checkRateLimit = sinon + ctx.CollaboratorsInviteController._checkRateLimit = sinon .stub() .resolves(true) }) describe('when generateNewInvite does not produce an error', function () { describe('and returns an invite object', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.calledWith(201).should.equal(true) + it('should produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.calledWith(201).should.equal(true) }) - it('should have called generateNewInvite', function () { - this.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( + it('should have called generateNewInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( 1 ) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should check the rate limit', function () { - this.CollaboratorsInviteController._checkRateLimit.callCount.should.equal( + it('should check the rate limit', function (ctx) { + ctx.CollaboratorsInviteController._checkRateLimit.callCount.should.equal( 1 ) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'resend-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('and returns a null invite', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.generateNewInvite.resolves( - null - ) - this.res.callback = () => done() - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.resolves( + null + ) + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should produce a 404 response when invite is null', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.should.have.been.calledWith(404) + it('should produce a 404 response when invite is null', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.should.have.been.calledWith(404) }) }) }) describe('when generateNewInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.generateNewInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.generateNewInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.generateNewInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(0) + it('should not produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called generateNewInvite', function () { - this.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( + it('should have called generateNewInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.generateNewInvite.callCount.should.equal( 1 ) }) @@ -1241,79 +1336,83 @@ describe('CollaboratorsInviteController', function () { }) describe('revokeInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - invite_id: this.invite._id.toString(), + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + invite_id: ctx.invite._id.toString(), } }) describe('when revokeInvite does not produce an error', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.revokeInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.revokeInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should produce a 204 response', function () { - this.res.sendStatus.callCount.should.equal(1) - this.res.sendStatus.should.have.been.calledWith(204) + it('should produce a 204 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(1) + ctx.res.sendStatus.should.have.been.calledWith(204) }) - it('should have called revokeInvite', function () { - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) }) - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - this.EditorRealTimeController.emitToRoom - .calledWith(this.projectId, 'project:membership:changed') + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + ctx.EditorRealTimeController.emitToRoom + .calledWith(ctx.projectId, 'project:membership:changed') .should.equal(true) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.addEntryInBackground.should.have.been.calledWith( + ctx.projectId, 'revoke-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when revokeInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.revokeInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.revokeInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.revokeInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not produce a 201 response', function () { - this.res.sendStatus.callCount.should.equal(0) + it('should not produce a 201 response', function (ctx) { + ctx.res.sendStatus.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called revokeInvite', function () { - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) }) @@ -1321,188 +1420,196 @@ describe('CollaboratorsInviteController', function () { }) describe('acceptInvite', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.projectId, - token: this.token, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.projectId, + token: ctx.token, } }) describe('when acceptInvite does not produce an error', function () { - beforeEach(function (done) { - this.res.callback = () => done() - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => resolve() + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) + }) + + it('should redirect to project page', function (ctx) { + ctx.res.redirect.should.have.been.calledOnce + ctx.res.redirect.should.have.been.calledWith( + `/project/${ctx.projectId}` ) }) - it('should redirect to project page', function () { - this.res.redirect.should.have.been.calledOnce - this.res.redirect.should.have.been.calledWith( - `/project/${this.projectId}` + it('should have called acceptInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.should.have.been.calledWith( + ctx.invite, + ctx.projectId, + ctx.currentUser ) }) - it('should have called acceptInvite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.should.have.been.calledWith( - this.invite, - this.projectId, - this.currentUser - ) - }) - - it('should have called emitToRoom', function () { - this.EditorRealTimeController.emitToRoom.should.have.been.calledOnce - this.EditorRealTimeController.emitToRoom.should.have.been.calledWith( - this.projectId, + it('should have called emitToRoom', function (ctx) { + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledOnce + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledWith( + ctx.projectId, 'project:membership:changed' ) }) - it('should add a project audit log entry', function () { - this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.projectId, + it('should add a project audit log entry', function (ctx) { + ctx.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( + ctx.projectId, 'accept-invite', - this.currentUser._id, - this.req.ip, + ctx.currentUser._id, + ctx.req.ip, { - inviteId: this.invite._id, - privileges: this.privileges, + inviteId: ctx.invite._id, + privileges: ctx.privileges, } ) }) }) describe('when the invite is not found', function () { - beforeEach(function (done) { - this.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteGetter.promises.getInviteByToken.resolves(null) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('throws a NotFoundError', function () { - expect(this.next).to.have.been.calledWith( + it('throws a NotFoundError', function (ctx) { + expect(ctx.next).to.have.been.calledWith( sinon.match.instanceOf(Errors.NotFoundError) ) }) }) describe('when acceptInvite produces an error', function () { - beforeEach(function (done) { - this.CollaboratorsInviteHandler.promises.acceptInvite.rejects( - new Error('woops') - ) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.rejects( + new Error('woops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not redirect to project page', function () { - this.res.redirect.callCount.should.equal(0) + it('should not redirect to project page', function (ctx) { + ctx.res.redirect.callCount.should.equal(0) }) - it('should call next with the error', function () { - this.next.callCount.should.equal(1) - this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) + it('should call next with the error', function (ctx) { + ctx.next.callCount.should.equal(1) + ctx.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true) }) - it('should have called acceptInvite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.callCount.should.equal( + it('should have called acceptInvite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.callCount.should.equal( 1 ) }) }) describe('when the project audit log entry fails', function () { - beforeEach(function (done) { - this.ProjectAuditLogHandler.promises.addEntry.rejects(new Error('oops')) - this.next.callsFake(() => done()) - this.CollaboratorsInviteController.acceptInvite( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectAuditLogHandler.promises.addEntry.rejects( + new Error('oops') + ) + ctx.next.callsFake(() => resolve()) + ctx.CollaboratorsInviteController.acceptInvite( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('should not accept the invite', function () { - this.CollaboratorsInviteHandler.promises.acceptInvite.should.not.have + it('should not accept the invite', function (ctx) { + ctx.CollaboratorsInviteHandler.promises.acceptInvite.should.not.have .been.called }) }) }) describe('_checkShouldInviteEmail', function () { - beforeEach(function () { - this.email = 'user@example.com' + beforeEach(function (ctx) { + ctx.email = 'user@example.com' }) describe('when we should be restricting to existing accounts', function () { - beforeEach(function () { - this.settings.restrictInvitesToExistingAccounts = true - this.call = () => - this.CollaboratorsInviteController._checkShouldInviteEmail(this.email) + beforeEach(function (ctx) { + ctx.settings.restrictInvitesToExistingAccounts = true + ctx.call = () => + ctx.CollaboratorsInviteController._checkShouldInviteEmail(ctx.email) }) describe('when user account is present', function () { - beforeEach(function () { - this.user = { _id: new ObjectId().toString() } - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.user = { _id: new ObjectId().toString() } + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) }) - it('should callback with `true`', async function () { + it('should callback with `true`', async function (ctx) { const shouldAllow = - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) expect(shouldAllow).to.equal(true) }) }) describe('when user account is absent', function () { - beforeEach(function () { - this.user = null - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.user = null + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) }) - it('should callback with `false`', async function () { + it('should callback with `false`', async function (ctx) { const shouldAllow = - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) expect(shouldAllow).to.equal(false) }) - it('should have called getUser', async function () { - await this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email + it('should have called getUser', async function (ctx) { + await ctx.CollaboratorsInviteController._checkShouldInviteEmail( + ctx.email ) - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.email, { _id: 1 }) + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.email, { _id: 1 }) .should.equal(true) }) }) describe('when getUser produces an error', function () { - beforeEach(function () { - this.user = null - this.UserGetter.promises.getUserByAnyEmail.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.user = null + ctx.UserGetter.promises.getUserByAnyEmail.rejects(new Error('woops')) }) - it('should callback with an error', async function () { + it('should callback with an error', async function (ctx) { await expect( - this.CollaboratorsInviteController._checkShouldInviteEmail( - this.email - ) + ctx.CollaboratorsInviteController._checkShouldInviteEmail(ctx.email) ).to.be.rejected }) }) @@ -1510,67 +1617,57 @@ describe('CollaboratorsInviteController', function () { }) describe('_checkRateLimit', function () { - beforeEach(function () { - this.settings.restrictInvitesToExistingAccounts = false - this.currentUserId = '32312313' - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + beforeEach(function (ctx) { + ctx.settings.restrictInvitesToExistingAccounts = false + ctx.currentUserId = '32312313' + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(17) }) - it('should callback with `true` when rate limit under', async function () { - const result = await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId + it('should callback with `true` when rate limit under', async function (ctx) { + const result = await ctx.CollaboratorsInviteController._checkRateLimit( + ctx.currentUserId ) + expect(ctx.rateLimiter.consume).to.have.been.calledWith(ctx.currentUserId) result.should.equal(true) }) - it('should callback with `false` when rate limit hit', async function () { - this.rateLimiter.consume.rejects({ remainingPoints: 0 }) - const result = await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId + it('should callback with `false` when rate limit hit', async function (ctx) { + ctx.rateLimiter.consume.rejects({ remainingPoints: 0 }) + const result = await ctx.CollaboratorsInviteController._checkRateLimit( + ctx.currentUserId ) + expect(ctx.rateLimiter.consume).to.have.been.calledWith(ctx.currentUserId) result.should.equal(false) }) - it('should allow 10x the collaborators', async function () { - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + it('should allow 10x the collaborators', async function (ctx) { + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 170) ) }) - it('should allow 200 requests when collaborators is -1', async function () { - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + it('should allow 200 requests when collaborators is -1', async function (ctx) { + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(-1) - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 200) ) }) - it('should allow 10 requests when user has no collaborators set', async function () { - this.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser - .withArgs(this.currentUserId) + it('should allow 10 requests when user has no collaborators set', async function (ctx) { + ctx.LimitationsManager.promises.allowedNumberOfCollaboratorsForUser + .withArgs(ctx.currentUserId) .resolves(null) - await this.CollaboratorsInviteController._checkRateLimit( - this.currentUserId - ) - expect(this.rateLimiter.consume).to.have.been.calledWith( - this.currentUserId, + await ctx.CollaboratorsInviteController._checkRateLimit(ctx.currentUserId) + expect(ctx.rateLimiter.consume).to.have.been.calledWith( + ctx.currentUserId, Math.floor(40000 / 10) ) }) diff --git a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs index f386648552..ec8f453536 100644 --- a/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs +++ b/services/web/test/unit/src/Collaborators/CollaboratorsInviteHandler.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import mongodb from 'mongodb-legacy' import Crypto from 'crypto' @@ -10,8 +10,8 @@ const MODULE_PATH = '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler.mjs' describe('CollaboratorsInviteHandler', function () { - beforeEach(async function () { - this.ProjectInvite = class ProjectInvite { + beforeEach(async function (ctx) { + ctx.ProjectInvite = class ProjectInvite { constructor(options) { if (options == null) { options = {} @@ -23,120 +23,174 @@ describe('CollaboratorsInviteHandler', function () { } } } - this.ProjectInvite.prototype.save = sinon.stub() - this.ProjectInvite.findOne = sinon.stub() - this.ProjectInvite.find = sinon.stub() - this.ProjectInvite.deleteOne = sinon.stub() - this.ProjectInvite.findOneAndDelete = sinon.stub() - this.ProjectInvite.countDocuments = sinon.stub() + ctx.ProjectInvite.prototype.save = sinon.stub() + ctx.ProjectInvite.findOne = sinon.stub() + ctx.ProjectInvite.find = sinon.stub() + ctx.ProjectInvite.deleteOne = sinon.stub() + ctx.ProjectInvite.findOneAndDelete = sinon.stub() + ctx.ProjectInvite.countDocuments = sinon.stub() - this.Crypto = { + ctx.Crypto = { randomBytes: sinon.stub().callsFake(Crypto.randomBytes), } - this.settings = {} - this.CollaboratorsEmailHandler = { promises: {} } - this.CollaboratorsHandler = { + ctx.settings = {} + ctx.CollaboratorsEmailHandler = { promises: {} } + ctx.CollaboratorsHandler = { promises: { addUserIdToProject: sinon.stub(), }, } - this.UserGetter = { promises: { getUser: sinon.stub() } } - this.ProjectGetter = { promises: { getProject: sinon.stub().resolves() } } - this.NotificationsBuilder = { promises: {} } - this.tokenHmac = 'jkhajkefhaekjfhkfg' - this.CollaboratorsInviteHelper = { - generateToken: sinon.stub().returns(this.Crypto.randomBytes(24)), - hashInviteToken: sinon.stub().returns(this.tokenHmac), + ctx.UserGetter = { promises: { getUser: sinon.stub() } } + ctx.ProjectGetter = { promises: { getProject: sinon.stub().resolves() } } + ctx.NotificationsBuilder = { promises: {} } + ctx.tokenHmac = 'jkhajkefhaekjfhkfg' + ctx.CollaboratorsInviteHelper = { + generateToken: sinon.stub().returns(ctx.Crypto.randomBytes(24)), + hashInviteToken: sinon.stub().returns(ctx.tokenHmac), } - this.CollaboratorsInviteGetter = { + ctx.CollaboratorsInviteGetter = { promises: { getAllInvites: sinon.stub(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignmentForUser: sinon.stub().resolves(), }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAcceptEditCollaboratorInvite: sinon.stub().resolves(), }, } - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, addEntryInBackground: sinon.stub(), } - this.logger = { + ctx.logger = { debug: sinon.stub(), warn: sinon.stub(), err: sinon.stub(), } - this.CollaboratorsInviteHandler = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/models/ProjectInvite.js': { - ProjectInvite: this.ProjectInvite, - }, - '@overleaf/logger': this.logger, - '../../../../app/src/Features/Collaborators/CollaboratorsEmailHandler.mjs': - this.CollaboratorsEmailHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js': - this.CollaboratorsHandler, - '../../../../app/src/Features/User/UserGetter.js': this.UserGetter, - '../../../../app/src/Features/Project/ProjectGetter.js': - this.ProjectGetter, - '../../../../app/src/Features/Notifications/NotificationsBuilder.js': - this.NotificationsBuilder, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHelper.js': - this.CollaboratorsInviteHelper, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter': - this.CollaboratorsInviteGetter, - '../../../../app/src/Features/SplitTests/SplitTestHandler.js': - this.SplitTestHandler, - '../../../../app/src/Features/Subscription/LimitationsManager.js': - this.LimitationsManager, - '../../../../app/src/Features/Project/ProjectAuditLogHandler.js': - this.ProjectAuditLogHandler, - crypto: this.CryptogetAssignmentForUser, - }) + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) - this.projectId = new ObjectId() - this.sendingUserId = new ObjectId() - this.sendingUser = { - _id: this.sendingUserId, + vi.doMock('../../../../app/src/models/ProjectInvite.js', () => ({ + ProjectInvite: ctx.ProjectInvite, + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsEmailHandler.mjs', + () => ({ + default: ctx.CollaboratorsEmailHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler.js', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter.js', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder.js', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHelper.js', + () => ({ + default: ctx.CollaboratorsInviteHelper, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteGetter', + () => ({ + default: ctx.CollaboratorsInviteGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler.js', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager.js', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler.js', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock('crypto', () => ({ + default: ctx.CryptogetAssignmentForUser, + })) + + ctx.CollaboratorsInviteHandler = (await import(MODULE_PATH)).default + + ctx.projectId = new ObjectId() + ctx.sendingUserId = new ObjectId() + ctx.sendingUser = { + _id: ctx.sendingUserId, name: 'Bob', } - this.email = 'user@example.com' - this.userId = new ObjectId() - this.user = { - _id: this.userId, + ctx.email = 'user@example.com' + ctx.userId = new ObjectId() + ctx.user = { + _id: ctx.userId, email: 'someone@example.com', } - this.inviteId = new ObjectId() - this.token = 'hnhteaosuhtaeosuahs' - this.privileges = 'readAndWrite' - this.fakeInvite = { - _id: this.inviteId, - email: this.email, - token: this.token, - tokenHmac: this.tokenHmac, - sendingUserId: this.sendingUserId, - projectId: this.projectId, - privileges: this.privileges, + ctx.inviteId = new ObjectId() + ctx.token = 'hnhteaosuhtaeosuahs' + ctx.privileges = 'readAndWrite' + ctx.fakeInvite = { + _id: ctx.inviteId, + email: ctx.email, + token: ctx.token, + tokenHmac: ctx.tokenHmac, + sendingUserId: ctx.sendingUserId, + projectId: ctx.projectId, + privileges: ctx.privileges, createdAt: new Date(), } }) describe('inviteToProject', function () { - beforeEach(function () { - this.ProjectInvite.prototype.save.callsFake(async function () { + beforeEach(function (ctx) { + ctx.ProjectInvite.prototype.save.callsFake(async function () { Object.defineProperty(this, 'toObject', { value: function () { return this @@ -147,191 +201,193 @@ describe('CollaboratorsInviteHandler', function () { }) return this }) - this.CollaboratorsInviteHandler.promises._sendMessages = sinon + ctx.CollaboratorsInviteHandler.promises._sendMessages = sinon .stub() .resolves() - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.inviteToProject( - this.projectId, - this.sendingUser, - this.email, - this.privileges + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.inviteToProject( + ctx.projectId, + ctx.sendingUser, + ctx.email, + ctx.privileges ) } }) describe('when all goes well', function () { - it('should produce the invite object', async function () { - const invite = await this.call() + it('should produce the invite object', async function (ctx) { + const invite = await ctx.call() expect(invite).to.not.equal(null) expect(invite).to.not.equal(undefined) expect(invite).to.be.instanceof(Object) expect(invite).to.have.all.keys(['_id', 'email', 'privileges']) }) - it('should have generated a random token', async function () { - await this.call() - this.Crypto.randomBytes.callCount.should.equal(1) + it('should have generated a random token', async function (ctx) { + await ctx.call() + ctx.Crypto.randomBytes.callCount.should.equal(1) }) - it('should have generated a HMAC token', async function () { - await this.call() - this.CollaboratorsInviteHelper.hashInviteToken.callCount.should.equal(1) + it('should have generated a HMAC token', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHelper.hashInviteToken.callCount.should.equal(1) }) - it('should have called ProjectInvite.save', async function () { - await this.call() - this.ProjectInvite.prototype.save.callCount.should.equal(1) + it('should have called ProjectInvite.save', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.prototype.save.callCount.should.equal(1) }) - it('should have called _sendMessages', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._sendMessages.callCount.should.equal( + it('should have called _sendMessages', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._sendMessages.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._sendMessages - .calledWith(this.projectId, this.sendingUser) + ctx.CollaboratorsInviteHandler.promises._sendMessages + .calledWith(ctx.projectId, ctx.sendingUser) .should.equal(true) }) }) describe('when saving model produces an error', function () { - beforeEach(function () { - this.ProjectInvite.prototype.save.rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectInvite.prototype.save.rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('_sendMessages', function () { - beforeEach(function () { - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon .stub() .resolves() - this.CollaboratorsInviteHandler.promises._trySendInviteNotification = - sinon.stub().resolves() - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._sendMessages( - this.projectId, - this.sendingUser, - this.fakeInvite + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification = sinon + .stub() + .resolves() + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._sendMessages( + ctx.projectId, + ctx.sendingUser, + ctx.fakeInvite ) } }) describe('when all goes well', function () { - it('should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', async function () { - await this.call() - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite.callCount.should.equal( + it('should call CollaboratorsEmailHandler.notifyUserOfProjectInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite.callCount.should.equal( 1 ) - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite - .calledWith(this.projectId, this.fakeInvite.email, this.fakeInvite) + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite + .calledWith(ctx.projectId, ctx.fakeInvite.email, ctx.fakeInvite) .should.equal(true) }) - it('should call _trySendInviteNotification', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._trySendInviteNotification.callCount.should.equal( + it('should call _trySendInviteNotification', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._trySendInviteNotification - .calledWith(this.projectId, this.sendingUser, this.fakeInvite) + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification + .calledWith(ctx.projectId, ctx.sendingUser, ctx.fakeInvite) .should.equal(true) }) }) describe('when CollaboratorsEmailHandler.notifyUserOfProjectInvite produces an error', function () { - beforeEach(function () { - this.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = - sinon.stub().rejects(new Error('woops')) + beforeEach(function (ctx) { + ctx.CollaboratorsEmailHandler.promises.notifyUserOfProjectInvite = sinon + .stub() + .rejects(new Error('woops')) }) - it('should not produce an error', async function () { - await expect(this.call()).to.be.fulfilled - expect(this.logger.err).to.be.calledOnce + it('should not produce an error', async function (ctx) { + await expect(ctx.call()).to.be.fulfilled + expect(ctx.logger.err).to.be.calledOnce }) }) describe('when _trySendInviteNotification produces an error', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises._trySendInviteNotification = + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification = sinon.stub().rejects(new Error('woops')) }) - it('should not produce an error', async function () { - await expect(this.call()).to.be.fulfilled - expect(this.logger.err).to.be.calledOnce + it('should not produce an error', async function (ctx) { + await expect(ctx.call()).to.be.fulfilled + expect(ctx.logger.err).to.be.calledOnce }) }) }) describe('revokeInviteForUser', function () { - beforeEach(function () { - this.targetInvite = { + beforeEach(function (ctx) { + ctx.targetInvite = { _id: new ObjectId(), email: 'fake2@example.org', two: 2, } - this.fakeInvites = [ + ctx.fakeInvites = [ { _id: new ObjectId(), email: 'fake1@example.org', one: 1 }, - this.targetInvite, + ctx.targetInvite, ] - this.fakeInvitesWithoutUser = [ + ctx.fakeInvitesWithoutUser = [ { _id: new ObjectId(), email: 'fake1@example.org', one: 1 }, { _id: new ObjectId(), email: 'fake3@example.org', two: 2 }, ] - this.targetEmail = [{ email: 'fake2@example.org' }] + ctx.targetEmail = [{ email: 'fake2@example.org' }] - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvites + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvites ) - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() - .resolves(this.targetInvite) + .resolves(ctx.targetInvite) - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.revokeInviteForUser( - this.projectId, - this.targetEmail + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.revokeInviteForUser( + ctx.projectId, + ctx.targetEmail ) } }) describe('for a valid user', function () { - it('should have called CollaboratorsInviteGetter.getAllInvites', async function () { - await this.call() - this.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( + it('should have called CollaboratorsInviteGetter.getAllInvites', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteGetter.promises.getAllInvites.callCount.should.equal( 1 ) - this.CollaboratorsInviteGetter.promises.getAllInvites - .calledWith(this.projectId) + ctx.CollaboratorsInviteGetter.promises.getAllInvites + .calledWith(ctx.projectId) .should.equal(true) }) - it('should have called revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should have called revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.revokeInvite - .calledWith(this.projectId, this.targetInvite._id) + ctx.CollaboratorsInviteHandler.promises.revokeInvite + .calledWith(ctx.projectId, ctx.targetInvite._id) .should.equal(true) }) }) describe('for a user without an invite in the project', function () { - beforeEach(function () { - this.CollaboratorsInviteGetter.promises.getAllInvites.resolves( - this.fakeInvitesWithoutUser + beforeEach(function (ctx) { + ctx.CollaboratorsInviteGetter.promises.getAllInvites.resolves( + ctx.fakeInvitesWithoutUser ) }) - it('should not have called CollaboratorsInviteHandler.revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should not have called CollaboratorsInviteHandler.revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 0 ) }) @@ -339,142 +395,142 @@ describe('CollaboratorsInviteHandler', function () { }) describe('revokeInvite', function () { - beforeEach(function () { - this.ProjectInvite.findOneAndDelete.returns({ - exec: sinon.stub().resolves(this.fakeInvite), + beforeEach(function (ctx) { + ctx.ProjectInvite.findOneAndDelete.returns({ + exec: sinon.stub().resolves(ctx.fakeInvite), }) - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = sinon.stub().resolves() - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.revokeInvite( - this.projectId, - this.inviteId + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.revokeInvite( + ctx.projectId, + ctx.inviteId ) } }) describe('when all goes well', function () { - it('should call ProjectInvite.findOneAndDelete', async function () { - await this.call() - this.ProjectInvite.findOneAndDelete.should.have.been.calledOnce - this.ProjectInvite.findOneAndDelete.should.have.been.calledWith({ - projectId: this.projectId, - _id: this.inviteId, + it('should call ProjectInvite.findOneAndDelete', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.findOneAndDelete.should.have.been.calledOnce + ctx.ProjectInvite.findOneAndDelete.should.have.been.calledWith({ + projectId: ctx.projectId, + _id: ctx.inviteId, }) }) - it('should call _tryCancelInviteNotification', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification.callCount.should.equal( + it('should call _tryCancelInviteNotification', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification - .calledWith(this.inviteId) + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification + .calledWith(ctx.inviteId) .should.equal(true) }) - it('should return the deleted invite', async function () { - const invite = await this.call() - expect(invite).to.deep.equal(this.fakeInvite) + it('should return the deleted invite', async function (ctx) { + const invite = await ctx.call() + expect(invite).to.deep.equal(ctx.fakeInvite) }) }) describe('when remove produces an error', function () { - beforeEach(function () { - this.ProjectInvite.findOneAndDelete.returns({ + beforeEach(function (ctx) { + ctx.ProjectInvite.findOneAndDelete.returns({ exec: sinon.stub().rejects(new Error('woops')), }) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('generateNewInvite', function () { - beforeEach(function () { - this.fakeInviteToProjectObject = { + beforeEach(function (ctx) { + ctx.fakeInviteToProjectObject = { _id: new ObjectId(), - email: this.email, - privileges: this.privileges, + email: ctx.email, + privileges: ctx.privileges, } - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() - .resolves(this.fakeInvite) - this.CollaboratorsInviteHandler.promises.inviteToProject = sinon + .resolves(ctx.fakeInvite) + ctx.CollaboratorsInviteHandler.promises.inviteToProject = sinon .stub() - .resolves(this.fakeInviteToProjectObject) - this.call = async () => { - return await this.CollaboratorsInviteHandler.promises.generateNewInvite( - this.projectId, - this.sendingUser, - this.inviteId + .resolves(ctx.fakeInviteToProjectObject) + ctx.call = async () => { + return await ctx.CollaboratorsInviteHandler.promises.generateNewInvite( + ctx.projectId, + ctx.sendingUser, + ctx.inviteId ) } }) describe('when all goes well', function () { - it('should call revokeInvite', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( + it('should call revokeInvite', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.revokeInvite.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.revokeInvite - .calledWith(this.projectId, this.inviteId) + ctx.CollaboratorsInviteHandler.promises.revokeInvite + .calledWith(ctx.projectId, ctx.inviteId) .should.equal(true) }) - it('should have called inviteToProject', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should have called inviteToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 1 ) - this.CollaboratorsInviteHandler.promises.inviteToProject + ctx.CollaboratorsInviteHandler.promises.inviteToProject .calledWith( - this.projectId, - this.sendingUser, - this.fakeInvite.email, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUser, + ctx.fakeInvite.email, + ctx.fakeInvite.privileges ) .should.equal(true) }) - it('should return the invite', async function () { - const invite = await this.call() - expect(invite).to.deep.equal(this.fakeInviteToProjectObject) + it('should return the invite', async function (ctx) { + const invite = await ctx.call() + expect(invite).to.deep.equal(ctx.fakeInviteToProjectObject) }) }) describe('when revokeInvite produces an error', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should not have called inviteToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) }) describe('when findOne does not find an invite', function () { - beforeEach(function () { - this.CollaboratorsInviteHandler.promises.revokeInvite = sinon + beforeEach(function (ctx) { + ctx.CollaboratorsInviteHandler.promises.revokeInvite = sinon .stub() .resolves(null) }) - it('should not have called inviteToProject', async function () { - await this.call() - this.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( + it('should not have called inviteToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsInviteHandler.promises.inviteToProject.callCount.should.equal( 0 ) }) @@ -482,91 +538,91 @@ describe('CollaboratorsInviteHandler', function () { }) describe('acceptInvite', function () { - beforeEach(function () { - this.fakeProject = { - _id: this.projectId, - owner_ref: this.sendingUserId, + beforeEach(function (ctx) { + ctx.fakeProject = { + _id: ctx.projectId, + owner_ref: ctx.sendingUserId, } - this.ProjectGetter.promises.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon .stub() - .resolves(this.fakeProject) - this.CollaboratorsHandler.promises.addUserIdToProject.resolves() - this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = + .resolves(ctx.fakeProject) + ctx.CollaboratorsHandler.promises.addUserIdToProject.resolves() + ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification = sinon.stub().resolves() - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) - this.ProjectInvite.deleteOne.returns({ exec: sinon.stub().resolves() }) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises.acceptInvite( - this.fakeInvite, - this.projectId, - this.user + ctx.ProjectInvite.deleteOne.returns({ exec: sinon.stub().resolves() }) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises.acceptInvite( + ctx.fakeInvite, + ctx.projectId, + ctx.user ) } }) describe('when all goes well', function () { - it('should add readAndWrite invitees to the project as normal', async function () { - await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + it('should add readAndWrite invitees to the project as normal', async function (ctx) { + await ctx.call() + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) }) - it('should have called ProjectInvite.deleteOne', async function () { - await this.call() - this.ProjectInvite.deleteOne.callCount.should.equal(1) - this.ProjectInvite.deleteOne - .calledWith({ _id: this.inviteId }) + it('should have called ProjectInvite.deleteOne', async function (ctx) { + await ctx.call() + ctx.ProjectInvite.deleteOne.callCount.should.equal(1) + ctx.ProjectInvite.deleteOne + .calledWith({ _id: ctx.inviteId }) .should.equal(true) }) }) describe('when the invite is for readOnly access', function () { - beforeEach(function () { - this.fakeInvite.privileges = 'readOnly' + beforeEach(function (ctx) { + ctx.fakeInvite.privileges = 'readOnly' }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await this.call() - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await ctx.call() + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject .calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) .should.equal(true) }) }) describe('when the project has no more edit collaborator slots', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( false ) }) - it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function () { - await this.call() - this.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( - this.projectId, + it('should add readAndWrite invitees to the project as readOnly (pendingEditor) users', async function (ctx) { + await ctx.call() + ctx.ProjectAuditLogHandler.promises.addEntry.should.have.been.calledWith( + ctx.projectId, 'editor-moved-to-pending', null, null, - { userId: this.userId.toString(), role: 'editor' } + { userId: ctx.userId.toString(), role: 'editor' } ) - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, 'readOnly', { pendingEditor: true } ) @@ -574,139 +630,139 @@ describe('CollaboratorsInviteHandler', function () { }) describe('when addUserIdToProject produces an error', function () { - beforeEach(function () { - this.CollaboratorsHandler.promises.addUserIdToProject.callsArgWith( + beforeEach(function (ctx) { + ctx.CollaboratorsHandler.promises.addUserIdToProject.callsArgWith( 4, new Error('woops') ) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject .calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) .should.equal(true) }) - it('should not have called ProjectInvite.deleteOne', async function () { - await expect(this.call()).to.be.rejected - this.ProjectInvite.deleteOne.callCount.should.equal(0) + it('should not have called ProjectInvite.deleteOne', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectInvite.deleteOne.callCount.should.equal(0) }) }) describe('when ProjectInvite.deleteOne produces an error', function () { - beforeEach(function () { - this.ProjectInvite.deleteOne.returns({ + beforeEach(function (ctx) { + ctx.ProjectInvite.deleteOne.returns({ exec: sinon.stub().rejects(new Error('woops')), }) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should have called CollaboratorsHandler.addUserIdToProject', async function () { - await expect(this.call()).to.be.rejected - this.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( + it('should have called CollaboratorsHandler.addUserIdToProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.CollaboratorsHandler.promises.addUserIdToProject.callCount.should.equal( 1 ) - this.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( - this.projectId, - this.sendingUserId, - this.userId, - this.fakeInvite.privileges + ctx.CollaboratorsHandler.promises.addUserIdToProject.should.have.been.calledWith( + ctx.projectId, + ctx.sendingUserId, + ctx.userId, + ctx.fakeInvite.privileges ) }) - it('should have called ProjectInvite.deleteOne', async function () { - await expect(this.call()).to.be.rejected - this.ProjectInvite.deleteOne.callCount.should.equal(1) + it('should have called ProjectInvite.deleteOne', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectInvite.deleteOne.callCount.should.equal(1) }) }) }) describe('_tryCancelInviteNotification', function () { - beforeEach(function () { - this.inviteId = new ObjectId() - this.currentUser = { _id: new ObjectId() } - this.notification = { read: sinon.stub().resolves() } - this.NotificationsBuilder.promises.projectInvite = sinon + beforeEach(function (ctx) { + ctx.inviteId = new ObjectId() + ctx.currentUser = { _id: new ObjectId() } + ctx.notification = { read: sinon.stub().resolves() } + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._tryCancelInviteNotification( - this.inviteId + .returns(ctx.notification) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._tryCancelInviteNotification( + ctx.inviteId ) } }) - it('should call notification.read', async function () { - await this.call() - this.notification.read.callCount.should.equal(1) + it('should call notification.read', async function (ctx) { + await ctx.call() + ctx.notification.read.callCount.should.equal(1) }) describe('when notification.read produces an error', function () { - beforeEach(function () { - this.notification = { + beforeEach(function (ctx) { + ctx.notification = { read: sinon.stub().rejects(new Error('woops')), } - this.NotificationsBuilder.promises.projectInvite = sinon + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) + .returns(ctx.notification) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejected + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejected }) }) }) describe('_trySendInviteNotification', function () { - beforeEach(function () { - this.invite = { + beforeEach(function (ctx) { + ctx.invite = { _id: new ObjectId(), token: 'some_token', sendingUserId: new ObjectId(), - projectId: this.project_id, + projectId: ctx.project_id, targetEmail: 'user@example.com', createdAt: new Date(), } - this.sendingUser = { + ctx.sendingUser = { _id: new ObjectId(), first_name: 'jim', } - this.existingUser = { _id: new ObjectId() } - this.UserGetter.promises.getUserByAnyEmail = sinon + ctx.existingUser = { _id: new ObjectId() } + ctx.UserGetter.promises.getUserByAnyEmail = sinon .stub() - .resolves(this.existingUser) - this.fakeProject = { - _id: this.project_id, + .resolves(ctx.existingUser) + ctx.fakeProject = { + _id: ctx.project_id, name: 'some project', } - this.ProjectGetter.promises.getProject = sinon + ctx.ProjectGetter.promises.getProject = sinon .stub() - .resolves(this.fakeProject) - this.notification = { create: sinon.stub().resolves() } - this.NotificationsBuilder.promises.projectInvite = sinon + .resolves(ctx.fakeProject) + ctx.notification = { create: sinon.stub().resolves() } + ctx.NotificationsBuilder.promises.projectInvite = sinon .stub() - .returns(this.notification) - this.call = async () => { - await this.CollaboratorsInviteHandler.promises._trySendInviteNotification( - this.project_id, - this.sendingUser, - this.invite + .returns(ctx.notification) + ctx.call = async () => { + await ctx.CollaboratorsInviteHandler.promises._trySendInviteNotification( + ctx.project_id, + ctx.sendingUser, + ctx.invite ) } }) @@ -714,119 +770,119 @@ describe('CollaboratorsInviteHandler', function () { describe('when the user exists', function () { beforeEach(function () {}) - it('should call getUser', async function () { - await this.call() - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await ctx.call() + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should call getProject', async function () { - await this.call() - this.ProjectGetter.promises.getProject.callCount.should.equal(1) - this.ProjectGetter.promises.getProject - .calledWith(this.project_id) + it('should call getProject', async function (ctx) { + await ctx.call() + ctx.ProjectGetter.promises.getProject.callCount.should.equal(1) + ctx.ProjectGetter.promises.getProject + .calledWith(ctx.project_id) .should.equal(true) }) - it('should call NotificationsBuilder.projectInvite.create', async function () { - await this.call() - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should call NotificationsBuilder.projectInvite.create', async function (ctx) { + await ctx.call() + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 1 ) - this.notification.create.callCount.should.equal(1) + ctx.notification.create.callCount.should.equal(1) }) describe('when getProject produces an error', function () { - beforeEach(function () { - this.ProjectGetter.promises.getProject.callsArgWith( + beforeEach(function (ctx) { + ctx.ProjectGetter.promises.getProject.callsArgWith( 2, new Error('woops') ) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await expect(this.call()).to.be.rejected - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) describe('when projectInvite.create produces an error', function () { - beforeEach(function () { - this.notification.create.callsArgWith(0, new Error('woops')) + beforeEach(function (ctx) { + ctx.notification.create.callsArgWith(0, new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) }) }) describe('when the user does not exist', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByAnyEmail = sinon.stub().resolves(null) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail = sinon.stub().resolves(null) }) - it('should call getUser', async function () { - await this.call() - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await ctx.call() + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should not call getProject', async function () { - await this.call() - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call getProject', async function (ctx) { + await ctx.call() + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await this.call() - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await ctx.call() + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) describe('when the getUser produces an error', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByAnyEmail = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail = sinon .stub() .rejects(new Error('woops')) }) - it('should produce an error', async function () { - await expect(this.call()).to.be.rejectedWith(Error) + it('should produce an error', async function (ctx) { + await expect(ctx.call()).to.be.rejectedWith(Error) }) - it('should call getUser', async function () { - await expect(this.call()).to.be.rejected - this.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) - this.UserGetter.promises.getUserByAnyEmail - .calledWith(this.invite.email) + it('should call getUser', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.UserGetter.promises.getUserByAnyEmail.callCount.should.equal(1) + ctx.UserGetter.promises.getUserByAnyEmail + .calledWith(ctx.invite.email) .should.equal(true) }) - it('should not call getProject', async function () { - await expect(this.call()).to.be.rejected - this.ProjectGetter.promises.getProject.callCount.should.equal(0) + it('should not call getProject', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.ProjectGetter.promises.getProject.callCount.should.equal(0) }) - it('should not call NotificationsBuilder.projectInvite.create', async function () { - await expect(this.call()).to.be.rejected - this.NotificationsBuilder.promises.projectInvite.callCount.should.equal( + it('should not call NotificationsBuilder.projectInvite.create', async function (ctx) { + await expect(ctx.call()).to.be.rejected + ctx.NotificationsBuilder.promises.projectInvite.callCount.should.equal( 0 ) - this.notification.create.callCount.should.equal(0) + ctx.notification.create.callCount.should.equal(0) }) }) }) diff --git a/services/web/test/unit/src/Contact/ContactController.test.mjs b/services/web/test/unit/src/Contact/ContactController.test.mjs index ea5a1d0220..2defc2c3a7 100644 --- a/services/web/test/unit/src/Contact/ContactController.test.mjs +++ b/services/web/test/unit/src/Contact/ContactController.test.mjs @@ -1,34 +1,47 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Contacts/ContactController.mjs' describe('ContactController', function () { - beforeEach(async function () { - this.SessionManager = { getLoggedInUserId: sinon.stub() } - this.ContactController = await esmock.strict(modulePath, { - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + beforeEach(async function (ctx) { + ctx.SessionManager = { getLoggedInUserId: sinon.stub() } + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: {}, }), - '../../../../app/src/Features/Contacts/ContactManager': - (this.ContactManager = { promises: {} }), - '../../../../app/src/Features/Authentication/SessionManager': - (this.SessionManager = {}), - '../../../../app/src/infrastructure/Modules': (this.Modules = { + })) + + vi.doMock('../../../../app/src/Features/Contacts/ContactManager', () => ({ + default: (ctx.ContactManager = { promises: {} }), + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: (ctx.SessionManager = {}), + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { promises: { hooks: {} }, }), - }) + })) - this.req = {} - this.res = new MockResponse() + ctx.ContactController = (await import(modulePath)).default + + ctx.req = {} + ctx.res = new MockResponse() }) describe('getContacts', function () { - beforeEach(function () { - this.user_id = 'mock-user-id' - this.contact_ids = ['contact-1', 'contact-2', 'contact-3'] - this.contacts = [ + beforeEach(function (ctx) { + ctx.user_id = 'mock-user-id' + ctx.contact_ids = ['contact-1', 'contact-2', 'contact-3'] + ctx.contacts = [ { _id: 'contact-1', email: 'joe@example.com', @@ -52,78 +65,84 @@ describe('ContactController', function () { unsued: 'foo', }, ] - this.SessionManager.getLoggedInUserId = sinon.stub().returns(this.user_id) - this.ContactManager.promises.getContactIds = sinon + ctx.SessionManager.getLoggedInUserId = sinon.stub().returns(ctx.user_id) + ctx.ContactManager.promises.getContactIds = sinon .stub() - .resolves(this.contact_ids) - this.UserGetter.promises.getUsers = sinon.stub().resolves(this.contacts) - this.Modules.promises.hooks.fire = sinon.stub() + .resolves(ctx.contact_ids) + ctx.UserGetter.promises.getUsers = sinon.stub().resolves(ctx.contacts) + ctx.Modules.promises.hooks.fire = sinon.stub() }) - it('should look up the logged in user id', async function () { - this.ContactController.getContacts(this.req, this.res) - this.SessionManager.getLoggedInUserId - .calledWith(this.req.session) + it('should look up the logged in user id', async function (ctx) { + ctx.ContactController.getContacts(ctx.req, ctx.res) + ctx.SessionManager.getLoggedInUserId + .calledWith(ctx.req.session) .should.equal(true) }) - it('should get the users contact ids', async function () { - this.res.callback = () => { + it('should get the users contact ids', async function (ctx) { + ctx.res.callback = () => { expect( - this.ContactManager.promises.getContactIds - ).to.have.been.calledWith(this.user_id, { limit: 50 }) + ctx.ContactManager.promises.getContactIds + ).to.have.been.calledWith(ctx.user_id, { limit: 50 }) } - this.ContactController.getContacts(this.req, this.res) + ctx.ContactController.getContacts(ctx.req, ctx.res) }) - it('should populate the users contacts ids', function (done) { - this.res.callback = () => { - expect(this.UserGetter.promises.getUsers).to.have.been.calledWith( - this.contact_ids, - { - email: 1, - first_name: 1, - last_name: 1, - holdingAccount: 1, - } - ) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should populate the users contacts ids', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.UserGetter.promises.getUsers).to.have.been.calledWith( + ctx.contact_ids, + { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1, + } + ) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) - it('should fire the getContact module hook', function (done) { - this.res.callback = () => { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( - 'getContacts', - this.user_id - ) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should fire the getContact module hook', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( + 'getContacts', + ctx.user_id + ) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) - it('should return a formatted list of contacts in contact list order, without holding accounts', function (done) { - this.res.callback = () => { - this.res.json.args[0][0].contacts.should.deep.equal([ - { - id: 'contact-1', - email: 'joe@example.com', - first_name: 'Joe', - last_name: 'Example', - type: 'user', - }, - { - id: 'contact-3', - email: 'jim@example.com', - first_name: 'Jim', - last_name: 'Example', - type: 'user', - }, - ]) - done() - } - this.ContactController.getContacts(this.req, this.res, done) + it('should return a formatted list of contacts in contact list order, without holding accounts', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.json.args[0][0].contacts.should.deep.equal([ + { + id: 'contact-1', + email: 'joe@example.com', + first_name: 'Joe', + last_name: 'Example', + type: 'user', + }, + { + id: 'contact-3', + email: 'jim@example.com', + first_name: 'Jim', + last_name: 'Example', + type: 'user', + }, + ]) + resolve() + } + ctx.ContactController.getContacts(ctx.req, ctx.res, resolve) + }) }) }) }) diff --git a/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs b/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs index 22d05fba56..2bb1ed81dd 100644 --- a/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs +++ b/services/web/test/unit/src/Cooldown/CooldownMiddleware.test.mjs @@ -1,15 +1,4 @@ -/* eslint-disable - max-len, - no-return-assign, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = new URL( @@ -18,116 +7,119 @@ const modulePath = new URL( ).pathname describe('CooldownMiddleware', function () { - beforeEach(async function () { - this.CooldownManager = { isProjectOnCooldown: sinon.stub() } - return (this.CooldownMiddleware = await esmock.strict(modulePath, { - '../../../../app/src/Features/Cooldown/CooldownManager.js': - this.CooldownManager, - })) + beforeEach(async function (ctx) { + ctx.CooldownManager = { isProjectOnCooldown: sinon.stub() } + + vi.doMock( + '../../../../app/src/Features/Cooldown/CooldownManager.js', + () => ({ + default: ctx.CooldownManager, + }) + ) + + ctx.CooldownMiddleware = (await import(modulePath)).default }) describe('freezeProject', function () { describe('when project is on cooldown', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, true) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('should not produce an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - return this.next.callCount.should.equal(0) + it('should not produce an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + return ctx.next.callCount.should.equal(0) }) - it('should send a 429 status', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.res.sendStatus.callCount.should.equal(1) - return this.res.sendStatus.calledWith(429).should.equal(true) + it('should send a 429 status', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.res.sendStatus.callCount.should.equal(1) + return ctx.res.sendStatus.calledWith(429).should.equal(true) }) }) describe('when project is not on cooldown', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, false) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('call next with no arguments', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args.length).to.equal(0) + it('call next with no arguments', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args.length).to.equal(0) }) }) describe('when isProjectOnCooldown produces an error', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, new Error('woops')) - this.req = { params: { Project_id: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { Project_id: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('should call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) - return this.CooldownManager.isProjectOnCooldown + it('should call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(1) + return ctx.CooldownManager.isProjectOnCooldown .calledWith('abc') .should.equal(true) }) - it('call next with an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('call next with an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) }) describe('when projectId is not part of route', function () { - beforeEach(function () { - this.CooldownManager.isProjectOnCooldown = sinon + beforeEach(function (ctx) { + ctx.CooldownManager.isProjectOnCooldown = sinon .stub() .callsArgWith(1, null, true) - this.req = { params: { lol: 'abc' } } - this.res = { sendStatus: sinon.stub() } - return (this.next = sinon.stub()) + ctx.req = { params: { lol: 'abc' } } + ctx.res = { sendStatus: sinon.stub() } + return (ctx.next = sinon.stub()) }) - it('call next with an error', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - this.next.callCount.should.equal(1) - return expect(this.next.lastCall.args[0]).to.be.instanceof(Error) + it('call next with an error', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + ctx.next.callCount.should.equal(1) + return expect(ctx.next.lastCall.args[0]).to.be.instanceof(Error) }) - it('should not call CooldownManager.isProjectOnCooldown', function () { - this.CooldownMiddleware.freezeProject(this.req, this.res, this.next) - return this.CooldownManager.isProjectOnCooldown.callCount.should.equal( - 0 - ) + it('should not call CooldownManager.isProjectOnCooldown', function (ctx) { + ctx.CooldownMiddleware.freezeProject(ctx.req, ctx.res, ctx.next) + return ctx.CooldownManager.isProjectOnCooldown.callCount.should.equal(0) }) }) }) diff --git a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs index 6a783d452e..095e598d39 100644 --- a/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs +++ b/services/web/test/unit/src/DocumentUpdater/DocumentUpdaterController.test.mjs @@ -1,92 +1,102 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const MODULE_PATH = '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterController.mjs' describe('DocumentUpdaterController', function () { - beforeEach(async function () { - this.DocumentUpdaterHandler = { + beforeEach(async function (ctx) { + ctx.DocumentUpdaterHandler = { promises: { getDocument: sinon.stub(), }, } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: { findElement: sinon.stub(), }, } - this.controller = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Project/ProjectLocator.js': - this.ProjectLocator, - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js': - this.DocumentUpdaterHandler, - }) - this.projectId = '2k3j1lk3j21lk3j' - this.fileId = '12321kklj1lk3jk12' - this.req = { + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator.js', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + ctx.controller = (await import(MODULE_PATH)).default + ctx.projectId = '2k3j1lk3j21lk3j' + ctx.fileId = '12321kklj1lk3jk12' + ctx.req = { params: { - Project_id: this.projectId, - Doc_id: this.docId, + Project_id: ctx.projectId, + Doc_id: ctx.docId, }, get(key) { return undefined }, } - this.lines = ['test', '', 'testing'] - this.res = new MockResponse() - this.next = sinon.stub() - this.doc = { name: 'myfile.tex' } + ctx.lines = ['test', '', 'testing'] + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.doc = { name: 'myfile.tex' } }) describe('getDoc', function () { - beforeEach(function () { - this.DocumentUpdaterHandler.promises.getDocument.resolves({ - lines: this.lines, + beforeEach(function (ctx) { + ctx.DocumentUpdaterHandler.promises.getDocument.resolves({ + lines: ctx.lines, }) - this.ProjectLocator.promises.findElement.resolves({ - element: this.doc, + ctx.ProjectLocator.promises.findElement.resolves({ + element: ctx.doc, }) - this.res = new MockResponse() + ctx.res = new MockResponse() }) - it('should call the document updater handler with the project_id and doc_id', async function () { - await this.controller.getDoc(this.req, this.res, this.next) + it('should call the document updater handler with the project_id and doc_id', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res, ctx.next) expect( - this.DocumentUpdaterHandler.promises.getDocument + ctx.DocumentUpdaterHandler.promises.getDocument ).to.have.been.calledOnceWith( - this.req.params.Project_id, - this.req.params.Doc_id, + ctx.req.params.Project_id, + ctx.req.params.Doc_id, -1 ) }) - it('should return the content', async function () { - await this.controller.getDoc(this.req, this.res) - expect(this.next).to.not.have.been.called - expect(this.res.statusCode).to.equal(200) - expect(this.res.body).to.equal('test\n\ntesting') + it('should return the content', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) + expect(ctx.next).to.not.have.been.called + expect(ctx.res.statusCode).to.equal(200) + expect(ctx.res.body).to.equal('test\n\ntesting') }) - it('should find the doc in the project', async function () { - await this.controller.getDoc(this.req, this.res) + it('should find the doc in the project', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) expect( - this.ProjectLocator.promises.findElement + ctx.ProjectLocator.promises.findElement ).to.have.been.calledOnceWith({ - project_id: this.projectId, - element_id: this.docId, + project_id: ctx.projectId, + element_id: ctx.docId, type: 'doc', }) }) - it('should set the Content-Disposition header', async function () { - await this.controller.getDoc(this.req, this.res) - expect(this.res.setContentDisposition).to.have.been.calledWith( + it('should set the Content-Disposition header', async function (ctx) { + await ctx.controller.getDoc(ctx.req, ctx.res) + expect(ctx.res.setContentDisposition).to.have.been.calledWith( 'attachment', - { filename: this.doc.name } + { filename: ctx.doc.name } ) }) }) diff --git a/services/web/test/unit/src/Documents/DocumentController.test.mjs b/services/web/test/unit/src/Documents/DocumentController.test.mjs index 813e8d65f3..06c971be91 100644 --- a/services/web/test/unit/src/Documents/DocumentController.test.mjs +++ b/services/web/test/unit/src/Documents/DocumentController.test.mjs @@ -1,5 +1,5 @@ +import { vi } from 'vitest' import sinon from 'sinon' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import Errors from '../../../../app/src/Features/Errors/Errors.js' @@ -8,14 +8,14 @@ const MODULE_PATH = '../../../../app/src/Features/Documents/DocumentController.mjs' describe('DocumentController', function () { - beforeEach(async function () { - this.res = new MockResponse() - this.req = new MockRequest() - this.next = sinon.stub() - this.doc = { _id: 'doc-id-123' } - this.doc_lines = ['one', 'two', 'three'] - this.version = 42 - this.ranges = { + beforeEach(async function (ctx) { + ctx.res = new MockResponse() + ctx.req = new MockRequest() + ctx.next = sinon.stub() + ctx.doc = { _id: 'doc-id-123' } + ctx.doc_lines = ['one', 'two', 'three'] + ctx.version = 42 + ctx.ranges = { comments: [ { id: 'comment1', @@ -35,11 +35,11 @@ describe('DocumentController', function () { }, ], } - this.pathname = '/a/b/c/file.tex' - this.lastUpdatedAt = new Date().getTime() - this.lastUpdatedBy = 'fake-last-updater-id' - this.rev = 5 - this.project = { + ctx.pathname = '/a/b/c/file.tex' + ctx.lastUpdatedAt = new Date().getTime() + ctx.lastUpdatedBy = 'fake-last-updater-id' + ctx.rev = 5 + ctx.project = { _id: 'project-id-123', overleaf: { history: { @@ -48,81 +48,100 @@ describe('DocumentController', function () { }, }, } - this.resolvedThreadIds = [ + ctx.resolvedThreadIds = [ 'comment2', 'comment4', // Comment in project but not in doc ] - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: { findElement: sinon .stub() - .resolves({ element: this.doc, path: { fileSystem: this.pathname } }), + .resolves({ element: ctx.doc, path: { fileSystem: ctx.pathname } }), }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { promises: { getDoc: sinon.stub().resolves({ - lines: this.doc_lines, - rev: this.rev, - version: this.version, - ranges: this.ranges, + lines: ctx.doc_lines, + rev: ctx.rev, + version: ctx.version, + ranges: ctx.ranges, }), }, } - this.ProjectEntityUpdateHandler = { + ctx.ProjectEntityUpdateHandler = { promises: { updateDocLines: sinon.stub().resolves(), }, } - this.ChatApiHandler = { + ctx.ChatApiHandler = { promises: { - getResolvedThreadIds: sinon.stub().resolves(this.resolvedThreadIds), + getResolvedThreadIds: sinon.stub().resolves(ctx.resolvedThreadIds), }, } - this.DocumentController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/Project/ProjectEntityHandler': - this.ProjectEntityHandler, - '../../../../app/src/Features/Project/ProjectEntityUpdateHandler': - this.ProjectEntityUpdateHandler, - '../../../../app/src/Features/Chat/ChatApiHandler': this.ChatApiHandler, - }) + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityUpdateHandler', + () => ({ + default: ctx.ProjectEntityUpdateHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Chat/ChatApiHandler', () => ({ + default: ctx.ChatApiHandler, + })) + + ctx.DocumentController = (await import(MODULE_PATH)).default }) describe('getDocument', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.project._id, - doc_id: this.doc._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.project._id, + doc_id: ctx.doc._id, } }) describe('when project exists with project history enabled', function () { - beforeEach(function (done) { - this.res.callback = err => { - done(err) - } - this.DocumentController.getDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('should return the history id and display setting to the client as JSON', function () { - this.res.type.should.equal('application/json') - JSON.parse(this.res.body).should.deep.equal({ - lines: this.doc_lines, - version: this.version, - ranges: this.ranges, - pathname: this.pathname, - projectHistoryId: this.project.overleaf.history.id, + it('should return the history id and display setting to the client as JSON', function (ctx) { + ctx.res.type.should.equal('application/json') + JSON.parse(ctx.res.body).should.deep.equal({ + lines: ctx.doc_lines, + version: ctx.version, + ranges: ctx.ranges, + pathname: ctx.pathname, + projectHistoryId: ctx.project.overleaf.history.id, projectHistoryType: 'project-history', resolvedCommentIds: ['comment2'], historyRangesSupport: false, @@ -132,75 +151,81 @@ describe('DocumentController', function () { }) describe('when the project does not exist', function () { - beforeEach(function (done) { - this.ProjectGetter.promises.getProject.resolves(null) - this.res.callback = err => { - done(err) - } - this.DocumentController.getDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.promises.getProject.resolves(null) + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.getDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('returns a 404', function () { - this.res.statusCode.should.equal(404) + it('returns a 404', function (ctx) { + ctx.res.statusCode.should.equal(404) }) }) }) describe('setDocument', function () { - beforeEach(function () { - this.req.params = { - Project_id: this.project._id, - doc_id: this.doc._id, + beforeEach(function (ctx) { + ctx.req.params = { + Project_id: ctx.project._id, + doc_id: ctx.doc._id, } }) describe('when the document exists', function () { - beforeEach(function (done) { - this.req.body = { - lines: this.doc_lines, - version: this.version, - ranges: this.ranges, - lastUpdatedAt: this.lastUpdatedAt, - lastUpdatedBy: this.lastUpdatedBy, - } - this.res.callback = err => { - done(err) - } - this.DocumentController.setDocument(this.req, this.res, this.next) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + lines: ctx.doc_lines, + version: ctx.version, + ranges: ctx.ranges, + lastUpdatedAt: ctx.lastUpdatedAt, + lastUpdatedBy: ctx.lastUpdatedBy, + } + ctx.res.callback = err => { + resolve(err) + } + ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next) + }) }) - it('should update the document in Mongo', function () { + it('should update the document in Mongo', function (ctx) { sinon.assert.calledWith( - this.ProjectEntityUpdateHandler.promises.updateDocLines, - this.project._id, - this.doc._id, - this.doc_lines, - this.version, - this.ranges, - this.lastUpdatedAt, - this.lastUpdatedBy + ctx.ProjectEntityUpdateHandler.promises.updateDocLines, + ctx.project._id, + ctx.doc._id, + ctx.doc_lines, + ctx.version, + ctx.ranges, + ctx.lastUpdatedAt, + ctx.lastUpdatedBy ) }) - it('should return a successful response', function () { - this.res.success.should.equal(true) + it('should return a successful response', function (ctx) { + ctx.res.success.should.equal(true) }) }) describe("when the document doesn't exist", function () { - beforeEach(function (done) { - this.ProjectEntityUpdateHandler.promises.updateDocLines.rejects( - new Errors.NotFoundError('document does not exist') - ) - this.req.body = { lines: this.doc_lines } - this.next.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectEntityUpdateHandler.promises.updateDocLines.rejects( + new Errors.NotFoundError('document does not exist') + ) + ctx.req.body = { lines: ctx.doc_lines } + ctx.next.callsFake(() => { + resolve() + }) + ctx.DocumentController.setDocument(ctx.req, ctx.res, ctx.next) }) - this.DocumentController.setDocument(this.req, this.res, this.next) }) - it('should call next with the NotFoundError', function () { - this.next + it('should call next with the NotFoundError', function (ctx) { + ctx.next .calledWith(sinon.match.instanceOf(Errors.NotFoundError)) .should.equal(true) }) diff --git a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs index db9cf19df7..1e339097fa 100644 --- a/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectDownloadsController.test.mjs @@ -1,3 +1,4 @@ +import { vi } from 'vitest' // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -6,136 +7,150 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import sinon from 'sinon' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Downloads/ProjectDownloadsController.mjs' describe('ProjectDownloadsController', function () { - beforeEach(async function () { - this.project_id = 'project-id-123' - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub() - this.DocumentUpdaterHandler = sinon.stub() - return (this.ProjectDownloadsController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs': - (this.ProjectZipStreamManager = {}), - '../../../../app/src/Features/Project/ProjectGetter.js': - (this.ProjectGetter = {}), - '@overleaf/metrics': (this.metrics = {}), - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js': - this.DocumentUpdaterHandler, + beforeEach(async function (ctx) { + ctx.project_id = 'project-id-123' + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.DocumentUpdaterHandler = sinon.stub() + + vi.doMock( + '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs', + () => ({ + default: (ctx.ProjectZipStreamManager = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter.js', () => ({ + default: (ctx.ProjectGetter = {}), })) + + vi.doMock('@overleaf/metrics', () => ({ + default: (ctx.metrics = {}), + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + ctx.ProjectDownloadsController = (await import(modulePath)).default }) describe('downloadProject', function () { - beforeEach(function () { - this.stream = { pipe: sinon.stub() } - this.ProjectZipStreamManager.createZipStreamForProject = sinon + beforeEach(function (ctx) { + ctx.stream = { pipe: sinon.stub() } + ctx.ProjectZipStreamManager.createZipStreamForProject = sinon .stub() - .callsArgWith(1, null, this.stream) - this.req.params = { Project_id: this.project_id } - this.project_name = 'project name with accênts' - this.ProjectGetter.getProject = sinon + .callsArgWith(1, null, ctx.stream) + ctx.req.params = { Project_id: ctx.project_id } + ctx.project_name = 'project name with accênts' + ctx.ProjectGetter.getProject = sinon .stub() - .callsArgWith(2, null, { name: this.project_name }) - this.DocumentUpdaterHandler.flushProjectToMongo = sinon + .callsArgWith(2, null, { name: ctx.project_name }) + ctx.DocumentUpdaterHandler.flushProjectToMongo = sinon .stub() .callsArgWith(1) - this.metrics.inc = sinon.stub() - return this.ProjectDownloadsController.downloadProject( - this.req, - this.res, - this.next + ctx.metrics.inc = sinon.stub() + return ctx.ProjectDownloadsController.downloadProject( + ctx.req, + ctx.res, + ctx.next ) }) - it('should create a zip from the project', function () { - return this.ProjectZipStreamManager.createZipStreamForProject - .calledWith(this.project_id) + it('should create a zip from the project', function (ctx) { + return ctx.ProjectZipStreamManager.createZipStreamForProject + .calledWith(ctx.project_id) .should.equal(true) }) - it('should stream the zip to the request', function () { - return this.stream.pipe.calledWith(this.res).should.equal(true) + it('should stream the zip to the request', function (ctx) { + return ctx.stream.pipe.calledWith(ctx.res).should.equal(true) }) - it('should set the correct content type on the request', function () { - return this.res.contentType + it('should set the correct content type on the request', function (ctx) { + return ctx.res.contentType .calledWith('application/zip') .should.equal(true) }) - it('should flush the project to mongo', function () { - return this.DocumentUpdaterHandler.flushProjectToMongo - .calledWith(this.project_id) + it('should flush the project to mongo', function (ctx) { + return ctx.DocumentUpdaterHandler.flushProjectToMongo + .calledWith(ctx.project_id) .should.equal(true) }) - it("should look up the project's name", function () { - return this.ProjectGetter.getProject - .calledWith(this.project_id, { name: true }) + it("should look up the project's name", function (ctx) { + return ctx.ProjectGetter.getProject + .calledWith(ctx.project_id, { name: true }) .should.equal(true) }) - it('should name the downloaded file after the project', function () { - this.res.headers.should.deep.equal({ - 'Content-Disposition': `attachment; filename="${this.project_name}.zip"`, + it('should name the downloaded file after the project', function (ctx) { + ctx.res.headers.should.deep.equal({ + 'Content-Disposition': `attachment; filename="${ctx.project_name}.zip"`, 'Content-Type': 'application/zip', 'X-Content-Type-Options': 'nosniff', }) }) - it('should record the action via Metrics', function () { - return this.metrics.inc.calledWith('zip-downloads').should.equal(true) + it('should record the action via Metrics', function (ctx) { + return ctx.metrics.inc.calledWith('zip-downloads').should.equal(true) }) }) describe('downloadMultipleProjects', function () { - beforeEach(function () { - this.stream = { pipe: sinon.stub() } - this.ProjectZipStreamManager.createZipStreamForMultipleProjects = sinon + beforeEach(function (ctx) { + ctx.stream = { pipe: sinon.stub() } + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects = sinon .stub() - .callsArgWith(1, null, this.stream) - this.project_ids = ['project-1', 'project-2'] - this.req.query = { project_ids: this.project_ids.join(',') } - this.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon + .callsArgWith(1, null, ctx.stream) + ctx.project_ids = ['project-1', 'project-2'] + ctx.req.query = { project_ids: ctx.project_ids.join(',') } + ctx.DocumentUpdaterHandler.flushMultipleProjectsToMongo = sinon .stub() .callsArgWith(1) - this.metrics.inc = sinon.stub() - return this.ProjectDownloadsController.downloadMultipleProjects( - this.req, - this.res, - this.next + ctx.metrics.inc = sinon.stub() + return ctx.ProjectDownloadsController.downloadMultipleProjects( + ctx.req, + ctx.res, + ctx.next ) }) - it('should create a zip from the project', function () { - return this.ProjectZipStreamManager.createZipStreamForMultipleProjects - .calledWith(this.project_ids) + it('should create a zip from the project', function (ctx) { + return ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects + .calledWith(ctx.project_ids) .should.equal(true) }) - it('should stream the zip to the request', function () { - return this.stream.pipe.calledWith(this.res).should.equal(true) + it('should stream the zip to the request', function (ctx) { + return ctx.stream.pipe.calledWith(ctx.res).should.equal(true) }) - it('should set the correct content type on the request', function () { - return this.res.contentType + it('should set the correct content type on the request', function (ctx) { + return ctx.res.contentType .calledWith('application/zip') .should.equal(true) }) - it('should flush the projects to mongo', function () { - return this.DocumentUpdaterHandler.flushMultipleProjectsToMongo - .calledWith(this.project_ids) + it('should flush the projects to mongo', function (ctx) { + return ctx.DocumentUpdaterHandler.flushMultipleProjectsToMongo + .calledWith(ctx.project_ids) .should.equal(true) }) - it('should name the downloaded file after the project', function () { - this.res.headers.should.deep.equal({ + it('should name the downloaded file after the project', function (ctx) { + ctx.res.headers.should.deep.equal({ 'Content-Disposition': 'attachment; filename="Overleaf Projects (2 items).zip"', 'Content-Type': 'application/zip', @@ -143,8 +158,8 @@ describe('ProjectDownloadsController', function () { }) }) - it('should record the action via Metrics', function () { - return this.metrics.inc + it('should record the action via Metrics', function (ctx) { + return ctx.metrics.inc .calledWith('zip-downloads-multiple') .should.equal(true) }) diff --git a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs index f86b99bd96..df7486e11d 100644 --- a/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs +++ b/services/web/test/unit/src/Downloads/ProjectZipStreamManager.test.mjs @@ -1,3 +1,4 @@ +import { vi } from 'vitest' // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* @@ -9,120 +10,143 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ import sinon from 'sinon' -import esmock from 'esmock' import { EventEmitter } from 'events' const modulePath = '../../../../app/src/Features/Downloads/ProjectZipStreamManager.mjs' describe('ProjectZipStreamManager', function () { - beforeEach(async function () { - this.project_id = 'project-id-123' - this.callback = sinon.stub() - this.archive = { + beforeEach(async function (ctx) { + ctx.project_id = 'project-id-123' + ctx.callback = sinon.stub() + ctx.archive = { on() {}, append: sinon.stub(), } - this.logger = { + ctx.logger = { error: sinon.stub(), info: sinon.stub(), debug: sinon.stub(), } - return (this.ProjectZipStreamManager = await esmock.strict(modulePath, { - archiver: (this.archiver = sinon.stub().returns(this.archive)), - '@overleaf/logger': this.logger, - '../../../../app/src/Features/Project/ProjectEntityHandler': - (this.ProjectEntityHandler = {}), - '../../../../app/src/Features/History/HistoryManager.js': - (this.HistoryManager = {}), - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = {}), - '../../../../app/src/Features/FileStore/FileStoreHandler': - (this.FileStoreHandler = {}), - '../../../../app/src/infrastructure/Features': (this.Features = { + vi.doMock('archiver', () => ({ + default: (ctx.archiver = sinon.stub().returns(ctx.archive)), + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: (ctx.ProjectEntityHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/History/HistoryManager.js', () => ({ + default: (ctx.HistoryManager = {}), + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = {}), + })) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: (ctx.FileStoreHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: (ctx.Features = { hasFeature: sinon .stub() .withArgs('project-history-blobs') .returns(true), }), })) + + ctx.ProjectZipStreamManager = (await import(modulePath)).default }) describe('createZipStreamForMultipleProjects', function () { describe('successfully', function () { - beforeEach(function (done) { - this.project_ids = ['project-1', 'project-2'] - this.zip_streams = { - 'project-1': new EventEmitter(), - 'project-2': new EventEmitter(), - } - - this.project_names = { - 'project-1': 'Project One Name', - 'project-2': 'Project Two Name', - } - - this.ProjectZipStreamManager.createZipStreamForProject = ( - projectId, - callback - ) => { - callback(null, this.zip_streams[projectId]) - setTimeout(() => { - return this.zip_streams[projectId].emit('end') - }) - return 0 - } - sinon.spy(this.ProjectZipStreamManager, 'createZipStreamForProject') - - this.ProjectGetter.getProject = (projectId, fields, callback) => { - return callback(null, { name: this.project_names[projectId] }) - } - sinon.spy(this.ProjectGetter, 'getProject') - - this.ProjectZipStreamManager.createZipStreamForMultipleProjects( - this.project_ids, - (...args) => { - return this.callback(...Array.from(args || [])) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project_ids = ['project-1', 'project-2'] + ctx.zip_streams = { + 'project-1': new EventEmitter(), + 'project-2': new EventEmitter(), } - ) - return (this.archive.finalize = () => done()) + ctx.project_names = { + 'project-1': 'Project One Name', + 'project-2': 'Project Two Name', + } + + ctx.ProjectZipStreamManager.createZipStreamForProject = ( + projectId, + callback + ) => { + callback(null, ctx.zip_streams[projectId]) + setTimeout(() => { + return ctx.zip_streams[projectId].emit('end') + }) + return 0 + } + sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject') + + ctx.ProjectGetter.getProject = (projectId, fields, callback) => { + return callback(null, { name: ctx.project_names[projectId] }) + } + sinon.spy(ctx.ProjectGetter, 'getProject') + + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects( + ctx.project_ids, + (...args) => { + return ctx.callback(...Array.from(args || [])) + } + ) + + return (ctx.archive.finalize = () => resolve()) + }) }) - it('should create a zip archive', function () { - return this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + return ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - return this.callback - .calledBefore(this.ProjectZipStreamManager.createZipStreamForProject) + return ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject) .should.equal(true) }) - it('should get a zip stream for all of the projects', function () { - return Array.from(this.project_ids).map(projectId => - this.ProjectZipStreamManager.createZipStreamForProject + it('should get a zip stream for all of the projects', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith(projectId) .should.equal(true) ) }) - it('should get the names of each project', function () { - return Array.from(this.project_ids).map(projectId => - this.ProjectGetter.getProject + it('should get the names of each project', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.ProjectGetter.getProject .calledWith(projectId, { name: true }) .should.equal(true) ) }) - it('should add all of the projects to the zip', function () { - return Array.from(this.project_ids).map(projectId => - this.archive.append - .calledWith(this.zip_streams[projectId], { - name: this.project_names[projectId] + '.zip', + it('should add all of the projects to the zip', function (ctx) { + return Array.from(ctx.project_ids).map(projectId => + ctx.archive.append + .calledWith(ctx.zip_streams[projectId], { + name: ctx.project_names[projectId] + '.zip', }) .should.equal(true) ) @@ -130,75 +154,77 @@ describe('ProjectZipStreamManager', function () { }) describe('with a project not existing', function () { - beforeEach(function (done) { - this.project_ids = ['project-1', 'wrong-id'] - this.project_names = { - 'project-1': 'Project One Name', - } - this.zip_streams = { - 'project-1': new EventEmitter(), - } + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project_ids = ['project-1', 'wrong-id'] + ctx.project_names = { + 'project-1': 'Project One Name', + } + ctx.zip_streams = { + 'project-1': new EventEmitter(), + } - this.ProjectZipStreamManager.createZipStreamForProject = ( - projectId, - callback - ) => { - callback(null, this.zip_streams[projectId]) - setTimeout(() => { - this.zip_streams[projectId].emit('end') - }) - } - sinon.spy(this.ProjectZipStreamManager, 'createZipStreamForProject') + ctx.ProjectZipStreamManager.createZipStreamForProject = ( + projectId, + callback + ) => { + callback(null, ctx.zip_streams[projectId]) + setTimeout(() => { + ctx.zip_streams[projectId].emit('end') + }) + } + sinon.spy(ctx.ProjectZipStreamManager, 'createZipStreamForProject') - this.ProjectGetter.getProject = (projectId, fields, callback) => { - const name = this.project_names[projectId] - callback(null, name ? { name } : undefined) - } - sinon.spy(this.ProjectGetter, 'getProject') + ctx.ProjectGetter.getProject = (projectId, fields, callback) => { + const name = ctx.project_names[projectId] + callback(null, name ? { name } : undefined) + } + sinon.spy(ctx.ProjectGetter, 'getProject') - this.ProjectZipStreamManager.createZipStreamForMultipleProjects( - this.project_ids, - this.callback - ) + ctx.ProjectZipStreamManager.createZipStreamForMultipleProjects( + ctx.project_ids, + ctx.callback + ) - this.archive.finalize = () => done() + ctx.archive.finalize = () => resolve() + }) }) - it('should create a zip archive', function () { - this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - this.callback - .calledBefore(this.ProjectZipStreamManager.createZipStreamForProject) + ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.createZipStreamForProject) .should.equal(true) }) - it('should get the names of each project', function () { - this.project_ids.map(projectId => - this.ProjectGetter.getProject + it('should get the names of each project', function (ctx) { + ctx.project_ids.map(projectId => + ctx.ProjectGetter.getProject .calledWith(projectId, { name: true }) .should.equal(true) ) }) - it('should get a zip stream only for the existing project', function () { - this.ProjectZipStreamManager.createZipStreamForProject + it('should get a zip stream only for the existing project', function (ctx) { + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith('project-1') .should.equal(true) - this.ProjectZipStreamManager.createZipStreamForProject + ctx.ProjectZipStreamManager.createZipStreamForProject .calledWith('wrong-id') .should.equal(false) }) - it('should only add the existing project to the zip', function () { - sinon.assert.calledOnce(this.archive.append) - this.archive.append - .calledWith(this.zip_streams['project-1'], { - name: this.project_names['project-1'] + '.zip', + it('should only add the existing project to the zip', function (ctx) { + sinon.assert.calledOnce(ctx.archive.append) + ctx.archive.append + .calledWith(ctx.zip_streams['project-1'], { + name: ctx.project_names['project-1'] + '.zip', }) .should.equal(true) }) @@ -207,160 +233,162 @@ describe('ProjectZipStreamManager', function () { describe('createZipStreamForProject', function () { describe('successfully', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArg(2) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArg(2) - this.archive.finalize = sinon.stub() - return this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + return ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should create a zip archive', function () { - return this.archiver.calledWith('zip').should.equal(true) + it('should create a zip archive', function (ctx) { + return ctx.archiver.calledWith('zip').should.equal(true) }) - it('should return a stream before any processing is done', function () { - this.callback - .calledWith(sinon.match.falsy, this.archive) + it('should return a stream before any processing is done', function (ctx) { + ctx.callback + .calledWith(sinon.match.falsy, ctx.archive) .should.equal(true) - this.callback - .calledBefore(this.ProjectZipStreamManager.addAllDocsToArchive) + ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.addAllDocsToArchive) .should.equal(true) - return this.callback - .calledBefore(this.ProjectZipStreamManager.addAllFilesToArchive) + return ctx.callback + .calledBefore(ctx.ProjectZipStreamManager.addAllFilesToArchive) .should.equal(true) }) - it('should add all of the project docs to the zip', function () { - return this.ProjectZipStreamManager.addAllDocsToArchive - .calledWith(this.project_id, this.archive) + it('should add all of the project docs to the zip', function (ctx) { + return ctx.ProjectZipStreamManager.addAllDocsToArchive + .calledWith(ctx.project_id, ctx.archive) .should.equal(true) }) - it('should add all of the project files to the zip', function () { - return this.ProjectZipStreamManager.addAllFilesToArchive - .calledWith(this.project_id, this.archive) + it('should add all of the project files to the zip', function (ctx) { + return ctx.ProjectZipStreamManager.addAllFilesToArchive + .calledWith(ctx.project_id, ctx.archive) .should.equal(true) }) - it('should finalise the stream', function () { - return this.archive.finalize.called.should.equal(true) + it('should finalise the stream', function (ctx) { + return ctx.archive.finalize.called.should.equal(true) }) }) describe('with an error adding docs', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArgWith(2, new Error('something went wrong')) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArg(2) - this.archive.finalize = sinon.stub() - this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should log out an error', function () { - return this.logger.error + it('should log out an error', function (ctx) { + return ctx.logger.error .calledWith(sinon.match.any, 'error adding docs to zip stream') .should.equal(true) }) - it('should continue with the process', function () { - this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + it('should continue with the process', function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( true ) - this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( true ) - return this.archive.finalize.called.should.equal(true) + return ctx.archive.finalize.called.should.equal(true) }) }) describe('with an error adding files', function () { - beforeEach(function () { - this.ProjectZipStreamManager.addAllDocsToArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive = sinon .stub() .callsArg(2) - this.ProjectZipStreamManager.addAllFilesToArchive = sinon + ctx.ProjectZipStreamManager.addAllFilesToArchive = sinon .stub() .callsArgWith(2, new Error('something went wrong')) - this.archive.finalize = sinon.stub() - return this.ProjectZipStreamManager.createZipStreamForProject( - this.project_id, - this.callback + ctx.archive.finalize = sinon.stub() + return ctx.ProjectZipStreamManager.createZipStreamForProject( + ctx.project_id, + ctx.callback ) }) - it('should log out an error', function () { - return this.logger.error + it('should log out an error', function (ctx) { + return ctx.logger.error .calledWith(sinon.match.any, 'error adding files to zip stream') .should.equal(true) }) - it('should continue with the process', function () { - this.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( + it('should continue with the process', function (ctx) { + ctx.ProjectZipStreamManager.addAllDocsToArchive.called.should.equal( true ) - this.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( + ctx.ProjectZipStreamManager.addAllFilesToArchive.called.should.equal( true ) - return this.archive.finalize.called.should.equal(true) + return ctx.archive.finalize.called.should.equal(true) }) }) }) describe('addAllDocsToArchive', function () { - beforeEach(function (done) { - this.docs = { - '/main.tex': { - lines: [ - '\\documentclass{article}', - '\\begin{document}', - 'Hello world', - '\\end{document}', - ], - }, - '/chapters/chapter1.tex': { - lines: ['chapter1', 'content'], - }, - } - this.ProjectEntityHandler.getAllDocs = sinon - .stub() - .callsArgWith(1, null, this.docs) - return this.ProjectZipStreamManager.addAllDocsToArchive( - this.project_id, - this.archive, - error => { - this.callback(error) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.docs = { + '/main.tex': { + lines: [ + '\\documentclass{article}', + '\\begin{document}', + 'Hello world', + '\\end{document}', + ], + }, + '/chapters/chapter1.tex': { + lines: ['chapter1', 'content'], + }, } - ) + ctx.ProjectEntityHandler.getAllDocs = sinon + .stub() + .callsArgWith(1, null, ctx.docs) + return ctx.ProjectZipStreamManager.addAllDocsToArchive( + ctx.project_id, + ctx.archive, + error => { + ctx.callback(error) + return resolve() + } + ) + }) }) - it('should get the docs for the project', function () { - return this.ProjectEntityHandler.getAllDocs - .calledWith(this.project_id) + it('should get the docs for the project', function (ctx) { + return ctx.ProjectEntityHandler.getAllDocs + .calledWith(ctx.project_id) .should.equal(true) }) - it('should add each doc to the archive', function () { + it('should add each doc to the archive', function (ctx) { return (() => { const result = [] - for (let path in this.docs) { - const doc = this.docs[path] + for (let path in ctx.docs) { + const doc = ctx.docs[path] path = path.slice(1) // remove "/" result.push( - this.archive.append + ctx.archive.append .calledWith(doc.lines.join('\n'), { name: path }) .should.equal(true) ) @@ -371,8 +399,8 @@ describe('ProjectZipStreamManager', function () { }) describe('addAllFilesToArchive', function () { - beforeEach(function () { - this.files = { + beforeEach(function (ctx) { + ctx.files = { '/image.png': { _id: 'file-id-1', hash: 'abc', @@ -382,93 +410,91 @@ describe('ProjectZipStreamManager', function () { hash: 'def', }, } - this.streams = { + ctx.streams = { 'file-id-1': new EventEmitter(), 'file-id-2': new EventEmitter(), } - this.ProjectEntityHandler.getAllFiles = sinon + ctx.ProjectEntityHandler.getAllFiles = sinon .stub() - .callsArgWith(1, null, this.files) + .callsArgWith(1, null, ctx.files) }) describe('with project-history-blobs feature enabled', function () { - beforeEach(function () { - this.HistoryManager.requestBlobWithFallback = ( + beforeEach(function (ctx) { + ctx.HistoryManager.requestBlobWithFallback = ( projectId, hash, fileId, callback ) => { - return callback(null, { stream: this.streams[fileId] }) + return callback(null, { stream: ctx.streams[fileId] }) } - sinon.spy(this.HistoryManager, 'requestBlobWithFallback') - this.ProjectZipStreamManager.addAllFilesToArchive( - this.project_id, - this.archive, - this.callback + sinon.spy(ctx.HistoryManager, 'requestBlobWithFallback') + ctx.ProjectZipStreamManager.addAllFilesToArchive( + ctx.project_id, + ctx.archive, + ctx.callback ) - for (const path in this.streams) { - const stream = this.streams[path] + for (const path in ctx.streams) { + const stream = ctx.streams[path] stream.emit('end') } }) - it('should get the files for the project', function () { - return this.ProjectEntityHandler.getAllFiles - .calledWith(this.project_id) + it('should get the files for the project', function (ctx) { + return ctx.ProjectEntityHandler.getAllFiles + .calledWith(ctx.project_id) .should.equal(true) }) - it('should get a stream for each file', function () { - for (const path in this.files) { - const file = this.files[path] + it('should get a stream for each file', function (ctx) { + for (const path in ctx.files) { + const file = ctx.files[path] - this.HistoryManager.requestBlobWithFallback - .calledWith(this.project_id, file.hash, file._id) + ctx.HistoryManager.requestBlobWithFallback + .calledWith(ctx.project_id, file.hash, file._id) .should.equal(true) } }) - it('should add each file to the archive', function () { - for (let path in this.files) { - const file = this.files[path] + it('should add each file to the archive', function (ctx) { + for (let path in ctx.files) { + const file = ctx.files[path] path = path.slice(1) // remove "/" - this.archive.append - .calledWith(this.streams[file._id], { name: path }) + ctx.archive.append + .calledWith(ctx.streams[file._id], { name: path }) .should.equal(true) } }) }) describe('with project-history-blobs feature disabled', function () { - beforeEach(function () { - this.FileStoreHandler.getFileStream = ( + beforeEach(function (ctx) { + ctx.FileStoreHandler.getFileStream = ( projectId, fileId, query, callback - ) => callback(null, this.streams[fileId]) + ) => callback(null, ctx.streams[fileId]) - sinon.spy(this.FileStoreHandler, 'getFileStream') - this.Features.hasFeature - .withArgs('project-history-blobs') - .returns(false) - this.ProjectZipStreamManager.addAllFilesToArchive( - this.project_id, - this.archive, - this.callback + sinon.spy(ctx.FileStoreHandler, 'getFileStream') + ctx.Features.hasFeature.withArgs('project-history-blobs').returns(false) + ctx.ProjectZipStreamManager.addAllFilesToArchive( + ctx.project_id, + ctx.archive, + ctx.callback ) - for (const path in this.streams) { - const stream = this.streams[path] + for (const path in ctx.streams) { + const stream = ctx.streams[path] stream.emit('end') } }) - it('should get a stream for each file', function () { - for (const path in this.files) { - const file = this.files[path] + it('should get a stream for each file', function (ctx) { + for (const path in ctx.files) { + const file = ctx.files[path] - this.FileStoreHandler.getFileStream - .calledWith(this.project_id, file._id) + ctx.FileStoreHandler.getFileStream + .calledWith(ctx.project_id, file._id) .should.equal(true) } }) diff --git a/services/web/test/unit/src/Exports/ExportsController.test.mjs b/services/web/test/unit/src/Exports/ExportsController.test.mjs index 65e6e16d27..af9c1483fb 100644 --- a/services/web/test/unit/src/Exports/ExportsController.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsController.test.mjs @@ -1,11 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' const modulePath = new URL( @@ -25,9 +18,9 @@ describe('ExportsController', function () { const license = 'other' const showSource = true - beforeEach(async function () { - this.handler = { getUserNotifications: sinon.stub().callsArgWith(1) } - this.req = { + beforeEach(async function (ctx) { + ctx.handler = { getUserNotifications: sinon.stub().callsArgWith(1) } + ctx.req = { params: { project_id: projectId, brand_variation_id: brandVariationId, @@ -45,152 +38,179 @@ describe('ExportsController', function () { translate() {}, }, } - this.res = { + ctx.res = { json: sinon.stub(), status: sinon.stub(), } - this.res.status.returns(this.res) - this.next = sinon.stub() - this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.req.session.user._id), + ctx.res.status.returns(ctx.res) + ctx.next = sinon.stub() + ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.req.session.user._id), } - return (this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Exports/ExportsHandler.mjs': this.handler, - '../../../../app/src/Features/Authentication/AuthenticationController.js': - this.AuthenticationController, - })) + + vi.doMock( + '../../../../app/src/Features/Exports/ExportsHandler.mjs', + () => ({ + default: ctx.handler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController.js', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + ctx.controller = (await import(modulePath)).default }) describe('without gallery fields', function () { - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ export_v1_id: 897, message: undefined }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: undefined, + }) + return resolve() + }, + }) }) }) }) describe('with a message from v1', function () { - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon.stub().yields(null, { - iAmAnExport: true, - v1_id: 897, - message: 'RESUBMISSION', - }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ - export_v1_id: 897, - message: 'RESUBMISSION', - }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon.stub().yields(null, { + iAmAnExport: true, + v1_id: 897, + message: 'RESUBMISSION', + }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: 'RESUBMISSION', + }) + return resolve() + }, + }) }) }) }) describe('with gallery fields', function () { - beforeEach(function () { - this.req.body.title = title - this.req.body.description = description - this.req.body.author = author - this.req.body.license = license - return (this.req.body.showSource = true) + beforeEach(function (ctx) { + ctx.req.body.title = title + ctx.req.body.description = description + ctx.req.body.author = author + ctx.req.body.license = license + return (ctx.req.body.showSource = true) }) - it('should ask the handler to perform the export', function (done) { - this.handler.exportProject = sinon - .stub() - .yields(null, { iAmAnExport: true, v1_id: 897 }) - const expected = { - project_id: projectId, - user_id: userId, - brand_variation_id: brandVariationId, - first_name: firstName, - last_name: lastName, - title, - description, - author, - license, - show_source: showSource, - } - return this.controller.exportProject(this.req, { - json: body => { - expect(this.handler.exportProject.args[0][0]).to.deep.equal(expected) - expect(body).to.deep.equal({ export_v1_id: 897, message: undefined }) - return done() - }, + it('should ask the handler to perform the export', function (ctx) { + return new Promise(resolve => { + ctx.handler.exportProject = sinon + .stub() + .yields(null, { iAmAnExport: true, v1_id: 897 }) + const expected = { + project_id: projectId, + user_id: userId, + brand_variation_id: brandVariationId, + first_name: firstName, + last_name: lastName, + title, + description, + author, + license, + show_source: showSource, + } + return ctx.controller.exportProject(ctx.req, { + json: body => { + expect(ctx.handler.exportProject.args[0][0]).to.deep.equal(expected) + expect(body).to.deep.equal({ + export_v1_id: 897, + message: undefined, + }) + return resolve() + }, + }) }) }) }) describe('with an error return from v1 to forward to the publish modal', function () { - it('should forward the response onward', function (done) { - this.error_json = { status: 422, message: 'nope' } - this.handler.exportProject = sinon - .stub() - .yields({ forwardResponse: this.error_json }) - this.controller.exportProject(this.req, this.res, this.next) - expect(this.res.json.args[0][0]).to.deep.equal(this.error_json) - expect(this.res.status.args[0][0]).to.equal(this.error_json.status) - return done() + it('should forward the response onward', function (ctx) { + return new Promise(resolve => { + ctx.error_json = { status: 422, message: 'nope' } + ctx.handler.exportProject = sinon + .stub() + .yields({ forwardResponse: ctx.error_json }) + ctx.controller.exportProject(ctx.req, ctx.res, ctx.next) + expect(ctx.res.json.args[0][0]).to.deep.equal(ctx.error_json) + expect(ctx.res.status.args[0][0]).to.equal(ctx.error_json.status) + return resolve() + }) }) }) - it('should ask the handler to return the status of an export', function (done) { - this.handler.fetchExport = sinon.stub().yields( - null, - `{ -"id":897, -"status_summary":"completed", -"status_detail":"all done", -"partner_submission_id":"abc123", -"v2_user_email":"la@tex.com", -"v2_user_first_name":"Arthur", -"v2_user_last_name":"Author", -"title":"my project", -"token":"token" -}` - ) + it('should ask the handler to return the status of an export', function (ctx) { + return new Promise(resolve => { + ctx.handler.fetchExport = sinon.stub().yields( + null, + `{ + "id":897, + "status_summary":"completed", + "status_detail":"all done", + "partner_submission_id":"abc123", + "v2_user_email":"la@tex.com", + "v2_user_first_name":"Arthur", + "v2_user_last_name":"Author", + "title":"my project", + "token":"token" + }` + ) - this.req.params = { project_id: projectId, export_id: 897 } - return this.controller.exportStatus(this.req, { - json: body => { - expect(body).to.deep.equal({ - export_json: { - status_summary: 'completed', - status_detail: 'all done', - partner_submission_id: 'abc123', - v2_user_email: 'la@tex.com', - v2_user_first_name: 'Arthur', - v2_user_last_name: 'Author', - title: 'my project', - token: 'token', - }, - }) - return done() - }, + ctx.req.params = { project_id: projectId, export_id: 897 } + return ctx.controller.exportStatus(ctx.req, { + json: body => { + expect(body).to.deep.equal({ + export_json: { + status_summary: 'completed', + status_detail: 'all done', + partner_submission_id: 'abc123', + v2_user_email: 'la@tex.com', + v2_user_first_name: 'Arthur', + v2_user_last_name: 'Author', + title: 'my project', + token: 'token', + }, + }) + return resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs index 1a7f985250..0eb8a98e26 100644 --- a/services/web/test/unit/src/Exports/ExportsHandler.test.mjs +++ b/services/web/test/unit/src/Exports/ExportsHandler.test.mjs @@ -1,697 +1,736 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ +import { vi } from 'vitest' import sinon from 'sinon' -import esmock from 'esmock' import { expect } from 'chai' const modulePath = '../../../../app/src/Features/Exports/ExportsHandler.mjs' describe('ExportsHandler', function () { - beforeEach(async function () { - this.stubRequest = {} - this.request = { + beforeEach(async function (ctx) { + ctx.stubRequest = {} + ctx.request = { defaults: () => { - return this.stubRequest + return ctx.stubRequest }, } - this.ExportsHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = {}), - '../../../../app/src/Features/Project/ProjectHistoryHandler': - (this.ProjectHistoryHandler = {}), - '../../../../app/src/Features/Project/ProjectLocator': - (this.ProjectLocator = {}), - '../../../../app/src/Features/Project/ProjectRootDocManager': - (this.ProjectRootDocManager = {}), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = {}), - '@overleaf/settings': (this.settings = {}), - request: this.request, - }) - this.project_id = 'project-id-123' - this.project_history_id = 987 - this.user_id = 'user-id-456' - this.brand_variation_id = 789 - this.title = 'title' - this.description = 'description' - this.author = 'author' - this.license = 'other' - this.show_source = true - this.export_params = { - project_id: this.project_id, - brand_variation_id: this.brand_variation_id, - user_id: this.user_id, - title: this.title, - description: this.description, - author: this.author, - license: this.license, - show_source: this.show_source, + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectHistoryHandler', + () => ({ + default: (ctx.ProjectHistoryHandler = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: (ctx.ProjectLocator = {}), + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ + default: (ctx.ProjectRootDocManager = {}), + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = {}), + })) + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = {}), + })) + + vi.doMock('request', () => ({ + default: ctx.request, + })) + + ctx.ExportsHandler = (await import(modulePath)).default + ctx.project_id = 'project-id-123' + ctx.project_history_id = 987 + ctx.user_id = 'user-id-456' + ctx.brand_variation_id = 789 + ctx.title = 'title' + ctx.description = 'description' + ctx.author = 'author' + ctx.license = 'other' + ctx.show_source = true + ctx.export_params = { + project_id: ctx.project_id, + brand_variation_id: ctx.brand_variation_id, + user_id: ctx.user_id, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + show_source: ctx.show_source, } - return (this.callback = sinon.stub()) + ctx.callback = sinon.stub() }) describe('exportProject', function () { - beforeEach(function () { - this.export_data = { iAmAnExport: true } - this.response_body = { iAmAResponseBody: true } - this.ExportsHandler._buildExport = sinon + beforeEach(function (ctx) { + ctx.export_data = { iAmAnExport: true } + ctx.response_body = { iAmAResponseBody: true } + ctx.ExportsHandler._buildExport = sinon .stub() - .yields(null, this.export_data) - return (this.ExportsHandler._requestExport = sinon + .yields(null, ctx.export_data) + ctx.ExportsHandler._requestExport = sinon .stub() - .yields(null, this.response_body)) + .yields(null, ctx.response_body) }) describe('when all goes well', function () { - beforeEach(function (done) { - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should build the export', function () { - return this.ExportsHandler._buildExport - .calledWith(this.export_params) + it('should build the export', function (ctx) { + ctx.ExportsHandler._buildExport + .calledWith(ctx.export_params) .should.equal(true) }) - it('should request the export', function () { - return this.ExportsHandler._requestExport - .calledWith(this.export_data) + it('should request the export', function (ctx) { + ctx.ExportsHandler._requestExport + .calledWith(ctx.export_data) .should.equal(true) }) - it('should return the export', function () { - return this.callback - .calledWith(null, this.export_data) - .should.equal(true) + it('should return the export', function (ctx) { + ctx.callback.calledWith(null, ctx.export_data).should.equal(true) }) }) describe("when request can't be built", function () { - beforeEach(function (done) { - this.ExportsHandler._buildExport = sinon - .stub() - .yields(new Error('cannot export project without root doc')) - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._buildExport = sinon + .stub() + .yields(new Error('cannot export project without root doc')) + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when export request returns an error to forward to the user', function () { - beforeEach(function (done) { - this.error_json = { status: 422, message: 'nope' } - this.ExportsHandler._requestExport = sinon - .stub() - .yields(null, { forwardResponse: this.error_json }) - return this.ExportsHandler.exportProject( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.error_json = { status: 422, message: 'nope' } + ctx.ExportsHandler._requestExport = sinon + .stub() + .yields(null, { forwardResponse: ctx.error_json }) + ctx.ExportsHandler.exportProject( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return success and the response to forward', function () { - ;(this.callback.args[0][0] instanceof Error).should.equal(false) - return this.callback.calledWith(null, { - forwardResponse: this.error_json, + it('should return success and the response to forward', function (ctx) { + expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) + ctx.callback.calledWith(null, { + forwardResponse: ctx.error_json, }) }) }) }) describe('_buildExport', function () { - beforeEach(function (done) { - this.project = { - id: this.project_id, - rootDoc_id: 'doc1_id', - compiler: 'pdflatex', - imageName: 'mock-image-name', - overleaf: { - id: this.project_history_id, // for projects imported from v1 - history: { - id: this.project_history_id, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project = { + id: ctx.project_id, + rootDoc_id: 'doc1_id', + compiler: 'pdflatex', + imageName: 'mock-image-name', + overleaf: { + id: ctx.project_history_id, // for projects imported from v1 + history: { + id: ctx.project_history_id, + }, }, - }, - } - this.user = { - id: this.user_id, - first_name: 'Arthur', - last_name: 'Author', - email: 'arthur.author@arthurauthoring.org', - overleaf: { - id: 876, - }, - } - this.rootDocPath = 'main.tex' - this.historyVersion = 777 - this.ProjectGetter.getProject = sinon.stub().yields(null, this.project) - this.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon - .stub() - .yields(null) - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'main.tex' }]) - this.ProjectRootDocManager.ensureRootDocumentIsValid = sinon - .stub() - .callsArgWith(1, null) - this.UserGetter.getUser = sinon.stub().yields(null, this.user) - this.ExportsHandler._requestVersion = sinon - .stub() - .yields(null, this.historyVersion) - return done() + } + ctx.user = { + id: ctx.user_id, + first_name: 'Arthur', + last_name: 'Author', + email: 'arthur.author@arthurauthoring.org', + overleaf: { + id: 876, + }, + } + ctx.rootDocPath = 'main.tex' + ctx.historyVersion = 777 + ctx.ProjectGetter.getProject = sinon.stub().yields(null, ctx.project) + ctx.ProjectHistoryHandler.ensureHistoryExistsForProject = sinon + .stub() + .yields(null) + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'main.tex' }]) + ctx.ProjectRootDocManager.ensureRootDocumentIsValid = sinon + .stub() + .callsArgWith(1, null) + ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user) + ctx.ExportsHandler._requestVersion = sinon + .stub() + .yields(null, ctx.historyVersion) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should ensure the project has history', function () { - return this.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( + it('should ensure the project has history', function (ctx) { + ctx.ProjectHistoryHandler.ensureHistoryExistsForProject.called.should.equal( true ) }) - it('should request the project history version', function () { - return this.ExportsHandler._requestVersion.called.should.equal(true) + it('should request the project history version', function (ctx) { + ctx.ExportsHandler._requestVersion.called.should.equal(true) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, - rootDocPath: this.rootDocPath, - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + id: ctx.project_id, + rootDocPath: ctx.rootDocPath, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when we send replacement user first and last name', function () { - beforeEach(function (done) { - this.custom_first_name = 'FIRST' - this.custom_last_name = 'LAST' - this.export_params.first_name = this.custom_first_name - this.export_params.last_name = this.custom_last_name - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.custom_first_name = 'FIRST' + ctx.custom_last_name = 'LAST' + ctx.export_params.first_name = ctx.custom_first_name + ctx.export_params.last_name = ctx.custom_last_name + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should send the data from the user input', function () { + it('should send the data from the user input', function (ctx) { const expectedExportData = { project: { - id: this.project_id, - rootDocPath: this.rootDocPath, - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + id: ctx.project_id, + rootDocPath: ctx.rootDocPath, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.custom_first_name, - lastName: this.custom_last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.custom_first_name, + lastName: ctx.custom_last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when project is not found', function () { - beforeEach(function (done) { - this.ProjectGetter.getProject = sinon - .stub() - .yields(new Error('project not found')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectGetter.getProject = sinon + .stub() + .yields(new Error('project not found')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when project has no root doc', function () { describe('when a root doc can be set automatically', function () { - beforeEach(function (done) { - this.project.rootDoc_id = null - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.rootDoc_id = null + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should set a root doc', function () { - return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + it('should set a root doc', function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( true ) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, + id: ctx.project_id, rootDocPath: 'other.tex', - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) }) describe('when project has an invalid root doc', function () { describe('when a new root doc can be set automatically', function () { - beforeEach(function (done) { - this.fakeDoc_id = '1a2b3c4d5e6f' - this.project.rootDoc_id = this.fakeDoc_id - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, { fileSystem: 'other.tex' }]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.fakeDoc_id = '1a2b3c4d5e6f' + ctx.project.rootDoc_id = ctx.fakeDoc_id + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, { fileSystem: 'other.tex' }]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should set a valid root doc', function () { - return this.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( + it('should set a valid root doc', function (ctx) { + ctx.ProjectRootDocManager.ensureRootDocumentIsValid.called.should.equal( true ) }) - it('should return export data', function () { + it('should return export data', function (ctx) { const expectedExportData = { project: { - id: this.project_id, + id: ctx.project_id, rootDocPath: 'other.tex', - historyId: this.project_history_id, - historyVersion: this.historyVersion, - v1ProjectId: this.project_history_id, + historyId: ctx.project_history_id, + historyVersion: ctx.historyVersion, + v1ProjectId: ctx.project_history_id, metadata: { compiler: 'pdflatex', imageName: 'mock-image-name', - title: this.title, - description: this.description, - author: this.author, - license: this.license, - showSource: this.show_source, + title: ctx.title, + description: ctx.description, + author: ctx.author, + license: ctx.license, + showSource: ctx.show_source, }, }, user: { - id: this.user_id, - firstName: this.user.first_name, - lastName: this.user.last_name, - email: this.user.email, + id: ctx.user_id, + firstName: ctx.user.first_name, + lastName: ctx.user.last_name, + email: ctx.user.email, orcidId: null, v1UserId: 876, }, destination: { - brandVariationId: this.brand_variation_id, + brandVariationId: ctx.brand_variation_id, }, options: { callbackUrl: null, }, } - return this.callback - .calledWith(null, expectedExportData) - .should.equal(true) + ctx.callback.calledWith(null, expectedExportData).should.equal(true) }) }) describe('when no root doc can be identified', function () { - beforeEach(function (done) { - this.ProjectLocator.findRootDoc = sinon - .stub() - .yields(null, [null, null]) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ProjectLocator.findRootDoc = sinon + .stub() + .yields(null, [null, null]) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) }) describe('when user is not found', function () { - beforeEach(function (done) { - this.UserGetter.getUser = sinon - .stub() - .yields(new Error('user not found')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.getUser = sinon + .stub() + .yields(new Error('user not found')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when project history request fails', function () { - beforeEach(function (done) { - this.ExportsHandler._requestVersion = sinon - .stub() - .yields(new Error('project history call failed')) - return this.ExportsHandler._buildExport( - this.export_params, - (error, exportData) => { - this.callback(error, exportData) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.ExportsHandler._requestVersion = sinon + .stub() + .yields(new Error('project history call failed')) + ctx.ExportsHandler._buildExport( + ctx.export_params, + (error, exportData) => { + ctx.callback(error, exportData) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) }) describe('_requestExport', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_data = { iAmAnExport: true } - this.export_id = 4096 - this.stubPost = sinon - .stub() - .yields(null, { statusCode: 200 }, { exportId: this.export_id }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_data = { iAmAnExport: true } + ctx.export_id = 4096 + ctx.stubPost = sinon + .stub() + .yields(null, { statusCode: 200 }, { exportId: ctx.export_id }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.post = this.stubPost - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.post = ctx.stubPost + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should issue the request', function () { - return expect(this.stubPost.getCall(0).args[0]).to.deep.equal({ - url: this.settings.apis.v1.url + '/api/v1/overleaf/exports', + it('should issue the request', function (ctx) { + expect(ctx.stubPost.getCall(0).args[0]).to.deep.equal({ + url: ctx.settings.apis.v1.url + '/api/v1/overleaf/exports', auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, - json: this.export_data, + json: ctx.export_data, timeout: 15000, }) }) - it('should return the body with v1 export id', function () { - return this.callback - .calledWith(null, { exportId: this.export_id }) + it('should return the body with v1 export id', function (ctx) { + ctx.callback + .calledWith(null, { exportId: ctx.export_id }) .should.equal(true) }) }) describe('when the request fails', function () { - beforeEach(function (done) { - this.stubRequest.post = sinon - .stub() - .yields(new Error('export request failed')) - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.post = sinon + .stub() + .yields(new Error('export request failed')) + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should return an error', function () { - return (this.callback.args[0][0] instanceof Error).should.equal(true) + it('should return an error', function (ctx) { + expect(ctx.callback.args[0][0]).to.be.instanceOf(Error) }) }) describe('when the request returns an error response to forward', function () { - beforeEach(function (done) { - this.error_code = 422 - this.error_json = { status: this.error_code, message: 'nope' } - this.stubRequest.post = sinon + beforeEach(function (ctx) { + ctx.error_code = 422 + ctx.error_json = { status: ctx.error_code, message: 'nope' } + ctx.stubRequest.post = sinon .stub() - .yields(null, { statusCode: this.error_code }, this.error_json) - return this.ExportsHandler._requestExport( - this.export_data, - (error, exportV1Id) => { - this.callback(error, exportV1Id) - return done() - } - ) + .yields(null, { statusCode: ctx.error_code }, ctx.error_json) + return new Promise(resolve => { + ctx.ExportsHandler._requestExport( + ctx.export_data, + (error, exportV1Id) => { + ctx.callback(error, exportV1Id) + resolve() + } + ) + }) }) - it('should return success and the response to forward', function () { - ;(this.callback.args[0][0] instanceof Error).should.equal(false) - return this.callback.calledWith(null, { - forwardResponse: this.error_json, + it('should return success and the response to forward', function (ctx) { + expect(ctx.callback.args[0][0]).not.to.be.instanceOf(Error) + ctx.callback.calledWith(null, { + forwardResponse: ctx.error_json, }) }) }) }) describe('fetchExport', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_id = 897 - this.body = '{"id":897, "status_summary":"completed"}' - this.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: this.body }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = '{"id":897, "status_summary":"completed"}' + ctx.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: ctx.body }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.get = this.stubGet - return this.ExportsHandler.fetchExport( - this.export_id, - (error, body) => { - this.callback(error, body) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.get = ctx.stubGet + ctx.ExportsHandler.fetchExport(ctx.export_id, (error, body) => { + ctx.callback(error, body) + resolve() + }) + }) }) - it('should issue the request', function () { - return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + it('should issue the request', function (ctx) { + expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ url: - this.settings.apis.v1.url + + ctx.settings.apis.v1.url + '/api/v1/overleaf/exports/' + - this.export_id, + ctx.export_id, auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, timeout: 15000, }) }) - it('should return the v1 export id', function () { - return this.callback - .calledWith(null, { body: this.body }) - .should.equal(true) + it('should return the v1 export id', function (ctx) { + ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) }) }) }) describe('fetchDownload', function () { - beforeEach(function (done) { - this.settings.apis = { - v1: { - url: 'http://127.0.0.1:5000', - user: 'overleaf', - pass: 'pass', - timeout: 15000, - }, - } - this.export_id = 897 - this.body = - 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' - this.stubGet = sinon - .stub() - .yields(null, { statusCode: 200 }, { body: this.body }) - return done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.settings.apis = { + v1: { + url: 'http://127.0.0.1:5000', + user: 'overleaf', + pass: 'pass', + timeout: 15000, + }, + } + ctx.export_id = 897 + ctx.body = + 'https://writelatex-conversions-dev.s3.amazonaws.com/exports/ieee_latexqc/tnb/2912/xggmprcrpfwbsnqzqqmvktddnrbqkqkr.zip?X-Amz-Expires=14400&X-Amz-Date=20180730T181003Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJDGDIJFGLNVGZH6A/20180730/us-east-1/s3/aws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=dec990336913cef9933f0e269afe99722d7ab2830ebf2c618a75673ee7159fee' + ctx.stubGet = sinon + .stub() + .yields(null, { statusCode: 200 }, { body: ctx.body }) + resolve() + }) }) describe('when all goes well', function () { - beforeEach(function (done) { - this.stubRequest.get = this.stubGet - return this.ExportsHandler.fetchDownload( - this.export_id, - 'zip', - (error, body) => { - this.callback(error, body) - return done() - } - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.stubRequest.get = ctx.stubGet + ctx.ExportsHandler.fetchDownload( + ctx.export_id, + 'zip', + (error, body) => { + ctx.callback(error, body) + resolve() + } + ) + }) }) - it('should issue the request', function () { - return expect(this.stubGet.getCall(0).args[0]).to.deep.equal({ + it('should issue the request', function (ctx) { + expect(ctx.stubGet.getCall(0).args[0]).to.deep.equal({ url: - this.settings.apis.v1.url + + ctx.settings.apis.v1.url + '/api/v1/overleaf/exports/' + - this.export_id + + ctx.export_id + '/zip_url', auth: { - user: this.settings.apis.v1.user, - pass: this.settings.apis.v1.pass, + user: ctx.settings.apis.v1.user, + pass: ctx.settings.apis.v1.pass, }, timeout: 15000, }) }) - it('should return the v1 export id', function () { - return this.callback - .calledWith(null, { body: this.body }) - .should.equal(true) + it('should return the v1 export id', function (ctx) { + ctx.callback.calledWith(null, { body: ctx.body }).should.equal(true) }) }) }) diff --git a/services/web/test/unit/src/FileStore/FileStoreController.test.mjs b/services/web/test/unit/src/FileStore/FileStoreController.test.mjs index 2758068ce3..5c46e516a0 100644 --- a/services/web/test/unit/src/FileStore/FileStoreController.test.mjs +++ b/services/web/test/unit/src/FileStore/FileStoreController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockResponse from '../helpers/MockResponse.js' @@ -12,34 +12,51 @@ const expectedFileHeaders = { 'X-Served-By': 'filestore', } +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('FileStoreController', function () { - beforeEach(async function () { - this.FileStoreHandler = { + beforeEach(async function (ctx) { + ctx.FileStoreHandler = { promises: { getFileStream: sinon.stub(), getFileSize: sinon.stub(), }, } - this.ProjectLocator = { promises: { findElement: sinon.stub() } } - this.Stream = { pipeline: sinon.stub().resolves() } - this.HistoryManager = {} - this.controller = await esmock.strict(MODULE_PATH, { - 'node:stream/promises': this.Stream, - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/FileStore/FileStoreHandler': - this.FileStoreHandler, - '../../../../app/src/Features/History/HistoryManager': - this.HistoryManager, - }) - this.stream = {} - this.projectId = '2k3j1lk3j21lk3j' - this.fileId = '12321kklj1lk3jk12' - this.req = { + ctx.ProjectLocator = { promises: { findElement: sinon.stub() } } + ctx.Stream = { pipeline: sinon.stub().resolves() } + ctx.HistoryManager = {} + + vi.doMock('node:stream/promises', () => ctx.Stream) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock( + '../../../../app/src/Features/FileStore/FileStoreHandler', + () => ({ + default: ctx.FileStoreHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/History/HistoryManager', () => ({ + default: ctx.HistoryManager, + })) + + ctx.controller = (await import(MODULE_PATH)).default + ctx.stream = {} + ctx.projectId = '2k3j1lk3j21lk3j' + ctx.fileId = '12321kklj1lk3jk12' + ctx.req = { params: { - Project_id: this.projectId, - File_id: this.fileId, + Project_id: ctx.projectId, + File_id: ctx.fileId, }, query: 'query string here', get(key) { @@ -49,61 +66,61 @@ describe('FileStoreController', function () { addFields: sinon.stub(), }, } - this.res = new MockResponse() - this.next = sinon.stub() - this.file = { name: 'myfile.png' } + ctx.res = new MockResponse() + ctx.next = sinon.stub() + ctx.file = { name: 'myfile.png' } }) describe('getFile', function () { - beforeEach(function () { - this.FileStoreHandler.promises.getFileStream.resolves(this.stream) - this.ProjectLocator.promises.findElement.resolves({ element: this.file }) + beforeEach(function (ctx) { + ctx.FileStoreHandler.promises.getFileStream.resolves(ctx.stream) + ctx.ProjectLocator.promises.findElement.resolves({ element: ctx.file }) }) - it('should call the file store handler with the project_id file_id and any query string', async function () { - await this.controller.getFile(this.req, this.res) - this.FileStoreHandler.promises.getFileStream.should.have.been.calledWith( - this.req.params.Project_id, - this.req.params.File_id, - this.req.query + it('should call the file store handler with the project_id file_id and any query string', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.FileStoreHandler.promises.getFileStream.should.have.been.calledWith( + ctx.req.params.Project_id, + ctx.req.params.File_id, + ctx.req.query ) }) - it('should pipe to res', async function () { - await this.controller.getFile(this.req, this.res) - this.Stream.pipeline.should.have.been.calledWith(this.stream, this.res) + it('should pipe to res', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.Stream.pipeline.should.have.been.calledWith(ctx.stream, ctx.res) }) - it('should get the file from the db', async function () { - await this.controller.getFile(this.req, this.res) - this.ProjectLocator.promises.findElement.should.have.been.calledWith({ - project_id: this.projectId, - element_id: this.fileId, + it('should get the file from the db', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.ProjectLocator.promises.findElement.should.have.been.calledWith({ + project_id: ctx.projectId, + element_id: ctx.fileId, type: 'file', }) }) - it('should set the Content-Disposition header', async function () { - await this.controller.getFile(this.req, this.res) - this.res.setContentDisposition.should.be.calledWith('attachment', { - filename: this.file.name, + it('should set the Content-Disposition header', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.setContentDisposition.should.be.calledWith('attachment', { + filename: ctx.file.name, }) }) - it('should return a 404 when not found', async function () { - this.ProjectLocator.promises.findElement.rejects( + it('should return a 404 when not found', async function (ctx) { + ctx.ProjectLocator.promises.findElement.rejects( new Errors.NotFoundError() ) - await this.controller.getFile(this.req, this.res) - expect(this.res.statusCode).to.equal(404) + await ctx.controller.getFile(ctx.req, ctx.res) + expect(ctx.res.statusCode).to.equal(404) }) // Test behaviour around handling html files ;['.html', '.htm', '.xhtml'].forEach(extension => { describe(`with a '${extension}' file extension`, function () { - beforeEach(function () { - this.file.name = `bad${extension}` - this.req.get = key => { + beforeEach(function (ctx) { + ctx.file.name = `bad${extension}` + ctx.req.get = key => { if (key === 'User-Agent') { return 'A generic browser' } @@ -111,26 +128,26 @@ describe('FileStoreController', function () { }) describe('from a non-ios browser', function () { - it('should not set Content-Type', async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it('should not set Content-Type', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, }) }) }) describe('from an iPhone', function () { - beforeEach(function () { - this.req.get = key => { + beforeEach(function (ctx) { + ctx.req.get = key => { if (key === 'User-Agent') { return 'An iPhone browser' } } }) - it("should set Content-Type to 'text/plain'", async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it("should set Content-Type to 'text/plain'", async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', @@ -139,17 +156,17 @@ describe('FileStoreController', function () { }) describe('from an iPad', function () { - beforeEach(function () { - this.req.get = key => { + beforeEach(function (ctx) { + ctx.req.get = key => { if (key === 'User-Agent') { return 'An iPad browser' } } }) - it("should set Content-Type to 'text/plain'", async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it("should set Content-Type to 'text/plain'", async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, 'Content-Type': 'text/plain; charset=utf-8', 'X-Content-Type-Options': 'nosniff', @@ -166,24 +183,24 @@ describe('FileStoreController', function () { 'somefile', ].forEach(filename => { describe(`with filename as '${filename}'`, function () { - beforeEach(function () { - this.user_agent = 'A generic browser' - this.file.name = filename - this.req.get = key => { + beforeEach(function (ctx) { + ctx.user_agent = 'A generic browser' + ctx.file.name = filename + ctx.req.get = key => { if (key === 'User-Agent') { - return this.user_agent + return ctx.user_agent } } }) ;['iPhone', 'iPad', 'Firefox', 'Chrome'].forEach(browser => { describe(`downloaded from ${browser}`, function () { - beforeEach(function () { - this.user_agent = `Some ${browser} thing` + beforeEach(function (ctx) { + ctx.user_agent = `Some ${browser} thing` }) - it('Should not set the Content-type', async function () { - await this.controller.getFile(this.req, this.res) - this.res.headers.should.deep.equal({ + it('Should not set the Content-type', async function (ctx) { + await ctx.controller.getFile(ctx.req, ctx.res) + ctx.res.headers.should.deep.equal({ ...expectedFileHeaders, }) }) @@ -194,42 +211,46 @@ describe('FileStoreController', function () { }) describe('getFileHead', function () { - beforeEach(function () { - this.ProjectLocator.promises.findElement.resolves({ element: this.file }) + beforeEach(function (ctx) { + ctx.ProjectLocator.promises.findElement.resolves({ element: ctx.file }) }) - it('reports the file size', function (done) { - const expectedFileSize = 99393 - this.FileStoreHandler.promises.getFileSize.rejects( - new Error('getFileSize: unexpected arguments') - ) - this.FileStoreHandler.promises.getFileSize - .withArgs(this.projectId, this.fileId) - .resolves(expectedFileSize) + it('reports the file size', function (ctx) { + return new Promise(resolve => { + const expectedFileSize = 99393 + ctx.FileStoreHandler.promises.getFileSize.rejects( + new Error('getFileSize: unexpected arguments') + ) + ctx.FileStoreHandler.promises.getFileSize + .withArgs(ctx.projectId, ctx.fileId) + .resolves(expectedFileSize) - this.res.end = () => { - expect(this.res.status.lastCall.args).to.deep.equal([200]) - expect(this.res.header.lastCall.args).to.deep.equal([ - 'Content-Length', - expectedFileSize, - ]) - done() - } + ctx.res.end = () => { + expect(ctx.res.status.lastCall.args).to.deep.equal([200]) + expect(ctx.res.header.lastCall.args).to.deep.equal([ + 'Content-Length', + expectedFileSize, + ]) + resolve() + } - this.controller.getFileHead(this.req, this.res) + ctx.controller.getFileHead(ctx.req, ctx.res) + }) }) - it('returns 404 on NotFoundError', function (done) { - this.FileStoreHandler.promises.getFileSize.rejects( - new Errors.NotFoundError() - ) + it('returns 404 on NotFoundError', function (ctx) { + return new Promise(resolve => { + ctx.FileStoreHandler.promises.getFileSize.rejects( + new Errors.NotFoundError() + ) - this.res.end = () => { - expect(this.res.status.lastCall.args).to.deep.equal([404]) - done() - } + ctx.res.end = () => { + expect(ctx.res.status.lastCall.args).to.deep.equal([404]) + resolve() + } - this.controller.getFileHead(this.req, this.res) + ctx.controller.getFileHead(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs b/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs index f1b7b58c10..b29d10bba4 100644 --- a/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs +++ b/services/web/test/unit/src/LinkedFiles/LinkedFilesController.test.mjs @@ -1,155 +1,205 @@ +import { vi } from 'vitest' import { expect } from 'chai' -import esmock from 'esmock' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/LinkedFiles/LinkedFilesController.mjs' describe('LinkedFilesController', function () { - beforeEach(function () { - this.fakeTime = new Date() - this.clock = sinon.useFakeTimers(this.fakeTime.getTime()) + beforeEach(function (ctx) { + ctx.fakeTime = new Date() + ctx.clock = sinon.useFakeTimers(ctx.fakeTime.getTime()) }) - afterEach(function () { - this.clock.restore() + afterEach(function (ctx) { + ctx.clock.restore() }) - beforeEach(async function () { - this.userId = 'user-id' - this.Agent = { + beforeEach(async function (ctx) { + ctx.userId = 'user-id' + ctx.Agent = { promises: { createLinkedFile: sinon.stub().resolves(), refreshLinkedFile: sinon.stub().resolves(), }, } - this.projectId = 'projectId' - this.provider = 'provider' - this.name = 'linked-file-name' - this.data = { customAgentData: 'foo' } - this.LinkedFilesHandler = { + ctx.projectId = 'projectId' + ctx.provider = 'provider' + ctx.fileName = 'linked-file-name' + ctx.data = { customAgentData: 'foo' } + ctx.LinkedFilesHandler = { promises: { getFileById: sinon.stub(), }, } - this.AnalyticsManager = {} - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.userId), + ctx.AnalyticsManager = {} + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.userId), } - this.EditorRealTimeController = {} - this.ReferencesHandler = {} - this.UrlAgent = {} - this.ProjectFileAgent = {} - this.ProjectOutputFileAgent = {} - this.EditorController = {} - this.ProjectLocator = {} - this.logger = { + ctx.EditorRealTimeController = {} + ctx.ReferencesHandler = {} + ctx.UrlAgent = {} + ctx.ProjectFileAgent = {} + ctx.ProjectOutputFileAgent = {} + ctx.EditorController = {} + ctx.ProjectLocator = {} + ctx.logger = { error: sinon.stub(), } - this.settings = { enabledLinkedFileTypes: [] } - this.LinkedFilesController = await esmock.strict(modulePath, { - '.../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Analytics/AnalyticsManager': - this.AnalyticsManager, - '../../../../app/src/Features/LinkedFiles/LinkedFilesHandler': - this.LinkedFilesHandler, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/References/ReferencesHandler': - this.ReferencesHandler, - '../../../../app/src/Features/LinkedFiles/UrlAgent': this.UrlAgent, - '../../../../app/src/Features/LinkedFiles/ProjectFileAgent': - this.ProjectFileAgent, - '../../../../app/src/Features/LinkedFiles/ProjectOutputFileAgent': - this.ProjectOutputFileAgent, - '../../../../app/src/Features/Editor/EditorController': - this.EditorController, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '@overleaf/logger': this.logger, - '@overleaf/settings': this.settings, - }) - this.LinkedFilesController._getAgent = sinon.stub().resolves(this.Agent) + ctx.settings = { enabledLinkedFileTypes: [] } + + vi.doMock( + '.../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/LinkedFilesHandler', + () => ({ + default: ctx.LinkedFilesHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/References/ReferencesHandler', + () => ({ + default: ctx.ReferencesHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/LinkedFiles/UrlAgent', () => ({ + default: ctx.UrlAgent, + })) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/ProjectFileAgent', + () => ({ + default: ctx.ProjectFileAgent, + }) + ) + + vi.doMock( + '../../../../app/src/Features/LinkedFiles/ProjectOutputFileAgent', + () => ({ + default: ctx.ProjectOutputFileAgent, + }) + ) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('@overleaf/logger', () => ({ + default: ctx.logger, + })) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + ctx.LinkedFilesController = (await import(modulePath)).default + ctx.LinkedFilesController._getAgent = sinon.stub().resolves(ctx.Agent) }) describe('createLinkedFile', function () { - beforeEach(function () { - this.req = { - params: { project_id: this.projectId }, + beforeEach(function (ctx) { + ctx.req = { + params: { project_id: ctx.projectId }, body: { - name: this.name, - provider: this.provider, - data: this.data, + name: ctx.fileName, + provider: ctx.provider, + data: ctx.data, }, } - this.next = sinon.stub() + ctx.next = sinon.stub() }) - it('sets importedAt timestamp on linkedFileData', function (done) { - this.next = sinon.stub().callsFake(() => done('unexpected error')) - this.res = { - json: () => { - expect(this.Agent.promises.createLinkedFile).to.have.been.calledWith( - this.projectId, - { ...this.data, importedAt: this.fakeTime.toISOString() }, - this.name, - undefined, - this.userId - ) - done() - }, - } - this.LinkedFilesController.createLinkedFile(this.req, this.res, this.next) + it('sets importedAt timestamp on linkedFileData', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(() => resolve('unexpected error')) + ctx.res = { + json: () => { + expect(ctx.Agent.promises.createLinkedFile).to.have.been.calledWith( + ctx.projectId, + { ...ctx.data, importedAt: ctx.fakeTime.toISOString() }, + ctx.fileName, + undefined, + ctx.userId + ) + resolve() + }, + } + ctx.LinkedFilesController.createLinkedFile(ctx.req, ctx.res, ctx.next) + }) }) }) describe('refreshLinkedFiles', function () { - beforeEach(function () { - this.data.provider = this.provider - this.file = { - name: this.name, + beforeEach(function (ctx) { + ctx.data.provider = ctx.provider + ctx.file = { + name: ctx.fileName, linkedFileData: { - ...this.data, + ...ctx.data, importedAt: new Date(2020, 1, 1).toISOString(), }, } - this.LinkedFilesHandler.promises.getFileById - .withArgs(this.projectId, 'file-id') + ctx.LinkedFilesHandler.promises.getFileById + .withArgs(ctx.projectId, 'file-id') .resolves({ - file: this.file, + file: ctx.file, path: 'fake-path', parentFolder: { _id: 'parent-folder-id', }, }) - this.req = { - params: { project_id: this.projectId, file_id: 'file-id' }, + ctx.req = { + params: { project_id: ctx.projectId, file_id: 'file-id' }, body: {}, } - this.next = sinon.stub() + ctx.next = sinon.stub() }) - it('resets importedAt timestamp on linkedFileData', function (done) { - this.next = sinon.stub().callsFake(() => done('unexpected error')) - this.res = { - json: () => { - expect(this.Agent.promises.refreshLinkedFile).to.have.been.calledWith( - this.projectId, - { - ...this.data, - importedAt: this.fakeTime.toISOString(), - }, - this.name, - 'parent-folder-id', - this.userId - ) - done() - }, - } - this.LinkedFilesController.refreshLinkedFile( - this.req, - this.res, - this.next - ) + it('resets importedAt timestamp on linkedFileData', function (ctx) { + return new Promise(resolve => { + ctx.next = sinon.stub().callsFake(() => resolve('unexpected error')) + ctx.res = { + json: () => { + expect( + ctx.Agent.promises.refreshLinkedFile + ).to.have.been.calledWith( + ctx.projectId, + { + ...ctx.data, + importedAt: ctx.fakeTime.toISOString(), + }, + ctx.name, + 'parent-folder-id', + ctx.userId + ) + resolve() + }, + } + ctx.LinkedFilesController.refreshLinkedFile(ctx.req, ctx.res, ctx.next) + }) }) }) }) diff --git a/services/web/test/unit/src/Metadata/MetaController.test.mjs b/services/web/test/unit/src/Metadata/MetaController.test.mjs index 5695d289f7..00b3568ae2 100644 --- a/services/web/test/unit/src/Metadata/MetaController.test.mjs +++ b/services/web/test/unit/src/Metadata/MetaController.test.mjs @@ -1,31 +1,38 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' import MockResponse from '../helpers/MockResponse.js' const modulePath = '../../../../app/src/Features/Metadata/MetaController.mjs' describe('MetaController', function () { - beforeEach(async function () { - this.EditorRealTimeController = { + beforeEach(async function (ctx) { + ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), } - this.MetaHandler = { + ctx.MetaHandler = { promises: { getAllMetaForProject: sinon.stub(), getMetaForDoc: sinon.stub(), }, } - this.MetadataController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Metadata/MetaHandler': this.MetaHandler, - }) + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/Features/Metadata/MetaHandler', () => ({ + default: ctx.MetaHandler, + })) + + ctx.MetadataController = (await import(modulePath)).default }) describe('getMetadata', function () { - it('should respond with json', async function () { + it('should respond with json', async function (ctx) { const projectMeta = { 'doc-id': { labels: ['foo'], @@ -34,7 +41,7 @@ describe('MetaController', function () { }, } - this.MetaHandler.promises.getAllMetaForProject = sinon + ctx.MetaHandler.promises.getAllMetaForProject = sinon .stub() .resolves(projectMeta) @@ -42,9 +49,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.getMetadata(req, res, next) + await ctx.MetadataController.getMetadata(req, res, next) - this.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( + ctx.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( 'project-id' ) res.json.should.have.been.calledOnceWith({ @@ -54,8 +61,8 @@ describe('MetaController', function () { next.should.not.have.been.called }) - it('should handle an error', async function () { - this.MetaHandler.promises.getAllMetaForProject = sinon + it('should handle an error', async function (ctx) { + ctx.MetaHandler.promises.getAllMetaForProject = sinon .stub() .throws(new Error('woops')) @@ -63,9 +70,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.getMetadata(req, res, next) + await ctx.MetadataController.getMetadata(req, res, next) - this.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( + ctx.MetaHandler.promises.getAllMetaForProject.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called @@ -74,14 +81,14 @@ describe('MetaController', function () { }) describe('broadcastMetadataForDoc', function () { - it('should broadcast on broadcast:true ', async function () { - this.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves({ + it('should broadcast on broadcast:true ', async function (ctx) { + ctx.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves({ labels: ['foo'], packages: { a: { commands: [] } }, packageNames: ['a'], }) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -90,32 +97,32 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called res.sendStatus.should.have.been.calledOnceWith(200) next.should.not.have.been.called - this.EditorRealTimeController.emitToRoom.should.have.been.calledOnce - const { lastCall } = this.EditorRealTimeController.emitToRoom + ctx.EditorRealTimeController.emitToRoom.should.have.been.calledOnce + const { lastCall } = ctx.EditorRealTimeController.emitToRoom expect(lastCall.args[0]).to.equal('project-id') expect(lastCall.args[1]).to.equal('broadcastDocMeta') expect(lastCall.args[2]).to.have.all.keys(['docId', 'meta']) }) - it('should return json on broadcast:false ', async function () { + it('should return json on broadcast:false ', async function (ctx) { const docMeta = { labels: ['foo'], packages: { a: [] }, packageNames: ['a'], } - this.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves(docMeta) + ctx.MetaHandler.promises.getMetaForDoc = sinon.stub().resolves(docMeta) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -124,12 +131,12 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) - this.EditorRealTimeController.emitToRoom.should.not.have.been.called + ctx.EditorRealTimeController.emitToRoom.should.not.have.been.called res.json.should.have.been.calledOnceWith({ docId: 'doc-id', meta: docMeta, @@ -137,12 +144,12 @@ describe('MetaController', function () { next.should.not.have.been.called }) - it('should handle an error', async function () { - this.MetaHandler.promises.getMetaForDoc = sinon + it('should handle an error', async function (ctx) { + ctx.MetaHandler.promises.getMetaForDoc = sinon .stub() .throws(new Error('woops')) - this.EditorRealTimeController.emitToRoom = sinon.stub() + ctx.EditorRealTimeController.emitToRoom = sinon.stub() const req = { params: { project_id: 'project-id', doc_id: 'doc-id' }, @@ -151,9 +158,9 @@ describe('MetaController', function () { const res = new MockResponse() const next = sinon.stub() - await this.MetadataController.broadcastMetadataForDoc(req, res, next) + await ctx.MetadataController.broadcastMetadataForDoc(req, res, next) - this.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( + ctx.MetaHandler.promises.getMetaForDoc.should.have.been.calledWith( 'project-id' ) res.json.should.not.have.been.called diff --git a/services/web/test/unit/src/Metadata/MetaHandler.test.mjs b/services/web/test/unit/src/Metadata/MetaHandler.test.mjs index 289fd0b164..c6009a2dd6 100644 --- a/services/web/test/unit/src/Metadata/MetaHandler.test.mjs +++ b/services/web/test/unit/src/Metadata/MetaHandler.test.mjs @@ -1,15 +1,15 @@ +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -import esmock from 'esmock' const modulePath = '../../../../app/src/Features/Metadata/MetaHandler.mjs' describe('MetaHandler', function () { - beforeEach(async function () { - this.projectId = 'someprojectid' - this.docId = 'somedocid' + beforeEach(async function (ctx) { + ctx.projectId = 'someprojectid' + ctx.docId = 'somedocid' - this.lines = [ + ctx.lines = [ '\\usepackage{ foo, bar }', '\\usepackage{baz}', 'one', @@ -23,28 +23,28 @@ describe('MetaHandler', function () { '\\begin{lstlisting}[label={lst:foo},caption={Test}]', // lst:foo should be in the returned labels ] - this.docs = { - [this.docId]: { - _id: this.docId, - lines: this.lines, + ctx.docs = { + [ctx.docId]: { + _id: ctx.docId, + lines: ctx.lines, }, } - this.ProjectEntityHandler = { + ctx.ProjectEntityHandler = { promises: { - getAllDocs: sinon.stub().resolves(this.docs), - getDoc: sinon.stub().resolves(this.docs[this.docId]), + getAllDocs: sinon.stub().resolves(ctx.docs), + getDoc: sinon.stub().resolves(ctx.docs[ctx.docId]), }, } - this.DocumentUpdaterHandler = { + ctx.DocumentUpdaterHandler = { promises: { flushDocToMongo: sinon.stub().resolves(), flushProjectToMongo: sinon.stub().resolves(), }, } - this.packageMapping = { + ctx.packageMapping = { foo: [ { caption: '\\bar', @@ -69,47 +69,58 @@ describe('MetaHandler', function () { ], } - this.MetaHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectEntityHandler': - this.ProjectEntityHandler, - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler': - this.DocumentUpdaterHandler, - '../../../../app/src/Features/Metadata/packageMapping': - this.packageMapping, - }) + vi.doMock( + '../../../../app/src/Features/Project/ProjectEntityHandler', + () => ({ + default: ctx.ProjectEntityHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: ctx.DocumentUpdaterHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Metadata/packageMapping', () => ({ + default: ctx.packageMapping, + })) + + ctx.MetaHandler = (await import(modulePath)).default }) describe('getMetaForDoc', function () { - it('should extract all the labels and packages', async function () { - const result = await this.MetaHandler.promises.getMetaForDoc( - this.projectId, - this.docId + it('should extract all the labels and packages', async function (ctx) { + const result = await ctx.MetaHandler.promises.getMetaForDoc( + ctx.projectId, + ctx.docId ) expect(result).to.deep.equal({ labels: ['aaa', 'ccc', 'ddd', 'e,f,g', 'foo', 'lst:foo'], packages: { - foo: this.packageMapping.foo, - baz: this.packageMapping.baz, + foo: ctx.packageMapping.foo, + baz: ctx.packageMapping.baz, }, packageNames: ['foo', 'bar', 'baz'], }) - this.DocumentUpdaterHandler.promises.flushDocToMongo.should.be.calledWith( - this.projectId, - this.docId + ctx.DocumentUpdaterHandler.promises.flushDocToMongo.should.be.calledWith( + ctx.projectId, + ctx.docId ) - this.ProjectEntityHandler.promises.getDoc.should.be.calledWith( - this.projectId, - this.docId + ctx.ProjectEntityHandler.promises.getDoc.should.be.calledWith( + ctx.projectId, + ctx.docId ) }) }) describe('getAllMetaForProject', function () { - it('should extract all metadata', async function () { - this.ProjectEntityHandler.promises.getAllDocs = sinon.stub().resolves({ + it('should extract all metadata', async function (ctx) { + ctx.ProjectEntityHandler.promises.getAllDocs = sinon.stub().resolves({ doc_one: { _id: 'id_one', lines: ['one', '\\label{aaa} two', 'three'], @@ -142,8 +153,8 @@ describe('MetaHandler', function () { }, }) - const result = await this.MetaHandler.promises.getAllMetaForProject( - this.projectId + const result = await ctx.MetaHandler.promises.getAllMetaForProject( + ctx.projectId ) expect(result).to.deep.equal({ @@ -206,12 +217,12 @@ describe('MetaHandler', function () { }, }) - this.DocumentUpdaterHandler.promises.flushProjectToMongo.should.be.calledWith( - this.projectId + ctx.DocumentUpdaterHandler.promises.flushProjectToMongo.should.be.calledWith( + ctx.projectId ) - this.ProjectEntityHandler.promises.getAllDocs.should.be.calledWith( - this.projectId + ctx.ProjectEntityHandler.promises.getAllDocs.should.be.calledWith( + ctx.projectId ) }) }) diff --git a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs index 0e22b228c5..6e1f9177c0 100644 --- a/services/web/test/unit/src/Notifications/NotificationsController.test.mjs +++ b/services/web/test/unit/src/Notifications/NotificationsController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = new URL( @@ -10,12 +10,12 @@ describe('NotificationsController', function () { const userId = '123nd3ijdks' const notificationId = '123njdskj9jlk' - beforeEach(async function () { - this.handler = { + beforeEach(async function (ctx) { + ctx.handler = { getUserNotifications: sinon.stub().callsArgWith(1), markAsRead: sinon.stub().callsArgWith(2), } - this.req = { + ctx.req = { params: { notificationId, }, @@ -28,39 +28,53 @@ describe('NotificationsController', function () { translate() {}, }, } - this.AuthenticationController = { - getLoggedInUserId: sinon.stub().returns(this.req.session.user._id), + ctx.AuthenticationController = { + getLoggedInUserId: sinon.stub().returns(ctx.req.session.user._id), } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Notifications/NotificationsHandler': - this.handler, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsHandler', + () => ({ + default: ctx.handler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + ctx.controller = (await import(modulePath)).default + }) + + it('should ask the handler for all unread notifications', function (ctx) { + return new Promise(resolve => { + const allNotifications = [{ _id: notificationId, user_id: userId }] + ctx.handler.getUserNotifications = sinon + .stub() + .callsArgWith(1, null, allNotifications) + ctx.controller.getAllUnreadNotifications(ctx.req, { + json: body => { + body.should.deep.equal(allNotifications) + ctx.handler.getUserNotifications.calledWith(userId).should.equal(true) + resolve() + }, + }) }) }) - it('should ask the handler for all unread notifications', function (done) { - const allNotifications = [{ _id: notificationId, user_id: userId }] - this.handler.getUserNotifications = sinon - .stub() - .callsArgWith(1, null, allNotifications) - this.controller.getAllUnreadNotifications(this.req, { - json: body => { - body.should.deep.equal(allNotifications) - this.handler.getUserNotifications.calledWith(userId).should.equal(true) - done() - }, - }) - }) - - it('should send a delete request when a delete has been received to mark a notification', function (done) { - this.controller.markNotificationAsRead(this.req, { - sendStatus: () => { - this.handler.markAsRead - .calledWith(userId, notificationId) - .should.equal(true) - done() - }, + it('should send a delete request when a delete has been received to mark a notification', function (ctx) { + return new Promise(resolve => { + ctx.controller.markNotificationAsRead(ctx.req, { + sendStatus: () => { + ctx.handler.markAsRead + .calledWith(userId, notificationId) + .should.equal(true) + resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs index 6df3c765b1..e4cf6e569f 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs +++ b/services/web/test/unit/src/PasswordReset/PasswordResetController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import MockResponse from '../helpers/MockResponse.js' @@ -9,16 +9,16 @@ const MODULE_PATH = new URL( ).pathname describe('PasswordResetController', function () { - beforeEach(async function () { - this.email = 'bob@bob.com' - this.user_id = 'mock-user-id' - this.token = 'my security token that was emailed to me' - this.password = 'my new password' - this.req = { + beforeEach(async function (ctx) { + ctx.email = 'bob@bob.com' + ctx.user_id = 'mock-user-id' + ctx.token = 'my security token that was emailed to me' + ctx.password = 'my new password' + ctx.req = { body: { - email: this.email, - passwordResetToken: this.token, - password: this.password, + email: ctx.email, + passwordResetToken: ctx.token, + password: ctx.password, }, i18n: { translate() { @@ -28,456 +28,540 @@ describe('PasswordResetController', function () { session: {}, query: {}, } - this.res = new MockResponse() + ctx.res = new MockResponse() - this.settings = {} - this.PasswordResetHandler = { + ctx.settings = {} + ctx.PasswordResetHandler = { generateAndEmailResetToken: sinon.stub(), promises: { generateAndEmailResetToken: sinon.stub(), setNewUserPassword: sinon.stub().resolves({ found: true, reset: true, - userID: this.user_id, + userID: ctx.user_id, mustReconfirm: true, }), getUserForPasswordResetToken: sinon .stub() - .withArgs(this.token) + .withArgs(ctx.token) .resolves({ - user: { _id: this.user_id }, + user: { _id: ctx.user_id }, remainingPeeks: 1, }), }, } - this.UserSessionsManager = { + ctx.UserSessionsManager = { promises: { removeSessionsFromRedis: sinon.stub().resolves(), }, } - this.UserUpdater = { + ctx.UserUpdater = { promises: { removeReconfirmFlag: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves('default'), }, } - this.PasswordResetController = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/PasswordReset/PasswordResetHandler': - this.PasswordResetHandler, - '../../../../app/src/Features/Authentication/AuthenticationManager': { - validatePassword: sinon.stub().returns(null), - }, - '../../../../app/src/Features/Authentication/AuthenticationController': - (this.AuthenticationController = { + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/Features/PasswordReset/PasswordResetHandler', + () => ({ + default: ctx.PasswordResetHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: { + validatePassword: sinon.stub().returns(null), + }, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: (ctx.AuthenticationController = { getLoggedInUserId: sinon.stub(), finishLogin: sinon.stub(), setAuditInfo: sinon.stub(), }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { promises: { getUser: sinon.stub(), }, }), - '../../../../app/src/Features/User/UserSessionsManager': - this.UserSessionsManager, - '../../../../app/src/Features/User/UserUpdater': this.UserUpdater, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - }) + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({ + default: ctx.UserUpdater, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + ctx.PasswordResetController = (await import(MODULE_PATH)).default }) describe('requestReset', function () { - it('should tell the handler to process that email', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'primary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(200) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - expect( - this.PasswordResetHandler.promises.generateAndEmailResetToken.lastCall - .args[0] - ).equal(this.email) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) - }) - - it('should send a 500 if there is an error', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.rejects( - new Error('error') - ) - this.PasswordResetController.requestReset(this.req, this.res, error => { - expect(error).to.exist - done() + it('should tell the handler to process that email', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'primary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(200) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + expect( + ctx.PasswordResetHandler.promises.generateAndEmailResetToken + .lastCall.args[0] + ).equal(ctx.email) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) }) }) - it("should send a 404 if the email doesn't exist", function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - null - ) - this.res.callback = () => { - this.res.statusCode.should.equal(404) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it('should send a 500 if there is an error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.rejects( + new Error('error') + ) + ctx.PasswordResetController.requestReset(ctx.req, ctx.res, error => { + expect(error).to.exist + resolve() + }) + }) }) - it('should send a 404 if the email is registered as a secondard email', function (done) { - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'secondary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(404) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it("should send a 404 if the email doesn't exist", function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + null + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(404) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) }) - it('should normalize the email address', function (done) { - this.email = ' UPperCaseEMAILWithSpacesAround@example.Com ' - this.req.body.email = this.email - this.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( - 'primary' - ) - this.res.callback = () => { - this.res.statusCode.should.equal(200) - this.res.json.calledWith(sinon.match.has('message')).should.equal(true) - done() - } - this.PasswordResetController.requestReset(this.req, this.res) + it('should send a 404 if the email is registered as a secondard email', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'secondary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(404) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) + }) + + it('should normalize the email address', function (ctx) { + return new Promise(resolve => { + ctx.email = ' UPperCaseEMAILWithSpacesAround@example.Com ' + ctx.req.body.email = ctx.email + ctx.PasswordResetHandler.promises.generateAndEmailResetToken.resolves( + 'primary' + ) + ctx.res.callback = () => { + ctx.res.statusCode.should.equal(200) + ctx.res.json.calledWith(sinon.match.has('message')).should.equal(true) + resolve() + } + ctx.PasswordResetController.requestReset(ctx.req, ctx.res) + }) }) }) describe('setNewUserPassword', function () { - beforeEach(function () { - this.req.session.resetToken = this.token + beforeEach(function (ctx) { + ctx.req.session.resetToken = ctx.token }) - it('should tell the user handler to reset the password', function (done) { - this.res.sendStatus = code => { - code.should.equal(200) - this.PasswordResetHandler.promises.setNewUserPassword - .calledWith(this.token, this.password) - .should.equal(true) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) - }) - - it('should preserve spaces in the password', function (done) { - this.password = this.req.body.password = ' oh! clever! spaces around! ' - this.res.sendStatus = code => { - code.should.equal(200) - this.PasswordResetHandler.promises.setNewUserPassword.should.have.been.calledWith( - this.token, - this.password - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) - }) - - it('should send 404 if the token was not found', function (done) { - this.PasswordResetHandler.promises.setNewUserPassword.resolves({ - found: false, - reset: false, - userId: this.user_id, + it('should tell the user handler to reset the password', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.PasswordResetHandler.promises.setNewUserPassword + .calledWith(ctx.token, ctx.password) + .should.equal(true) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.res.status = code => { - code.should.equal(404) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('token-expired') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) - it('should return 500 if not reset', function (done) { - this.PasswordResetHandler.promises.setNewUserPassword.resolves({ - found: true, - reset: false, - userId: this.user_id, + it('should preserve spaces in the password', function (ctx) { + return new Promise(resolve => { + ctx.password = ctx.req.body.password = ' oh! clever! spaces around! ' + ctx.res.sendStatus = code => { + code.should.equal(200) + ctx.PasswordResetHandler.promises.setNewUserPassword.should.have.been.calledWith( + ctx.token, + ctx.password + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.res.status = code => { - code.should.equal(500) - return this.res - } - this.res.json = data => { - expect(data.message).to.exist - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) - it('should return 400 (Bad Request) if there is no password', function (done) { - this.req.body.password = '' - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - false - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should send 404 if the token was not found', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({ + found: false, + reset: false, + userId: ctx.user_id, + }) + ctx.res.status = code => { + code.should.equal(404) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('token-expired') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 (Bad Request) if there is no passwordResetToken', function (done) { - this.req.body.passwordResetToken = '' - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - false - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 500 if not reset', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.promises.setNewUserPassword.resolves({ + found: true, + reset: false, + userId: ctx.user_id, + }) + ctx.res.status = code => { + code.should.equal(500) + return ctx.res + } + ctx.res.json = data => { + expect(data.message).to.exist + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 (Bad Request) if the password is invalid', function (done) { - this.req.body.password = 'correct horse battery staple' - const err = new Error('bad') - err.name = 'InvalidPasswordError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(err) - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - this.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( - true - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if there is no password', function (ctx) { + return new Promise(resolve => { + ctx.req.body.password = '' + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + false + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should clear sessions', function (done) { - this.res.sendStatus = code => { - this.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal( - 1 - ) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if there is no passwordResetToken', function (ctx) { + return new Promise(resolve => { + ctx.req.body.passwordResetToken = '' + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + false + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should call removeReconfirmFlag if user.must_reconfirm', function (done) { - this.res.sendStatus = code => { - this.UserUpdater.promises.removeReconfirmFlag.callCount.should.equal(1) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 (Bad Request) if the password is invalid', function (ctx) { + return new Promise(resolve => { + ctx.req.body.password = 'correct horse battery staple' + const err = new Error('bad') + err.name = 'InvalidPasswordError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(err) + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + ctx.PasswordResetHandler.promises.setNewUserPassword.called.should.equal( + true + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) + }) + + it('should clear sessions', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.UserSessionsManager.promises.removeSessionsFromRedis.callCount.should.equal( + 1 + ) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) + }) + + it('should call removeReconfirmFlag if user.must_reconfirm', function (ctx) { + return new Promise(resolve => { + ctx.res.sendStatus = code => { + ctx.UserUpdater.promises.removeReconfirmFlag.callCount.should.equal(1) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) describe('catch errors', function () { - it('should return 404 for NotFoundError', function (done) { - const anError = new Error('oops') - anError.name = 'NotFoundError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(404) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('token-expired') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 404 for NotFoundError', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + anError.name = 'NotFoundError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(404) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('token-expired') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 400 for InvalidPasswordError', function (done) { - const anError = new Error('oops') - anError.name = 'InvalidPasswordError' - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(400) - return this.res - } - this.res.json = data => { - data.message.key.should.equal('invalid-password') - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 400 for InvalidPasswordError', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + anError.name = 'InvalidPasswordError' + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(400) + return ctx.res + } + ctx.res.json = data => { + data.message.key.should.equal('invalid-password') + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) - it('should return 500 for other errors', function (done) { - const anError = new Error('oops') - this.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) - this.res.status = code => { - code.should.equal(500) - return this.res - } - this.res.json = data => { - expect(data.message).to.exist - done() - } - this.res.sendStatus = code => { - code.should.equal(500) - done() - } - this.PasswordResetController.setNewUserPassword(this.req, this.res) + it('should return 500 for other errors', function (ctx) { + return new Promise(resolve => { + const anError = new Error('oops') + ctx.PasswordResetHandler.promises.setNewUserPassword.rejects(anError) + ctx.res.status = code => { + code.should.equal(500) + return ctx.res + } + ctx.res.json = data => { + expect(data.message).to.exist + resolve() + } + ctx.res.sendStatus = code => { + code.should.equal(500) + resolve() + } + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) + }) }) }) describe('when doLoginAfterPasswordReset is set', function () { - beforeEach(function () { - this.user = { - _id: this.userId, + beforeEach(function (ctx) { + ctx.user = { + _id: ctx.userId, email: 'joe@example.com', } - this.UserGetter.promises.getUser.resolves(this.user) - this.req.session.doLoginAfterPasswordReset = 'true' + ctx.UserGetter.promises.getUser.resolves(ctx.user) + ctx.req.session.doLoginAfterPasswordReset = 'true' }) - it('should login user', function (done) { - this.AuthenticationController.finishLogin.callsFake((...args) => { - expect(args[0]).to.equal(this.user) - done() + it('should login user', function (ctx) { + return new Promise(resolve => { + ctx.AuthenticationController.finishLogin.callsFake((...args) => { + expect(args[0]).to.equal(ctx.user) + resolve() + }) + ctx.PasswordResetController.setNewUserPassword(ctx.req, ctx.res) }) - this.PasswordResetController.setNewUserPassword(this.req, this.res) }) }) }) describe('renderSetPasswordForm', function () { describe('with token in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token }) - it('should set session.resetToken and redirect', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with expired token in query', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.PasswordResetHandler.promises.getUserForPasswordResetToken = sinon + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = sinon .stub() - .withArgs(this.token) - .resolves({ user: { _id: this.user_id }, remainingPeeks: 0 }) + .withArgs(ctx.token) + .resolves({ user: { _id: ctx.user_id }, remainingPeeks: 0 }) }) - it('should redirect to the reset request page with an error message', function (done) { - this.res.redirect = path => { - path.should.equal('/user/password/reset?error=token_expired') - this.req.session.should.not.have.property('resetToken') - done() - } - this.res.render = (templatePath, options) => { - done('should not render') - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should redirect to the reset request page with an error message', function (ctx) { + return new Promise(resolve => { + ctx.res.redirect = path => { + path.should.equal('/user/password/reset?error=token_expired') + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.res.render = (templatePath, options) => { + resolve('should not render') + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = 'foo@bar.com' + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = 'foo@bar.com' }) - it('should set session.resetToken and redirect with email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set?email=foo%40bar.com') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect with email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set?email=foo%40bar.com') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and invalid email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = 'not-an-email' + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = 'not-an-email' }) - it('should set session.resetToken and redirect without email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect without email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('with token and non-string email in query-string', function () { - beforeEach(function () { - this.req.query.passwordResetToken = this.token - this.req.query.email = { foo: 'bar' } + beforeEach(function (ctx) { + ctx.req.query.passwordResetToken = ctx.token + ctx.req.query.email = { foo: 'bar' } }) - it('should set session.resetToken and redirect without email', function (done) { - this.req.session.should.not.have.property('resetToken') - this.res.redirect = path => { - path.should.equal('/user/password/set') - this.req.session.resetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should set session.resetToken and redirect without email', function (ctx) { + return new Promise(resolve => { + ctx.req.session.should.not.have.property('resetToken') + ctx.res.redirect = path => { + path.should.equal('/user/password/set') + ctx.req.session.resetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('without a token in query-string', function () { describe('with token in session', function () { - beforeEach(function () { - this.req.session.resetToken = this.token + beforeEach(function (ctx) { + ctx.req.session.resetToken = ctx.token }) - it('should render the page, passing the reset token', function (done) { - this.res.render = (templatePath, options) => { - options.passwordResetToken.should.equal(this.token) - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should render the page, passing the reset token', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (templatePath, options) => { + options.passwordResetToken.should.equal(ctx.token) + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) - it('should clear the req.session.resetToken', function (done) { - this.res.render = (templatePath, options) => { - this.req.session.should.not.have.property('resetToken') - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should clear the req.session.resetToken', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (templatePath, options) => { + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) describe('without a token in session', function () { - it('should redirect to the reset request page', function (done) { - this.res.redirect = path => { - path.should.equal('/user/password/reset') - this.req.session.should.not.have.property('resetToken') - done() - } - this.PasswordResetController.renderSetPasswordForm(this.req, this.res) + it('should redirect to the reset request page', function (ctx) { + return new Promise(resolve => { + ctx.res.redirect = path => { + path.should.equal('/user/password/reset') + ctx.req.session.should.not.have.property('resetToken') + resolve() + } + ctx.PasswordResetController.renderSetPasswordForm(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs b/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs index b99cc527e2..25d664b795 100644 --- a/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs +++ b/services/web/test/unit/src/PasswordReset/PasswordResetHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = new URL( @@ -7,9 +7,9 @@ const modulePath = new URL( ).pathname describe('PasswordResetHandler', function () { - beforeEach(async function () { - this.settings = { siteUrl: 'https://www.overleaf.com' } - this.OneTimeTokenHandler = { + beforeEach(async function (ctx) { + ctx.settings = { siteUrl: 'https://www.overleaf.com' } + ctx.OneTimeTokenHandler = { promises: { getNewToken: sinon.stub(), peekValueFromToken: sinon.stub(), @@ -17,7 +17,7 @@ describe('PasswordResetHandler', function () { peekValueFromToken: sinon.stub(), expireToken: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { getUserByMainEmail: sinon.stub(), getUser: sinon.stub(), promises: { @@ -25,123 +25,153 @@ describe('PasswordResetHandler', function () { getUserByMainEmail: sinon.stub(), }, } - this.EmailHandler = { promises: { sendEmail: sinon.stub() } } - this.AuthenticationManager = { + ctx.EmailHandler = { promises: { sendEmail: sinon.stub() } } + ctx.AuthenticationManager = { setUserPasswordInV2: sinon.stub(), promises: { setUserPassword: sinon.stub().resolves(), }, } - this.PasswordResetHandler = await esmock.strict(modulePath, { - '../../../../app/src/Features/User/UserAuditLogHandler': - (this.UserAuditLogHandler = { - promises: { - addEntry: sinon.stub().resolves(), - }, - }), - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Security/OneTimeTokenHandler': - this.OneTimeTokenHandler, - '../../../../app/src/Features/Email/EmailHandler': this.EmailHandler, - '../../../../app/src/Features/Authentication/AuthenticationManager': - this.AuthenticationManager, - '@overleaf/settings': this.settings, - '../../../../app/src/Features/Authorization/PermissionsManager': - (this.PermissionsManager = { + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: (ctx.UserAuditLogHandler = { + promises: { + addEntry: sinon.stub().resolves(), + }, + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Security/OneTimeTokenHandler', + () => ({ + default: ctx.OneTimeTokenHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationManager', + () => ({ + default: ctx.AuthenticationManager, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock( + '../../../../app/src/Features/Authorization/PermissionsManager', + () => ({ + default: (ctx.PermissionsManager = { promises: { assertUserPermissions: sinon.stub(), }, }), - }) - this.token = '12312321i' - this.user_id = 'user_id_here' - this.user = { email: (this.email = 'bob@bob.com'), _id: this.user_id } - this.password = 'my great secret password' - this.callback = sinon.stub() + }) + ) + + ctx.PasswordResetHandler = (await import(modulePath)).default + ctx.token = '12312321i' + ctx.user_id = 'user_id_here' + ctx.user = { email: (ctx.email = 'bob@bob.com'), _id: ctx.user_id } + ctx.password = 'my great secret password' + ctx.callback = sinon.stub() // this should not have any effect now - this.settings.overleaf = true + ctx.settings.overleaf = true }) - afterEach(function () { - this.settings.overleaf = false + afterEach(function (ctx) { + ctx.settings.overleaf = false }) describe('generateAndEmailResetToken', function () { - it('should check the user exists', function () { - this.UserGetter.promises.getUserByAnyEmail.resolves() - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - this.callback + it('should check the user exists', function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves() + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + ctx.callback ) - this.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith( - this.user.email + ctx.UserGetter.promises.getUserByAnyEmail.should.have.been.calledWith( + ctx.user.email ) }) - it('should send the email with the token', function (done) { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) - this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token) - this.EmailHandler.promises.sendEmail.resolves() - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - (err, status) => { - expect(err).to.not.exist - this.EmailHandler.promises.sendEmail.called.should.equal(true) - status.should.equal('primary') - const args = this.EmailHandler.promises.sendEmail.args[0] - args[0].should.equal('passwordResetRequested') - args[1].setNewPasswordUrl.should.equal( - `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ - this.token - }&email=${encodeURIComponent(this.user.email)}` - ) - done() - } - ) + it('should send the email with the token', function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token) + ctx.EmailHandler.promises.sendEmail.resolves() + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + (err, status) => { + expect(err).to.not.exist + ctx.EmailHandler.promises.sendEmail.called.should.equal(true) + status.should.equal('primary') + const args = ctx.EmailHandler.promises.sendEmail.args[0] + args[0].should.equal('passwordResetRequested') + args[1].setNewPasswordUrl.should.equal( + `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${ + ctx.token + }&email=${encodeURIComponent(ctx.user.email)}` + ) + resolve() + } + ) + }) }) - it('should return errors from getUserByAnyEmail', function (done) { - const err = new Error('oops') - this.UserGetter.promises.getUserByAnyEmail.rejects(err) - this.PasswordResetHandler.generateAndEmailResetToken( - this.user.email, - err => { - expect(err).to.equal(err) - done() - } - ) + it('should return errors from getUserByAnyEmail', function (ctx) { + return new Promise(resolve => { + const err = new Error('oops') + ctx.UserGetter.promises.getUserByAnyEmail.rejects(err) + ctx.PasswordResetHandler.generateAndEmailResetToken( + ctx.user.email, + err => { + expect(err).to.equal(err) + resolve() + } + ) + }) }) describe('when the email exists', function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) - this.OneTimeTokenHandler.promises.getNewToken.resolves(this.token) - this.EmailHandler.promises.sendEmail.resolves() + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.promises.getNewToken.resolves(ctx.token) + ctx.EmailHandler.promises.sendEmail.resolves() result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( - this.email + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( + ctx.email ) }) - it('should set the password token data to the user id and email', function () { - this.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith( + it('should set the password token data to the user id and email', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.should.have.been.calledWith( 'password', { - email: this.email, - user_id: this.user._id, + email: ctx.email, + user_id: ctx.user._id, } ) }) - it('should send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(true) - const args = this.EmailHandler.promises.sendEmail.args[0] + it('should send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(true) + const args = ctx.EmailHandler.promises.sendEmail.args[0] args[0].should.equal('passwordResetRequested') args[1].setNewPasswordUrl.should.equal( - `${this.settings.siteUrl}/user/password/set?passwordResetToken=${ - this.token - }&email=${encodeURIComponent(this.user.email)}` + `${ctx.settings.siteUrl}/user/password/set?passwordResetToken=${ + ctx.token + }&email=${encodeURIComponent(ctx.user.email)}` ) }) @@ -152,20 +182,20 @@ describe('PasswordResetHandler', function () { describe("when the email doesn't exist", function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(null) + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(null) result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( - this.email + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( + ctx.email ) }) - it('should not set the password token data', function () { - this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) + it('should not set the password token data', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) }) - it('should send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(false) + it('should send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(false) }) it('should return status == null', function () { @@ -175,20 +205,20 @@ describe('PasswordResetHandler', function () { describe('when the email is a secondary email', function () { let result - beforeEach(async function () { - this.UserGetter.promises.getUserByAnyEmail.resolves(this.user) + beforeEach(async function (ctx) { + ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user) result = - await this.PasswordResetHandler.promises.generateAndEmailResetToken( + await ctx.PasswordResetHandler.promises.generateAndEmailResetToken( 'secondary@email.com' ) }) - it('should not set the password token data', function () { - this.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) + it('should not set the password token data', function (ctx) { + ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(false) }) - it('should not send an email with the token', function () { - this.EmailHandler.promises.sendEmail.called.should.equal(false) + it('should not send an email with the token', function (ctx) { + ctx.EmailHandler.promises.sendEmail.called.should.equal(false) }) it('should return status == secondary', function () { @@ -198,19 +228,19 @@ describe('PasswordResetHandler', function () { }) describe('setNewUserPassword', function () { - beforeEach(function () { - this.auditLog = { ip: '0:0:0:0' } + beforeEach(function (ctx) { + ctx.auditLog = { ip: '0:0:0:0' } }) describe('when no data is found', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null) + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves(null) }) - it('should return found == false and reset == false', function () { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, + it('should return found == false and reset == false', function (ctx) { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, (error, result) => { expect(error).to.not.exist expect(result).to.deep.equal({ @@ -224,202 +254,220 @@ describe('PasswordResetHandler', function () { }) describe('when the token has a user_id and email', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - user_id: this.user._id, - email: this.email, + user_id: ctx.user._id, + email: ctx.email, }, }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .resolves(true) - this.OneTimeTokenHandler.expireToken = sinon - .stub() - .callsArgWith(2, null) + ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null) }) describe('when no user is found with this email', function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) .yields(null, null) }) - it('should return found == false and reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { found, reset } = result - expect(err).to.not.exist - expect(found).to.be.false - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0) - done() - } - ) + it('should return found == false and reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { found, reset } = result + expect(err).to.not.exist + expect(found).to.be.false + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal( + 0 + ) + resolve() + } + ) + }) }) }) describe("when the email and user don't match", function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) - .yields(null, { _id: 'not-the-same', email: this.email }) - this.OneTimeTokenHandler.expireToken.callsArgWith(2, null) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) + .yields(null, { _id: 'not-the-same', email: ctx.email }) + ctx.OneTimeTokenHandler.expireToken.callsArgWith(2, null) }) - it('should return found == false and reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { found, reset } = result - expect(err).to.not.exist - expect(found).to.be.false - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.callCount).to.equal(0) - done() - } - ) + it('should return found == false and reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { found, reset } = result + expect(err).to.not.exist + expect(found).to.be.false + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.callCount).to.equal( + 0 + ) + resolve() + } + ) + }) }) }) describe('when the email and user match', function () { describe('success', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByMainEmail.resolves(this.user) - this.OneTimeTokenHandler.expireToken = sinon + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user) + ctx.OneTimeTokenHandler.expireToken = sinon .stub() .callsArgWith(2, null) }) - it('should update the user audit log', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, result) => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user_id, - 'reset-password', - undefined, - this.auditLog.ip, - { token: this.token.substring(0, 10) } - ) - expect(error).to.not.exist - done() - } - ) + it('should update the user audit log', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, result) => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user_id, + 'reset-password', + undefined, + ctx.auditLog.ip, + { token: ctx.token.substring(0, 10) } + ) + expect(error).to.not.exist + resolve() + } + ) + }) }) - it('should return reset == true and the user id', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset, userId } = result - expect(err).to.not.exist - expect(reset).to.be.true - expect(userId).to.equal(this.user._id) - done() - } - ) + it('should return reset == true and the user id', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset, userId } = result + expect(err).to.not.exist + expect(reset).to.be.true + expect(userId).to.equal(ctx.user._id) + resolve() + } + ) + }) }) - it('should expire the token', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (_err, _result) => { - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - true - ) - done() - } - ) + it('should expire the token', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (_err, _result) => { + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + true + ) + resolve() + } + ) + }) }) describe('when logged in', function () { - beforeEach(function () { - this.auditLog.initiatorId = this.user_id + beforeEach(function (ctx) { + ctx.auditLog.initiatorId = ctx.user_id }) - it('should update the user audit log with initiatorId', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, result) => { - expect(error).to.not.exist - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user_id, - 'reset-password', - this.user_id, - this.auditLog.ip, - { token: this.token.substring(0, 10) } - ) - done() - } - ) + it('should update the user audit log with initiatorId', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, result) => { + expect(error).to.not.exist + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user_id, + 'reset-password', + ctx.user_id, + ctx.auditLog.ip, + { token: ctx.token.substring(0, 10) } + ) + resolve() + } + ) + }) }) }) }) describe('errors', function () { describe('via setUserPassword', function () { - beforeEach(function () { - this.PasswordResetHandler.promises.getUserForPasswordResetToken = - sinon.stub().withArgs(this.token).resolves({ user: this.user }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + beforeEach(function (ctx) { + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = + sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user }) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .rejects() }) - it('should return the error', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, _result) => { - expect(error).to.exist - expect( - this.UserAuditLogHandler.promises.addEntry.callCount - ).to.equal(1) - done() - } - ) + it('should return the error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, _result) => { + expect(error).to.exist + expect( + ctx.UserAuditLogHandler.promises.addEntry.callCount + ).to.equal(1) + resolve() + } + ) + }) }) }) describe('via UserAuditLogHandler', function () { - beforeEach(function () { - this.PasswordResetHandler.promises.getUserForPasswordResetToken = - sinon.stub().withArgs(this.token).resolves({ user: this.user }) - this.UserAuditLogHandler.promises.addEntry.rejects( + beforeEach(function (ctx) { + ctx.PasswordResetHandler.promises.getUserForPasswordResetToken = + sinon.stub().withArgs(ctx.token).resolves({ user: ctx.user }) + ctx.UserAuditLogHandler.promises.addEntry.rejects( new Error('oops') ) }) - it('should return the error', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (error, _result) => { - expect(error).to.exist - expect( - this.UserAuditLogHandler.promises.addEntry.callCount - ).to.equal(1) - expect(this.AuthenticationManager.promises.setUserPassword).to - .not.have.been.called - done() - } - ) + it('should return the error', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (error, _result) => { + expect(error).to.exist + expect( + ctx.UserAuditLogHandler.promises.addEntry.callCount + ).to.equal(1) + expect(ctx.AuthenticationManager.promises.setUserPassword) + .to.not.have.been.called + resolve() + } + ) + }) }) }) }) @@ -427,120 +475,126 @@ describe('PasswordResetHandler', function () { }) describe('when the token has a v1_user_id and email', function () { - beforeEach(function () { - this.user.overleaf = { id: 184 } - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.user.overleaf = { id: 184 } + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - v1_user_id: this.user.overleaf.id, - email: this.email, + v1_user_id: ctx.user.overleaf.id, + email: ctx.email, }, }) - this.AuthenticationManager.promises.setUserPassword - .withArgs(this.user, this.password) + ctx.AuthenticationManager.promises.setUserPassword + .withArgs(ctx.user, ctx.password) .resolves(true) - this.OneTimeTokenHandler.expireToken = sinon - .stub() - .callsArgWith(2, null) + ctx.OneTimeTokenHandler.expireToken = sinon.stub().callsArgWith(2, null) }) describe('when no user is reset with this email', function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail - .withArgs(this.email) + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail + .withArgs(ctx.email) .yields(null, null) }) - it('should return reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset } = result - expect(err).to.not.exist - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - false - ) - done() - } - ) + it('should return reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset } = result + expect(err).to.not.exist + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + false + ) + resolve() + } + ) + }) }) }) describe("when the email and user don't match", function () { - beforeEach(function () { - this.UserGetter.getUserByMainEmail.withArgs(this.email).yields(null, { - _id: this.user._id, - email: this.email, + beforeEach(function (ctx) { + ctx.UserGetter.getUserByMainEmail.withArgs(ctx.email).yields(null, { + _id: ctx.user._id, + email: ctx.email, overleaf: { id: 'not-the-same' }, }) }) - it('should return reset == false', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset } = result - expect(err).to.not.exist - expect(reset).to.be.false - expect(this.OneTimeTokenHandler.expireToken.called).to.equal( - false - ) - done() - } - ) + it('should return reset == false', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset } = result + expect(err).to.not.exist + expect(reset).to.be.false + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + false + ) + resolve() + } + ) + }) }) }) describe('when the email and user match', function () { - beforeEach(function () { - this.UserGetter.promises.getUserByMainEmail.resolves(this.user) + beforeEach(function (ctx) { + ctx.UserGetter.promises.getUserByMainEmail.resolves(ctx.user) }) - it('should return reset == true and the user id', function (done) { - this.PasswordResetHandler.setNewUserPassword( - this.token, - this.password, - this.auditLog, - (err, result) => { - const { reset, userId } = result - expect(err).to.not.exist - expect(reset).to.be.true - expect(userId).to.equal(this.user._id) - expect(this.OneTimeTokenHandler.expireToken.called).to.equal(true) - done() - } - ) + it('should return reset == true and the user id', function (ctx) { + return new Promise(resolve => { + ctx.PasswordResetHandler.setNewUserPassword( + ctx.token, + ctx.password, + ctx.auditLog, + (err, result) => { + const { reset, userId } = result + expect(err).to.not.exist + expect(reset).to.be.true + expect(userId).to.equal(ctx.user._id) + expect(ctx.OneTimeTokenHandler.expireToken.called).to.equal( + true + ) + resolve() + } + ) + }) }) }) }) }) describe('getUserForPasswordResetToken', function () { - beforeEach(function () { - this.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ + beforeEach(function (ctx) { + ctx.OneTimeTokenHandler.promises.peekValueFromToken.resolves({ data: { - user_id: this.user._id, - email: this.email, + user_id: ctx.user._id, + email: ctx.email, }, remainingPeeks: 1, }) - this.UserGetter.promises.getUserByMainEmail.resolves({ - _id: this.user._id, - email: this.email, + ctx.UserGetter.promises.getUserByMainEmail.resolves({ + _id: ctx.user._id, + email: ctx.email, }) }) - it('should returns errors from user permissions', async function () { + it('should returns errors from user permissions', async function (ctx) { let error const err = new Error('nope') - this.PermissionsManager.promises.assertUserPermissions.rejects(err) + ctx.PermissionsManager.promises.assertUserPermissions.rejects(err) try { - await this.PasswordResetHandler.promises.getUserForPasswordResetToken( + await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken( 'abc123' ) } catch (e) { @@ -549,13 +603,13 @@ describe('PasswordResetHandler', function () { expect(error).to.deep.equal(error) }) - it('returns user when user has permissions and remaining peaks', async function () { + it('returns user when user has permissions and remaining peaks', async function (ctx) { const result = - await this.PasswordResetHandler.promises.getUserForPasswordResetToken( + await ctx.PasswordResetHandler.promises.getUserForPasswordResetToken( 'abc123' ) expect(result).to.deep.equal({ - user: { _id: this.user._id, email: this.email }, + user: { _id: ctx.user._id, email: ctx.email }, remainingPeeks: 1, }) }) diff --git a/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs b/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs index 4f1f3b4f5f..55c4187f83 100644 --- a/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs +++ b/services/web/test/unit/src/Project/DocLinesComparitor.test.mjs @@ -1,16 +1,14 @@ -import esmock from 'esmock' - const modulePath = '../../../../app/src/Features/Project/DocLinesComparitor.mjs' describe('doc lines comparitor', function () { - beforeEach(async function () { - this.comparitor = await esmock.strict(modulePath, {}) + beforeEach(async function (ctx) { + ctx.comparitor = (await import(modulePath)).default }) - it('should return true when the lines are the same', function () { + it('should return true when the lines are the same', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) ;[ @@ -23,58 +21,58 @@ describe('doc lines comparitor', function () { lines2: ['hello', 'wrld'], }, ].forEach(({ lines1, lines2 }) => { - it('should return false when the lines are different', function () { - const result = this.comparitor.areSame(lines1, lines2) + it('should return false when the lines are different', function (ctx) { + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) }) - it('should return true when the lines are same', function () { + it('should return true when the lines are same', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) - it('should return false if the doc lines are different in length', function () { + it('should return false if the doc lines are different in length', function (ctx) { const lines1 = ['hello', 'world'] const lines2 = ['hello', 'world', 'please'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the first array is undefined', function () { + it('should return false if the first array is undefined', function (ctx) { const lines1 = undefined const lines2 = ['hello', 'world'] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the second array is undefined', function () { + it('should return false if the second array is undefined', function (ctx) { const lines1 = ['hello'] const lines2 = undefined - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return false if the second array is not an array', function () { + it('should return false if the second array is not an array', function (ctx) { const lines1 = ['hello'] const lines2 = '' - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) - it('should return true when comparing equal orchard docs', function () { + it('should return true when comparing equal orchard docs', function (ctx) { const lines1 = [{ text: 'hello world' }] const lines2 = [{ text: 'hello world' }] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(true) }) - it('should return false when comparing different orchard docs', function () { + it('should return false when comparing different orchard docs', function (ctx) { const lines1 = [{ text: 'goodbye world' }] const lines2 = [{ text: 'hello world' }] - const result = this.comparitor.areSame(lines1, lines2) + const result = ctx.comparitor.areSame(lines1, lines2) result.should.equal(false) }) }) diff --git a/services/web/test/unit/src/Project/ProjectApiController.test.mjs b/services/web/test/unit/src/Project/ProjectApiController.test.mjs index bda54a932c..c73f327cd2 100644 --- a/services/web/test/unit/src/Project/ProjectApiController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectApiController.test.mjs @@ -1,57 +1,57 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/Project/ProjectApiController' describe('Project api controller', function () { - beforeEach(async function () { - this.ProjectDetailsHandler = { getDetails: sinon.stub() } - this.controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Project/ProjectDetailsHandler': - this.ProjectDetailsHandler, - }) - this.project_id = '321l3j1kjkjl' - this.req = { + beforeEach(async function (ctx) { + ctx.ProjectDetailsHandler = { getDetails: sinon.stub() } + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + ctx.controller = (await import(modulePath)).default + ctx.project_id = '321l3j1kjkjl' + ctx.req = { params: { - project_id: this.project_id, + project_id: ctx.project_id, }, session: { destroy: sinon.stub(), }, } - this.res = {} - this.next = sinon.stub() - return (this.projDetails = { name: 'something' }) + ctx.res = {} + ctx.next = sinon.stub() + return (ctx.projDetails = { name: 'something' }) }) describe('getProjectDetails', function () { - it('should ask the project details handler for proj details', function (done) { - this.ProjectDetailsHandler.getDetails.callsArgWith( - 1, - null, - this.projDetails - ) - this.res.json = data => { - this.ProjectDetailsHandler.getDetails - .calledWith(this.project_id) - .should.equal(true) - data.should.deep.equal(this.projDetails) - return done() - } - return this.controller.getProjectDetails(this.req, this.res) + it('should ask the project details handler for proj details', function (ctx) { + return new Promise(resolve => { + ctx.ProjectDetailsHandler.getDetails.callsArgWith( + 1, + null, + ctx.projDetails + ) + ctx.res.json = data => { + ctx.ProjectDetailsHandler.getDetails + .calledWith(ctx.project_id) + .should.equal(true) + data.should.deep.equal(ctx.projDetails) + return resolve() + } + return ctx.controller.getProjectDetails(ctx.req, ctx.res) + }) }) - it('should send a 500 if there is an error', function () { - this.ProjectDetailsHandler.getDetails.callsArgWith(1, 'error') - this.controller.getProjectDetails(this.req, this.res, this.next) - return this.next.calledWith('error').should.equal(true) + it('should send a 500 if there is an error', function (ctx) { + ctx.ProjectDetailsHandler.getDetails.callsArgWith(1, 'error') + ctx.controller.getProjectDetails(ctx.req, ctx.res, ctx.next) + return ctx.next.calledWith('error').should.equal(true) }) }) }) diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index 827d16b737..a051382279 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -12,10 +12,10 @@ const MODULE_PATH = new URL( ).pathname describe('ProjectListController', function () { - beforeEach(async function () { - this.project_id = new ObjectId('abcdefabcdefabcdefabcdef') + beforeEach(async function (ctx) { + ctx.project_id = new ObjectId('abcdefabcdefabcdefabcdef') - this.user = { + ctx.user = { _id: new ObjectId('123456123456123456123456'), email: 'test@overleaf.com', first_name: 'bjkdsjfk', @@ -23,7 +23,7 @@ describe('ProjectListController', function () { emails: [{ email: 'test@overleaf.com' }], lastLoginIp: '111.111.111.112', } - this.users = { + ctx.users = { 'user-1': { first_name: 'James', }, @@ -31,17 +31,17 @@ describe('ProjectListController', function () { first_name: 'Henry', }, } - this.users[this.user._id] = this.user // Owner - this.usersArr = Object.entries(this.users).map(([key, value]) => ({ + ctx.users[ctx.user._id] = ctx.user // Owner + ctx.usersArr = Object.entries(ctx.users).map(([key, value]) => ({ _id: key, ...value, })) - this.tags = [ + ctx.tags = [ { name: 1, project_ids: ['1', '2', '3'] }, { name: 2, project_ids: ['a', '1'] }, { name: 3, project_ids: ['a', 'b', 'c', 'd'] }, ] - this.notifications = [ + ctx.notifications = [ { _id: '1', user_id: '2', @@ -50,63 +50,63 @@ describe('ProjectListController', function () { key: '5', }, ] - this.settings = { + ctx.settings = { siteUrl: 'https://overleaf.com', } - this.TagsHandler = { + ctx.TagsHandler = { promises: { - getAllTags: sinon.stub().resolves(this.tags), + getAllTags: sinon.stub().resolves(ctx.tags), }, } - this.NotificationsHandler = { + ctx.NotificationsHandler = { promises: { - getUserNotifications: sinon.stub().resolves(this.notifications), + getUserNotifications: sinon.stub().resolves(ctx.notifications), }, } - this.UserModel = { - findById: sinon.stub().resolves(this.user), + ctx.UserModel = { + findById: sinon.stub().resolves(ctx.user), } - this.UserPrimaryEmailCheckHandler = { + ctx.UserPrimaryEmailCheckHandler = { requiresPrimaryEmailCheck: sinon.stub().returns(false), } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { findAllUsersProjects: sinon.stub(), }, } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchived: sinon.stub(), isTrashed: sinon.stub(), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.UserController = { + ctx.UserController = { logout: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUsers: sinon.stub().resolves(this.usersArr), + getUsers: sinon.stub().resolves(ctx.usersArr), getUserFullEmails: sinon.stub().resolves([]), }, } - this.Features = { + ctx.Features = { hasFeature: sinon.stub(), } - this.Metrics = { + ctx.Metrics = { inc: sinon.stub(), } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, } - this.SplitTestSessionHandler = { + ctx.SplitTestSessionHandler = { promises: { sessionMaintenance: sinon.stub().resolves(), }, } - this.SubscriptionViewModelBuilder = { + ctx.SubscriptionViewModelBuilder = { promises: { getUsersSubscriptionDetails: sinon.stub().resolves({ bestSubscription: { type: 'free' }, @@ -115,17 +115,17 @@ describe('ProjectListController', function () { }), }, } - this.SurveyHandler = { + ctx.SurveyHandler = { promises: { getSurvey: sinon.stub().resolves({}), }, } - this.NotificationBuilder = { + ctx.NotificationBuilder = { promises: { ipMatcherAffiliation: sinon.stub().returns({ create: sinon.stub() }), }, } - this.GeoIpLookup = { + ctx.GeoIpLookup = { promises: { getCurrencyCode: sinon.stub().resolves({ countryCode: 'US', @@ -133,11 +133,11 @@ describe('ProjectListController', function () { }), }, } - this.TutorialHandler = { + ctx.TutorialHandler = { getInactiveTutorials: sinon.stub().returns([]), } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves([]), @@ -145,58 +145,133 @@ describe('ProjectListController', function () { }, } - this.ProjectListController = await esmock.strict(MODULE_PATH, { - 'mongodb-legacy': { ObjectId }, - '@overleaf/settings': this.settings, - '@overleaf/metrics': this.Metrics, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/SplitTests/SplitTestSessionHandler': - this.SplitTestSessionHandler, - '../../../../app/src/Features/User/UserController': this.UserController, - '../../../../app/src/Features/Project/ProjectHelper': this.ProjectHelper, - '../../../../app/src/Features/Tags/TagsHandler': this.TagsHandler, - '../../../../app/src/Features/Notifications/NotificationsHandler': - this.NotificationsHandler, - '../../../../app/src/models/User': { User: this.UserModel }, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/infrastructure/Features': this.Features, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder': - this.SubscriptionViewModelBuilder, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/Survey/SurveyHandler': this.SurveyHandler, - '../../../../app/src/Features/User/UserPrimaryEmailCheckHandler': - this.UserPrimaryEmailCheckHandler, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationBuilder, - '../../../../app/src/infrastructure/GeoIpLookup': this.GeoIpLookup, - '../../../../app/src/Features/Tutorial/TutorialHandler': - this.TutorialHandler, - }) + vi.doMock('mongodb-legacy', () => ({ + default: { ObjectId }, + })) - this.req = { + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.Metrics, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestSessionHandler', + () => ({ + default: ctx.SplitTestSessionHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserController', () => ({ + default: ctx.UserController, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsHandler', + () => ({ + default: ctx.NotificationsHandler, + }) + ) + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.UserModel, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionViewModelBuilder', + () => ({ + default: ctx.SubscriptionViewModelBuilder, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock('../../../../app/src/Features/Survey/SurveyHandler', () => ({ + default: ctx.SurveyHandler, + })) + + vi.doMock( + '../../../../app/src/Features/User/UserPrimaryEmailCheckHandler', + () => ({ + default: ctx.UserPrimaryEmailCheckHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationBuilder, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/GeoIpLookup', () => ({ + default: ctx.GeoIpLookup, + })) + + vi.doMock('../../../../app/src/Features/Tutorial/TutorialHandler', () => ({ + default: ctx.TutorialHandler, + })) + + ctx.ProjectListController = (await import(MODULE_PATH)).default + + ctx.req = { query: {}, params: { - Project_id: this.project_id, + Project_id: ctx.project_id, }, headers: {}, session: { - user: this.user, + user: ctx.user, }, body: {}, i18n: { translate() {}, }, } - this.res = {} + ctx.res = {} }) describe('projectListPage', function () { - beforeEach(function () { - this.projects = [ + beforeEach(function (ctx) { + ctx.projects = [ { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, { _id: 2, @@ -205,184 +280,206 @@ describe('ProjectListController', function () { lastUpdatedBy: 'user-1', }, ] - this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] - this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] - this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] - this.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }] - this.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }] - this.allProjects = { - owned: this.projects, - readAndWrite: this.readAndWrite, - readOnly: this.readOnly, - tokenReadAndWrite: this.tokenReadAndWrite, - tokenReadOnly: this.tokenReadOnly, - review: this.review, + ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + ctx.tokenReadOnly = [{ _id: 7, lastUpdated: 4, owner_ref: 'user-5' }] + ctx.review = [{ _id: 8, lastUpdated: 4, owner_ref: 'user-6' }] + ctx.allProjects = { + owned: ctx.projects, + readAndWrite: ctx.readAndWrite, + readOnly: ctx.readOnly, + tokenReadAndWrite: ctx.tokenReadAndWrite, + tokenReadOnly: ctx.tokenReadOnly, + review: ctx.review, } - this.ProjectGetter.promises.findAllUsersProjects.resolves( - this.allProjects - ) + ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects) }) - it('should render the project/list-react page', function (done) { - this.res.render = (pageName, opts) => { - pageName.should.equal('project/list-react') - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should invoke the session maintenance', function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.res.render = () => { - this.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( - this.req, - this.user - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the tags', function (done) { - this.res.render = (pageName, opts) => { - opts.tags.length.should.equal(this.tags.length) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should create trigger ip matcher notifications', function (done) { - this.settings.overleaf = true - this.req.ip = '111.111.111.111' - this.res.render = (pageName, opts) => { - this.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal( - true - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the projects', function (done) { - this.res.render = (pageName, opts) => { - opts.prefetchedProjectsBlob.projects.length.should.equal( - this.projects.length + - this.readAndWrite.length + - this.readOnly.length + - this.tokenReadAndWrite.length + - this.tokenReadOnly.length + - this.review.length - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should send the user', function (done) { - this.res.render = (pageName, opts) => { - opts.user.should.deep.equal(this.user) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should inject the users', function (done) { - this.res.render = (pageName, opts) => { - const projects = opts.prefetchedProjectsBlob.projects - - projects - .filter(p => p.id === '1')[0] - .owner.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 1)[0].owner_ref] - .first_name - ) - projects - .filter(p => p.id === '2')[0] - .owner.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 2)[0].owner_ref] - .first_name - ) - projects - .filter(p => p.id === '2')[0] - .lastUpdatedBy.firstName.should.equal( - this.users[this.projects.filter(p => p._id === 2)[0].lastUpdatedBy] - .first_name - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it("should send the user's best subscription when saas feature present", function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.res.render = (pageName, opts) => { - expect(opts.usersBestSubscription).to.deep.include({ type: 'free' }) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should not return a best subscription without saas feature', function (done) { - this.Features.hasFeature.withArgs('saas').returns(false) - this.res.render = (pageName, opts) => { - expect(opts.usersBestSubscription).to.be.undefined - done() - } - this.ProjectListController.projectListPage(this.req, this.res) - }) - - it('should show INR Banner for Indian users with free account', function (done) { - // usersBestSubscription is only available when saas feature is present - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( - { - bestSubscription: { - type: 'free', - }, + it('should render the project/list-react page', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + pageName.should.equal('project/list-react') + resolve() } - ) - this.GeoIpLookup.promises.getCurrencyCode.resolves({ - countryCode: 'IN', + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - this.res.render = (pageName, opts) => { - expect(opts.showInrGeoBanner).to.be.true - done() - } - this.ProjectListController.projectListPage(this.req, this.res) }) - it('should not show INR Banner for Indian users with premium account', function (done) { - // usersBestSubscription is only available when saas feature is present - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( - { - bestSubscription: { - type: 'individual', - }, + it('should invoke the session maintenance', function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.res.render = () => { + ctx.SplitTestSessionHandler.promises.sessionMaintenance.should.have.been.calledWith( + ctx.req, + ctx.user + ) + resolve() } - ) - this.GeoIpLookup.promises.getCurrencyCode.resolves({ - countryCode: 'IN', + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the tags', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.tags.length.should.equal(ctx.tags.length) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should create trigger ip matcher notifications', function (ctx) { + return new Promise(resolve => { + ctx.settings.overleaf = true + ctx.req.ip = '111.111.111.111' + ctx.res.render = (pageName, opts) => { + ctx.NotificationBuilder.promises.ipMatcherAffiliation.called.should.equal( + true + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the projects', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.prefetchedProjectsBlob.projects.length.should.equal( + ctx.projects.length + + ctx.readAndWrite.length + + ctx.readOnly.length + + ctx.tokenReadAndWrite.length + + ctx.tokenReadOnly.length + + ctx.review.length + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should send the user', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.user.should.deep.equal(ctx.user) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should inject the users', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + const projects = opts.prefetchedProjectsBlob.projects + + projects + .filter(p => p.id === '1')[0] + .owner.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 1)[0].owner_ref] + .first_name + ) + projects + .filter(p => p.id === '2')[0] + .owner.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 2)[0].owner_ref] + .first_name + ) + projects + .filter(p => p.id === '2')[0] + .lastUpdatedBy.firstName.should.equal( + ctx.users[ctx.projects.filter(p => p._id === 2)[0].lastUpdatedBy] + .first_name + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it("should send the user's best subscription when saas feature present", function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.res.render = (pageName, opts) => { + expect(opts.usersBestSubscription).to.deep.include({ type: 'free' }) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should not return a best subscription without saas feature', function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saas').returns(false) + ctx.res.render = (pageName, opts) => { + expect(opts.usersBestSubscription).to.be.undefined + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should show INR Banner for Indian users with free account', function (ctx) { + return new Promise(resolve => { + // usersBestSubscription is only available when saas feature is present + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + { + bestSubscription: { + type: 'free', + }, + } + ) + ctx.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'IN', + }) + ctx.res.render = (pageName, opts) => { + expect(opts.showInrGeoBanner).to.be.true + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) + }) + + it('should not show INR Banner for Indian users with premium account', function (ctx) { + return new Promise(resolve => { + // usersBestSubscription is only available when saas feature is present + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + { + bestSubscription: { + type: 'individual', + }, + } + ) + ctx.GeoIpLookup.promises.getCurrencyCode.resolves({ + countryCode: 'IN', + }) + ctx.res.render = (pageName, opts) => { + expect(opts.showInrGeoBanner).to.be.false + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - this.res.render = (pageName, opts) => { - expect(opts.showInrGeoBanner).to.be.false - done() - } - this.ProjectListController.projectListPage(this.req, this.res) }) describe('With Institution SSO feature', function () { - beforeEach(function (done) { - this.institutionEmail = 'test@overleaf.com' - this.institutionName = 'Overleaf' - this.Features.hasFeature.withArgs('saml').returns(true) - this.Features.hasFeature.withArgs('affiliations').returns(true) - this.Features.hasFeature.withArgs('saas').returns(true) - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.institutionEmail = 'test@overleaf.com' + ctx.institutionName = 'Overleaf' + ctx.Features.hasFeature.withArgs('saml').returns(true) + ctx.Features.hasFeature.withArgs('affiliations').returns(true) + ctx.Features.hasFeature.withArgs('saas').returns(true) + resolve() + }) }) - it('should show institution SSO available notification for confirmed domains', function () { - this.UserGetter.promises.getUserFullEmails.resolves([ + it('should show institution SSO available notification for confirmed domains', function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@overleaf.com', affiliation: { @@ -396,64 +493,64 @@ describe('ProjectListController', function () { }, }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, + email: ctx.institutionEmail, institutionId: 1, - institutionName: this.institutionName, + institutionName: ctx.institutionName, templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a linked notification', function () { - this.req.session.saml = { - institutionEmail: this.institutionEmail, + it('should show a linked notification', function (ctx) { + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, - institutionName: this.institutionName, + email: ctx.institutionEmail, + institutionName: ctx.institutionName, templateKey: 'notification_institution_sso_linked', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a linked another email notification', function () { + it('should show a linked another email notification', function (ctx) { // when they request to link an email but the institution returns // a different email - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - institutionEmail: this.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: 'requested@overleaf.com', templateKey: 'notification_institution_sso_non_canonical', }) } - this.req.session.saml = { - emailNonCanonical: this.institutionEmail, - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + emailNonCanonical: ctx.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: 'requested@overleaf.com', linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show a notification when intent was to register via SSO but account existed', function () { - this.res.render = (pageName, opts) => { + it('should show a notification when intent was to register via SSO but account existed', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ - email: this.institutionEmail, + email: ctx.institutionEmail, templateKey: 'notification_institution_sso_already_registered', }) } - this.req.session.saml = { - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, universityName: 'Overleaf', @@ -463,29 +560,29 @@ describe('ProjectListController', function () { name: 'Example University', }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should not show a register notification if the flow was abandoned', function () { + it('should not show a register notification if the flow was abandoned', function (ctx) { // could initially start to register with an SSO email and then // abandon flow and login with an existing non-institution SSO email - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', templateKey: 'notification_institution_sso_already_registered', }) } - this.req.session.saml = { + ctx.req.session.saml = { registerIntercept: { id: 1, name: 'Example University', }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should show error notification', function () { - this.res.render = (pageName, opts) => { + it('should show error notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution.length).to.equal(1) expect(opts.notificationsInstitution[0].templateKey).to.equal( 'notification_institution_sso_error' @@ -494,81 +591,85 @@ describe('ProjectListController', function () { Errors.SAMLAlreadyLinkedError ) } - this.req.session.saml = { - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + institutionEmail: ctx.institutionEmail, error: new Errors.SAMLAlreadyLinkedError(), } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) describe('for an unconfirmed domain for an SSO institution', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUserFullEmails.resolves([ - { - email: 'test@overleaf-uncofirmed.com', - affiliation: { - institution: { - id: 1, - confirmed: false, - name: 'Overleaf', - ssoBeta: false, - ssoEnabled: true, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserFullEmails.resolves([ + { + email: 'test@overleaf-uncofirmed.com', + affiliation: { + institution: { + id: 1, + confirmed: false, + name: 'Overleaf', + ssoBeta: false, + ssoEnabled: true, + }, }, }, - }, - ]) - done() + ]) + resolve() + }) }) - it('should not show institution SSO available notification', function () { - this.res.render = (pageName, opts) => { + it('should not show institution SSO available notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution.length).to.equal(0) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('when linking/logging in initiated on institution side', function () { - it('should not show a linked another email notification', function () { + it('should not show a linked another email notification', function (ctx) { // this is only used when initated on Overleaf, // because we keep track of the requested email they tried to link - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.not.deep.include({ - institutionEmail: this.institutionEmail, + institutionEmail: ctx.institutionEmail, requestedEmail: undefined, templateKey: 'notification_institution_sso_non_canonical', }) } - this.req.session.saml = { - emailNonCanonical: this.institutionEmail, - institutionEmail: this.institutionEmail, + ctx.req.session.saml = { + emailNonCanonical: ctx.institutionEmail, + institutionEmail: ctx.institutionEmail, linked: { hasEntitlement: false, - universityName: this.institutionName, + universityName: ctx.institutionName, }, } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('Institution with SSO beta testable', function () { - beforeEach(function (done) { - this.UserGetter.promises.getUserFullEmails.resolves([ - { - email: 'beta@beta.com', - affiliation: { - institution: { - id: 2, - confirmed: true, - name: 'Beta University', - ssoBeta: true, - ssoEnabled: false, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.UserGetter.promises.getUserFullEmails.resolves([ + { + email: 'beta@beta.com', + affiliation: { + institution: { + id: 2, + confirmed: true, + name: 'Beta University', + ssoBeta: true, + ssoEnabled: false, + }, }, }, - }, - ]) - done() + ]) + resolve() + }) }) - it('should show institution SSO available notification when on a beta testing session', function () { - this.req.session.samlBeta = true - this.res.render = (pageName, opts) => { + it('should show institution SSO available notification when on a beta testing session', function (ctx) { + ctx.req.session.samlBeta = true + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.include({ email: 'beta@beta.com', institutionId: 2, @@ -576,11 +677,11 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('should not show institution SSO available notification when not on a beta testing session', function () { - this.req.session.samlBeta = false - this.res.render = (pageName, opts) => { + it('should not show institution SSO available notification when not on a beta testing session', function (ctx) { + ctx.req.session.samlBeta = false + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', institutionId: 1, @@ -588,18 +689,20 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) }) describe('Without Institution SSO feature', function () { - beforeEach(function (done) { - this.Features.hasFeature.withArgs('saml').returns(false) - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.Features.hasFeature.withArgs('saml').returns(false) + resolve() + }) }) - it('should not show institution sso available notification', function () { - this.res.render = (pageName, opts) => { + it('should not show institution sso available notification', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.notificationsInstitution).to.deep.not.include({ email: 'test@overleaf.com', institutionId: 1, @@ -607,35 +710,33 @@ describe('ProjectListController', function () { templateKey: 'notification_institution_sso_available', }) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('enterprise banner', function () { - beforeEach(function (done) { - this.Features.hasFeature.withArgs('saas').returns(true) - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + beforeEach(function (ctx) { + ctx.Features.hasFeature.withArgs('saas').returns(true) + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( { memberGroupSubscriptions: [] } ) - this.UserGetter.promises.getUserFullEmails.resolves([ + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@test-domain.com', }, ]) - - done() }) describe('normal enterprise banner', function () { - it('shows banner', function () { - this.res.render = (pageName, opts) => { + it('shows banner', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.true } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('does not show banner if user is part of any affiliation', function () { - this.UserGetter.promises.getUserFullEmails.resolves([ + it('does not show banner if user is part of any affiliation', function (ctx) { + ctx.UserGetter.promises.getUserFullEmails.resolves([ { email: 'test@overleaf.com', affiliation: { @@ -651,36 +752,36 @@ describe('ProjectListController', function () { }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('does not show banner if user is part of any group subscription', function () { - this.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( + it('does not show banner if user is part of any group subscription', function (ctx) { + ctx.SubscriptionViewModelBuilder.promises.getUsersSubscriptionDetails.resolves( { memberGroupSubscriptions: [{}] } ) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) - it('have a banner variant of "FOMO" or "on-premise"', function () { - this.res.render = (pageName, opts) => { + it('have a banner variant of "FOMO" or "on-premise"', function (ctx) { + ctx.res.render = (pageName, opts) => { expect(opts.groupsAndEnterpriseBannerVariant).to.be.oneOf([ 'FOMO', 'on-premise', ]) } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) describe('US government enterprise banner', function () { - it('does not show enterprise banner if US government enterprise banner is shown', function () { + it('does not show enterprise banner if US government enterprise banner is shown', function (ctx) { const emails = [ { email: 'test@test.mil', @@ -688,8 +789,8 @@ describe('ProjectListController', function () { }, ] - this.UserGetter.promises.getUserFullEmails.resolves(emails) - this.Modules.promises.hooks.fire + ctx.UserGetter.promises.getUserFullEmails.resolves(emails) + ctx.Modules.promises.hooks.fire .withArgs('getUSGovBanner', emails, false, []) .resolves([ { @@ -697,66 +798,68 @@ describe('ProjectListController', function () { usGovBannerVariant: 'variant', }, ]) - this.res.render = (pageName, opts) => { + ctx.res.render = (pageName, opts) => { expect(opts.showGroupsAndEnterpriseBanner).to.be.false expect(opts.showUSGovBanner).to.be.true } - this.ProjectListController.projectListPage(this.req, this.res) + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) }) }) }) }) describe('projectListReactPage with duplicate projects', function () { - beforeEach(function () { - this.projects = [ + beforeEach(function (ctx) { + ctx.projects = [ { _id: 1, lastUpdated: 1, owner_ref: 'user-1' }, { _id: 2, lastUpdated: 2, owner_ref: 'user-2' }, ] - this.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] - this.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] - this.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] - this.tokenReadOnly = [ + ctx.readAndWrite = [{ _id: 5, lastUpdated: 5, owner_ref: 'user-1' }] + ctx.readOnly = [{ _id: 3, lastUpdated: 3, owner_ref: 'user-1' }] + ctx.tokenReadAndWrite = [{ _id: 6, lastUpdated: 5, owner_ref: 'user-4' }] + ctx.tokenReadOnly = [ { _id: 6, lastUpdated: 5, owner_ref: 'user-4' }, // Also in tokenReadAndWrite { _id: 7, lastUpdated: 4, owner_ref: 'user-5' }, ] - this.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }] - this.allProjects = { - owned: this.projects, - readAndWrite: this.readAndWrite, - readOnly: this.readOnly, - tokenReadAndWrite: this.tokenReadAndWrite, - tokenReadOnly: this.tokenReadOnly, - review: this.review, + ctx.review = [{ _id: 8, lastUpdated: 5, owner_ref: 'user-6' }] + ctx.allProjects = { + owned: ctx.projects, + readAndWrite: ctx.readAndWrite, + readOnly: ctx.readOnly, + tokenReadAndWrite: ctx.tokenReadAndWrite, + tokenReadOnly: ctx.tokenReadOnly, + review: ctx.review, } - this.ProjectGetter.promises.findAllUsersProjects.resolves( - this.allProjects - ) + ctx.ProjectGetter.promises.findAllUsersProjects.resolves(ctx.allProjects) }) - it('should render the project/list-react page', function (done) { - this.res.render = (pageName, opts) => { - pageName.should.equal('project/list-react') - done() - } - this.ProjectListController.projectListPage(this.req, this.res) + it('should render the project/list-react page', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + pageName.should.equal('project/list-react') + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) }) - it('should omit one of the projects', function (done) { - this.res.render = (pageName, opts) => { - opts.prefetchedProjectsBlob.projects.length.should.equal( - this.projects.length + - this.readAndWrite.length + - this.readOnly.length + - this.tokenReadAndWrite.length + - this.tokenReadOnly.length + - this.review.length - - 1 - ) - done() - } - this.ProjectListController.projectListPage(this.req, this.res) + it('should omit one of the projects', function (ctx) { + return new Promise(resolve => { + ctx.res.render = (pageName, opts) => { + opts.prefetchedProjectsBlob.projects.length.should.equal( + ctx.projects.length + + ctx.readAndWrite.length + + ctx.readOnly.length + + ctx.tokenReadAndWrite.length + + ctx.tokenReadOnly.length + + ctx.review.length - + 1 + ) + resolve() + } + ctx.ProjectListController.projectListPage(ctx.req, ctx.res) + }) }) }) }) diff --git a/services/web/test/unit/src/Referal/ReferalConnect.test.mjs b/services/web/test/unit/src/Referal/ReferalConnect.test.mjs index c6e56c3c6a..33e6c6816e 100644 --- a/services/web/test/unit/src/Referal/ReferalConnect.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalConnect.test.mjs @@ -1,132 +1,153 @@ -import esmock from 'esmock' const modulePath = new URL( '../../../../app/src/Features/Referal/ReferalConnect.mjs', import.meta.url ).pathname describe('Referal connect middle wear', function () { - beforeEach(async function () { - this.connect = await esmock.strict(modulePath, {}) + beforeEach(async function (ctx) { + ctx.connect = (await import(modulePath)).default }) - it('should take a referal query string and put it on the session if it exists', function (done) { - const req = { - query: { referal: '12345' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal(req.query.referal) - done() + it('should take a referal query string and put it on the session if it exists', function (ctx) { + return new Promise(resolve => { + const req = { + query: { referal: '12345' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal(req.query.referal) + resolve() + }) }) }) - it('should not change the referal_id on the session if not in query', function (done) { - const req = { - query: {}, - session: { referal_id: 'same' }, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal('same') - done() + it('should not change the referal_id on the session if not in query', function (ctx) { + return new Promise(resolve => { + const req = { + query: {}, + session: { referal_id: 'same' }, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal('same') + resolve() + }) }) }) - it('should take a facebook referal query string and put it on the session if it exists', function (done) { - const req = { - query: { fb_ref: '12345' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_id.should.equal(req.query.fb_ref) - done() + it('should take a facebook referal query string and put it on the session if it exists', function (ctx) { + return new Promise(resolve => { + const req = { + query: { fb_ref: '12345' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_id.should.equal(req.query.fb_ref) + resolve() + }) }) }) - it('should map the facebook medium into the session', function (done) { - const req = { - query: { rm: 'fb' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('facebook') - done() + it('should map the facebook medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'fb' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('facebook') + resolve() + }) }) }) - it('should map the twitter medium into the session', function (done) { - const req = { - query: { rm: 't' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('twitter') - done() + it('should map the twitter medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 't' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('twitter') + resolve() + }) }) }) - it('should map the google plus medium into the session', function (done) { - const req = { - query: { rm: 'gp' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('google_plus') - done() + it('should map the google plus medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'gp' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('google_plus') + resolve() + }) }) }) - it('should map the email medium into the session', function (done) { - const req = { - query: { rm: 'e' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('email') - done() + it('should map the email medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'e' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('email') + resolve() + }) }) }) - it('should map the direct medium into the session', function (done) { - const req = { - query: { rm: 'd' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_medium.should.equal('direct') - done() + it('should map the direct medium into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rm: 'd' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_medium.should.equal('direct') + resolve() + }) }) }) - it('should map the bonus source into the session', function (done) { - const req = { - query: { rs: 'b' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('bonus') - done() + it('should map the bonus source into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'b' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('bonus') + resolve() + }) }) }) - it('should map the public share source into the session', function (done) { - const req = { - query: { rs: 'ps' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('public_share') - done() + it('should map the public share source into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'ps' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('public_share') + resolve() + }) }) }) - it('should map the collaborator invite into the session', function (done) { - const req = { - query: { rs: 'ci' }, - session: {}, - } - this.connect.use(req, {}, () => { - req.session.referal_source.should.equal('collaborator_invite') - done() + it('should map the collaborator invite into the session', function (ctx) { + return new Promise(resolve => { + const req = { + query: { rs: 'ci' }, + session: {}, + } + ctx.connect.use(req, {}, () => { + req.session.referal_source.should.equal('collaborator_invite') + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/Referal/ReferalController.test.mjs b/services/web/test/unit/src/Referal/ReferalController.test.mjs index 523fd23728..0a7b8aa87d 100644 --- a/services/web/test/unit/src/Referal/ReferalController.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalController.test.mjs @@ -1,11 +1,7 @@ -import esmock from 'esmock' -const modulePath = new URL( - '../../../../app/src/Features/Referal/ReferalController.js', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Referal/ReferalController.js' -describe('Referal controller', function () { - beforeEach(async function () { - this.controller = await esmock.strict(modulePath, {}) +describe.skip('Referal controller', function () { + beforeEach(async function (ctx) { + ctx.controller = (await import(modulePath)).default }) }) diff --git a/services/web/test/unit/src/Referal/ReferalHandler.test.mjs b/services/web/test/unit/src/Referal/ReferalHandler.test.mjs index 6fd58a6569..5174918bd7 100644 --- a/services/web/test/unit/src/Referal/ReferalHandler.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalHandler.test.mjs @@ -1,88 +1,86 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' -const modulePath = new URL( - '../../../../app/src/Features/Referal/ReferalHandler.mjs', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Referal/ReferalHandler.mjs' describe('Referal handler', function () { - beforeEach(async function () { - this.User = { + beforeEach(async function (ctx) { + ctx.User = { findById: sinon.stub().returns({ exec: sinon.stub(), }), } - this.handler = await esmock.strict(modulePath, { - '../../../../app/src/models/User': { - User: this.User, - }, - }) - this.user_id = '12313' + + vi.doMock('../../../../app/src/models/User', () => ({ + User: ctx.User, + })) + + ctx.handler = (await import(modulePath)).default + ctx.user_id = '12313' }) describe('getting refered user_ids', function () { - it('should get the user from mongo and return the refered users array', async function () { + it('should get the user from mongo and return the refered users array', async function (ctx) { const user = { refered_users: ['1234', '312312', '3213129'], refered_user_count: 3, } - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUsers: passedReferedUserIds, referedUserCount: passedReferedUserCount, - } = await this.handler.promises.getReferedUsers(this.user_id) + } = await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserIds.should.deep.equal(user.refered_users) passedReferedUserCount.should.equal(3) }) - it('should return an empty array if it is not set', async function () { + it('should return an empty array if it is not set', async function (ctx) { const user = {} - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUsers: passedReferedUserIds } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserIds.length.should.equal(0) }) - it('should return a zero count if neither it or the array are set', async function () { + it('should return a zero count if neither it or the array are set', async function (ctx) { const user = {} - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUserCount: passedReferedUserCount } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserCount.should.equal(0) }) - it('should return the array length if count is not set', async function () { + it('should return the array length if count is not set', async function (ctx) { const user = { refered_users: ['1234', '312312', '3213129'] } - this.User.findById.returns({ + ctx.User.findById.returns({ exec: sinon.stub().resolves(user), }) const { referedUserCount: passedReferedUserCount } = - await this.handler.promises.getReferedUsers(this.user_id) + await ctx.handler.promises.getReferedUsers(ctx.user_id) passedReferedUserCount.should.equal(3) }) - it('should error if finding the user fails', async function () { - this.User.findById.returns({ + it('should error if finding the user fails', async function (ctx) { + ctx.User.findById.returns({ exec: sinon.stub().rejects(new Error('user not found')), }) expect( - this.handler.promises.getReferedUsers(this.user_id) + ctx.handler.promises.getReferedUsers(ctx.user_id) ).to.be.rejectedWith('user not found') }) }) diff --git a/services/web/test/unit/src/References/ReferencesController.test.mjs b/services/web/test/unit/src/References/ReferencesController.test.mjs index fca2acea12..679e835840 100644 --- a/services/web/test/unit/src/References/ReferencesController.test.mjs +++ b/services/web/test/unit/src/References/ReferencesController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' @@ -6,182 +6,207 @@ const modulePath = '../../../../app/src/Features/References/ReferencesController' describe('ReferencesController', function () { - beforeEach(async function () { - this.projectId = '2222' - this.controller = await esmock.strict(modulePath, { - '@overleaf/settings': (this.settings = { + beforeEach(async function (ctx) { + ctx.projectId = '2222' + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { apis: { web: { url: 'http://some.url' } }, }), - '../../../../app/src/Features/References/ReferencesHandler': - (this.ReferencesHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/References/ReferencesHandler', + () => ({ + default: (ctx.ReferencesHandler = { index: sinon.stub(), indexAll: sinon.stub(), }), - '../../../../app/src/Features/Editor/EditorRealTimeController': - (this.EditorRealTimeController = { + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: (ctx.EditorRealTimeController = { emitToRoom: sinon.stub(), }), - }) - this.req = new MockRequest() - this.req.params.Project_id = this.projectId - this.req.body = { - docIds: (this.docIds = ['aaa', 'bbb']), + }) + ) + + ctx.controller = (await import(modulePath)).default + ctx.req = new MockRequest() + ctx.req.params.Project_id = ctx.projectId + ctx.req.body = { + docIds: (ctx.docIds = ['aaa', 'bbb']), shouldBroadcast: false, } - this.res = new MockResponse() - this.res.json = sinon.stub() - this.res.sendStatus = sinon.stub() - this.next = sinon.stub() - this.fakeResponseData = { - projectId: this.projectId, + ctx.res = new MockResponse() + ctx.res.json = sinon.stub() + ctx.res.sendStatus = sinon.stub() + ctx.next = sinon.stub() + ctx.fakeResponseData = { + projectId: ctx.projectId, keys: ['one', 'two', 'three'], } }) describe('indexAll', function () { - beforeEach(function () { - this.req.body = { shouldBroadcast: false } - this.ReferencesHandler.indexAll.callsArgWith( - 1, - null, - this.fakeResponseData - ) - this.call = callback => { - this.controller.indexAll(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.req.body = { shouldBroadcast: false } + ctx.ReferencesHandler.indexAll.callsArgWith(1, null, ctx.fakeResponseData) + ctx.call = callback => { + ctx.controller.indexAll(ctx.req, ctx.res, ctx.next) return callback() } }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) - it('should call ReferencesHandler.indexAll', function (done) { - this.call(() => { - this.ReferencesHandler.indexAll.callCount.should.equal(1) - this.ReferencesHandler.indexAll - .calledWith(this.projectId) - .should.equal(true) - done() + it('should call ReferencesHandler.indexAll', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.ReferencesHandler.indexAll.callCount.should.equal(1) + ctx.ReferencesHandler.indexAll + .calledWith(ctx.projectId) + .should.equal(true) + resolve() + }) }) }) describe('when shouldBroadcast is true', function () { - beforeEach(function () { - this.ReferencesHandler.index.callsArgWith( - 2, - null, - this.fakeResponseData - ) - this.req.body.shouldBroadcast = true + beforeEach(function (ctx) { + ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData) + ctx.req.body.shouldBroadcast = true }) - it('should call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(1) - done() + it('should call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(1) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should still return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should still return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) }) describe('when shouldBroadcast is false', function () { - beforeEach(function () { - this.ReferencesHandler.index.callsArgWith( - 2, - null, - this.fakeResponseData - ) - this.req.body.shouldBroadcast = false + beforeEach(function (ctx) { + ctx.ReferencesHandler.index.callsArgWith(2, null, ctx.fakeResponseData) + ctx.req.body.shouldBroadcast = false }) - it('should not call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) - done() + it('should not call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should still return data', function (done) { - this.call(() => { - this.res.json.callCount.should.equal(1) - this.res.json.calledWith(this.fakeResponseData).should.equal(true) - done() + it('should still return data', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.callCount.should.equal(1) + ctx.res.json.calledWith(ctx.fakeResponseData).should.equal(true) + resolve() + }) }) }) }) }) describe('there is no data', function () { - beforeEach(function () { - this.ReferencesHandler.indexAll.callsArgWith(1) - this.call = callback => { - this.controller.indexAll(this.req, this.res, this.next) + beforeEach(function (ctx) { + ctx.ReferencesHandler.indexAll.callsArgWith(1) + ctx.call = callback => { + ctx.controller.indexAll(ctx.req, ctx.res, ctx.next) callback() } }) - it('should not call EditorRealTimeController.emitToRoom', function (done) { - this.call(() => { - this.EditorRealTimeController.emitToRoom.callCount.should.equal(0) - done() + it('should not call EditorRealTimeController.emitToRoom', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.EditorRealTimeController.emitToRoom.callCount.should.equal(0) + resolve() + }) }) }) - it('should not produce an error', function (done) { - this.call(() => { - this.res.sendStatus.callCount.should.equal(0) - this.res.sendStatus.calledWith(500).should.equal(false) - this.res.sendStatus.calledWith(400).should.equal(false) - done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.sendStatus.callCount.should.equal(0) + ctx.res.sendStatus.calledWith(500).should.equal(false) + ctx.res.sendStatus.calledWith(400).should.equal(false) + resolve() + }) }) }) - it('should send a response with an empty keys list', function (done) { - this.call(() => { - this.res.json.called.should.equal(true) - this.res.json - .calledWith({ projectId: this.projectId, keys: [] }) - .should.equal(true) - done() + it('should send a response with an empty keys list', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.res.json.called.should.equal(true) + ctx.res.json + .calledWith({ projectId: ctx.projectId, keys: [] }) + .should.equal(true) + resolve() + }) }) }) }) diff --git a/services/web/test/unit/src/References/ReferencesHandler.test.mjs b/services/web/test/unit/src/References/ReferencesHandler.test.mjs index 57570dcf12..ae7b86822a 100644 --- a/services/web/test/unit/src/References/ReferencesHandler.test.mjs +++ b/services/web/test/unit/src/References/ReferencesHandler.test.mjs @@ -1,11 +1,4 @@ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -import esmock from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import sinon from 'sinon' @@ -13,13 +6,17 @@ import Errors from '../../../../app/src/Features/Errors/Errors.js' const modulePath = '../../../../app/src/Features/References/ReferencesHandler.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('ReferencesHandler', function () { - beforeEach(async function () { - this.projectId = '222' - this.historyId = 42 - this.fakeProject = { - _id: this.projectId, - owner_ref: (this.fakeOwner = { + beforeEach(async function (ctx) { + ctx.projectId = '222' + ctx.historyId = 42 + ctx.fakeProject = { + _id: ctx.projectId, + owner_ref: (ctx.fakeOwner = { _id: 'some_owner', features: { references: false, @@ -43,11 +40,12 @@ describe('ReferencesHandler', function () { ], }, ], - overleaf: { history: { id: this.historyId } }, + overleaf: { history: { id: ctx.historyId } }, } - this.docIds = ['aaa', 'ccc'] - this.handler = await esmock.strict(modulePath, { - '@overleaf/settings': (this.settings = { + ctx.docIds = ['aaa', 'ccc'] + + vi.doMock('@overleaf/settings', () => ({ + default: (ctx.settings = { apis: { references: { url: 'http://some.url/references' }, docstore: { url: 'http://some.url/docstore' }, @@ -56,228 +54,278 @@ describe('ReferencesHandler', function () { }, enableProjectHistoryBlobs: true, }), - request: (this.request = { + })) + + vi.doMock('request', () => ({ + default: (ctx.request = { get: sinon.stub(), post: sinon.stub(), }), - '../../../../app/src/Features/Project/ProjectGetter': - (this.ProjectGetter = { - getProject: sinon.stub().callsArgWith(2, null, this.fakeProject), - }), - '../../../../app/src/Features/User/UserGetter': (this.UserGetter = { + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: (ctx.ProjectGetter = { + getProject: sinon.stub().callsArgWith(2, null, ctx.fakeProject), + }), + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: (ctx.UserGetter = { getUser: sinon.stub(), }), - '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler': - (this.DocumentUpdaterHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler', + () => ({ + default: (ctx.DocumentUpdaterHandler = { flushDocToMongo: sinon.stub().callsArgWith(2, null), }), - '../../../../app/src/infrastructure/Features': (this.Features = { + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: (ctx.Features = { hasFeature: sinon.stub().returns(true), }), - }) - this.fakeResponseData = { - projectId: this.projectId, + })) + + ctx.handler = (await import(modulePath)).default + ctx.fakeResponseData = { + projectId: ctx.projectId, keys: ['k1', 'k2'], } }) describe('indexAll', function () { - beforeEach(function () { - sinon.stub(this.handler, '_findBibDocIds').returns(['aaa', 'ccc']) + beforeEach(function (ctx) { + sinon.stub(ctx.handler, '_findBibDocIds').returns(['aaa', 'ccc']) sinon - .stub(this.handler, '_findBibFileRefs') + .stub(ctx.handler, '_findBibFileRefs') .returns([{ _id: 'fff' }, { _id: 'ggg', hash: 'hash' }]) - sinon.stub(this.handler, '_isFullIndex').callsArgWith(1, null, true) - this.request.post.callsArgWith( + sinon.stub(ctx.handler, '_isFullIndex').callsArgWith(1, null, true) + ctx.request.post.callsArgWith( 1, null, { statusCode: 200 }, - this.fakeResponseData + ctx.fakeResponseData ) - return (this.call = callback => { - return this.handler.indexAll(this.projectId, callback) + return (ctx.call = callback => { + return ctx.handler.indexAll(ctx.projectId, callback) }) }) - it('should call _findBibDocIds', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.handler._findBibDocIds.callCount.should.equal(1) - this.handler._findBibDocIds - .calledWith(this.fakeProject) - .should.equal(true) - return done() + it('should call _findBibDocIds', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.handler._findBibDocIds.callCount.should.equal(1) + ctx.handler._findBibDocIds + .calledWith(ctx.fakeProject) + .should.equal(true) + return resolve() + }) }) }) - it('should call _findBibFileRefs', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.handler._findBibDocIds.callCount.should.equal(1) - this.handler._findBibDocIds - .calledWith(this.fakeProject) - .should.equal(true) - return done() + it('should call _findBibFileRefs', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.handler._findBibDocIds.callCount.should.equal(1) + ctx.handler._findBibDocIds + .calledWith(ctx.fakeProject) + .should.equal(true) + return resolve() + }) }) }) - it('should call DocumentUpdaterHandler.flushDocToMongo', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) - return done() + it('should call DocumentUpdaterHandler.flushDocToMongo', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.DocumentUpdaterHandler.flushDocToMongo.callCount.should.equal(2) + return resolve() + }) }) }) - it('should make a request to references service', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - this.request.post.callCount.should.equal(1) - const arg = this.request.post.firstCall.args[0] - expect(arg.json).to.have.all.keys('docUrls', 'sourceURLs', 'fullIndex') - expect(arg.json.docUrls.length).to.equal(4) - expect(arg.json.docUrls).to.deep.equal([ - `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/aaa/raw`, - `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/ccc/raw`, - `${this.settings.apis.filestore.url}/project/${this.projectId}/file/fff?from=bibFileUrls`, - `${this.settings.apis.filestore.url}/project/${this.projectId}/file/ggg?from=bibFileUrls`, - ]) - expect(arg.json.sourceURLs.length).to.equal(4) - expect(arg.json.sourceURLs).to.deep.equal([ - { - url: `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/aaa/raw`, - }, - { - url: `${this.settings.apis.docstore.url}/project/${this.projectId}/doc/ccc/raw`, - }, - { - url: `${this.settings.apis.filestore.url}/project/${this.projectId}/file/fff?from=bibFileUrls`, - }, - { - url: `${this.settings.apis.project_history.url}/project/${this.historyId}/blob/hash`, - fallbackURL: `${this.settings.apis.filestore.url}/project/${this.projectId}/file/ggg?from=bibFileUrls`, - }, - ]) - expect(arg.json.fullIndex).to.equal(true) - return done() + it('should make a request to references service', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + ctx.request.post.callCount.should.equal(1) + const arg = ctx.request.post.firstCall.args[0] + expect(arg.json).to.have.all.keys( + 'docUrls', + 'sourceURLs', + 'fullIndex' + ) + expect(arg.json.docUrls.length).to.equal(4) + expect(arg.json.docUrls).to.deep.equal([ + `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`, + `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`, + `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/fff?from=bibFileUrls`, + `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/ggg?from=bibFileUrls`, + ]) + expect(arg.json.sourceURLs.length).to.equal(4) + expect(arg.json.sourceURLs).to.deep.equal([ + { + url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/aaa/raw`, + }, + { + url: `${ctx.settings.apis.docstore.url}/project/${ctx.projectId}/doc/ccc/raw`, + }, + { + url: `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/fff?from=bibFileUrls`, + }, + { + url: `${ctx.settings.apis.project_history.url}/project/${ctx.historyId}/blob/hash`, + fallbackURL: `${ctx.settings.apis.filestore.url}/project/${ctx.projectId}/file/ggg?from=bibFileUrls`, + }, + ]) + expect(arg.json.fullIndex).to.equal(true) + return resolve() + }) }) }) - it('should not produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.equal(null) - return done() + it('should not produce an error', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.equal(null) + return resolve() + }) }) }) - it('should return data', function (done) { - return this.call((err, data) => { - expect(err).to.be.null - expect(data).to.not.equal(null) - expect(data).to.not.equal(undefined) - expect(data).to.equal(this.fakeResponseData) - return done() + it('should return data', function (ctx) { + return new Promise(resolve => { + return ctx.call((err, data) => { + expect(err).to.be.null + expect(data).to.not.equal(null) + expect(data).to.not.equal(undefined) + expect(data).to.equal(ctx.fakeResponseData) + return resolve() + }) }) }) describe('when ProjectGetter.getProject produces an error', function () { - beforeEach(function () { - return this.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, new Error('woops')) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when ProjectGetter.getProject returns null', function () { - beforeEach(function () { - return this.ProjectGetter.getProject.callsArgWith(2, null) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Errors.NotFoundError) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Errors.NotFoundError) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when _isFullIndex produces an error', function () { - beforeEach(function () { - this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) - return this.handler._isFullIndex.callsArgWith(1, new Error('woops')) + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject) + ctx.handler._isFullIndex.callsArgWith(1, new Error('woops')) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) describe('when flushDocToMongo produces an error', function () { - beforeEach(function () { - this.ProjectGetter.getProject.callsArgWith(2, null, this.fakeProject) - this.handler._isFullIndex.callsArgWith(1, false) - return this.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( + beforeEach(function (ctx) { + ctx.ProjectGetter.getProject.callsArgWith(2, null, ctx.fakeProject) + ctx.handler._isFullIndex.callsArgWith(1, false) + ctx.DocumentUpdaterHandler.flushDocToMongo.callsArgWith( 2, new Error('woops') ) }) - it('should produce an error', function (done) { - return this.call((err, data) => { - expect(err).to.not.equal(null) - expect(err).to.be.instanceof(Error) - expect(data).to.equal(undefined) - return done() + it('should produce an error', function (ctx) { + return new Promise(resolve => { + ctx.call((err, data) => { + expect(err).to.not.equal(null) + expect(err).to.be.instanceof(Error) + expect(data).to.equal(undefined) + resolve() + }) }) }) - it('should not send request', function (done) { - return this.call(() => { - this.request.post.callCount.should.equal(0) - return done() + it('should not send request', function (ctx) { + return new Promise(resolve => { + ctx.call(() => { + ctx.request.post.callCount.should.equal(0) + resolve() + }) }) }) }) }) describe('_findBibDocIds', function () { - beforeEach(function () { - this.fakeProject = { + beforeEach(function (ctx) { + ctx.fakeProject = { rootFolder: [ { docs: [ @@ -290,24 +338,24 @@ describe('ReferencesHandler', function () { }, ], } - return (this.expectedIds = ['aaa', 'ccc']) + ctx.expectedIds = ['aaa', 'ccc'] }) - it('should select the correct docIds', function () { - const result = this.handler._findBibDocIds(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should select the correct docIds', function (ctx) { + const result = ctx.handler._findBibDocIds(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) - it('should not error with a non array of folders from dirty data', function () { - this.fakeProject.rootFolder[0].folders[0].folders = {} - const result = this.handler._findBibDocIds(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should not error with a non array of folders from dirty data', function (ctx) { + ctx.fakeProject.rootFolder[0].folders[0].folders = {} + const result = ctx.handler._findBibDocIds(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) }) describe('_findBibFileRefs', function () { - beforeEach(function () { - this.fakeProject = { + beforeEach(function (ctx) { + ctx.fakeProject = { rootFolder: [ { docs: [ @@ -325,73 +373,73 @@ describe('ReferencesHandler', function () { }, ], } - this.expectedIds = [ - this.fakeProject.rootFolder[0].fileRefs[0], - this.fakeProject.rootFolder[0].folders[0].fileRefs[0], + ctx.expectedIds = [ + ctx.fakeProject.rootFolder[0].fileRefs[0], + ctx.fakeProject.rootFolder[0].folders[0].fileRefs[0], ] }) - it('should select the correct docIds', function () { - const result = this.handler._findBibFileRefs(this.fakeProject) - return expect(result).to.deep.equal(this.expectedIds) + it('should select the correct docIds', function (ctx) { + const result = ctx.handler._findBibFileRefs(ctx.fakeProject) + expect(result).to.deep.equal(ctx.expectedIds) }) }) describe('_isFullIndex', function () { - beforeEach(function () { - this.fakeProject = { owner_ref: (this.owner_ref = 'owner-ref-123') } - this.owner = { + beforeEach(function (ctx) { + ctx.fakeProject = { owner_ref: (ctx.owner_ref = 'owner-ref-123') } + ctx.owner = { features: { references: false, }, } - this.UserGetter.getUser = sinon.stub() - this.UserGetter.getUser - .withArgs(this.owner_ref, { features: true }) - .yields(null, this.owner) - return (this.call = callback => { - return this.handler._isFullIndex(this.fakeProject, callback) - }) + ctx.UserGetter.getUser = sinon.stub() + ctx.UserGetter.getUser + .withArgs(ctx.owner_ref, { features: true }) + .yields(null, ctx.owner) + ctx.call = callback => { + ctx.handler._isFullIndex(ctx.fakeProject, callback) + } }) describe('with references feature on', function () { - beforeEach(function () { - return (this.owner.features.references = true) + beforeEach(function (ctx) { + ctx.owner.features.references = true }) - it('should return true', function () { - return this.call((err, isFullIndex) => { + it('should return true', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(true) + expect(isFullIndex).to.equal(true) }) }) }) describe('with references feature off', function () { - beforeEach(function () { - return (this.owner.features.references = false) + beforeEach(function (ctx) { + ctx.owner.features.references = false }) - it('should return false', function () { - return this.call((err, isFullIndex) => { + it('should return false', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(false) + expect(isFullIndex).to.equal(false) }) }) }) describe('with referencesSearch', function () { - beforeEach(function () { - return (this.owner.features = { + beforeEach(function (ctx) { + ctx.owner.features = { referencesSearch: true, references: false, - }) + } }) - it('should return true', function () { - return this.call((err, isFullIndex) => { + it('should return true', function (ctx) { + ctx.call((err, isFullIndex) => { expect(err).to.equal(null) - return expect(isFullIndex).to.equal(true) + expect(isFullIndex).to.equal(true) }) }) }) diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs index c1ce6733ca..30301ec8cc 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs @@ -1,68 +1,68 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' const modulePath = '../../../../app/src/Features/Subscription/SubscriptionGroupController' describe('SubscriptionGroupController', function () { - beforeEach(async function () { - this.user = { _id: '!@312431', email: 'user@email.com' } - this.adminUserId = '123jlkj' - this.subscriptionId = '123434325412' - this.user_email = 'bob@gmail.com' - this.req = { + beforeEach(async function (ctx) { + ctx.user = { _id: '!@312431', email: 'user@email.com' } + ctx.adminUserId = '123jlkj' + ctx.subscriptionId = '123434325412' + ctx.user_email = 'bob@gmail.com' + ctx.req = { session: { user: { - _id: this.adminUserId, - email: this.user_email, + _id: ctx.adminUserId, + email: ctx.user_email, }, }, params: { - subscriptionId: this.subscriptionId, + subscriptionId: ctx.subscriptionId, }, query: {}, } - this.subscription = { - _id: this.subscriptionId, + ctx.subscription = { + _id: ctx.subscriptionId, teamName: 'Cool group', groupPlan: true, membersLimit: 5, } - this.plan = { + ctx.plan = { canUseFlexibleLicensing: true, } - this.recurlySubscription = { + ctx.recurlySubscription = { get isCollectionMethodManual() { return true }, } - this.previewSubscriptionChangeData = { + ctx.previewSubscriptionChangeData = { change: {}, currency: 'USD', } - this.createSubscriptionChangeData = { adding: 1 } + ctx.createSubscriptionChangeData = { adding: 1 } - this.paymentMethod = { cardType: 'Visa', lastFour: '1111' } + ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' } - this.SubscriptionGroupHandler = { + ctx.SubscriptionGroupHandler = { promises: { removeUserFromGroup: sinon.stub().resolves(), getUsersGroupSubscriptionDetails: sinon.stub().resolves({ - subscription: this.subscription, - plan: this.plan, - recurlySubscription: this.recurlySubscription, + subscription: ctx.subscription, + plan: ctx.plan, + recurlySubscription: ctx.recurlySubscription, }), previewAddSeatsSubscriptionChange: sinon .stub() - .resolves(this.previewSubscriptionChangeData), + .resolves(ctx.previewSubscriptionChangeData), createAddSeatsSubscriptionChange: sinon .stub() - .resolves(this.createSubscriptionChangeData), + .resolves(ctx.createSubscriptionChangeData), ensureFlexibleLicensingEnabled: sinon.stub().resolves(), ensureSubscriptionIsActive: sinon.stub().resolves(), ensureSubscriptionCollectionMethodIsNotManual: sinon.stub().resolves(), @@ -70,19 +70,19 @@ describe('SubscriptionGroupController', function () { ensureSubscriptionHasNoPastDueInvoice: sinon.stub().resolves(), getGroupPlanUpgradePreview: sinon .stub() - .resolves(this.previewSubscriptionChangeData), - checkBillingInfoExistence: sinon.stub().resolves(this.paymentMethod), + .resolves(ctx.previewSubscriptionChangeData), + checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod), updateSubscriptionPaymentTerms: sinon.stub().resolves(), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { - getSubscription: sinon.stub().resolves(this.subscription), + getSubscription: sinon.stub().resolves(ctx.subscription), }, } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId(session) { return session.user._id }, @@ -91,13 +91,13 @@ describe('SubscriptionGroupController', function () { }, } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves(), @@ -105,35 +105,35 @@ describe('SubscriptionGroupController', function () { }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'enabled' }), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUserEmail: sinon.stub().resolves(this.user.email), + getUserEmail: sinon.stub().resolves(ctx.user.email), }, } - this.paymentMethod = { cardType: 'Visa', lastFour: '1111' } + ctx.paymentMethod = { cardType: 'Visa', lastFour: '1111' } - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { - getPaymentMethod: sinon.stub().resolves(this.paymentMethod), + getPaymentMethod: sinon.stub().resolves(ctx.paymentMethod), }, } - this.SubscriptionController = {} + ctx.SubscriptionController = {} - this.SubscriptionModel = { Subscription: {} } + ctx.SubscriptionModel = { Subscription: {} } - this.PlansHelper = { + ctx.PlansHelper = { isProfessionalGroupPlan: sinon.stub().returns(false), } - this.Errors = { + ctx.Errors = { MissingBillingInfoError: class extends Error {}, ManuallyCollectedError: class extends Error {}, PendingChangeError: class extends Error {}, @@ -142,632 +142,743 @@ describe('SubscriptionGroupController', function () { HasPastDueInvoiceError: class extends Error {}, } - this.Controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Subscription/SubscriptionGroupHandler': - this.SubscriptionGroupHandler, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/User/UserAuditLogHandler': - this.UserAuditLogHandler, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Errors/ErrorController': - (this.ErrorController = { - notFound: sinon.stub(), - }), - '../../../../app/src/Features/Subscription/SubscriptionController': - this.SubscriptionController, - '../../../../app/src/Features/Subscription/RecurlyClient': - this.RecurlyClient, - '../../../../app/src/Features/Subscription/PlansHelper': this.PlansHelper, - '../../../../app/src/Features/Subscription/Errors': this.Errors, - '../../../../app/src/models/Subscription': this.SubscriptionModel, - '@overleaf/logger': { + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionGroupHandler', + () => ({ + default: ctx.SubscriptionGroupHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: (ctx.ErrorController = { + notFound: sinon.stub(), + }), + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionController', + () => ({ + default: ctx.SubscriptionController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/PlansHelper', + () => ctx.PlansHelper + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/Errors', + () => ctx.Errors + ) + + vi.doMock( + '../../../../app/src/models/Subscription', + () => ctx.SubscriptionModel + ) + + vi.doMock('@overleaf/logger', () => ({ + default: { err: sinon.stub(), error: sinon.stub(), warn: sinon.stub(), log: sinon.stub(), debug: sinon.stub(), }, - }) + })) + + ctx.Controller = (await import(modulePath)).default }) describe('removeUserFromGroup', function () { - it('should use the subscription id for the logged in user and take the user id from the params', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription + it('should use the subscription id for the logged in user and take the user id from the params', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription - const res = { - sendStatus: () => { - this.SubscriptionGroupHandler.promises.removeUserFromGroup - .calledWith(this.subscriptionId, userIdToRemove, { - initiatorId: this.req.session.user._id, - ipAddress: this.req.ip, - }) - .should.equal(true) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + ctx.SubscriptionGroupHandler.promises.removeUserFromGroup + .calledWith(ctx.subscriptionId, userIdToRemove, { + initiatorId: ctx.req.session.user._id, + ipAddress: ctx.req.ip, + }) + .should.equal(true) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) - it('should log that the user has been removed', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription + it('should log that the user has been removed', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - userIdToRemove, - 'remove-from-group-subscription', - this.adminUserId, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) - }) - - it('should call the group SSO hooks with group SSO enabled', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([true]) - - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith( - 'unlinkUserFromGroupSSO', + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, userIdToRemove, - this.subscriptionId + 'remove-from-group-subscription', + ctx.adminUserId, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } ) - .should.equal(true) - sinon.assert.calledTwice(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) - it('should call the group SSO hooks with group SSO disabled', function (done) { - const userIdToRemove = '31231' - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([false]) + it('should call the group SSO hooks with group SSO enabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([true]) - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeUserFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + ctx.Modules.promises.hooks.fire + .calledWith( + 'unlinkUserFromGroupSSO', + userIdToRemove, + ctx.subscriptionId + ) + .should.equal(true) + sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO disabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([false]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeUserFromGroup(ctx.req, res, resolve) + }) }) }) describe('removeSelfFromGroup', function () { - it('gets subscription and remove user', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = 123456789 - this.req.session.user._id = memberUserIdToremove + it('gets subscription and remove user', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = 123456789 + ctx.req.session.user._id = memberUserIdToremove - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.SubscriptionLocator.promises.getSubscription, - this.subscriptionId - ) - sinon.assert.calledWith( - this.SubscriptionGroupHandler.promises.removeUserFromGroup, - this.subscriptionId, - memberUserIdToremove, - { - initiatorId: this.req.session.user._id, - ipAddress: this.req.ip, - } - ) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) - }) - - it('should log that the user has left the subscription', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = '123456789' - this.req.session.user._id = memberUserIdToremove - - const res = { - sendStatus: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - memberUserIdToremove, - 'remove-from-group-subscription', - memberUserIdToremove, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) - }) - - it('should call the group SSO hooks with group SSO enabled', function (done) { - this.req.query = { subscriptionId: this.subscriptionId } - const memberUserIdToremove = '123456789' - this.req.session.user._id = memberUserIdToremove - - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([true]) - - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - this.Modules.promises.hooks.fire - .calledWith( - 'unlinkUserFromGroupSSO', - memberUserIdToremove, - this.subscriptionId + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.SubscriptionLocator.promises.getSubscription, + ctx.subscriptionId ) - .should.equal(true) - sinon.assert.calledTwice(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) + sinon.assert.calledWith( + ctx.SubscriptionGroupHandler.promises.removeUserFromGroup, + ctx.subscriptionId, + memberUserIdToremove, + { + initiatorId: ctx.req.session.user._id, + ipAddress: ctx.req.ip, + } + ) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) }) - it('should call the group SSO hooks with group SSO disabled', function (done) { - const userIdToRemove = '31231' - this.req.session.user._id = userIdToRemove - this.req.params = { user_id: userIdToRemove } - this.req.entity = this.subscription - this.Modules.promises.hooks.fire - .withArgs('hasGroupSSOEnabled', this.subscription) - .resolves([false]) + it('should log that the user has left the subscription', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = '123456789' + ctx.req.session.user._id = memberUserIdToremove - const res = { - sendStatus: () => { - this.Modules.promises.hooks.fire - .calledWith('hasGroupSSOEnabled', this.subscription) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - done() - }, - } - this.Controller.removeSelfFromGroup(this.req, res, done) + const res = { + sendStatus: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + memberUserIdToremove, + 'remove-from-group-subscription', + memberUserIdToremove, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } + ) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.req.query = { subscriptionId: ctx.subscriptionId } + const memberUserIdToremove = '123456789' + ctx.req.session.user._id = memberUserIdToremove + + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([true]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + ctx.Modules.promises.hooks.fire + .calledWith( + 'unlinkUserFromGroupSSO', + memberUserIdToremove, + ctx.subscriptionId + ) + .should.equal(true) + sinon.assert.calledTwice(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) + }) + + it('should call the group SSO hooks with group SSO disabled', function (ctx) { + return new Promise(resolve => { + const userIdToRemove = '31231' + ctx.req.session.user._id = userIdToRemove + ctx.req.params = { user_id: userIdToRemove } + ctx.req.entity = ctx.subscription + ctx.Modules.promises.hooks.fire + .withArgs('hasGroupSSOEnabled', ctx.subscription) + .resolves([false]) + + const res = { + sendStatus: () => { + ctx.Modules.promises.hooks.fire + .calledWith('hasGroupSSOEnabled', ctx.subscription) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + resolve() + }, + } + ctx.Controller.removeSelfFromGroup(ctx.req, res, resolve) + }) }) }) describe('addSeatsToGroupSubscription', function () { - it('should render the "add seats" page', function (done) { - const res = { - render: (page, props) => { - this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails - .calledWith(this.req.session.user._id) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled - .calledWith(this.plan) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges - .calledWith(this.recurlySubscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive - .calledWith(this.subscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice - .calledWith(this.subscription) - .should.equal(true) - this.SubscriptionGroupHandler.promises.checkBillingInfoExistence - .calledWith(this.recurlySubscription, this.adminUserId) - .should.equal(true) - page.should.equal('subscriptions/add-seats') - props.subscriptionId.should.equal(this.subscriptionId) - props.groupName.should.equal(this.subscription.teamName) - props.totalLicenses.should.equal(this.subscription.membersLimit) - props.isProfessional.should.equal(false) - props.isCollectionMethodManual.should.equal(true) - done() - }, - } + it('should render the "add seats" page', function (ctx) { + return new Promise((resolve, reject) => { + const res = { + render: (page, props) => { + ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails + .calledWith(ctx.req.session.user._id) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled + .calledWith(ctx.plan) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges + .calledWith(ctx.recurlySubscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive + .calledWith(ctx.subscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice + .calledWith(ctx.subscription) + .should.equal(true) + ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence + .calledWith(ctx.recurlySubscription, ctx.adminUserId) + .should.equal(true) + page.should.equal('subscriptions/add-seats') + props.subscriptionId.should.equal(ctx.subscriptionId) + props.groupName.should.equal(ctx.subscription.teamName) + props.totalLicenses.should.equal(ctx.subscription.membersLimit) + props.isProfessional.should.equal(false) + props.isCollectionMethodManual.should.equal(true) + resolve() + }, + } - this.Controller.addSeatsToGroupSubscription(this.req, res) + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) }) - it('should redirect to subscription page when getting subscription details fails', function (done) { - this.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails = + it('should redirect to subscription page when getting subscription details fails', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when flexible licensing is not enabled', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled = + sinon.stub().rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to missing billing information page when billing information is missing', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon + .stub() + .throws(new ctx.Errors.MissingBillingInfoError()) + + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/missing-billing-information' + ) + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when there is a pending change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges = + sinon.stub().throws(new ctx.Errors.PendingChangeError()) + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when subscription is not active', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon + .stub() + .rejects() + + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } + + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) + }) + + it('should redirect to subscription page when subscription has pending invoice', function (ctx) { + ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice = sinon.stub().rejects() + return new Promise(resolve => { + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when flexible licensing is not enabled', function (done) { - this.SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled = - sinon.stub().rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to missing billing information page when billing information is missing', function (done) { - this.SubscriptionGroupHandler.promises.checkBillingInfoExistence = sinon - .stub() - .throws(new this.Errors.MissingBillingInfoError()) - - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/missing-billing-information' - ) - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when there is a pending change', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges = - sinon.stub().throws(new this.Errors.PendingChangeError()) - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when subscription is not active', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionIsActive = sinon - .stub() - .rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) - }) - - it('should redirect to subscription page when subscription has pending invoice', function (done) { - this.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice = - sinon.stub().rejects() - - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } - - this.Controller.addSeatsToGroupSubscription(this.req, res) + ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) + }) }) }) describe('previewAddSeatsSubscriptionChange', function () { - it('should preview "add seats" change', function (done) { - this.req.body = { adding: 2 } + it('should preview "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } - const res = { - json: data => { - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange - .calledWith(this.req.session.user._id, this.req.body.adding) - .should.equal(true) - data.should.deep.equal(this.previewSubscriptionChangeData) - done() - }, - } + const res = { + json: data => { + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange + .calledWith(ctx.req.session.user._id, ctx.req.body.adding) + .should.equal(true) + data.should.deep.equal(ctx.previewSubscriptionChangeData) + resolve() + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail previewing "add seats" change', function (done) { - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = - sinon.stub().rejects() + it('should fail previewing "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = + sinon.stub().rejects() - const res = { - status: statusCode => { - statusCode.should.equal(500) + const res = { + status: statusCode => { + statusCode.should.equal(500) - return { - end: () => { - done() - }, - } - }, - } + return { + end: () => { + resolve() + }, + } + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail previewing "add seats" change with SubtotalLimitExceededError', function (done) { - this.req.body = { adding: 2 } - this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = - sinon.stub().throws(new this.Errors.SubtotalLimitExceededError()) + it('should fail previewing "add seats" change with SubtotalLimitExceededError', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } + ctx.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange = + sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - status: statusCode => { - statusCode.should.equal(422) + const res = { + status: statusCode => { + statusCode.should.equal(422) - return { - json: data => { - data.should.deep.equal({ - code: 'subtotal_limit_exceeded', - adding: this.req.body.adding, - }) - done() - }, - } - }, - } + return { + json: data => { + data.should.deep.equal({ + code: 'subtotal_limit_exceeded', + adding: ctx.req.body.adding, + }) + resolve() + }, + } + }, + } - this.Controller.previewAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.previewAddSeatsSubscriptionChange(ctx.req, res) + }) }) }) describe('createAddSeatsSubscriptionChange', function () { - it('should apply "add seats" change', function (done) { - this.req.body = { adding: 2 } + it('should apply "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } - const res = { - json: data => { - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange - .calledWith(this.req.session.user._id, this.req.body.adding) - .should.equal(true) - data.should.deep.equal(this.createSubscriptionChangeData) - done() - }, - } + const res = { + json: data => { + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange + .calledWith(ctx.req.session.user._id, ctx.req.body.adding) + .should.equal(true) + data.should.deep.equal(ctx.createSubscriptionChangeData) + resolve() + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail applying "add seats" change', function (done) { - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = - sinon.stub().rejects() + it('should fail applying "add seats" change', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = + sinon.stub().rejects() - const res = { - status: statusCode => { - statusCode.should.equal(500) + const res = { + status: statusCode => { + statusCode.should.equal(500) - return { - end: () => { - done() - }, - } - }, - } + return { + end: () => { + resolve() + }, + } + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) - it('should fail applying "add seats" change with SubtotalLimitExceededError', function (done) { - this.req.body = { adding: 2 } - this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = - sinon.stub().throws(new this.Errors.SubtotalLimitExceededError()) + it('should fail applying "add seats" change with SubtotalLimitExceededError', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { adding: 2 } + ctx.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange = + sinon.stub().throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - status: statusCode => { - statusCode.should.equal(422) + const res = { + status: statusCode => { + statusCode.should.equal(422) - return { - json: data => { - data.should.deep.equal({ - code: 'subtotal_limit_exceeded', - adding: this.req.body.adding, - }) - done() - }, - } - }, - } + return { + json: data => { + data.should.deep.equal({ + code: 'subtotal_limit_exceeded', + adding: ctx.req.body.adding, + }) + resolve() + }, + } + }, + } - this.Controller.createAddSeatsSubscriptionChange(this.req, res) + ctx.Controller.createAddSeatsSubscriptionChange(ctx.req, res) + }) }) }) describe('submitForm', function () { - it('should build and pass the request body to the sales submit handler', function (done) { - const adding = 100 - const poNumber = 'PO123456' - this.req.body = { adding, poNumber } + it('should build and pass the request body to the sales submit handler', function (ctx) { + return new Promise(resolve => { + const adding = 100 + const poNumber = 'PO123456' + ctx.req.body = { adding, poNumber } - const res = { - sendStatus: code => { - this.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms( - this.adminUserId, - this.recurlySubscription, - poNumber - ) - this.Modules.promises.hooks.fire - .calledWith('sendSupportRequest', { - email: this.user.email, - subject: 'Sales Contact Form', - message: - '\n' + - '**Overleaf Sales Contact Form:**\n' + - '\n' + - '**Subject:** Self-Serve Group User Increase Request\n' + - '\n' + - `**Estimated Number of Users:** ${adding}\n` + - '\n' + - `**PO Number:** ${poNumber}\n` + - '\n' + - `**Message:** This email has been generated on behalf of user with email **${this.user.email}** to request an increase in the total number of users for their subscription.`, - inbox: 'sales', - }) - .should.equal(true) - sinon.assert.calledOnce(this.Modules.promises.hooks.fire) - code.should.equal(204) - done() - }, - } - this.Controller.submitForm(this.req, res, done) + const res = { + sendStatus: code => { + ctx.SubscriptionGroupHandler.promises.updateSubscriptionPaymentTerms( + ctx.adminUserId, + ctx.recurlySubscription, + poNumber + ) + ctx.Modules.promises.hooks.fire + .calledWith('sendSupportRequest', { + email: ctx.user.email, + subject: 'Sales Contact Form', + message: + '\n' + + '**Overleaf Sales Contact Form:**\n' + + '\n' + + '**Subject:** Self-Serve Group User Increase Request\n' + + '\n' + + `**Estimated Number of Users:** ${adding}\n` + + '\n' + + `**PO Number:** ${poNumber}\n` + + '\n' + + `**Message:** This email has been generated on behalf of user with email **${ctx.user.email}** to request an increase in the total number of users for their subscription.`, + inbox: 'sales', + }) + .should.equal(true) + sinon.assert.calledOnce(ctx.Modules.promises.hooks.fire) + code.should.equal(204) + resolve() + }, + } + ctx.Controller.submitForm(ctx.req, res, resolve) + }) }) }) describe('subscriptionUpgradePage', function () { - it('should render "subscription upgrade" page', function (done) { - const olSubscription = { membersLimit: 1, teamName: 'test team' } - this.SubscriptionModel.Subscription.findOne = () => { - return { - exec: () => olSubscription, + it('should render "subscription upgrade" page', function (ctx) { + return new Promise(resolve => { + const olSubscription = { membersLimit: 1, teamName: 'test team' } + ctx.SubscriptionModel.Subscription.findOne = () => { + return { + exec: () => olSubscription, + } } - } - const res = { - render: (page, data) => { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview - .calledWith(this.req.session.user._id) - .should.equal(true) - page.should.equal('subscriptions/upgrade-group-subscription-react') - data.totalLicenses.should.equal(olSubscription.membersLimit) - data.groupName.should.equal(olSubscription.teamName) - data.changePreview.should.equal(this.previewSubscriptionChangeData) - done() - }, - } + const res = { + render: (page, data) => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview + .calledWith(ctx.req.session.user._id) + .should.equal(true) + page.should.equal('subscriptions/upgrade-group-subscription-react') + data.totalLicenses.should.equal(olSubscription.membersLimit) + data.groupName.should.equal(olSubscription.teamName) + data.changePreview.should.equal(ctx.previewSubscriptionChangeData) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect if failed to generate preview', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .rejects() + it('should redirect if failed to generate preview', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .rejects() - const res = { - redirect: url => { - url.should.equal('/user/subscription') - done() - }, - } + const res = { + redirect: url => { + url.should.equal('/user/subscription') + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to missing billing information page when billing information is missing', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.MissingBillingInfoError()) + it('should redirect to missing billing information page when billing information is missing', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.MissingBillingInfoError()) - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/missing-billing-information' - ) - done() - }, - } + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/missing-billing-information' + ) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to manually collected subscription error page when collection method is manual', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.ManuallyCollectedError()) + it('should redirect to manually collected subscription error page when collection method is manual', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.ManuallyCollectedError()) - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/manually-collected-subscription' - ) - done() - }, - } + const res = { + redirect: url => { + url.should.equal( + '/user/subscription/group/manually-collected-subscription' + ) + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) - it('should redirect to subtotal limit exceeded page', function (done) { - this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon - .stub() - .throws(new this.Errors.SubtotalLimitExceededError()) + it('should redirect to subtotal limit exceeded page', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon + .stub() + .throws(new ctx.Errors.SubtotalLimitExceededError()) - const res = { - redirect: url => { - url.should.equal('/user/subscription/group/subtotal-limit-exceeded') - done() - }, - } + const res = { + redirect: url => { + url.should.equal('/user/subscription/group/subtotal-limit-exceeded') + resolve() + }, + } - this.Controller.subscriptionUpgradePage(this.req, res) + ctx.Controller.subscriptionUpgradePage(ctx.req, res) + }) }) }) describe('upgradeSubscription', function () { - it('should send 200 response', function (done) { - this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon - .stub() - .resolves() + it('should send 200 response', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon + .stub() + .resolves() - const res = { - sendStatus: code => { - code.should.equal(200) - done() - }, - } + const res = { + sendStatus: code => { + code.should.equal(200) + resolve() + }, + } - this.Controller.upgradeSubscription(this.req, res) + ctx.Controller.upgradeSubscription(ctx.req, res) + }) }) - it('should send 500 response', function (done) { - this.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon - .stub() - .rejects() + it('should send 500 response', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionGroupHandler.promises.upgradeGroupPlan = sinon + .stub() + .rejects() - const res = { - sendStatus: code => { - code.should.equal(500) - done() - }, - } + const res = { + sendStatus: code => { + code.should.equal(500) + resolve() + }, + } - this.Controller.upgradeSubscription(this.req, res) + ctx.Controller.upgradeSubscription(ctx.req, res) + }) }) }) }) diff --git a/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs b/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs index 3a1e8c3462..b72a406ac0 100644 --- a/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs +++ b/services/web/test/unit/src/Subscription/TeamInvitesController.test.mjs @@ -1,20 +1,20 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' const modulePath = '../../../../app/src/Features/Subscription/TeamInvitesController' describe('TeamInvitesController', function () { - beforeEach(async function () { - this.user = { _id: '!@312431', email: 'user@email.com' } - this.adminUserId = '123jlkj' - this.subscriptionId = '123434325412' - this.user_email = 'bob@gmail.com' - this.req = { + beforeEach(async function (ctx) { + ctx.user = { _id: '!@312431', email: 'user@email.com' } + ctx.adminUserId = '123jlkj' + ctx.subscriptionId = '123434325412' + ctx.user_email = 'bob@gmail.com' + ctx.req = { session: { user: { - _id: this.adminUserId, - email: this.user_email, + _id: ctx.adminUserId, + email: ctx.user_email, }, }, params: {}, @@ -22,33 +22,33 @@ describe('TeamInvitesController', function () { ip: '0.0.0.0', } - this.subscription = { - _id: this.subscriptionId, + ctx.subscription = { + _id: ctx.subscriptionId, } - this.TeamInvitesHandler = { + ctx.TeamInvitesHandler = { promises: { - acceptInvite: sinon.stub().resolves(this.subscription), + acceptInvite: sinon.stub().resolves(ctx.subscription), getInvite: sinon.stub().resolves({ invite: { - email: this.user.email, + email: ctx.user.email, token: 'token123', - inviterName: this.user_email, + inviterName: ctx.user_email, }, - subscription: this.subscription, + subscription: ctx.subscription, }), }, } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { hasSSOEnabled: sinon.stub().resolves(true), getUsersSubscription: sinon.stub().resolves(), }, } - this.ErrorController = { notFound: sinon.stub() } + ctx.ErrorController = { notFound: sinon.stub() } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId(session) { return session.user?._id }, @@ -57,74 +57,112 @@ describe('TeamInvitesController', function () { }, } - this.UserAuditLogHandler = { + ctx.UserAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.UserGetter = { + ctx.UserGetter = { promises: { - getUser: sinon.stub().resolves(this.user), - getUserByMainEmail: sinon.stub().resolves(this.user), - getUserByAnyEmail: sinon.stub().resolves(this.user), + getUser: sinon.stub().resolves(ctx.user), + getUserByMainEmail: sinon.stub().resolves(ctx.user), + getUserByAnyEmail: sinon.stub().resolves(ctx.user), }, } - this.EmailHandler = { + ctx.EmailHandler = { sendDeferredEmail: sinon.stub().resolves(), } - this.RateLimiter = { + ctx.RateLimiter = { RateLimiter: class {}, } - this.Controller = await esmock.strict(modulePath, { - '../../../../app/src/Features/Subscription/TeamInvitesHandler': - this.TeamInvitesHandler, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/Features/User/UserAuditLogHandler': - this.UserAuditLogHandler, - '../../../../app/src/Features/Errors/ErrorController': - this.ErrorController, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Email/EmailHandler': this.EmailHandler, - '../../../../app/src/infrastructure/RateLimiter': this.RateLimiter, - '../../../../app/src/infrastructure/Modules': (this.Modules = { + vi.doMock( + '../../../../app/src/Features/Subscription/TeamInvitesHandler', + () => ({ + default: ctx.TeamInvitesHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({ + default: ctx.UserAuditLogHandler, + })) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: ctx.ErrorController, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({ + default: ctx.EmailHandler, + })) + + vi.doMock( + '../../../../app/src/infrastructure/RateLimiter', + () => ctx.RateLimiter + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: (ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves([]), }, }, }), - '../../../../app/src/Features/SplitTests/SplitTestHandler': - (this.SplitTestHandler = { + })) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: (ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({}), }, }), - }) + }) + ) + + ctx.Controller = (await import(modulePath)).default }) describe('acceptInvite', function () { - it('should add an audit log entry', function (done) { - this.req.params.token = 'foo' - this.req.session.user = this.user - const res = { - json: () => { - sinon.assert.calledWith( - this.UserAuditLogHandler.promises.addEntry, - this.user._id, - 'accept-group-invitation', - this.user._id, - this.req.ip, - { subscriptionId: this.subscriptionId } - ) - done() - }, - } - this.Controller.acceptInvite(this.req, res) + it('should add an audit log entry', function (ctx) { + return new Promise(resolve => { + ctx.req.params.token = 'foo' + ctx.req.session.user = ctx.user + const res = { + json: () => { + sinon.assert.calledWith( + ctx.UserAuditLogHandler.promises.addEntry, + ctx.user._id, + 'accept-group-invitation', + ctx.user._id, + ctx.req.ip, + { subscriptionId: ctx.subscriptionId } + ) + resolve() + }, + } + ctx.Controller.acceptInvite(ctx.req, res) + }) }) }) @@ -138,90 +176,102 @@ describe('TeamInvitesController', function () { } describe('hasIndividualRecurlySubscription', function () { - it('is true for personal subscription', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: false, + it('is true for personal subscription', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: false, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.true + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.true - done() - }, - } - this.Controller.viewInvite(req, res) }) - it('is true for group subscriptions', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: true, + it('is true for group subscriptions', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: true, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.false + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.false - done() - }, - } - this.Controller.viewInvite(req, res) }) - it('is false for canceled subscriptions', function (done) { - this.SubscriptionLocator.promises.getUsersSubscription.resolves({ - recurlySubscription_id: 'subscription123', - groupPlan: false, - recurlyStatus: { - state: 'canceled', - }, + it('is false for canceled subscriptions', function (ctx) { + return new Promise(resolve => { + ctx.SubscriptionLocator.promises.getUsersSubscription.resolves({ + recurlySubscription_id: 'subscription123', + groupPlan: false, + recurlyStatus: { + state: 'canceled', + }, + }) + const res = { + render: (template, data) => { + expect(data.hasIndividualRecurlySubscription).to.be.false + resolve() + }, + } + ctx.Controller.viewInvite(req, res) }) - const res = { - render: (template, data) => { - expect(data.hasIndividualRecurlySubscription).to.be.false - done() - }, - } - this.Controller.viewInvite(req, res) }) }) describe('when user is logged out', function () { - it('renders logged out invite page', function (done) { - const res = { - render: (template, data) => { - expect(template).to.equal('subscriptions/team/invite_logged_out') - expect(data.groupSSOActive).to.be.undefined - done() - }, - } - this.Controller.viewInvite( - { params: { token: 'token123' }, session: {} }, - res - ) + it('renders logged out invite page', function (ctx) { + return new Promise(resolve => { + const res = { + render: (template, data) => { + expect(template).to.equal('subscriptions/team/invite_logged_out') + expect(data.groupSSOActive).to.be.undefined + resolve() + }, + } + ctx.Controller.viewInvite( + { params: { token: 'token123' }, session: {} }, + res + ) + }) }) - it('includes groupSSOActive flag when the group has SSO enabled', function (done) { - this.Modules.promises.hooks.fire = sinon.stub().resolves([true]) - const res = { - render: (template, data) => { - expect(data.groupSSOActive).to.be.true - done() - }, - } - this.Controller.viewInvite( - { params: { token: 'token123' }, session: {} }, - res - ) + it('includes groupSSOActive flag when the group has SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.Modules.promises.hooks.fire = sinon.stub().resolves([true]) + const res = { + render: (template, data) => { + expect(data.groupSSOActive).to.be.true + resolve() + }, + } + ctx.Controller.viewInvite( + { params: { token: 'token123' }, session: {} }, + res + ) + }) }) }) - it('renders the view', function (done) { - const res = { - render: template => { - expect(template).to.equal('subscriptions/team/invite') - done() - }, - } - this.Controller.viewInvite(req, res) + it('renders the view', function (ctx) { + return new Promise(resolve => { + const res = { + render: template => { + expect(template).to.equal('subscriptions/team/invite') + resolve() + }, + } + ctx.Controller.viewInvite(req, res) + }) }) }) }) diff --git a/services/web/test/unit/src/Tags/TagsController.test.mjs b/services/web/test/unit/src/Tags/TagsController.test.mjs index 4474ba0d38..927c6283a5 100644 --- a/services/web/test/unit/src/Tags/TagsController.test.mjs +++ b/services/web/test/unit/src/Tags/TagsController.test.mjs @@ -1,17 +1,14 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { assert } from 'chai' -const modulePath = new URL( - '../../../../app/src/Features/Tags/TagsController.mjs', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/Tags/TagsController.mjs' describe('TagsController', function () { const userId = '123nd3ijdks' const projectId = '123njdskj9jlk' - beforeEach(async function () { - this.TagsHandler = { + beforeEach(async function (ctx) { + ctx.TagsHandler = { promises: { addProjectToTag: sinon.stub().resolves(), addProjectsToTag: sinon.stub().resolves(), @@ -23,17 +20,25 @@ describe('TagsController', function () { createTag: sinon.stub().resolves(), }, } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId: session => { return session.user._id }, } - this.TagsController = await esmock.strict(modulePath, { - '../../../../app/src/Features/Tags/TagsHandler': this.TagsHandler, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - }) - this.req = { + + vi.doMock('../../../../app/src/Features/Tags/TagsHandler', () => ({ + default: ctx.TagsHandler, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + ctx.TagsController = (await import(modulePath)).default + ctx.req = { params: { projectId, }, @@ -45,149 +50,235 @@ describe('TagsController', function () { body: {}, } - this.res = {} - this.res.status = sinon.stub().returns(this.res) - this.res.end = sinon.stub() - this.res.json = sinon.stub() + ctx.res = {} + ctx.res.status = sinon.stub().returns(ctx.res) + ctx.res.end = sinon.stub() + ctx.res.json = sinon.stub() }) - it('get all tags', function (done) { - const allTags = [{ name: 'tag', projects: ['123423', '423423'] }] - this.TagsHandler.promises.getAllTags = sinon.stub().resolves(allTags) - this.TagsController.getAllTags(this.req, { - json: body => { - body.should.equal(allTags) - sinon.assert.calledWith(this.TagsHandler.promises.getAllTags, userId) - done() - return { - end: () => {}, - } - }, + it('get all tags', function (ctx) { + return new Promise(resolve => { + const allTags = [{ name: 'tag', projects: ['123423', '423423'] }] + ctx.TagsHandler.promises.getAllTags = sinon.stub().resolves(allTags) + ctx.TagsController.getAllTags(ctx.req, { + json: body => { + body.should.equal(allTags) + sinon.assert.calledWith(ctx.TagsHandler.promises.getAllTags, userId) + resolve() + return { + end: () => {}, + } + }, + }) }) }) describe('create a tag', function (done) { - it('without a color', function (done) { - this.tag = { mock: 'tag' } - this.TagsHandler.promises.createTag = sinon.stub().resolves(this.tag) - this.req.session.user._id = this.userId = 'user-id-123' - this.req.body = { name: (this.name = 'tag-name') } - this.TagsController.createTag(this.req, { - json: () => { - sinon.assert.calledWith( - this.TagsHandler.promises.createTag, - this.userId, - this.name - ) - done() - return { - end: () => {}, - } - }, + it('without a color', function (ctx) { + return new Promise(resolve => { + ctx.tag = { mock: 'tag' } + ctx.TagsHandler.promises.createTag = sinon.stub().resolves(ctx.tag) + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.req.body = { name: (ctx.tagName = 'tag-name') } + ctx.TagsController.createTag(ctx.req, { + json: () => { + sinon.assert.calledWith( + ctx.TagsHandler.promises.createTag, + ctx.userId, + ctx.tagName + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) - it('with a color', function (done) { - this.tag = { mock: 'tag' } - this.TagsHandler.promises.createTag = sinon.stub().resolves(this.tag) - this.req.session.user._id = this.userId = 'user-id-123' - this.req.body = { - name: (this.name = 'tag-name'), - color: (this.color = '#123456'), - } - this.TagsController.createTag(this.req, { - json: () => { - sinon.assert.calledWith( - this.TagsHandler.promises.createTag, - this.userId, - this.name, - this.color - ) - done() - return { - end: () => {}, - } - }, + it('with a color', function (ctx) { + return new Promise(resolve => { + ctx.tag = { mock: 'tag' } + ctx.TagsHandler.promises.createTag = sinon.stub().resolves(ctx.tag) + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.req.body = { + name: (ctx.tagName = 'tag-name'), + color: (ctx.color = '#123456'), + } + ctx.TagsController.createTag(ctx.req, { + json: () => { + sinon.assert.calledWith( + ctx.TagsHandler.promises.createTag, + ctx.userId, + ctx.tagName, + ctx.color + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) }) - it('delete a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.deleteTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.deleteTag, - this.userId, - this.tagId - ) - done() - return { - end: () => {}, - } - }, + it('delete a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.deleteTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.deleteTag, + ctx.userId, + ctx.tagId + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) describe('edit a tag', function () { - beforeEach(function () { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.session.user._id = this.userId = 'user-id-123' + beforeEach(function (ctx) { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' }) - it('with a name and no color', function (done) { - this.req.body = { - name: (this.name = 'new-name'), - } - this.TagsController.editTag(this.req, { + it('with a name and no color', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + name: (ctx.tagName = 'new-name'), + } + ctx.TagsController.editTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.editTag, + ctx.userId, + ctx.tagId, + ctx.tagName + ) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + + it('with a name and color', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { + name: (ctx.tagName = 'new-name'), + color: (ctx.color = '#FF0011'), + } + ctx.TagsController.editTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.editTag, + ctx.userId, + ctx.tagId, + ctx.tagName, + ctx.color + ) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + + it('without a name', function (ctx) { + return new Promise(resolve => { + ctx.req.body = { name: undefined } + ctx.TagsController.renameTag(ctx.req, { + status: code => { + assert.equal(code, 400) + sinon.assert.notCalled(ctx.TagsHandler.promises.renameTag) + resolve() + return { + end: () => {}, + } + }, + }) + }) + }) + }) + + it('add a project to a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.params.projectId = ctx.projectId = 'project-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.addProjectToTag(ctx.req, { status: code => { assert.equal(code, 204) sinon.assert.calledWith( - this.TagsHandler.promises.editTag, - this.userId, - this.tagId, - this.name + ctx.TagsHandler.promises.addProjectToTag, + ctx.userId, + ctx.tagId, + ctx.projectId ) - done() + resolve() return { end: () => {}, } }, }) }) + }) - it('with a name and color', function (done) { - this.req.body = { - name: (this.name = 'new-name'), - color: (this.color = '#FF0011'), - } - this.TagsController.editTag(this.req, { + it('add projects to a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.body.projectIds = ctx.projectIds = [ + 'project-id-123', + 'project-id-234', + ] + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.addProjectsToTag(ctx.req, { status: code => { assert.equal(code, 204) sinon.assert.calledWith( - this.TagsHandler.promises.editTag, - this.userId, - this.tagId, - this.name, - this.color + ctx.TagsHandler.promises.addProjectsToTag, + ctx.userId, + ctx.tagId, + ctx.projectIds ) - done() + resolve() return { end: () => {}, } }, }) }) + }) - it('without a name', function (done) { - this.req.body = { name: undefined } - this.TagsController.renameTag(this.req, { + it('remove a project from a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.params.projectId = ctx.projectId = 'project-id-123' + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.removeProjectFromTag(ctx.req, { status: code => { - assert.equal(code, 400) - sinon.assert.notCalled(this.TagsHandler.promises.renameTag) - done() + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.removeProjectFromTag, + ctx.userId, + ctx.tagId, + ctx.projectId + ) + resolve() return { end: () => {}, } @@ -196,93 +287,29 @@ describe('TagsController', function () { }) }) - it('add a project to a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.params.projectId = this.projectId = 'project-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.addProjectToTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.addProjectToTag, - this.userId, - this.tagId, - this.projectId - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('add projects to a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.body.projectIds = this.projectIds = [ - 'project-id-123', - 'project-id-234', - ] - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.addProjectsToTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.addProjectsToTag, - this.userId, - this.tagId, - this.projectIds - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('remove a project from a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.params.projectId = this.projectId = 'project-id-123' - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.removeProjectFromTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectFromTag, - this.userId, - this.tagId, - this.projectId - ) - done() - return { - end: () => {}, - } - }, - }) - }) - - it('remove projects from a tag', function (done) { - this.req.params.tagId = this.tagId = 'tag-id-123' - this.req.body.projectIds = this.projectIds = [ - 'project-id-123', - 'project-id-234', - ] - this.req.session.user._id = this.userId = 'user-id-123' - this.TagsController.removeProjectsFromTag(this.req, { - status: code => { - assert.equal(code, 204) - sinon.assert.calledWith( - this.TagsHandler.promises.removeProjectsFromTag, - this.userId, - this.tagId, - this.projectIds - ) - done() - return { - end: () => {}, - } - }, + it('remove projects from a tag', function (ctx) { + return new Promise(resolve => { + ctx.req.params.tagId = ctx.tagId = 'tag-id-123' + ctx.req.body.projectIds = ctx.projectIds = [ + 'project-id-123', + 'project-id-234', + ] + ctx.req.session.user._id = ctx.userId = 'user-id-123' + ctx.TagsController.removeProjectsFromTag(ctx.req, { + status: code => { + assert.equal(code, 204) + sinon.assert.calledWith( + ctx.TagsHandler.promises.removeProjectsFromTag, + ctx.userId, + ctx.tagId, + ctx.projectIds + ) + resolve() + return { + end: () => {}, + } + }, + }) }) }) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs index 4dd72b117f..313f2d2456 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import mongodb from 'mongodb-legacy' import { expect } from 'chai' -import esmock from 'esmock' import sinon from 'sinon' import Errors from '../../../../app/src/Features/Errors/Errors.js' import MockResponse from '../helpers/MockResponse.js' @@ -12,498 +12,557 @@ const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsController.mjs' describe('TpdsController', function () { - beforeEach(async function () { - this.metadata = { + beforeEach(async function (ctx) { + ctx.metadata = { projectId: new ObjectId(), entityId: new ObjectId(), folderId: new ObjectId(), entityType: 'doc', rev: 2, } - this.TpdsUpdateHandler = { + ctx.TpdsUpdateHandler = { promises: { - newUpdate: sinon.stub().resolves(this.metadata), - deleteUpdate: sinon.stub().resolves(this.metadata.entityId), + newUpdate: sinon.stub().resolves(ctx.metadata), + deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId), createFolder: sinon.stub().resolves(), }, } - this.UpdateMerger = { + ctx.UpdateMerger = { promises: { - mergeUpdate: sinon.stub().resolves(this.metadata), - deleteUpdate: sinon.stub().resolves(this.metadata.entityId), + mergeUpdate: sinon.stub().resolves(ctx.metadata), + deleteUpdate: sinon.stub().resolves(ctx.metadata.entityId), }, } - this.NotificationsBuilder = { + ctx.NotificationsBuilder = { tpdsFileLimit: sinon.stub().returns({ create: sinon.stub() }), } - this.SessionManager = { + ctx.SessionManager = { getLoggedInUserId: sinon.stub().returns('user-id'), } - this.TpdsQueueManager = { + ctx.TpdsQueueManager = { promises: { getQueues: sinon.stub().resolves('queues'), }, } - this.HttpErrorHandler = { + ctx.HttpErrorHandler = { conflict: sinon.stub(), } - this.newProject = { _id: new ObjectId() } - this.ProjectCreationHandler = { - promises: { createBlankProject: sinon.stub().resolves(this.newProject) }, + ctx.newProject = { _id: new ObjectId() } + ctx.ProjectCreationHandler = { + promises: { createBlankProject: sinon.stub().resolves(ctx.newProject) }, } - this.ProjectDetailsHandler = { + ctx.ProjectDetailsHandler = { promises: { generateUniqueName: sinon.stub().resolves('unique'), }, } - this.TpdsController = await esmock.strict(MODULE_PATH, { - '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler': - this.TpdsUpdateHandler, - '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger': - this.UpdateMerger, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationsBuilder, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Errors/HttpErrorHandler': - this.HttpErrorHandler, - '../../../../app/src/Features/ThirdPartyDataStore/TpdsQueueManager': - this.TpdsQueueManager, - '../../../../app/src/Features/Project/ProjectCreationHandler': - this.ProjectCreationHandler, - '../../../../app/src/Features/Project/ProjectDetailsHandler': - this.ProjectDetailsHandler, - }) - this.user_id = 'dsad29jlkjas' + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler', + () => ({ + default: ctx.TpdsUpdateHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger', + () => ({ + default: ctx.UpdateMerger, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({ + default: ctx.HttpErrorHandler, + })) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/TpdsQueueManager', + () => ({ + default: ctx.TpdsQueueManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectDetailsHandler', + () => ({ + default: ctx.ProjectDetailsHandler, + }) + ) + + ctx.TpdsController = (await import(MODULE_PATH)).default + + ctx.user_id = 'dsad29jlkjas' }) describe('creating a project', function () { - it('should yield the new projects id', function (done) { - const res = new MockResponse() - const req = new MockRequest() - req.params.user_id = this.user_id - req.body = { projectName: 'foo' } - res.callback = err => { - if (err) done(err) - expect(res.body).to.equal( - JSON.stringify({ projectId: this.newProject._id.toString() }) - ) - expect( - this.ProjectDetailsHandler.promises.generateUniqueName - ).to.have.been.calledWith(this.user_id, 'foo') - expect( - this.ProjectCreationHandler.promises.createBlankProject - ).to.have.been.calledWith( - this.user_id, - 'unique', - {}, - { skipCreatingInTPDS: true } - ) - done() - } - this.TpdsController.createProject(req, res) + it('should yield the new projects id', function (ctx) { + return new Promise(resolve => { + const res = new MockResponse() + const req = new MockRequest() + req.params.user_id = ctx.user_id + req.body = { projectName: 'foo' } + res.callback = err => { + if (err) resolve(err) + expect(res.body).to.equal( + JSON.stringify({ projectId: ctx.newProject._id.toString() }) + ) + expect( + ctx.ProjectDetailsHandler.promises.generateUniqueName + ).to.have.been.calledWith(ctx.user_id, 'foo') + expect( + ctx.ProjectCreationHandler.promises.createBlankProject + ).to.have.been.calledWith( + ctx.user_id, + 'unique', + {}, + { skipCreatingInTPDS: true } + ) + resolve() + } + ctx.TpdsController.createProject(req, res) + }) }) }) describe('getting an update', function () { - beforeEach(function () { - this.projectName = 'projectName' - this.path = '/here.txt' - this.req = { + beforeEach(function (ctx) { + ctx.projectName = 'projectName' + ctx.path = '/here.txt' + ctx.req = { params: { - 0: `${this.projectName}${this.path}`, - user_id: this.user_id, + 0: `${ctx.projectName}${ctx.path}`, + user_id: ctx.user_id, project_id: '', }, headers: { - 'x-update-source': (this.source = 'dropbox'), + 'x-update-source': (ctx.source = 'dropbox'), }, } }) - it('should process the update with the update receiver by name', function (done) { - const res = { - json: payload => { - expect(payload).to.deep.equal({ - status: 'applied', - projectId: this.metadata.projectId.toString(), - entityId: this.metadata.entityId.toString(), - folderId: this.metadata.folderId.toString(), - entityType: this.metadata.entityType, - rev: this.metadata.rev.toString(), - }) - this.TpdsUpdateHandler.promises.newUpdate - .calledWith( - this.user_id, - '', // projectId - this.projectName, - this.path, - this.req, - this.source + it('should process the update with the update receiver by name', function (ctx) { + return new Promise(resolve => { + const res = { + json: payload => { + expect(payload).to.deep.equal({ + status: 'applied', + projectId: ctx.metadata.projectId.toString(), + entityId: ctx.metadata.entityId.toString(), + folderId: ctx.metadata.folderId.toString(), + entityType: ctx.metadata.entityType, + rev: ctx.metadata.rev.toString(), + }) + ctx.TpdsUpdateHandler.promises.newUpdate + .calledWith( + ctx.user_id, + '', // projectId + ctx.projectName, + ctx.path, + ctx.req, + ctx.source + ) + .should.equal(true) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should indicate in the response when the update was rejected', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.resolves(null) + const res = { + json: payload => { + expect(payload).to.deep.equal({ status: 'rejected' }) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should process the update with the update receiver by id', function (ctx) { + return new Promise(resolve => { + const path = '/here.txt' + const req = { + pause() {}, + params: { 0: path, user_id: ctx.user_id, project_id: '123' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + json: () => { + ctx.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith( + ctx.user_id, + '123', + '', // projectName + '/here.txt', + req, + ctx.source ) - .should.equal(true) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) - }) - - it('should indicate in the response when the update was rejected', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.resolves(null) - const res = { - json: payload => { - expect(payload).to.deep.equal({ status: 'rejected' }) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) - }) - - it('should process the update with the update receiver by id', function (done) { - const path = '/here.txt' - const req = { - pause() {}, - params: { 0: path, user_id: this.user_id, project_id: '123' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - json: () => { - this.TpdsUpdateHandler.promises.newUpdate.should.have.been.calledWith( - this.user_id, - '123', - '', // projectName - '/here.txt', - req, - this.source - ) - done() - }, - } - this.TpdsController.mergeUpdate(req, res) - }) - - it('should return a 500 error when the update receiver fails', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects(new Error()) - const res = { - json: sinon.stub(), - } - this.TpdsController.mergeUpdate(this.req, res, err => { - expect(err).to.exist - expect(res.json).not.to.have.been.called - done() + resolve() + }, + } + ctx.TpdsController.mergeUpdate(req, res) }) }) - it('should return a 400 error when the project is too big', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects({ - message: 'project_has_too_many_files', + it('should return a 500 error when the update receiver fails', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects(new Error()) + const res = { + json: sinon.stub(), + } + ctx.TpdsController.mergeUpdate(ctx.req, res, err => { + expect(err).to.exist + expect(res.json).not.to.have.been.called + resolve() + }) }) - const res = { - sendStatus: status => { - expect(status).to.equal(400) - this.NotificationsBuilder.tpdsFileLimit.should.have.been.calledWith( - this.user_id - ) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) }) - it('should return a 429 error when the update receiver fails due to too many requests error', function (done) { - this.TpdsUpdateHandler.promises.newUpdate.rejects( - new Errors.TooManyRequestsError('project on cooldown') - ) - const res = { - sendStatus: status => { - expect(status).to.equal(429) - done() - }, - } - this.TpdsController.mergeUpdate(this.req, res) + it('should return a 400 error when the project is too big', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects({ + message: 'project_has_too_many_files', + }) + const res = { + sendStatus: status => { + expect(status).to.equal(400) + ctx.NotificationsBuilder.tpdsFileLimit.should.have.been.calledWith( + ctx.user_id + ) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) + }) + + it('should return a 429 error when the update receiver fails due to too many requests error', function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.newUpdate.rejects( + new Errors.TooManyRequestsError('project on cooldown') + ) + const res = { + sendStatus: status => { + expect(status).to.equal(429) + resolve() + }, + } + ctx.TpdsController.mergeUpdate(ctx.req, res) + }) }) }) describe('getting a delete update', function () { - it('should process the delete with the update receiver by name', function (done) { - const path = '/projectName/here.txt' - const req = { - params: { 0: path, user_id: this.user_id, project_id: '' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - sendStatus: () => { - this.TpdsUpdateHandler.promises.deleteUpdate - .calledWith( - this.user_id, - '', - 'projectName', - '/here.txt', - this.source - ) - .should.equal(true) - done() - }, - } - this.TpdsController.deleteUpdate(req, res) + it('should process the delete with the update receiver by name', function (ctx) { + return new Promise(resolve => { + const path = '/projectName/here.txt' + const req = { + params: { 0: path, user_id: ctx.user_id, project_id: '' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + sendStatus: () => { + ctx.TpdsUpdateHandler.promises.deleteUpdate + .calledWith( + ctx.user_id, + '', + 'projectName', + '/here.txt', + ctx.source + ) + .should.equal(true) + resolve() + }, + } + ctx.TpdsController.deleteUpdate(req, res) + }) }) - it('should process the delete with the update receiver by id', function (done) { - const path = '/here.txt' - const req = { - params: { 0: path, user_id: this.user_id, project_id: '123' }, - session: { - destroy() {}, - }, - headers: { - 'x-update-source': (this.source = 'dropbox'), - }, - } - const res = { - sendStatus: () => { - this.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith( - this.user_id, - '123', - '', // projectName - '/here.txt', - this.source - ) - done() - }, - } - this.TpdsController.deleteUpdate(req, res) + it('should process the delete with the update receiver by id', function (ctx) { + return new Promise(resolve => { + const path = '/here.txt' + const req = { + params: { 0: path, user_id: ctx.user_id, project_id: '123' }, + session: { + destroy() {}, + }, + headers: { + 'x-update-source': (ctx.source = 'dropbox'), + }, + } + const res = { + sendStatus: () => { + ctx.TpdsUpdateHandler.promises.deleteUpdate.should.have.been.calledWith( + ctx.user_id, + '123', + '', // projectName + '/here.txt', + ctx.source + ) + resolve() + }, + } + ctx.TpdsController.deleteUpdate(req, res) + }) }) }) describe('updateFolder', function () { - beforeEach(function () { - this.req = { - body: { userId: this.user_id, path: '/abc/def/ghi.txt' }, + beforeEach(function (ctx) { + ctx.req = { + body: { userId: ctx.user_id, path: '/abc/def/ghi.txt' }, } - this.res = { + ctx.res = { json: sinon.stub(), } }) - it("creates a folder if it doesn't exist", function (done) { - const metadata = { - folderId: new ObjectId(), - projectId: new ObjectId(), - path: '/def/ghi.txt', - parentFolderId: new ObjectId(), - } - this.TpdsUpdateHandler.promises.createFolder.resolves(metadata) - this.res.json.callsFake(body => { - expect(body).to.deep.equal({ - entityId: metadata.folderId.toString(), - projectId: metadata.projectId.toString(), - path: metadata.path, - folderId: metadata.parentFolderId.toString(), + it("creates a folder if it doesn't exist", function (ctx) { + return new Promise(resolve => { + const metadata = { + folderId: new ObjectId(), + projectId: new ObjectId(), + path: '/def/ghi.txt', + parentFolderId: new ObjectId(), + } + ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata) + ctx.res.json.callsFake(body => { + expect(body).to.deep.equal({ + entityId: metadata.folderId.toString(), + projectId: metadata.projectId.toString(), + path: metadata.path, + folderId: metadata.parentFolderId.toString(), + }) + resolve() }) - done() + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) - it('supports top level folders', function (done) { - const metadata = { - folderId: new ObjectId(), - projectId: new ObjectId(), - path: '/', - parentFolderId: null, - } - this.TpdsUpdateHandler.promises.createFolder.resolves(metadata) - this.res.json.callsFake(body => { - expect(body).to.deep.equal({ - entityId: metadata.folderId.toString(), - projectId: metadata.projectId.toString(), - path: metadata.path, - folderId: null, + it('supports top level folders', function (ctx) { + return new Promise(resolve => { + const metadata = { + folderId: new ObjectId(), + projectId: new ObjectId(), + path: '/', + parentFolderId: null, + } + ctx.TpdsUpdateHandler.promises.createFolder.resolves(metadata) + ctx.res.json.callsFake(body => { + expect(body).to.deep.equal({ + entityId: metadata.folderId.toString(), + projectId: metadata.projectId.toString(), + path: metadata.path, + folderId: null, + }) + resolve() }) - done() + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) - it("returns a 409 if the folder couldn't be created", function (done) { - this.TpdsUpdateHandler.promises.createFolder.resolves(null) - this.HttpErrorHandler.conflict.callsFake((req, res) => { - expect(req).to.equal(this.req) - expect(res).to.equal(this.res) - done() + it("returns a 409 if the folder couldn't be created", function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.promises.createFolder.resolves(null) + ctx.HttpErrorHandler.conflict.callsFake((req, res) => { + expect(req).to.equal(ctx.req) + expect(res).to.equal(ctx.res) + resolve() + }) + ctx.TpdsController.updateFolder(ctx.req, ctx.res) }) - this.TpdsController.updateFolder(this.req, this.res) }) }) describe('parseParams', function () { - it('should take the project name off the start and replace with slash', function () { + it('should take the project name off the start and replace with slash', function (ctx) { const path = 'noSlashHere' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) - result.userId.should.equal(this.user_id) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) + result.userId.should.equal(ctx.user_id) result.filePath.should.equal('/') result.projectName.should.equal(path) }) - it('should take the project name off the start and it with no slashes in', function () { + it('should take the project name off the start and it with no slashes in', function (ctx) { const path = '/project/file.tex' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) - result.userId.should.equal(this.user_id) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) + result.userId.should.equal(ctx.user_id) result.filePath.should.equal('/file.tex') result.projectName.should.equal('project') }) - it('should take the project name of and return a slash for the file path', function () { + it('should take the project name of and return a slash for the file path', function (ctx) { const path = '/project_name' - const req = { params: { 0: path, user_id: this.user_id } } - const result = this.TpdsController.parseParams(req) + const req = { params: { 0: path, user_id: ctx.user_id } } + const result = ctx.TpdsController.parseParams(req) result.projectName.should.equal('project_name') result.filePath.should.equal('/') }) }) describe('updateProjectContents', function () { - beforeEach(async function () { - this.req = { + beforeEach(async function (ctx) { + ctx.req = { params: { - 0: (this.path = 'chapters/main.tex'), - project_id: (this.project_id = 'project-id-123'), + 0: (ctx.path = 'chapters/main.tex'), + project_id: (ctx.project_id = 'project-id-123'), }, session: { destroy: sinon.stub(), }, headers: { - 'x-update-source': (this.source = 'github'), + 'x-update-source': (ctx.source = 'github'), }, } - this.res = { + ctx.res = { json: sinon.stub(), sendStatus: sinon.stub(), } - await this.TpdsController.promises.updateProjectContents( - this.req, - this.res - ) + await ctx.TpdsController.promises.updateProjectContents(ctx.req, ctx.res) }) - it('should merge the update', function () { - this.UpdateMerger.promises.mergeUpdate.should.be.calledWith( + it('should merge the update', function (ctx) { + ctx.UpdateMerger.promises.mergeUpdate.should.be.calledWith( null, - this.project_id, - `/${this.path}`, - this.req, - this.source + ctx.project_id, + `/${ctx.path}`, + ctx.req, + ctx.source ) }) - it('should return a success', function () { - this.res.json.should.be.calledWith({ - entityId: this.metadata.entityId.toString(), - rev: this.metadata.rev, + it('should return a success', function (ctx) { + ctx.res.json.should.be.calledWith({ + entityId: ctx.metadata.entityId.toString(), + rev: ctx.metadata.rev, }) }) }) describe('deleteProjectContents', function () { - beforeEach(async function () { - this.req = { + beforeEach(async function (ctx) { + ctx.req = { params: { - 0: (this.path = 'chapters/main.tex'), - project_id: (this.project_id = 'project-id-123'), + 0: (ctx.path = 'chapters/main.tex'), + project_id: (ctx.project_id = 'project-id-123'), }, session: { destroy: sinon.stub(), }, headers: { - 'x-update-source': (this.source = 'github'), + 'x-update-source': (ctx.source = 'github'), }, } - this.res = { + ctx.res = { sendStatus: sinon.stub(), json: sinon.stub(), } - await this.TpdsController.promises.deleteProjectContents( - this.req, - this.res - ) + await ctx.TpdsController.promises.deleteProjectContents(ctx.req, ctx.res) }) - it('should delete the file', function () { - this.UpdateMerger.promises.deleteUpdate.should.be.calledWith( + it('should delete the file', function (ctx) { + ctx.UpdateMerger.promises.deleteUpdate.should.be.calledWith( null, - this.project_id, - `/${this.path}`, - this.source + ctx.project_id, + `/${ctx.path}`, + ctx.source ) }) - it('should return a success', function () { - this.res.json.should.be.calledWith({ - entityId: this.metadata.entityId, + it('should return a success', function (ctx) { + ctx.res.json.should.be.calledWith({ + entityId: ctx.metadata.entityId, }) }) }) describe('getQueues', function () { - beforeEach(function () { - this.req = {} - this.res = { json: sinon.stub() } - this.next = sinon.stub() + beforeEach(function (ctx) { + ctx.req = {} + ctx.res = { json: sinon.stub() } + ctx.next = sinon.stub() }) describe('success', function () { - beforeEach(function (done) { - this.res.json.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.json.callsFake(() => { + resolve() + }) + ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next) }) - this.TpdsController.getQueues(this.req, this.res, this.next) }) - it('should use userId from session', function () { - this.SessionManager.getLoggedInUserId.should.have.been.calledOnce - this.TpdsQueueManager.promises.getQueues.should.have.been.calledWith( + it('should use userId from session', function (ctx) { + ctx.SessionManager.getLoggedInUserId.should.have.been.calledOnce + ctx.TpdsQueueManager.promises.getQueues.should.have.been.calledWith( 'user-id' ) }) - it('should call json with response', function () { - this.res.json.should.have.been.calledWith('queues') - this.next.should.not.have.been.called + it('should call json with response', function (ctx) { + ctx.res.json.should.have.been.calledWith('queues') + ctx.next.should.not.have.been.called }) }) describe('error', function () { - beforeEach(function (done) { - this.err = new Error() - this.TpdsQueueManager.promises.getQueues = sinon - .stub() - .rejects(this.err) - this.next.callsFake(() => { - done() + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.err = new Error() + ctx.TpdsQueueManager.promises.getQueues = sinon + .stub() + .rejects(ctx.err) + ctx.next.callsFake(() => { + resolve() + }) + ctx.TpdsController.getQueues(ctx.req, ctx.res, ctx.next) }) - this.TpdsController.getQueues(this.req, this.res, this.next) }) - it('should call next with error', function () { - this.res.json.should.not.have.been.called - this.next.should.have.been.calledWith(this.err) + it('should call next with error', function (ctx) { + ctx.res.json.should.not.have.been.called + ctx.next.should.have.been.calledWith(ctx.err) }) }) }) diff --git a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs index a5ca099b5b..96cc22279e 100644 --- a/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs +++ b/services/web/test/unit/src/ThirdPartyDataStore/TpdsUpdateHandler.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -9,120 +9,158 @@ const ObjectId = mongodb.ObjectId const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.mjs' +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('TpdsUpdateHandler', function () { - beforeEach(async function () { - this.projectName = 'My recipes' - this.projects = { - active1: { _id: new ObjectId(), name: this.projectName }, - active2: { _id: new ObjectId(), name: this.projectName }, + beforeEach(async function (ctx) { + ctx.projectName = 'My recipes' + ctx.projects = { + active1: { _id: new ObjectId(), name: ctx.projectName }, + active2: { _id: new ObjectId(), name: ctx.projectName }, archived1: { _id: new ObjectId(), - name: this.projectName, - archived: [this.userId], + name: ctx.projectName, + archived: [ctx.userId], }, archived2: { _id: new ObjectId(), - name: this.projectName, - archived: [this.userId], + name: ctx.projectName, + archived: [ctx.userId], }, } - this.userId = new ObjectId() - this.source = 'dropbox' - this.path = `/some/file` - this.update = {} - this.folderPath = '/some/folder' - this.folder = { + ctx.userId = new ObjectId() + ctx.source = 'dropbox' + ctx.path = `/some/file` + ctx.update = {} + ctx.folderPath = '/some/folder' + ctx.folder = { _id: new ObjectId(), parentFolder_id: new ObjectId(), } - this.CooldownManager = { + ctx.CooldownManager = { promises: { isProjectOnCooldown: sinon.stub().resolves(false), }, } - this.FileTypeManager = { + ctx.FileTypeManager = { promises: { shouldIgnore: sinon.stub().resolves(false), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves() }, }, } - this.notification = { + ctx.notification = { create: sinon.stub().resolves(), } - this.NotificationsBuilder = { + ctx.NotificationsBuilder = { promises: { - dropboxDuplicateProjectNames: sinon.stub().returns(this.notification), + dropboxDuplicateProjectNames: sinon.stub().returns(ctx.notification), }, } - this.ProjectCreationHandler = { + ctx.ProjectCreationHandler = { promises: { - createBlankProject: sinon.stub().resolves(this.projects.active1), + createBlankProject: sinon.stub().resolves(ctx.projects.active1), }, } - this.ProjectDeleter = { + ctx.ProjectDeleter = { promises: { markAsDeletedByExternalSource: sinon.stub().resolves(), }, } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { findUsersProjectsByName: sinon.stub(), findAllUsersProjects: sinon .stub() - .resolves({ owned: [this.projects.active1], readAndWrite: [] }), + .resolves({ owned: [ctx.projects.active1], readAndWrite: [] }), }, } - this.ProjectHelper = { + ctx.ProjectHelper = { isArchivedOrTrashed: sinon.stub().returns(false), } - this.ProjectHelper.isArchivedOrTrashed - .withArgs(this.projects.archived1, this.userId) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(ctx.projects.archived1, ctx.userId) .returns(true) - this.ProjectHelper.isArchivedOrTrashed - .withArgs(this.projects.archived2, this.userId) + ctx.ProjectHelper.isArchivedOrTrashed + .withArgs(ctx.projects.archived2, ctx.userId) .returns(true) - this.RootDocManager = { + ctx.RootDocManager = { setRootDocAutomaticallyInBackground: sinon.stub(), } - this.UpdateMerger = { + ctx.UpdateMerger = { promises: { deleteUpdate: sinon.stub().resolves(), mergeUpdate: sinon.stub().resolves(), - createFolder: sinon.stub().resolves(this.folder), + createFolder: sinon.stub().resolves(ctx.folder), }, } - this.TpdsUpdateHandler = await esmock.strict(MODULE_PATH, { - '.../../../../app/src/Features/Cooldown/CooldownManager': - this.CooldownManager, - '../../../../app/src/Features/Uploads/FileTypeManager': - this.FileTypeManager, - '../../../../app/src/infrastructure/Modules': this.Modules, - '../../../../app/src/Features/Notifications/NotificationsBuilder': - this.NotificationsBuilder, - '../../../../app/src/Features/Project/ProjectCreationHandler': - this.ProjectCreationHandler, - '../../../../app/src/Features/Project/ProjectDeleter': - this.ProjectDeleter, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Project/ProjectHelper': this.ProjectHelper, - '../../../../app/src/Features/Project/ProjectRootDocManager': - this.RootDocManager, - '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger': - this.UpdateMerger, - }) + vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({ + default: ctx.CooldownManager, + })) + + vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({ + default: ctx.FileTypeManager, + })) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + + vi.doMock( + '../../../../app/src/Features/Notifications/NotificationsBuilder', + () => ({ + default: ctx.NotificationsBuilder, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectCreationHandler', + () => ({ + default: ctx.ProjectCreationHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({ + default: ctx.ProjectDeleter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({ + default: ctx.ProjectHelper, + })) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectRootDocManager', + () => ({ + default: ctx.RootDocManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger', + () => ({ + default: ctx.UpdateMerger, + }) + ) + + ctx.TpdsUpdateHandler = (await import(MODULE_PATH)).default }) describe('getting an update', function () { describe('byId', function () { describe('with no matching project', function () { - beforeEach(function () { - this.projectId = new ObjectId().toString() + beforeEach(function (ctx) { + ctx.projectId = new ObjectId().toString() }) receiveUpdateById() expectProjectNotCreated() @@ -130,8 +168,8 @@ describe('TpdsUpdateHandler', function () { }) describe('with one matching active project', function () { - beforeEach(function () { - this.projectId = this.projects.active1._id.toString() + beforeEach(function (ctx) { + ctx.projectId = ctx.projects.active1._id.toString() }) receiveUpdateById() expectProjectNotCreated() @@ -187,8 +225,8 @@ describe('TpdsUpdateHandler', function () { describe('update to a file that should be ignored', async function () { setupMatchingProjects(['active1']) - beforeEach(function () { - this.FileTypeManager.promises.shouldIgnore.resolves(true) + beforeEach(function (ctx) { + ctx.FileTypeManager.promises.shouldIgnore.resolves(true) }) receiveUpdate() expectProjectNotCreated() @@ -199,15 +237,15 @@ describe('TpdsUpdateHandler', function () { describe('update to a project on cooldown', async function () { setupMatchingProjects(['active1']) setupProjectOnCooldown() - beforeEach(async function () { + beforeEach(async function (ctx) { await expect( - this.TpdsUpdateHandler.promises.newUpdate( - this.userId, + ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.update, - this.source + ctx.projectName, + ctx.path, + ctx.update, + ctx.source ) ).to.be.rejectedWith(Errors.TooManyRequestsError) }) @@ -218,8 +256,8 @@ describe('TpdsUpdateHandler', function () { describe('getting a file delete', function () { describe('byId', function () { describe('with no matching project', function () { - beforeEach(function () { - this.projectId = new ObjectId().toString() + beforeEach(function (ctx) { + ctx.projectId = new ObjectId().toString() }) receiveFileDeleteById() expectDeleteNotProcessed() @@ -227,8 +265,8 @@ describe('TpdsUpdateHandler', function () { }) describe('with one matching active project', function () { - beforeEach(function () { - this.projectId = this.projects.active1._id.toString() + beforeEach(function (ctx) { + ctx.projectId = ctx.projects.active1._id.toString() }) receiveFileDeleteById() expectDeleteProcessed() @@ -379,13 +417,13 @@ describe('TpdsUpdateHandler', function () { describe('update to a project on cooldown', async function () { setupMatchingProjects(['active1']) setupProjectOnCooldown() - beforeEach(async function () { + beforeEach(async function (ctx) { await expect( - this.TpdsUpdateHandler.promises.createFolder( - this.userId, - this.projectId, - this.projectName, - this.path + ctx.TpdsUpdateHandler.promises.createFolder( + ctx.userId, + ctx.projectId, + ctx.projectName, + ctx.path ) ).to.be.rejectedWith(Errors.TooManyRequestsError) }) @@ -397,18 +435,18 @@ describe('TpdsUpdateHandler', function () { /* Setup helpers */ function setupMatchingProjects(projectKeys) { - beforeEach(function () { - const projects = projectKeys.map(key => this.projects[key]) - this.ProjectGetter.promises.findUsersProjectsByName - .withArgs(this.userId, this.projectName) + beforeEach(function (ctx) { + const projects = projectKeys.map(key => ctx.projects[key]) + ctx.ProjectGetter.promises.findUsersProjectsByName + .withArgs(ctx.userId, ctx.projectName) .resolves(projects) }) } function setupProjectOnCooldown() { - beforeEach(function () { - this.CooldownManager.promises.isProjectOnCooldown - .withArgs(this.projects.active1._id) + beforeEach(function (ctx) { + ctx.CooldownManager.promises.isProjectOnCooldown + .withArgs(ctx.projects.active1._id) .resolves(true) }) } @@ -416,76 +454,77 @@ function setupProjectOnCooldown() { /* Test helpers */ function receiveUpdate() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.newUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.update, - this.source + ctx.projectName, + ctx.path, + ctx.update, + ctx.source ) }) } function receiveUpdateById() { - beforeEach(function (done) { - this.TpdsUpdateHandler.newUpdate( - this.userId, - this.projectId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.newUpdate( + ctx.userId, + ctx.projectId, '', // projectName - this.path, - this.update, - this.source, - done + ctx.path, + ctx.update, + ctx.source ) }) } function receiveFileDelete() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.deleteUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.deleteUpdate( + ctx.userId, '', // projectId - this.projectName, - this.path, - this.source + ctx.projectName, + ctx.path, + ctx.source ) }) } function receiveFileDeleteById() { - beforeEach(function (done) { - this.TpdsUpdateHandler.deleteUpdate( - this.userId, - this.projectId, - '', // projectName - this.path, - this.source, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TpdsUpdateHandler.deleteUpdate( + ctx.userId, + ctx.projectId, + '', // projectName + ctx.path, + ctx.source, + resolve + ) + }) }) } function receiveProjectDelete() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.deleteUpdate( - this.userId, + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.deleteUpdate( + ctx.userId, '', // projectId - this.projectName, + ctx.projectName, '/', - this.source + ctx.source ) }) } function receiveFolderUpdate() { - beforeEach(async function () { - await this.TpdsUpdateHandler.promises.createFolder( - this.userId, - this.projectId, - this.projectName, - this.folderPath + beforeEach(async function (ctx) { + await ctx.TpdsUpdateHandler.promises.createFolder( + ctx.userId, + ctx.projectId, + ctx.projectName, + ctx.folderPath ) }) } @@ -493,121 +532,121 @@ function receiveFolderUpdate() { /* Expectations */ function expectProjectCreated() { - it('creates a project', function () { + it('creates a project', function (ctx) { expect( - this.ProjectCreationHandler.promises.createBlankProject - ).to.have.been.calledWith(this.userId, this.projectName) + ctx.ProjectCreationHandler.promises.createBlankProject + ).to.have.been.calledWith(ctx.userId, ctx.projectName) }) - it('sets the root doc', function () { + it('sets the root doc', function (ctx) { expect( - this.RootDocManager.setRootDocAutomaticallyInBackground - ).to.have.been.calledWith(this.projects.active1._id) + ctx.RootDocManager.setRootDocAutomaticallyInBackground + ).to.have.been.calledWith(ctx.projects.active1._id) }) } function expectProjectNotCreated() { - it('does not create a project', function () { - expect(this.ProjectCreationHandler.promises.createBlankProject).not.to.have + it('does not create a project', function (ctx) { + expect(ctx.ProjectCreationHandler.promises.createBlankProject).not.to.have .been.called }) - it('does not set the root doc', function () { - expect(this.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have + it('does not set the root doc', function (ctx) { + expect(ctx.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have .been.called }) } function expectUpdateProcessed() { - it('processes the update', function () { - expect(this.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith( - this.userId, - this.projects.active1._id, - this.path, - this.update, - this.source + it('processes the update', function (ctx) { + expect(ctx.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith( + ctx.userId, + ctx.projects.active1._id, + ctx.path, + ctx.update, + ctx.source ) }) } function expectUpdateNotProcessed() { - it('does not process the update', function () { - expect(this.UpdateMerger.promises.mergeUpdate).not.to.have.been.called + it('does not process the update', function (ctx) { + expect(ctx.UpdateMerger.promises.mergeUpdate).not.to.have.been.called }) } function expectFolderUpdateProcessed() { - it('processes the folder update', function () { - expect(this.UpdateMerger.promises.createFolder).to.have.been.calledWith( - this.projects.active1._id, - this.folderPath, - this.userId + it('processes the folder update', function (ctx) { + expect(ctx.UpdateMerger.promises.createFolder).to.have.been.calledWith( + ctx.projects.active1._id, + ctx.folderPath, + ctx.userId ) }) } function expectFolderUpdateNotProcessed() { - it("doesn't process the folder update", function () { - expect(this.UpdateMerger.promises.createFolder).not.to.have.been.called + it("doesn't process the folder update", function (ctx) { + expect(ctx.UpdateMerger.promises.createFolder).not.to.have.been.called }) } function expectDropboxUnlinked() { - it('unlinks Dropbox', function () { - expect(this.Modules.promises.hooks.fire).to.have.been.calledWith( + it('unlinks Dropbox', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith( 'removeDropbox', - this.userId, + ctx.userId, 'duplicate-projects' ) }) - it('creates a notification that dropbox was unlinked', function () { + it('creates a notification that dropbox was unlinked', function (ctx) { expect( - this.NotificationsBuilder.promises.dropboxDuplicateProjectNames - ).to.have.been.calledWith(this.userId) - expect(this.notification.create).to.have.been.calledWith(this.projectName) + ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames + ).to.have.been.calledWith(ctx.userId) + expect(ctx.notification.create).to.have.been.calledWith(ctx.projectName) }) } function expectDropboxNotUnlinked() { - it('does not unlink Dropbox', function () { - expect(this.Modules.promises.hooks.fire).not.to.have.been.called + it('does not unlink Dropbox', function (ctx) { + expect(ctx.Modules.promises.hooks.fire).not.to.have.been.called }) - it('does not create a notification that dropbox was unlinked', function () { - expect(this.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not + it('does not create a notification that dropbox was unlinked', function (ctx) { + expect(ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not .to.have.been.called }) } function expectDeleteProcessed() { - it('processes the delete', function () { - expect(this.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith( - this.userId, - this.projects.active1._id, - this.path, - this.source + it('processes the delete', function (ctx) { + expect(ctx.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith( + ctx.userId, + ctx.projects.active1._id, + ctx.path, + ctx.source ) }) } function expectDeleteNotProcessed() { - it('does not process the delete', function () { - expect(this.UpdateMerger.promises.deleteUpdate).not.to.have.been.called + it('does not process the delete', function (ctx) { + expect(ctx.UpdateMerger.promises.deleteUpdate).not.to.have.been.called }) } function expectProjectDeleted() { - it('deletes the project', function () { + it('deletes the project', function (ctx) { expect( - this.ProjectDeleter.promises.markAsDeletedByExternalSource - ).to.have.been.calledWith(this.projects.active1._id) + ctx.ProjectDeleter.promises.markAsDeletedByExternalSource + ).to.have.been.calledWith(ctx.projects.active1._id) }) } function expectProjectNotDeleted() { - it('does not delete the project', function () { - expect(this.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to + it('does not delete the project', function (ctx) { + expect(ctx.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to .have.been.called }) } diff --git a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs index 8097218076..3408c3bb32 100644 --- a/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs +++ b/services/web/test/unit/src/TokenAccess/TokenAccessController.test.mjs @@ -1,4 +1,4 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' import mongodb from 'mongodb-legacy' @@ -13,27 +13,27 @@ const MODULE_PATH = '../../../../app/src/Features/TokenAccess/TokenAccessController' describe('TokenAccessController', function () { - beforeEach(async function () { - this.token = 'abc123' - this.user = { _id: new ObjectId() } - this.project = { + beforeEach(async function (ctx) { + ctx.token = 'abc123' + ctx.user = { _id: new ObjectId() } + ctx.project = { _id: new ObjectId(), - owner_ref: this.user._id, + owner_ref: ctx.user._id, name: 'test', tokenAccessReadAndWrite_refs: [], tokenAccessReadOnly_refs: [], } - this.req = new MockRequest() - this.res = new MockResponse() - this.next = sinon.stub().returns() + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.next = sinon.stub().returns() - this.Settings = { + ctx.Settings = { siteUrl: 'https://www.dev-overleaf.com', adminPrivilegeAvailable: false, adminUrl: 'https://admin.dev-overleaf.com', adminDomains: ['overleaf.com'], } - this.TokenAccessHandler = { + ctx.TokenAccessHandler = { TOKEN_TYPES: { READ_ONLY: 'readOnly', READ_AND_WRITE: 'readAndWrite', @@ -46,7 +46,7 @@ describe('TokenAccessController', function () { grantSessionTokenAccess: sinon.stub(), promises: { addReadOnlyUserToProject: sinon.stub().resolves(), - getProjectByToken: sinon.stub().resolves(this.project), + getProjectByToken: sinon.stub().resolves(ctx.project), getV1DocPublishedInfo: sinon.stub().resolves({ allow: true }), getV1DocInfo: sinon.stub(), removeReadAndWriteUserFromProject: sinon.stub().resolves(), @@ -54,16 +54,16 @@ describe('TokenAccessController', function () { }, } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.user._id), } - this.AuthenticationController = { + ctx.AuthenticationController = { setRedirectInSession: sinon.stub(), } - this.AuthorizationManager = { + ctx.AuthorizationManager = { promises: { getPrivilegeLevelForProject: sinon .stub() @@ -71,35 +71,35 @@ describe('TokenAccessController', function () { }, } - this.AuthorizationMiddleware = {} + ctx.AuthorizationMiddleware = {} - this.ProjectAuditLogHandler = { + ctx.ProjectAuditLogHandler = { promises: { addEntry: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), getAssignmentForUser: sinon.stub().resolves({ variant: 'default' }), }, } - this.CollaboratorsInviteHandler = { + ctx.CollaboratorsInviteHandler = { promises: { revokeInviteForUser: sinon.stub().resolves(), }, } - this.CollaboratorsHandler = { + ctx.CollaboratorsHandler = { promises: { addUserIdToProject: sinon.stub().resolves(), setCollaboratorPrivilegeLevel: sinon.stub().resolves(), }, } - this.CollaboratorsGetter = { + ctx.CollaboratorsGetter = { promises: { userIsReadWriteTokenMember: sinon.stub().resolves(), isUserInvitedReadWriteMemberOfProject: sinon.stub().resolves(), @@ -107,24 +107,24 @@ describe('TokenAccessController', function () { }, } - this.EditorRealTimeController = { emitToRoom: sinon.stub() } + ctx.EditorRealTimeController = { emitToRoom: sinon.stub() } - this.ProjectGetter = { + ctx.ProjectGetter = { promises: { - getProject: sinon.stub().resolves(this.project), + getProject: sinon.stub().resolves(ctx.project), }, } - this.AnalyticsManager = { + ctx.AnalyticsManager = { recordEventForSession: sinon.stub(), recordEventForUserInBackground: sinon.stub(), } - this.UserGetter = { + ctx.UserGetter = { promises: { getUser: sinon.stub().callsFake(async (userId, filter) => { - if (userId === this.userId) { - return this.user + if (userId === ctx.userId) { + return ctx.user } else { return null } @@ -134,322 +134,415 @@ describe('TokenAccessController', function () { }, } - this.LimitationsManager = { + ctx.LimitationsManager = { promises: { canAcceptEditCollaboratorInvite: sinon.stub().resolves(), }, } - this.TokenAccessController = await esmock.strict(MODULE_PATH, { - '@overleaf/settings': this.Settings, - '../../../../app/src/Features/TokenAccess/TokenAccessHandler': - this.TokenAccessHandler, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Authorization/AuthorizationManager': - this.AuthorizationManager, - '../../../../app/src/Features/Authorization/AuthorizationMiddleware': - this.AuthorizationMiddleware, - '../../../../app/src/Features/Project/ProjectAuditLogHandler': - this.ProjectAuditLogHandler, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/Errors/Errors': (this.Errors = { + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock( + '../../../../app/src/Features/TokenAccess/TokenAccessHandler', + () => ({ + default: ctx.TokenAccessHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authorization/AuthorizationManager', + () => ({ + default: ctx.AuthorizationManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authorization/AuthorizationMiddleware', + () => ({ + default: ctx.AuthorizationMiddleware, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Project/ProjectAuditLogHandler', + () => ({ + default: ctx.ProjectAuditLogHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/Errors', () => ({ + default: (ctx.Errors = { NotFoundError: sinon.stub(), }), - '../../../../app/src/Features/Collaborators/CollaboratorsHandler': - this.CollaboratorsHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler': - this.CollaboratorsInviteHandler, - '../../../../app/src/Features/Collaborators/CollaboratorsGetter': - this.CollaboratorsGetter, - '../../../../app/src/Features/Editor/EditorRealTimeController': - this.EditorRealTimeController, - '../../../../app/src/Features/Project/ProjectGetter': this.ProjectGetter, - '../../../../app/src/Features/Helpers/AsyncFormHelper': - (this.AsyncFormHelper = { - redirect: sinon.stub(), - }), - '../../../../app/src/Features/Helpers/AdminAuthorizationHelper': - (this.AdminAuthorizationHelper = { - canRedirectToAdminDomain: sinon.stub(), - }), - '../../../../app/src/Features/Helpers/UrlHelper': (this.UrlHelper = { - getSafeAdminDomainRedirect: sinon - .stub() - .callsFake( - path => `${this.Settings.adminUrl}${getSafeRedirectPath(path)}` - ), + })) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsHandler', + () => ({ + default: ctx.CollaboratorsHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsInviteHandler', + () => ({ + default: ctx.CollaboratorsInviteHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Collaborators/CollaboratorsGetter', + () => ({ + default: ctx.CollaboratorsGetter, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Editor/EditorRealTimeController', + () => ({ + default: ctx.EditorRealTimeController, + }) + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({ + default: ctx.ProjectGetter, + })) + + vi.doMock('../../../../app/src/Features/Helpers/AsyncFormHelper', () => ({ + default: (ctx.AsyncFormHelper = { + redirect: sinon.stub(), }), - '../../../../app/src/Features/Analytics/AnalyticsManager': - this.AnalyticsManager, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/Subscription/LimitationsManager': - this.LimitationsManager, - }) + })) + + vi.doMock( + '../../../../app/src/Features/Helpers/AdminAuthorizationHelper', + () => + (ctx.AdminAuthorizationHelper = { + canRedirectToAdminDomain: sinon.stub(), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Helpers/UrlHelper', + () => + (ctx.UrlHelper = { + getSafeAdminDomainRedirect: sinon + .stub() + .callsFake( + path => `${ctx.Settings.adminUrl}${getSafeRedirectPath(path)}` + ), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Analytics/AnalyticsManager', + () => ({ + default: ctx.AnalyticsManager, + }) + ) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock( + '../../../../app/src/Features/Subscription/LimitationsManager', + () => ({ + default: ctx.LimitationsManager, + }) + ) + + ctx.TokenAccessController = (await import(MODULE_PATH)).default }) describe('grantTokenAccessReadAndWrite', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) }) describe('normal case (edit slot available)', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - true - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + true + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read and write invited member', function () { + it('adds the user as a read and write invited member', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readAndWrite' } ) }) - it('records a project-joined event for the user', function () { + it('records a project-joined event for the user', function (ctx) { expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(ctx.user._id, 'project-joined', { mode: 'edit', - projectId: this.project._id.toString(), - ownerId: this.project.owner_ref.toString(), + projectId: ctx.project._id.toString(), + ownerId: ctx.project.owner_ref.toString(), role: PrivilegeLevels.READ_AND_WRITE, source: 'link-sharing', }) }) - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('emits a project membership changed event', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'project:membership:changed', { members: true, invites: true } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when there are no edit collaborator slots available', function () { - beforeEach(function (done) { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( - false - ) - this.req.params = { token: this.token } - this.req.body = { - confirmedByUser: true, - tokenHashPrefix: '#prefix', - } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + false + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = { + confirmedByUser: true, + tokenHashPrefix: '#prefix', + } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read only invited member instead (pendingEditor)', function () { + it('adds the user as a read only invited member instead (pendingEditor)', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_ONLY, { pendingEditor: true } ) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readOnly', pendingEditor: true } ) }) - it('records a project-joined event for the user', function () { + it('records a project-joined event for the user', function (ctx) { expect( - this.AnalyticsManager.recordEventForUserInBackground - ).to.have.been.calledWith(this.user._id, 'project-joined', { + ctx.AnalyticsManager.recordEventForUserInBackground + ).to.have.been.calledWith(ctx.user._id, 'project-joined', { mode: 'view', - projectId: this.project._id.toString(), + projectId: ctx.project._id.toString(), pendingEditor: true, - ownerId: this.project.owner_ref.toString(), + ownerId: ctx.project.owner_ref.toString(), role: PrivilegeLevels.READ_ONLY, source: 'link-sharing', }) }) - it('emits a project membership changed event', function () { - expect( - this.EditorRealTimeController.emitToRoom - ).to.have.been.calledWith( - this.project._id, + it('emits a project membership changed event', function (ctx) { + expect(ctx.EditorRealTimeController.emitToRoom).to.have.been.calledWith( + ctx.project._id, 'project:membership:changed', { members: true, invites: true } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when the access was already granted', function () { - beforeEach(function (done) { - this.project.tokenAccessReadAndWrite_refs.push(this.user._id) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.tokenAccessReadAndWrite_refs.push(ctx.user._id) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'accept-via-link-sharing', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readAndWrite' } ) }) - it('checks token hash', function () { + it('checks token hash', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('hash prefix missing in request', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('adds the user as a read and write invited member', function () { + it('adds the user as a read and write invited member', function (ctx) { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) }) - it('checks the hash prefix', function () { + it('checks the hash prefix', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('user is owner of project', function () { - beforeEach(function (done) { - this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( - PrivilegeLevels.OWNER - ) - this.req.params = { token: this.token } - this.req.body = {} - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( + PrivilegeLevels.OWNER + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = {} + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('checks token hash and includes log data', function () { + it('checks token hash and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readAndWrite', - this.user._id, + ctx.user._id, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'user already has higher or same privilege', } ) @@ -457,33 +550,35 @@ describe('TokenAccessController', function () { }) describe('when user is not logged in', function () { - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(null) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.SessionManager.getLoggedInUserId.returns(null) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } }) describe('ANONYMOUS_READ_AND_WRITE_ENABLED is undefined', function () { - beforeEach(function (done) { - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to restricted', function () { - expect(this.res.json).to.have.been.calledWith({ + it('redirects to restricted', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ redirect: '/restricted', anonWriteAccessDenied: true, }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', null, @@ -493,42 +588,44 @@ describe('TokenAccessController', function () { ) }) - it('saves redirect URL with URL fragment', function () { + it('saves redirect URL with URL fragment', function (ctx) { expect( - this.AuthenticationController.setRedirectInSession.lastCall.args[1] + ctx.AuthenticationController.setRedirectInSession.lastCall.args[1] ).to.equal('/#prefix') }) }) describe('ANONYMOUS_READ_AND_WRITE_ENABLED is true', function () { - beforeEach(function (done) { - this.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true - this.res.callback = done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.ANONYMOUS_READ_AND_WRITE_ENABLED = true + ctx.res.callback = resolve - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to project', function () { - expect(this.res.json).to.have.been.calledWith({ - redirect: `/project/${this.project._id}`, + it('redirects to project', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `/project/${ctx.project._id}`, grantAnonymousAccess: 'readAndWrite', }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', null, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'granting read-write anonymous access', } ) @@ -537,44 +634,48 @@ describe('TokenAccessController', function () { }) describe('when Overleaf SaaS', function () { - beforeEach(function () { - this.Settings.overleaf = {} + beforeEach(function (ctx) { + ctx.Settings.overleaf = {} }) describe('when token is for v1 project', function () { - beforeEach(function (done) { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.TokenAccessHandler.promises.getV1DocInfo.resolves({ - exists: true, - has_owner: true, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves( + undefined + ) + ctx.TokenAccessHandler.promises.getV1DocInfo.resolves({ + exists: true, + has_owner: true, + }) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) }) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) }) - it('returns v1 import data', function () { - expect(this.res.json).to.have.been.calledWith({ + it('returns v1 import data', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ v1Import: { status: 'canDownloadZip', - projectId: this.token, + projectId: ctx.token, hasOwner: true, name: 'Untitled', brandInfo: undefined, }, }) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, + ctx.user._id, { action: 'import v1', } @@ -583,31 +684,35 @@ describe('TokenAccessController', function () { }) describe('when token is not for a v1 or v2 project', function () { - beforeEach(function (done) { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.TokenAccessHandler.promises.getV1DocInfo.resolves({ - exists: false, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves( + undefined + ) + ctx.TokenAccessHandler.promises.getV1DocInfo.resolves({ + exists: false, + }) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + resolve + ) }) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - done - ) }) - it('returns 404', function () { - expect(this.res.sendStatus).to.have.been.calledWith(404) + it('returns 404', function (ctx) { + expect(ctx.res.sendStatus).to.have.been.calledWith(404) }) - it('checks the hash prefix and includes log data', function () { + it('checks the hash prefix and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readAndWrite', - this.user._id, + ctx.user._id, { action: '404', } @@ -617,62 +722,67 @@ describe('TokenAccessController', function () { }) describe('not Overleaf SaaS', function () { - beforeEach(function () { - this.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.TokenAccessHandler.promises.getProjectByToken.resolves(undefined) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } }) - it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (done) { - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) + it('passes Errors.NotFoundError to next when project not found and still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { - action: '404', - } - ) + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readAndWrite', + ctx.user._id, + { + action: '404', + } + ) - done() - } - ) + resolve() + } + ) + }) }) }) describe('when user is admin', function () { const admin = { _id: new ObjectId(), isAdmin: true } - beforeEach(function () { - this.SessionManager.getLoggedInUserId.returns(admin._id) - this.SessionManager.getSessionUser.returns(admin) - this.AdminAuthorizationHelper.canRedirectToAdminDomain.returns(true) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } + beforeEach(function (ctx) { + ctx.SessionManager.getLoggedInUserId.returns(admin._id) + ctx.SessionManager.getSessionUser.returns(admin) + ctx.AdminAuthorizationHelper.canRedirectToAdminDomain.returns(true) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } }) - it('redirects if project owner is non-admin', function () { - this.UserGetter.promises.getUserConfirmedEmails = sinon + it('redirects if project owner is non-admin', function (ctx) { + ctx.UserGetter.promises.getUserConfirmedEmails = sinon .stub() .resolves([{ email: 'test@not-overleaf.com' }]) - this.res.callback = () => { - expect(this.res.json).to.have.been.calledWith({ - redirect: `${this.Settings.adminUrl}/#prefix`, - }) - } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res - ) + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `${ctx.Settings.adminUrl}/#prefix`, + }) + resolve() + } + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res + ) + }) }) - it('grants access if project owner is an internal staff', function () { + it('grants access if project owner is an internal staff', function (ctx) { const internalStaff = { _id: new ObjectId(), isAdmin: true } const projectFromInternalStaff = { _id: new ObjectId(), @@ -681,16 +791,16 @@ describe('TokenAccessController', function () { tokenAccessReadOnly_refs: [], owner_ref: internalStaff._id, } - this.UserGetter.promises.getUser = sinon.stub().resolves(internalStaff) - this.UserGetter.promises.getUserConfirmedEmails = sinon + ctx.UserGetter.promises.getUser = sinon.stub().resolves(internalStaff) + ctx.UserGetter.promises.getUserConfirmedEmails = sinon .stub() .resolves([{ email: 'test@overleaf.com' }]) - this.TokenAccessHandler.promises.getProjectByToken = sinon + ctx.TokenAccessHandler.promises.getProjectByToken = sinon .stub() .resolves(projectFromInternalStaff) - this.res.callback = () => { + ctx.res.callback = () => { expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( projectFromInternalStaff._id, undefined, @@ -698,327 +808,345 @@ describe('TokenAccessController', function () { PrivilegeLevels.READ_AND_WRITE ) } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res + ctx.TokenAccessController.grantTokenAccessReadAndWrite(ctx.req, ctx.res) + }) + }) + + it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadAndWrite( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) + + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readAndWrite', + ctx.user._id, + { + projectId: ctx.project._id, + action: 'token access not enabled', + } + ) + + resolve() + } ) }) }) - it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) { - this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) - - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readAndWrite', - this.user._id, - { - projectId: this.project._id, - action: 'token access not enabled', - } - ) - - done() - } - ) - }) - - it('returns 400 when not using a read write token', function () { - this.TokenAccessHandler.isReadAndWriteToken.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadAndWrite( - this.req, - this.res - ) - expect(this.res.sendStatus).to.have.been.calledWith(400) + it('returns 400 when not using a read write token', function (ctx) { + ctx.TokenAccessHandler.isReadAndWriteToken.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadAndWrite(ctx.req, ctx.res) + expect(ctx.res.sendStatus).to.have.been.calledWith(400) }) }) describe('grantTokenAccessReadOnly', function () { describe('normal case', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true, tokenHashPrefix: '#prefix' } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) + }) + + it('grants read-only access', function (ctx) { + expect( + ctx.TokenAccessHandler.promises.addReadOnlyUserToProject + ).to.have.been.calledWith( + ctx.user._id, + ctx.project._id, + ctx.project.owner_ref ) }) - it('grants read-only access', function () { + it('writes a project audit log', function (ctx) { expect( - this.TokenAccessHandler.promises.addReadOnlyUserToProject + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.user._id, - this.project._id, - this.project.owner_ref - ) - }) - - it('writes a project audit log', function () { - expect( - this.ProjectAuditLogHandler.promises.addEntry - ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'join-via-token', - this.user._id, - this.req.ip, + ctx.user._id, + ctx.req.ip, { privileges: 'readOnly' } ) }) - it('checks if hash prefix matches', function () { + it('checks if hash prefix matches', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, '#prefix', 'readOnly', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) describe('when the access was already granted', function () { - beforeEach(function (done) { - this.project.tokenAccessReadOnly_refs.push(this.user._id) - this.req.params = { token: this.token } - this.req.body = { confirmedByUser: true } - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.project.tokenAccessReadOnly_refs.push(ctx.user._id) + ctx.req.params = { token: ctx.token } + ctx.req.body = { confirmedByUser: true } + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it("doesn't write a project audit log", function () { - expect(this.ProjectAuditLogHandler.promises.addEntry).to.not.have.been + it("doesn't write a project audit log", function (ctx) { + expect(ctx.ProjectAuditLogHandler.promises.addEntry).to.not.have.been .called }) - it('still checks if hash prefix matches', function () { + it('still checks if hash prefix matches', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readOnly', - this.user._id, - { projectId: this.project._id, action: 'continue' } + ctx.user._id, + { projectId: ctx.project._id, action: 'continue' } ) }) }) - it('returns 400 when not using a read only token', function () { - this.TokenAccessHandler.isReadOnlyToken.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadOnly(this.req, this.res) - expect(this.res.sendStatus).to.have.been.calledWith(400) + it('returns 400 when not using a read only token', function (ctx) { + ctx.TokenAccessHandler.isReadOnlyToken.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadOnly(ctx.req, ctx.res) + expect(ctx.res.sendStatus).to.have.been.calledWith(400) }) describe('anonymous users', function () { - beforeEach(function (done) { - this.req.params = { token: this.token } - this.SessionManager.getLoggedInUserId.returns(null) - this.res.callback = done + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.req.params = { token: ctx.token } + ctx.SessionManager.getLoggedInUserId.returns(null) + ctx.res.callback = resolve - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('allows anonymous users and checks the token hash', function () { - expect(this.res.json).to.have.been.calledWith({ - redirect: `/project/${this.project._id}`, + it('allows anonymous users and checks the token hash', function (ctx) { + expect(ctx.res.json).to.have.been.calledWith({ + redirect: `/project/${ctx.project._id}`, grantAnonymousAccess: 'readOnly', }) expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith(this.token, undefined, 'readOnly', null, { - projectId: this.project._id, + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith(ctx.token, undefined, 'readOnly', null, { + projectId: ctx.project._id, action: 'granting read-only anonymous access', }) }) }) describe('user is owner of project', function () { - beforeEach(function (done) { - this.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( - PrivilegeLevels.OWNER - ) - this.req.params = { token: this.token } - this.req.body = {} - this.res.callback = done - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AuthorizationManager.promises.getPrivilegeLevelForProject.returns( + PrivilegeLevels.OWNER + ) + ctx.req.params = { token: ctx.token } + ctx.req.body = {} + ctx.res.callback = resolve + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('checks token hash and includes log data', function () { + it('checks token hash and includes log data', function (ctx) { expect( - this.TokenAccessHandler.checkTokenHashPrefix + ctx.TokenAccessHandler.checkTokenHashPrefix ).to.have.been.calledWith( - this.token, + ctx.token, undefined, 'readOnly', - this.user._id, + ctx.user._id, { - projectId: this.project._id, + projectId: ctx.project._id, action: 'user already has higher or same privilege', } ) }) }) - it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (done) { - this.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) - this.req.params = { token: this.token } - this.req.body = { tokenHashPrefix: '#prefix' } - this.TokenAccessController.grantTokenAccessReadOnly( - this.req, - this.res, - args => { - expect(args).to.be.instanceof(this.Errors.NotFoundError) + it('passes Errors.NotFoundError to next when token access is not enabled but still checks token hash', function (ctx) { + return new Promise(resolve => { + ctx.TokenAccessHandler.tokenAccessEnabledForProject.returns(false) + ctx.req.params = { token: ctx.token } + ctx.req.body = { tokenHashPrefix: '#prefix' } + ctx.TokenAccessController.grantTokenAccessReadOnly( + ctx.req, + ctx.res, + args => { + expect(args).to.be.instanceof(ctx.Errors.NotFoundError) - expect( - this.TokenAccessHandler.checkTokenHashPrefix - ).to.have.been.calledWith( - this.token, - '#prefix', - 'readOnly', - this.user._id, - { - projectId: this.project._id, - action: 'token access not enabled', - } - ) + expect( + ctx.TokenAccessHandler.checkTokenHashPrefix + ).to.have.been.calledWith( + ctx.token, + '#prefix', + 'readOnly', + ctx.user._id, + { + projectId: ctx.project._id, + action: 'token access not enabled', + } + ) - done() - } - ) + resolve() + } + ) + }) }) }) describe('ensureUserCanUseSharingUpdatesConsentPage', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('when not in link sharing changes test', function () { - beforeEach(function (done) { - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.AsyncFormHelper.redirect = sinon.stub().callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) describe('when link sharing changes test active', function () { - beforeEach(function () { - this.SplitTestHandler.promises.getAssignmentForUser.resolves({ + beforeEach(function (ctx) { + ctx.SplitTestHandler.promises.getAssignmentForUser.resolves({ variant: 'active', }) }) describe('when user is not an invited editor and is a read write token member', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - false - ) - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - true - ) - this.next.callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - this.next - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + false + ) + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + true + ) + ctx.next.callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + ctx.next + ) + }) }) - it('calls next', function () { + it('calls next', function (ctx) { expect( - this.CollaboratorsGetter.promises + ctx.CollaboratorsGetter.promises .isUserInvitedReadWriteMemberOfProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember - ).to.have.been.calledWith(this.user._id, this.project._id) - expect(this.next).to.have.been.calledOnce - expect(this.next.firstCall.args[0]).to.not.exist + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) + expect(ctx.next).to.have.been.calledOnce + expect(ctx.next.firstCall.args[0]).to.not.exist }) }) describe('when user is already an invited editor', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( - true - ) - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedReadWriteMemberOfProject.resolves( + true + ) + ctx.AsyncFormHelper.redirect = sinon + .stub() + .callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) describe('when user not a read write token member', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( - false - ) - this.AsyncFormHelper.redirect = sinon.stub().callsFake(() => done()) - this.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.userIsReadWriteTokenMember.resolves( + false + ) + ctx.AsyncFormHelper.redirect = sinon + .stub() + .callsFake(() => resolve()) + ctx.TokenAccessController.ensureUserCanUseSharingUpdatesConsentPage( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('redirects to the project/editor', function () { - expect(this.AsyncFormHelper.redirect).to.have.been.calledWith( - this.req, - this.res, - `/project/${this.project._id}` + it('redirects to the project/editor', function (ctx) { + expect(ctx.AsyncFormHelper.redirect).to.have.been.calledWith( + ctx.req, + ctx.res, + `/project/${ctx.project._id}` ) }) }) @@ -1026,116 +1154,122 @@ describe('TokenAccessController', function () { }) describe('moveReadWriteToCollaborators', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('when there are collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( true ) }) describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToCollaborators( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('sets the privilege level to read and write for the invited viewer', function () { + it('sets the privilege level to read and write for the invited viewer', function (ctx) { expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ctx.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_AND_WRITE ) - expect(this.res.sendStatus).to.have.been.calledWith(204) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) }) }) describe('when there are no edit collaborator slots available', function () { - beforeEach(function () { - this.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( + beforeEach(function (ctx) { + ctx.LimitationsManager.promises.canAcceptEditCollaboratorInvite.resolves( false ) }) describe('previously joined token access user moving to named collaborator', function () { - beforeEach(function (done) { - this.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( - false - ) - this.res.callback = done - this.TokenAccessController.moveReadWriteToCollaborators( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.CollaboratorsGetter.promises.isUserInvitedMemberOfProject.resolves( + false + ) + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToCollaborators( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('sets the privilege level to read only for the invited viewer (pendingEditor)', function () { + it('sets the privilege level to read only for the invited viewer (pendingEditor)', function (ctx) { expect( - this.TokenAccessHandler.promises.removeReadAndWriteUserFromProject - ).to.have.been.calledWith(this.user._id, this.project._id) + ctx.TokenAccessHandler.promises.removeReadAndWriteUserFromProject + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) expect( - this.CollaboratorsHandler.promises.addUserIdToProject + ctx.CollaboratorsHandler.promises.addUserIdToProject ).to.have.been.calledWith( - this.project._id, + ctx.project._id, undefined, - this.user._id, + ctx.user._id, PrivilegeLevels.READ_ONLY, { pendingEditor: true } ) - expect(this.res.sendStatus).to.have.been.calledWith(204) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) }) }) }) describe('moveReadWriteToReadOnly', function () { - beforeEach(function () { - this.req.params = { Project_id: this.project._id } + beforeEach(function (ctx) { + ctx.req.params = { Project_id: ctx.project._id } }) describe('previously joined token access user moving to anonymous viewer', function () { - beforeEach(function (done) { - this.res.callback = done - this.TokenAccessController.moveReadWriteToReadOnly( - this.req, - this.res, - done - ) + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.res.callback = resolve + ctx.TokenAccessController.moveReadWriteToReadOnly( + ctx.req, + ctx.res, + resolve + ) + }) }) - it('removes them from read write token access refs and adds them to read only token access refs', function () { + it('removes them from read write token access refs and adds them to read only token access refs', function (ctx) { expect( - this.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly - ).to.have.been.calledWith(this.user._id, this.project._id) - expect(this.res.sendStatus).to.have.been.calledWith(204) + ctx.TokenAccessHandler.promises.moveReadAndWriteUserToReadOnly + ).to.have.been.calledWith(ctx.user._id, ctx.project._id) + expect(ctx.res.sendStatus).to.have.been.calledWith(204) }) - it('writes a project audit log', function () { + it('writes a project audit log', function (ctx) { expect( - this.ProjectAuditLogHandler.promises.addEntry + ctx.ProjectAuditLogHandler.promises.addEntry ).to.have.been.calledWith( - this.project._id, + ctx.project._id, 'readonly-via-sharing-updates', - this.user._id, - this.req.ip + ctx.user._id, + ctx.req.ip ) }) }) diff --git a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs index 35682f346c..1f6fd7adb9 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs @@ -2,15 +2,12 @@ // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns * DS206: Consider reworking classes to avoid initClass * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ +import { vi } from 'vitest' import sinon from 'sinon' - import { expect } from 'chai' - -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import ArchiveErrors from '../../../../app/src/Features/Uploads/ArchiveErrors.js' @@ -19,12 +16,12 @@ const modulePath = '../../../../app/src/Features/Uploads/ProjectUploadController.mjs' describe('ProjectUploadController', function () { - beforeEach(async function () { + beforeEach(async function (ctx) { let Timer - this.req = new MockRequest() - this.res = new MockResponse() - this.user_id = 'user-id-123' - this.metrics = { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.user_id = 'user-id-123' + ctx.metrics = { Timer: (Timer = (function () { Timer = class Timer { static initClass() { @@ -35,262 +32,298 @@ describe('ProjectUploadController', function () { return Timer })()), } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user_id), + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user_id), } - this.ProjectLocator = { + ctx.ProjectLocator = { promises: {}, } - this.EditorController = { + ctx.EditorController = { promises: {}, } - return (this.ProjectUploadController = await esmock.strict(modulePath, { - multer: sinon.stub(), - '@overleaf/settings': { path: {} }, - '../../../../app/src/Features/Uploads/ProjectUploadManager': - (this.ProjectUploadManager = {}), - '../../../../app/src/Features/Uploads/FileSystemImportManager': - (this.FileSystemImportManager = {}), - '@overleaf/metrics': this.metrics, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/Uploads/ArchiveErrors': ArchiveErrors, - '../../../../app/src/Features/Project/ProjectLocator': - this.ProjectLocator, - '../../../../app/src/Features/Editor/EditorController': - this.EditorController, - fs: (this.fs = {}), + vi.doMock('multer', () => ({ + default: sinon.stub(), })) + + vi.doMock('@overleaf/settings', () => ({ + default: { path: {} }, + })) + + vi.doMock( + '../../../../app/src/Features/Uploads/ProjectUploadManager', + () => ({ + default: (ctx.ProjectUploadManager = {}), + }) + ) + + vi.doMock( + '../../../../app/src/Features/Uploads/FileSystemImportManager', + () => ({ + default: (ctx.FileSystemImportManager = {}), + }) + ) + + vi.doMock('@overleaf/metrics', () => ({ + default: ctx.metrics, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Uploads/ArchiveErrors', + () => ArchiveErrors + ) + + vi.doMock('../../../../app/src/Features/Project/ProjectLocator', () => ({ + default: ctx.ProjectLocator, + })) + + vi.doMock('../../../../app/src/Features/Editor/EditorController', () => ({ + default: ctx.EditorController, + })) + + vi.doMock('fs', () => ({ + default: (ctx.fs = {}), + })) + + ctx.ProjectUploadController = (await import(modulePath)).default }) describe('uploadProject', function () { - beforeEach(function () { - this.path = '/path/to/file/on/disk.zip' - this.name = 'filename.zip' - this.req.file = { - path: this.path, + beforeEach(function (ctx) { + ctx.path = '/path/to/file/on/disk.zip' + ctx.fileName = 'filename.zip' + ctx.req.file = { + path: ctx.path, } - this.req.body = { - name: this.name, + ctx.req.body = { + name: ctx.fileName, } - this.req.session = { + ctx.req.session = { user: { - _id: this.user_id, + _id: ctx.user_id, }, } - this.project = { _id: (this.project_id = 'project-id-123') } + ctx.project = { _id: (ctx.project_id = 'project-id-123') } - return (this.fs.unlink = sinon.stub()) + ctx.fs.unlink = sinon.stub() }) describe('successfully', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() - .callsArgWith(3, null, this.project) - return this.ProjectUploadController.uploadProject(this.req, this.res) + .callsArgWith(3, null, ctx.project) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should create a project owned by the logged in user', function () { - return this.ProjectUploadManager.createProjectFromZipArchive - .calledWith(this.user_id) + it('should create a project owned by the logged in user', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive + .calledWith(ctx.user_id) .should.equal(true) }) - it('should create a project with the same name as the zip archive', function () { - return this.ProjectUploadManager.createProjectFromZipArchive + it('should create a project with the same name as the zip archive', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive .calledWith(sinon.match.any, 'filename', sinon.match.any) .should.equal(true) }) - it('should create a project from the zip archive', function () { - return this.ProjectUploadManager.createProjectFromZipArchive - .calledWith(sinon.match.any, sinon.match.any, this.path) + it('should create a project from the zip archive', function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive + .calledWith(sinon.match.any, sinon.match.any, ctx.path) .should.equal(true) }) - it('should return a successful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a successful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: true, - project_id: this.project_id, + project_id: ctx.project_id, }) ) }) - it('should record the time taken to do the upload', function () { - return this.metrics.Timer.prototype.done.called.should.equal(true) + it('should record the time taken to do the upload', function (ctx) { + ctx.metrics.Timer.prototype.done.called.should.equal(true) }) - it('should remove the uploaded file', function () { - return this.fs.unlink.calledWith(this.path).should.equal(true) + it('should remove the uploaded file', function (ctx) { + ctx.fs.unlink.calledWith(ctx.path).should.equal(true) }) }) describe('when ProjectUploadManager.createProjectFromZipArchive fails', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() - .callsArgWith(3, new Error('Something went wrong'), this.project) - return this.ProjectUploadController.uploadProject(this.req, this.res) + .callsArgWith(3, new Error('Something went wrong'), ctx.project) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should return a failed response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a failed response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'upload_failed' }) ) }) }) describe('when ProjectUploadManager.createProjectFromZipArchive reports the file as invalid', function () { - beforeEach(function () { - this.ProjectUploadManager.createProjectFromZipArchive = sinon + beforeEach(function (ctx) { + ctx.ProjectUploadManager.createProjectFromZipArchive = sinon .stub() .callsArgWith( 3, new ArchiveErrors.ZipContentsTooLargeError(), - this.project + ctx.project ) - return this.ProjectUploadController.uploadProject(this.req, this.res) + ctx.ProjectUploadController.uploadProject(ctx.req, ctx.res) }) - it('should return the reported error to the FileUploader client', function () { - expect(JSON.parse(this.res.body)).to.deep.equal({ + it('should return the reported error to the FileUploader client', function (ctx) { + expect(JSON.parse(ctx.res.body)).to.deep.equal({ success: false, error: 'zip_contents_too_large', }) }) - it("should return an 'unprocessable entity' status code", function () { - return expect(this.res.statusCode).to.equal(422) + it("should return an 'unprocessable entity' status code", function (ctx) { + expect(ctx.res.statusCode).to.equal(422) }) }) }) describe('uploadFile', function () { - beforeEach(function () { - this.project_id = 'project-id-123' - this.folder_id = 'folder-id-123' - this.path = '/path/to/file/on/disk.png' - this.name = 'filename.png' - this.req.file = { - path: this.path, + beforeEach(function (ctx) { + ctx.project_id = 'project-id-123' + ctx.folder_id = 'folder-id-123' + ctx.path = '/path/to/file/on/disk.png' + ctx.fileName = 'filename.png' + ctx.req.file = { + path: ctx.path, } - this.req.body = { - name: this.name, + ctx.req.body = { + name: ctx.fileName, } - this.req.session = { + ctx.req.session = { user: { - _id: this.user_id, + _id: ctx.user_id, }, } - this.req.params = { Project_id: this.project_id } - this.req.query = { folder_id: this.folder_id } - return (this.fs.unlink = sinon.stub()) + ctx.req.params = { Project_id: ctx.project_id } + ctx.req.query = { folder_id: ctx.folder_id } + ctx.fs.unlink = sinon.stub() }) describe('successfully', function () { - beforeEach(function () { - this.entity = { + beforeEach(function (ctx) { + ctx.entity = { _id: '1234', type: 'file', } - this.FileSystemImportManager.addEntity = sinon + ctx.FileSystemImportManager.addEntity = sinon .stub() - .callsArgWith(6, null, this.entity) - return this.ProjectUploadController.uploadFile(this.req, this.res) + .callsArgWith(6, null, ctx.entity) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should insert the file', function () { - return this.FileSystemImportManager.addEntity + it('should insert the file', function (ctx) { + return ctx.FileSystemImportManager.addEntity .calledWith( - this.user_id, - this.project_id, - this.folder_id, - this.name, - this.path + ctx.user_id, + ctx.project_id, + ctx.folder_id, + ctx.fileName, + ctx.path ) .should.equal(true) }) - it('should return a successful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return a successful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: true, - entity_id: this.entity._id, + entity_id: ctx.entity._id, entity_type: 'file', }) ) }) - it('should time the request', function () { - return this.metrics.Timer.prototype.done.called.should.equal(true) + it('should time the request', function (ctx) { + ctx.metrics.Timer.prototype.done.called.should.equal(true) }) - it('should remove the uploaded file', function () { - return this.fs.unlink.calledWith(this.path).should.equal(true) + it('should remove the uploaded file', function (ctx) { + ctx.fs.unlink.calledWith(ctx.path).should.equal(true) }) }) describe('with folder structure', function () { - beforeEach(function (done) { - this.entity = { - _id: '1234', - type: 'file', - } - this.FileSystemImportManager.addEntity = sinon - .stub() - .callsArgWith(6, null, this.entity) - this.ProjectLocator.promises.findElement = sinon.stub().resolves({ - path: { fileSystem: '/test' }, + beforeEach(function (ctx) { + return new Promise(resolve => { + ctx.entity = { + _id: '1234', + type: 'file', + } + ctx.FileSystemImportManager.addEntity = sinon + .stub() + .callsArgWith(6, null, ctx.entity) + ctx.ProjectLocator.promises.findElement = sinon.stub().resolves({ + path: { fileSystem: '/test' }, + }) + ctx.EditorController.promises.mkdirp = sinon.stub().resolves({ + lastFolder: { _id: 'folder-id' }, + }) + ctx.req.body.relativePath = 'foo/bar/' + ctx.fileName + ctx.res.json = data => { + expect(data.success).to.be.true + resolve() + } + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - this.EditorController.promises.mkdirp = sinon.stub().resolves({ - lastFolder: { _id: 'folder-id' }, - }) - this.req.body.relativePath = 'foo/bar/' + this.name - this.res.json = data => { - expect(data.success).to.be.true - done() - } - this.ProjectUploadController.uploadFile(this.req, this.res) }) - it('should insert the file', function () { - this.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly( + it('should insert the file', function (ctx) { + ctx.ProjectLocator.promises.findElement.should.be.calledOnceWithExactly( { - project_id: this.project_id, - element_id: this.folder_id, + project_id: ctx.project_id, + element_id: ctx.folder_id, type: 'folder', } ) - this.EditorController.promises.mkdirp.should.be.calledWith( - this.project_id, + ctx.EditorController.promises.mkdirp.should.be.calledWith( + ctx.project_id, '/test/foo/bar', - this.user_id + ctx.user_id ) - this.FileSystemImportManager.addEntity.should.be.calledOnceWith( - this.user_id, - this.project_id, + ctx.FileSystemImportManager.addEntity.should.be.calledOnceWith( + ctx.user_id, + ctx.project_id, 'folder-id', - this.name, - this.path + ctx.fileName, + ctx.path ) }) }) describe('when FileSystemImportManager.addEntity returns a generic error', function () { - beforeEach(function () { - this.FileSystemImportManager.addEntity = sinon + beforeEach(function (ctx) { + ctx.FileSystemImportManager.addEntity = sinon .stub() .callsArgWith(6, new Error('Sorry something went wrong')) - return this.ProjectUploadController.uploadFile(this.req, this.res) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return an unsuccessful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return an unsuccessful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, }) @@ -299,15 +332,15 @@ describe('ProjectUploadController', function () { }) describe('when FileSystemImportManager.addEntity returns a too many files error', function () { - beforeEach(function () { - this.FileSystemImportManager.addEntity = sinon + beforeEach(function (ctx) { + ctx.FileSystemImportManager.addEntity = sinon .stub() .callsArgWith(6, new Error('project_has_too_many_files')) - return this.ProjectUploadController.uploadFile(this.req, this.res) + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return an unsuccessful response to the FileUploader client', function () { - return expect(this.res.body).to.deep.equal( + it('should return an unsuccessful response to the FileUploader client', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'project_has_too_many_files', @@ -317,13 +350,13 @@ describe('ProjectUploadController', function () { }) describe('with an invalid filename', function () { - beforeEach(function () { - this.req.body.name = '' - return this.ProjectUploadController.uploadFile(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.body.name = '' + ctx.ProjectUploadController.uploadFile(ctx.req, ctx.res) }) - it('should return a a non success response', function () { - return expect(this.res.body).to.deep.equal( + it('should return a a non success response', function (ctx) { + expect(ctx.res.body).to.deep.equal( JSON.stringify({ success: false, error: 'invalid_filename', diff --git a/services/web/test/unit/src/User/UserPagesController.test.mjs b/services/web/test/unit/src/User/UserPagesController.test.mjs index 6b19ef03f5..181c9513ae 100644 --- a/services/web/test/unit/src/User/UserPagesController.test.mjs +++ b/services/web/test/unit/src/User/UserPagesController.test.mjs @@ -1,18 +1,15 @@ -import esmock from 'esmock' +import { vi } from 'vitest' import assert from 'assert' import sinon from 'sinon' import { expect } from 'chai' import MockResponse from '../helpers/MockResponse.js' import MockRequest from '../helpers/MockRequest.js' -const modulePath = new URL( - '../../../../app/src/Features/User/UserPagesController', - import.meta.url -).pathname +const modulePath = '../../../../app/src/Features/User/UserPagesController' describe('UserPagesController', function () { - beforeEach(async function () { - this.settings = { + beforeEach(async function (ctx) { + ctx.settings = { apis: { v1: { url: 'some.host', @@ -21,8 +18,8 @@ describe('UserPagesController', function () { }, }, } - this.user = { - _id: (this.user_id = 'kwjewkl'), + ctx.user = { + _id: (ctx.user_id = 'kwjewkl'), features: {}, email: 'joe@example.com', ip_address: '1.1.1.1', @@ -39,414 +36,507 @@ describe('UserPagesController', function () { papers: { encrypted: 'cccc' }, }, } - this.adminEmail = 'group-admin-email@overleaf.com' - this.subscriptionViewModel = { + ctx.adminEmail = 'group-admin-email@overleaf.com' + ctx.subscriptionViewModel = { memberGroupSubscriptions: [], } - this.UserGetter = { + ctx.UserGetter = { getUser: sinon.stub(), promises: { getUser: sinon.stub() }, } - this.UserSessionsManager = { getAllUserSessions: sinon.stub() } - this.dropboxStatus = {} - this.ErrorController = { notFound: sinon.stub() } - this.SessionManager = { - getLoggedInUserId: sinon.stub().returns(this.user._id), - getSessionUser: sinon.stub().returns(this.user), + ctx.UserSessionsManager = { getAllUserSessions: sinon.stub() } + ctx.dropboxStatus = {} + ctx.ErrorController = { notFound: sinon.stub() } + ctx.SessionManager = { + getLoggedInUserId: sinon.stub().returns(ctx.user._id), + getSessionUser: sinon.stub().returns(ctx.user), } - this.NewsletterManager = { + ctx.NewsletterManager = { subscribed: sinon.stub().yields(), } - this.AuthenticationController = { + ctx.AuthenticationController = { getRedirectFromSession: sinon.stub(), setRedirectInSession: sinon.stub(), } - this.Features = { + ctx.Features = { hasFeature: sinon.stub().returns(false), } - this.PersonalAccessTokenManager = { + ctx.PersonalAccessTokenManager = { listTokens: sinon.stub().returns([]), } - this.SubscriptionLocator = { + ctx.SubscriptionLocator = { promises: { - getAdminEmail: sinon.stub().returns(this.adminEmail), + getAdminEmail: sinon.stub().returns(ctx.adminEmail), getMemberSubscriptions: sinon.stub().resolves(), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().returns('default'), }, } - this.Modules = { + ctx.Modules = { promises: { hooks: { fire: sinon.stub().resolves(), }, }, } - this.UserPagesController = await esmock.strict(modulePath, { - '@overleaf/settings': this.settings, - '../../../../app/src/Features/User/UserGetter': this.UserGetter, - '../../../../app/src/Features/User/UserSessionsManager': - this.UserSessionsManager, - '../../../../app/src/Features/Newsletter/NewsletterManager': - this.NewsletterManager, - '../../../../app/src/Features/Errors/ErrorController': - this.ErrorController, - '../../../../app/src/Features/Authentication/AuthenticationController': - this.AuthenticationController, - '../../../../app/src/Features/Subscription/SubscriptionLocator': - this.SubscriptionLocator, - '../../../../app/src/infrastructure/Features': this.Features, - '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager': - this.PersonalAccessTokenManager, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/infrastructure/Modules': this.Modules, - request: (this.request = sinon.stub()), - }) - this.req = new MockRequest() - this.req.session.user = this.user - this.res = new MockResponse() + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.settings, + })) + + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ + default: ctx.UserGetter, + })) + + vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({ + default: ctx.UserSessionsManager, + })) + + vi.doMock( + '../../../../app/src/Features/Newsletter/NewsletterManager', + () => ({ + default: ctx.NewsletterManager, + }) + ) + + vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({ + default: ctx.ErrorController, + })) + + vi.doMock( + '../../../../app/src/Features/Authentication/AuthenticationController', + () => ({ + default: ctx.AuthenticationController, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/SubscriptionLocator', + () => ({ + default: ctx.SubscriptionLocator, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Features', () => ({ + default: ctx.Features, + })) + + vi.doMock( + '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager', + () => ({ + default: ctx.PersonalAccessTokenManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock('../../../../app/src/infrastructure/Modules', () => ({ + default: ctx.Modules, + })) + ctx.request = sinon.stub() + vi.doMock('request', () => ({ + default: ctx.request, + })) + + ctx.UserPagesController = (await import(modulePath)).default + ctx.req = new MockRequest() + ctx.req.session.user = ctx.user + ctx.res = new MockResponse() }) describe('registerPage', function () { - it('should render the register page', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/register') - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + it('should render the register page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/register') + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should set sharedProjectData', function (done) { - this.req.session.sharedProjectData = { - project_name: 'myProject', - user_first_name: 'user_first_name_here', - } + it('should set sharedProjectData', function (ctx) { + return new Promise(resolve => { + ctx.req.session.sharedProjectData = { + project_name: 'myProject', + user_first_name: 'user_first_name_here', + } - this.res.callback = () => { - this.res.renderedVariables.sharedProjectData.project_name.should.equal( - 'myProject' - ) - this.res.renderedVariables.sharedProjectData.user_first_name.should.equal( - 'user_first_name_here' - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + ctx.res.callback = () => { + ctx.res.renderedVariables.sharedProjectData.project_name.should.equal( + 'myProject' + ) + ctx.res.renderedVariables.sharedProjectData.user_first_name.should.equal( + 'user_first_name_here' + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should set newTemplateData', function (done) { - this.req.session.templateData = { templateName: 'templateName' } + it('should set newTemplateData', function (ctx) { + return new Promise(resolve => { + ctx.req.session.templateData = { templateName: 'templateName' } - this.res.callback = () => { - this.res.renderedVariables.newTemplateData.templateName.should.equal( - 'templateName' - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + ctx.res.callback = () => { + ctx.res.renderedVariables.newTemplateData.templateName.should.equal( + 'templateName' + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) - it('should not set the newTemplateData if there is nothing in the session', function (done) { - this.res.callback = () => { - assert.equal( - this.res.renderedVariables.newTemplateData.templateName, - undefined - ) - done() - } - this.UserPagesController.registerPage(this.req, this.res, done) + it('should not set the newTemplateData if there is nothing in the session', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + assert.equal( + ctx.res.renderedVariables.newTemplateData.templateName, + undefined + ) + resolve() + } + ctx.UserPagesController.registerPage(ctx.req, ctx.res, resolve) + }) }) }) describe('loginForm', function () { - it('should render the login page', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/login') - done() - } - this.UserPagesController.loginPage(this.req, this.res, done) + it('should render the login page', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/login') + resolve() + } + ctx.UserPagesController.loginPage(ctx.req, ctx.res, resolve) + }) }) describe('when an explicit redirect is set via query string', function () { - beforeEach(function () { - this.AuthenticationController.getRedirectFromSession = sinon + beforeEach(function (ctx) { + ctx.AuthenticationController.getRedirectFromSession = sinon .stub() .returns(null) - this.AuthenticationController.setRedirectInSession = sinon.stub() - this.req.query.redir = '/somewhere/in/particular' + ctx.AuthenticationController.setRedirectInSession = sinon.stub() + ctx.req.query.redir = '/somewhere/in/particular' }) - it('should set a redirect', function (done) { - this.res.callback = page => { - this.AuthenticationController.setRedirectInSession.callCount.should.equal( - 1 - ) - expect( - this.AuthenticationController.setRedirectInSession.lastCall.args[1] - ).to.equal(this.req.query.redir) - done() - } - this.UserPagesController.loginPage(this.req, this.res, done) + it('should set a redirect', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = page => { + ctx.AuthenticationController.setRedirectInSession.callCount.should.equal( + 1 + ) + expect( + ctx.AuthenticationController.setRedirectInSession.lastCall.args[1] + ).to.equal(ctx.req.query.redir) + resolve() + } + ctx.UserPagesController.loginPage(ctx.req, ctx.res, resolve) + }) }) }) }) describe('sessionsPage', function () { - beforeEach(function () { - this.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) + beforeEach(function (ctx) { + ctx.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, []) }) - it('should render user/sessions', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/sessions') - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should render user/sessions', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/sessions') + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) - it('should include current session data in the view', function (done) { - this.res.callback = () => { - expect(this.res.renderedVariables.currentSession).to.deep.equal({ - ip_address: '1.1.1.1', - session_created: 'timestamp', - }) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should include current session data in the view', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.currentSession).to.deep.equal({ + ip_address: '1.1.1.1', + session_created: 'timestamp', + }) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) - it('should have called getAllUserSessions', function (done) { - this.res.callback = page => { - this.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, done) + it('should have called getAllUserSessions', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = page => { + ctx.UserSessionsManager.getAllUserSessions.callCount.should.equal(1) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, resolve) + }) }) describe('when getAllUserSessions produces an error', function () { - beforeEach(function () { - this.UserSessionsManager.getAllUserSessions.callsArgWith( + beforeEach(function (ctx) { + ctx.UserSessionsManager.getAllUserSessions.callsArgWith( 2, new Error('woops') ) }) - it('should call next with an error', function (done) { - this.next = err => { - assert(err !== null) - assert(err instanceof Error) - done() - } - this.UserPagesController.sessionsPage(this.req, this.res, this.next) + it('should call next with an error', function (ctx) { + return new Promise(resolve => { + ctx.next = err => { + assert(err !== null) + assert(err instanceof Error) + resolve() + } + ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, ctx.next) + }) }) }) }) describe('emailPreferencesPage', function () { - beforeEach(function () { - this.UserGetter.getUser = sinon.stub().yields(null, this.user) + beforeEach(function (ctx) { + ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user) }) - it('render page with subscribed status', function (done) { - this.NewsletterManager.subscribed.yields(null, true) - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/email-preferences') - this.res.renderedVariables.title.should.equal('newsletter_info_title') - this.res.renderedVariables.subscribed.should.equal(true) - done() - } - this.UserPagesController.emailPreferencesPage(this.req, this.res, done) + it('render page with subscribed status', function (ctx) { + return new Promise(resolve => { + ctx.NewsletterManager.subscribed.yields(null, true) + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/email-preferences') + ctx.res.renderedVariables.title.should.equal('newsletter_info_title') + ctx.res.renderedVariables.subscribed.should.equal(true) + resolve() + } + ctx.UserPagesController.emailPreferencesPage(ctx.req, ctx.res, resolve) + }) }) - it('render page with unsubscribed status', function (done) { - this.NewsletterManager.subscribed.yields(null, false) - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/email-preferences') - this.res.renderedVariables.title.should.equal('newsletter_info_title') - this.res.renderedVariables.subscribed.should.equal(false) - done() - } - this.UserPagesController.emailPreferencesPage(this.req, this.res, done) + it('render page with unsubscribed status', function (ctx) { + return new Promise(resolve => { + ctx.NewsletterManager.subscribed.yields(null, false) + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/email-preferences') + ctx.res.renderedVariables.title.should.equal('newsletter_info_title') + ctx.res.renderedVariables.subscribed.should.equal(false) + resolve() + } + ctx.UserPagesController.emailPreferencesPage(ctx.req, ctx.res, resolve) + }) }) }) describe('settingsPage', function () { - beforeEach(function () { - this.request.get = sinon + beforeEach(function (ctx) { + ctx.request.get = sinon .stub() .callsArgWith(1, null, { statusCode: 200 }, { has_password: true }) - this.UserGetter.promises.getUser = sinon.stub().resolves(this.user) + ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user) }) - it('should render user/settings', function (done) { - this.res.callback = () => { - this.res.renderedTemplate.should.equal('user/settings') - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should render user/settings', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedTemplate.should.equal('user/settings') + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send user', function (done) { - this.res.callback = () => { - this.res.renderedVariables.user.id.should.equal(this.user._id) - this.res.renderedVariables.user.email.should.equal(this.user.email) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should send user', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.user.id.should.equal(ctx.user._id) + ctx.res.renderedVariables.user.email.should.equal(ctx.user.email) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it("should set 'shouldAllowEditingDetails' to true", function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal(true) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it("should set 'shouldAllowEditingDetails' to true", function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(true) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should restructure thirdPartyIdentifiers data for template use', function (done) { - const expectedResult = { - google: 'testId', - } - this.res.callback = () => { - expect(this.res.renderedVariables.thirdPartyIds).to.include( - expectedResult - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should restructure thirdPartyIdentifiers data for template use', function (ctx) { + return new Promise(resolve => { + const expectedResult = { + google: 'testId', + } + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.thirdPartyIds).to.include( + expectedResult + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it("should set and clear 'projectSyncSuccessMessage'", function (done) { - this.req.session.projectSyncSuccessMessage = 'Some Sync Success' - this.res.callback = () => { - this.res.renderedVariables.projectSyncSuccessMessage.should.equal( - 'Some Sync Success' - ) - expect(this.req.session.projectSyncSuccessMessage).to.not.exist - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it("should set and clear 'projectSyncSuccessMessage'", function (ctx) { + return new Promise(resolve => { + ctx.req.session.projectSyncSuccessMessage = 'Some Sync Success' + ctx.res.callback = () => { + ctx.res.renderedVariables.projectSyncSuccessMessage.should.equal( + 'Some Sync Success' + ) + expect(ctx.req.session.projectSyncSuccessMessage).to.not.exist + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should cast refProviders to booleans', function (done) { - this.res.callback = () => { - expect(this.res.renderedVariables.user.refProviders).to.deep.equal({ - mendeley: true, - papers: true, - zotero: true, - }) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should cast refProviders to booleans', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect(ctx.res.renderedVariables.user.refProviders).to.deep.equal({ + mendeley: true, + papers: true, + zotero: true, + }) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send the correct managed user admin email', function (done) { - this.res.callback = () => { - expect( - this.res.renderedVariables.currentManagedUserAdminEmail - ).to.equal(this.adminEmail) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should send the correct managed user admin email', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + expect( + ctx.res.renderedVariables.currentManagedUserAdminEmail + ).to.equal(ctx.adminEmail) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) - it('should send info for groups with SSO enabled', function (done) { - this.user.enrollment = { - sso: [ - { - groupId: 'abc123abc123', - primary: true, - linkedAt: new Date(), + it('should send info for groups with SSO enabled', function (ctx) { + return new Promise(resolve => { + ctx.user.enrollment = { + sso: [ + { + groupId: 'abc123abc123', + primary: true, + linkedAt: new Date(), + }, + ], + } + const group1 = { + _id: 'abc123abc123', + teamName: 'Group SSO Rulz', + admin_id: { + email: 'admin.email@ssolove.com', }, - ], - } - const group1 = { - _id: 'abc123abc123', - teamName: 'Group SSO Rulz', - admin_id: { - email: 'admin.email@ssolove.com', - }, - linked: true, - } - const group2 = { - _id: 'def456def456', - admin_id: { - email: 'someone.else@noname.co.uk', - }, - linked: false, - } - - this.Modules.promises.hooks.fire - .withArgs('getUserGroupsSSOEnrollmentStatus') - .resolves([[group1, group2]]) - - this.res.callback = () => { - expect( - this.res.renderedVariables.memberOfSSOEnabledGroups - ).to.deep.equal([ - { - groupId: 'abc123abc123', - groupName: 'Group SSO Rulz', - adminEmail: 'admin.email@ssolove.com', - linked: true, + linked: true, + } + const group2 = { + _id: 'def456def456', + admin_id: { + email: 'someone.else@noname.co.uk', }, - { - groupId: 'def456def456', - groupName: undefined, - adminEmail: 'someone.else@noname.co.uk', - linked: false, - }, - ]) - done() - } + linked: false, + } - this.UserPagesController.settingsPage(this.req, this.res, done) + ctx.Modules.promises.hooks.fire + .withArgs('getUserGroupsSSOEnrollmentStatus') + .resolves([[group1, group2]]) + + ctx.res.callback = () => { + expect( + ctx.res.renderedVariables.memberOfSSOEnabledGroups + ).to.deep.equal([ + { + groupId: 'abc123abc123', + groupName: 'Group SSO Rulz', + adminEmail: 'admin.email@ssolove.com', + linked: true, + }, + { + groupId: 'def456def456', + groupName: undefined, + adminEmail: 'someone.else@noname.co.uk', + linked: false, + }, + ]) + resolve() + } + + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) describe('when ldap.updateUserDetailsOnLogin is true', function () { - beforeEach(function () { - this.settings.ldap = { updateUserDetailsOnLogin: true } + beforeEach(function (ctx) { + ctx.settings.ldap = { updateUserDetailsOnLogin: true } }) - afterEach(function () { - delete this.settings.ldap + afterEach(function (ctx) { + delete ctx.settings.ldap }) - it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal( - false - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should set "shouldAllowEditingDetails" to false', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) }) describe('when saml.updateUserDetailsOnLogin is true', function () { - beforeEach(function () { - this.settings.saml = { updateUserDetailsOnLogin: true } + beforeEach(function (ctx) { + ctx.settings.saml = { updateUserDetailsOnLogin: true } }) - afterEach(function () { - delete this.settings.saml + afterEach(function (ctx) { + delete ctx.settings.saml }) - it('should set "shouldAllowEditingDetails" to false', function (done) { - this.res.callback = () => { - this.res.renderedVariables.shouldAllowEditingDetails.should.equal( - false - ) - done() - } - this.UserPagesController.settingsPage(this.req, this.res, done) + it('should set "shouldAllowEditingDetails" to false', function (ctx) { + return new Promise(resolve => { + ctx.res.callback = () => { + ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal( + false + ) + resolve() + } + ctx.UserPagesController.settingsPage(ctx.req, ctx.res, resolve) + }) }) }) }) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs b/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs index f6dedf2097..55bc62cd2d 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs +++ b/services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs @@ -1,6 +1,6 @@ +import { vi } from 'vitest' import sinon from 'sinon' import { expect } from 'chai' -import esmock from 'esmock' import MockRequest from '../helpers/MockRequest.js' import MockResponse from '../helpers/MockResponse.js' import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.js' @@ -15,27 +15,39 @@ const assertCalledWith = sinon.assert.calledWith const modulePath = '../../../../app/src/Features/UserMembership/UserMembershipController.mjs' +vi.mock( + '../../../../app/src/Features/UserMembership/UserMembershipErrors.js', + () => + vi.importActual( + '../../../../app/src/Features/UserMembership/UserMembershipErrors.js' + ) +) + +vi.mock('../../../../app/src/Features/Errors/Errors.js', () => + vi.importActual('../../../../app/src/Features/Errors/Errors.js') +) + describe('UserMembershipController', function () { - beforeEach(async function () { - this.req = new MockRequest() - this.req.params.id = 'mock-entity-id' - this.user = { _id: 'mock-user-id' } - this.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' } - this.subscription = { + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.req.params.id = 'mock-entity-id' + ctx.user = { _id: 'mock-user-id' } + ctx.newUser = { _id: 'mock-new-user-id', email: 'new-user-email@foo.bar' } + ctx.subscription = { _id: 'mock-subscription-id', admin_id: 'mock-admin-id', - fetchV1Data: callback => callback(null, this.subscription), + fetchV1Data: callback => callback(null, ctx.subscription), } - this.institution = { + ctx.institution = { _id: 'mock-institution-id', v1Id: 123, fetchV1Data: callback => { - const institution = Object.assign({}, this.institution) + const institution = Object.assign({}, ctx.institution) institution.name = 'Test Institution Name' callback(null, institution) }, } - this.users = [ + ctx.users = [ { _id: 'mock-member-id-1', email: 'mock-email-1@foo.com', @@ -50,106 +62,136 @@ describe('UserMembershipController', function () { }, ] - this.Settings = { + ctx.Settings = { managedUsers: { enabled: false, }, } - this.SessionManager = { - getSessionUser: sinon.stub().returns(this.user), - getLoggedInUserId: sinon.stub().returns(this.user._id), + ctx.SessionManager = { + getSessionUser: sinon.stub().returns(ctx.user), + getLoggedInUserId: sinon.stub().returns(ctx.user._id), } - this.SSOConfig = { + ctx.SSOConfig = { findById: sinon .stub() .returns({ exec: sinon.stub().resolves({ enabled: true }) }), } - this.UserMembershipHandler = { - getEntity: sinon.stub().yields(null, this.subscription), - createEntity: sinon.stub().yields(null, this.institution), - getUsers: sinon.stub().yields(null, this.users), - addUser: sinon.stub().yields(null, this.newUser), + ctx.UserMembershipHandler = { + getEntity: sinon.stub().yields(null, ctx.subscription), + createEntity: sinon.stub().yields(null, ctx.institution), + getUsers: sinon.stub().yields(null, ctx.users), + addUser: sinon.stub().yields(null, ctx.newUser), removeUser: sinon.stub().yields(null), promises: { - getUsers: sinon.stub().resolves(this.users), + getUsers: sinon.stub().resolves(ctx.users), }, } - this.SplitTestHandler = { + ctx.SplitTestHandler = { promises: { getAssignment: sinon.stub().resolves({ variant: 'default' }), }, getAssignment: sinon.stub().yields(null, { variant: 'default' }), } - this.RecurlyClient = { + ctx.RecurlyClient = { promises: { getSubscription: sinon.stub().resolves({}), }, } - this.UserMembershipController = await esmock.strict(modulePath, { - '../../../../app/src/Features/UserMembership/UserMembershipErrors': { + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipErrors', + () => ({ UserIsManagerError, UserNotFoundError, UserAlreadyAddedError, - }, - '../../../../app/src/Features/Authentication/SessionManager': - this.SessionManager, - '../../../../app/src/Features/SplitTests/SplitTestHandler': - this.SplitTestHandler, - '../../../../app/src/Features/UserMembership/UserMembershipHandler': - this.UserMembershipHandler, - '../../../../app/src/Features/Subscription/RecurlyClient': - this.RecurlyClient, - '@overleaf/settings': this.Settings, - '../../../../app/src/models/SSOConfig': { SSOConfig: this.SSOConfig }, - }) + }) + ) + + vi.doMock( + '../../../../app/src/Features/Authentication/SessionManager', + () => ({ + default: ctx.SessionManager, + }) + ) + + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/UserMembership/UserMembershipHandler', + () => ({ + default: ctx.UserMembershipHandler, + }) + ) + + vi.doMock( + '../../../../app/src/Features/Subscription/RecurlyClient', + () => ({ + default: ctx.RecurlyClient, + }) + ) + + vi.doMock('@overleaf/settings', () => ({ + default: ctx.Settings, + })) + + vi.doMock('../../../../app/src/models/SSOConfig', () => ({ + SSOConfig: ctx.SSOConfig, + })) + + ctx.UserMembershipController = (await import(modulePath)).default }) describe('index', function () { - beforeEach(function () { - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.group + beforeEach(function (ctx) { + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.group }) - it('get users', async function () { - await this.UserMembershipController.manageGroupMembers(this.req, { + it('get users', async function (ctx) { + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: () => { sinon.assert.calledWithMatch( - this.UserMembershipHandler.promises.getUsers, - this.subscription, + ctx.UserMembershipHandler.promises.getUsers, + ctx.subscription, { modelName: 'Subscription' } ) }, }) }) - it('render group view', async function () { - this.subscription.managedUsersEnabled = false - await this.UserMembershipController.manageGroupMembers(this.req, { + it('render group view', async function (ctx) { + ctx.subscription.managedUsersEnabled = false + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-members-react') - expect(viewParams.users).to.deep.equal(this.users) - expect(viewParams.groupSize).to.equal(this.subscription.membersLimit) + expect(viewParams.users).to.deep.equal(ctx.users) + expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit) expect(viewParams.managedUsersActive).to.equal(false) }, }) }) - it('render group view with managed users', async function () { - this.subscription.managedUsersEnabled = true - await this.UserMembershipController.manageGroupMembers(this.req, { + it('render group view with managed users', async function (ctx) { + ctx.subscription.managedUsersEnabled = true + await ctx.UserMembershipController.manageGroupMembers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-members-react') - expect(viewParams.users).to.deep.equal(this.users) - expect(viewParams.groupSize).to.equal(this.subscription.membersLimit) + expect(viewParams.users).to.deep.equal(ctx.users) + expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit) expect(viewParams.managedUsersActive).to.equal(true) }, }) }) - it('render group managers view', async function () { - this.req.entityConfig = EntityConfigs.groupManagers - await this.UserMembershipController.manageGroupManagers(this.req, { + it('render group managers view', async function (ctx) { + ctx.req.entityConfig = EntityConfigs.groupManagers + await ctx.UserMembershipController.manageGroupManagers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal('user_membership/group-managers-react') expect(viewParams.groupSize).to.equal(undefined) @@ -157,10 +199,10 @@ describe('UserMembershipController', function () { }) }) - it('render institution view', async function () { - this.req.entity = this.institution - this.req.entityConfig = EntityConfigs.institution - await this.UserMembershipController.manageInstitutionManagers(this.req, { + it('render institution view', async function (ctx) { + ctx.req.entity = ctx.institution + ctx.req.entityConfig = EntityConfigs.institution + await ctx.UserMembershipController.manageInstitutionManagers(ctx.req, { render: (viewPath, viewParams) => { expect(viewPath).to.equal( 'user_membership/institution-managers-react' @@ -173,207 +215,233 @@ describe('UserMembershipController', function () { }) describe('add', function () { - beforeEach(function () { - this.req.body.email = this.newUser.email - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers + beforeEach(function (ctx) { + ctx.req.body.email = ctx.newUser.email + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers }) - it('add user', function (done) { - this.UserMembershipController.add(this.req, { - json: () => { - sinon.assert.calledWithMatch( - this.UserMembershipHandler.addUser, - this.subscription, - { modelName: 'Subscription' }, - this.newUser.email - ) - done() - }, - }) - }) - - it('return user object', function (done) { - this.UserMembershipController.add(this.req, { - json: payload => { - payload.user.should.equal(this.newUser) - done() - }, - }) - }) - - it('handle readOnly entity', function (done) { - this.req.entityConfig = EntityConfigs.group - this.UserMembershipController.add(this.req, null, error => { - expect(error).to.exist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - }) - }) - - it('handle user already added', function (done) { - this.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError()) - this.UserMembershipController.add(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('user_already_added') - done() + it('add user', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.add(ctx.req, { + json: () => { + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.addUser, + ctx.subscription, + { modelName: 'Subscription' }, + ctx.newUser.email + ) + resolve() }, - }), + }) }) }) - it('handle user not found', function (done) { - this.UserMembershipHandler.addUser.yields(new UserNotFoundError()) - this.UserMembershipController.add(this.req, { - status: () => ({ + it('return user object', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.add(ctx.req, { json: payload => { - expect(payload.error.code).to.equal('user_not_found') - done() + payload.user.should.equal(ctx.newUser) + resolve() }, - }), + }) }) }) - it('handle invalid email', function (done) { - this.req.body.email = 'not_valid_email' - this.UserMembershipController.add(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('invalid_email') - done() - }, - }), + it('handle readOnly entity', function (ctx) { + return new Promise(resolve => { + ctx.req.entityConfig = EntityConfigs.group + ctx.UserMembershipController.add(ctx.req, null, error => { + expect(error).to.exist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + resolve() + }) + }) + }) + + it('handle user already added', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.addUser.yields(new UserAlreadyAddedError()) + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_already_added') + resolve() + }, + }), + }) + }) + }) + + it('handle user not found', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.addUser.yields(new UserNotFoundError()) + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('user_not_found') + resolve() + }, + }), + }) + }) + }) + + it('handle invalid email', function (ctx) { + return new Promise(resolve => { + ctx.req.body.email = 'not_valid_email' + ctx.UserMembershipController.add(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('invalid_email') + resolve() + }, + }), + }) }) }) }) describe('remove', function () { - beforeEach(function () { - this.req.params.userId = this.newUser._id - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers + beforeEach(function (ctx) { + ctx.req.params.userId = ctx.newUser._id + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers }) - it('remove user', function (done) { - this.UserMembershipController.remove(this.req, { - sendStatus: () => { - sinon.assert.calledWithMatch( - this.UserMembershipHandler.removeUser, - this.subscription, - { modelName: 'Subscription' }, - this.newUser._id - ) - done() - }, - }) - }) - - it('handle readOnly entity', function (done) { - this.req.entityConfig = EntityConfigs.group - this.UserMembershipController.remove(this.req, null, error => { - expect(error).to.exist - expect(error).to.be.an.instanceof(Errors.NotFoundError) - done() - }) - }) - - it('prevent self removal', function (done) { - this.req.params.userId = this.user._id - this.UserMembershipController.remove(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('managers_cannot_remove_self') - done() + it('remove user', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.remove(ctx.req, { + sendStatus: () => { + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.removeUser, + ctx.subscription, + { modelName: 'Subscription' }, + ctx.newUser._id + ) + resolve() }, - }), + }) }) }) - it('prevent admin removal', function (done) { - this.UserMembershipHandler.removeUser.yields(new UserIsManagerError()) - this.UserMembershipController.remove(this.req, { - status: () => ({ - json: payload => { - expect(payload.error.code).to.equal('managers_cannot_remove_admin') - done() - }, - }), + it('handle readOnly entity', function (ctx) { + return new Promise(resolve => { + ctx.req.entityConfig = EntityConfigs.group + ctx.UserMembershipController.remove(ctx.req, null, error => { + expect(error).to.exist + expect(error).to.be.an.instanceof(Errors.NotFoundError) + resolve() + }) + }) + }) + + it('prevent self removal', function (ctx) { + return new Promise(resolve => { + ctx.req.params.userId = ctx.user._id + ctx.UserMembershipController.remove(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal('managers_cannot_remove_self') + resolve() + }, + }), + }) + }) + }) + + it('prevent admin removal', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipHandler.removeUser.yields(new UserIsManagerError()) + ctx.UserMembershipController.remove(ctx.req, { + status: () => ({ + json: payload => { + expect(payload.error.code).to.equal( + 'managers_cannot_remove_admin' + ) + resolve() + }, + }), + }) }) }) }) describe('exportCsv', function () { - beforeEach(function () { - this.req.entity = this.subscription - this.req.entityConfig = EntityConfigs.groupManagers - this.res = new MockResponse() - this.UserMembershipController.exportCsv(this.req, this.res) + beforeEach(function (ctx) { + ctx.req.entity = ctx.subscription + ctx.req.entityConfig = EntityConfigs.groupManagers + ctx.res = new MockResponse() + ctx.UserMembershipController.exportCsv(ctx.req, ctx.res) }) - it('get users', function () { + it('get users', function (ctx) { sinon.assert.calledWithMatch( - this.UserMembershipHandler.getUsers, - this.subscription, + ctx.UserMembershipHandler.getUsers, + ctx.subscription, { modelName: 'Subscription' } ) }) - it('should set the correct content type on the request', function () { - assertCalledWith(this.res.contentType, 'text/csv; charset=utf-8') + it('should set the correct content type on the request', function (ctx) { + assertCalledWith(ctx.res.contentType, 'text/csv; charset=utf-8') }) - it('should name the exported csv file', function () { + it('should name the exported csv file', function (ctx) { assertCalledWith( - this.res.header, + ctx.res.header, 'Content-Disposition', 'attachment; filename="Group.csv"' ) }) - it('should export the correct csv', function () { + it('should export the correct csv', function (ctx) { assertCalledWith( - this.res.send, + ctx.res.send, '"email","last_logged_in_at","last_active_at"\n"mock-email-1@foo.com","2020-08-09T12:43:11.467Z","2021-08-09T12:43:11.467Z"\n"mock-email-2@foo.com","2020-05-20T10:41:11.407Z","2021-05-20T10:41:11.407Z"' ) }) }) describe('new', function () { - beforeEach(function () { - this.req.params.name = 'publisher' - this.req.params.id = 'abc' + beforeEach(function (ctx) { + ctx.req.params.name = 'publisher' + ctx.req.params.id = 'abc' }) - it('renders view', function (done) { - this.UserMembershipController.new(this.req, { - render: (viewPath, data) => { - expect(data.entityName).to.eq('publisher') - expect(data.entityId).to.eq('abc') - done() - }, + it('renders view', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.new(ctx.req, { + render: (viewPath, data) => { + expect(data.entityName).to.eq('publisher') + expect(data.entityId).to.eq('abc') + resolve() + }, + }) }) }) }) describe('create', function () { - beforeEach(function () { - this.req.params.name = 'institution' - this.req.entityConfig = EntityConfigs.institution - this.req.params.id = 123 + beforeEach(function (ctx) { + ctx.req.params.name = 'institution' + ctx.req.entityConfig = EntityConfigs.institution + ctx.req.params.id = 123 }) - it('creates institution', function (done) { - this.UserMembershipController.create(this.req, { - redirect: path => { - expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index) - sinon.assert.calledWithMatch( - this.UserMembershipHandler.createEntity, - 123, - { modelName: 'Institution' } - ) - done() - }, + it('creates institution', function (ctx) { + return new Promise(resolve => { + ctx.UserMembershipController.create(ctx.req, { + redirect: path => { + expect(path).to.eq(EntityConfigs.institution.pathsFor(123).index) + sinon.assert.calledWithMatch( + ctx.UserMembershipHandler.createEntity, + 123, + { modelName: 'Institution' } + ) + resolve() + }, + }) }) }) }) diff --git a/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs b/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs index 01fe5d7a0d..4d8479a9cb 100644 --- a/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs +++ b/services/web/test/unit/src/infrastructure/ServeStaticWrapper.test.mjs @@ -1,24 +1,22 @@ -import { strict as esmock } from 'esmock' +import { vi } from 'vitest' import { expect } from 'chai' import Path from 'node:path' -import { fileURLToPath } from 'node:url' import sinon from 'sinon' import MockResponse from '../helpers/MockResponse.js' import MockRequest from '../helpers/MockRequest.js' -const __dirname = fileURLToPath(new URL('.', import.meta.url)) const modulePath = Path.join( - __dirname, + import.meta.dirname, '../../../../app/src/infrastructure/ServeStaticWrapper' ) describe('ServeStaticWrapperTests', function () { let error = null - beforeEach(async function () { - this.req = new MockRequest() - this.res = new MockResponse() - this.express = { + beforeEach(async function (ctx) { + ctx.req = new MockRequest() + ctx.res = new MockResponse() + ctx.express = { static: () => (req, res, next) => { if (error) { next(error) @@ -27,36 +25,39 @@ describe('ServeStaticWrapperTests', function () { } }, } - this.serveStaticWrapper = await esmock(modulePath, { - express: this.express, - }) + + vi.doMock('express', () => ({ + default: ctx.express, + })) + + ctx.serveStaticWrapper = (await import(modulePath)).default }) - this.afterEach(() => { + afterEach(() => { error = null }) - it('Premature close error thrown', async function () { + it('Premature close error thrown', async function (ctx) { error = new Error() error.code = 'ERR_STREAM_PREMATURE_CLOSE' - const middleware = this.serveStaticWrapper('test_folder', {}) + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next.called).to.be.false }) - it('No error thrown', async function () { - const middleware = this.serveStaticWrapper('test_folder', {}) + it('No error thrown', async function (ctx) { + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next).to.be.calledWith() }) - it('Other error thrown', async function () { + it('Other error thrown', async function (ctx) { error = new Error() - const middleware = this.serveStaticWrapper('test_folder', {}) + const middleware = ctx.serveStaticWrapper('test_folder', {}) const next = sinon.stub() - middleware(this.req, this.res, next) + middleware(ctx.req, ctx.res, next) expect(next).to.be.calledWith(error) }) }) From 5b764953c0beecfc0cbfb6910c0d218f80ca1359 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Tue, 6 May 2025 14:11:51 +0100 Subject: [PATCH 016/595] Add eslint rules for skipped/focused tests (and fix issues) GitOrigin-RevId: 01735e0805a28609a68df667cd2a4c3d89c5b968 --- services/web/.eslintrc.js | 4 ++++ services/web/test/unit/src/Referal/ReferalController.test.mjs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/.eslintrc.js b/services/web/.eslintrc.js index b505080b98..2fa9e8f547 100644 --- a/services/web/.eslintrc.js +++ b/services/web/.eslintrc.js @@ -109,6 +109,10 @@ module.exports = { }, plugins: ['@vitest', 'chai-expect', 'chai-friendly'], // still using chai for now rules: { + // vitest-specific rules + '@vitest/no-focused-tests': 'error', + '@vitest/no-disabled-tests': 'error', + // Swap the no-unused-expressions rule with a more chai-friendly one 'no-unused-expressions': 'off', 'chai-friendly/no-unused-expressions': 'error', diff --git a/services/web/test/unit/src/Referal/ReferalController.test.mjs b/services/web/test/unit/src/Referal/ReferalController.test.mjs index 0a7b8aa87d..383902946f 100644 --- a/services/web/test/unit/src/Referal/ReferalController.test.mjs +++ b/services/web/test/unit/src/Referal/ReferalController.test.mjs @@ -1,6 +1,6 @@ const modulePath = '../../../../app/src/Features/Referal/ReferalController.js' -describe.skip('Referal controller', function () { +describe.todo('Referal controller', function () { beforeEach(async function (ctx) { ctx.controller = (await import(modulePath)).default }) From ee8044d1624fd286535d301c61265359d8eaf312 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 15 May 2025 17:34:18 +0100 Subject: [PATCH 017/595] Update script to handle multiple directories and no vitest tests scenarios GitOrigin-RevId: 92a394387c2326d350b64c6a25e3b34c92e342aa --- services/web/bin/test_unit_run_dir | 57 +++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/services/web/bin/test_unit_run_dir b/services/web/bin/test_unit_run_dir index 20f580cf06..50ab911b33 100755 --- a/services/web/bin/test_unit_run_dir +++ b/services/web/bin/test_unit_run_dir @@ -1,9 +1,28 @@ #!/bin/bash -TARGET_DIR=$1 +declare -a vitest_args=("$@") +has_mocha_test=0 +has_mocha_mjs_test=0 +has_vitest_test=0 -declare -a vitest_args=("$TARGET_DIR") +mocha_mjs_dirs=() + +for dir_path in "$@"; do + if [ -n "$(find "$dir_path" -name "*.js" -type f -print -quit 2>/dev/null)" ]; then + has_mocha_test=1 + fi + + if [ -n "$(find "$dir_path" -name "*Tests.mjs" -type f -print -quit 2>/dev/null)" ]; then + has_mocha_mjs_test=1 + mocha_mjs_dirs+=("$dir_path/**/*Tests.mjs") + fi + + if [ -n "$(find "$dir_path" -name "*.test.mjs" -type f -print -quit 2>/dev/null)" ]; then + has_vitest_test=1 + fi + +done if [[ -n "$MOCHA_GREP" ]]; then vitest_args+=("--testNamePattern" "$MOCHA_GREP") @@ -14,31 +33,43 @@ if [[ -n "$VITEST_NO_CACHE" ]]; then vitest_args+=("--no-cache") fi -echo "Running unit tests in directory: $TARGET_DIR" +echo "Running unit tests in directory: $*" -npm run test:unit:esm -- "${vitest_args[@]}" - -vitest_status=$? - -if find "$TARGET_DIR" -type f -name "*.js" -print -quit | grep -q '.'; then - mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=js "$TARGET_DIR" - mocha_status=$? +# Remove this if/else when we have converted all module tests to vitest. +if (( has_vitest_test == 1 )); then + npm run test:unit:esm -- "${vitest_args[@]}" + vitest_status=$? else + echo "No vitest tests found in $*, skipping vitest step." + vitest_status=0 +fi + +if (( has_mocha_test == 1 )); then + mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=js "$@" + mocha_status=$? +fi +# Remove this if/else when we have converted all module tests to vitest. +if (( has_mocha_mjs_test == 1)); then + mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=mjs "${mocha_mjs_dirs[@]}" + mocha_status=$((mocha_status != 0 ? mocha_status : $?)) +fi + +if (( has_mocha_mjs_test == 0 && has_mocha_test == 0 )); then echo "No mocha tests found in $TARGET_DIR, skipping mocha step." mocha_status=0 fi -if [ $mocha_status -eq 0 ] && [ $vitest_status -eq 0 ]; then +if [ "$mocha_status" -eq 0 ] && [ "$vitest_status" -eq 0 ]; then exit 0 fi # Report status briefly at the end for failures -if [ $mocha_status -ne 0 ]; then +if [ "$mocha_status" -ne 0 ]; then echo "Mocha tests failed with status: $mocha_status" fi -if [ $vitest_status -ne 0 ]; then +if [ "$vitest_status" -ne 0 ]; then echo "Vitest tests failed with status: $vitest_status" fi From b35b54cb807b34fab53c16c7f1a069690961c748 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 15 May 2025 17:35:12 +0100 Subject: [PATCH 018/595] Use vi for logger mocks GitOrigin-RevId: aeff4a82f96300ec3f81c8418e8373e923b8c4d4 --- services/web/test/unit/vitest_bootstrap.mjs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/services/web/test/unit/vitest_bootstrap.mjs b/services/web/test/unit/vitest_bootstrap.mjs index fc4d883b1a..2244faefd3 100644 --- a/services/web/test/unit/vitest_bootstrap.mjs +++ b/services/web/test/unit/vitest_bootstrap.mjs @@ -4,16 +4,15 @@ import sinon from 'sinon' import logger from '@overleaf/logger' vi.mock('@overleaf/logger', async () => { - const sinon = (await import('sinon')).default return { default: { - debug: sinon.stub(), - info: sinon.stub(), - log: sinon.stub(), - warn: sinon.stub(), - err: sinon.stub(), - error: sinon.stub(), - fatal: sinon.stub(), + debug: vi.fn(), + info: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + err: vi.fn(), + error: vi.fn(), + fatal: vi.fn(), }, } }) From 18c0634011ad80fb55acdec330670a3fa9147349 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 15 May 2025 17:36:03 +0100 Subject: [PATCH 019/595] Disable test isolation Isolation isn't required and it takes the setup contribution to our tests down from over 60 seconds to single figures, greatly speeding up the tests. GitOrigin-RevId: 72516e420583fa2dfcef13f2cc50b0769a100baf --- services/web/vitest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/vitest.config.js b/services/web/vitest.config.js index 3b84690447..51f4ed811f 100644 --- a/services/web/vitest.config.js +++ b/services/web/vitest.config.js @@ -8,5 +8,6 @@ module.exports = defineConfig({ ], setupFiles: ['./test/unit/vitest_bootstrap.mjs'], globals: true, + isolate: false, }, }) From c8d4b644bf7814501c7699868e1440384be517f6 Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Wed, 28 May 2025 12:48:43 +0200 Subject: [PATCH 020/595] Update the Labs button's content and border colour (#25942) GitOrigin-RevId: 36de10a13ff5d8721ffcac25c5c002fe25f7a125 --- .../frontend/stylesheets/bootstrap-5/abstracts/mixins.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss index d1a823a120..1a79a1221c 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss @@ -62,9 +62,9 @@ @mixin labs-button { @include ol-button-variant( - $color: var(--content-positive), + $color: var(--green-60), $background: var(--bg-accent-03), - $border: var(--green-40), + $border: var(--green-50), $hover-background: var(--bg-accent-03), $hover-border: var(--green-40), $borderless: false From 9f821b4cfa5dbf78cfaf2443aa88de652ae68429 Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Wed, 28 May 2025 12:48:51 +0200 Subject: [PATCH 021/595] Add landmark for the cookie banner and update its links color (#25823) * Update cookie banner link color * Add landmark for the cookie banner GitOrigin-RevId: 9500cdfd7ddacbc2442680ed477ca1ac793720f7 --- services/web/app/views/_cookie_banner.pug | 4 ++-- .../frontend/stylesheets/bootstrap-5/components/footer.scss | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/_cookie_banner.pug b/services/web/app/views/_cookie_banner.pug index a164e48e83..2d5631f9c8 100644 --- a/services/web/app/views/_cookie_banner.pug +++ b/services/web/app/views/_cookie_banner.pug @@ -1,5 +1,5 @@ -.cookie-banner.hidden-print.hidden +section.cookie-banner.hidden-print.hidden(aria-label="Cookie banner") .cookie-banner-content We only use cookies for essential purposes and to improve your experience on our site. You can find out more in our cookie policy. .cookie-banner-actions button(type="button" class="btn btn-link btn-sm" data-ol-cookie-banner-set-consent="essential") Essential cookies only - button(type="button" class="btn btn-primary btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies \ No newline at end of file + button(type="button" class="btn btn-primary btn-sm" data-ol-cookie-banner-set-consent="all") Accept all cookies diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss b/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss index 139994ab08..cd92669668 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/footer.scss @@ -395,6 +395,11 @@ footer.site-footer { white-space: nowrap; padding-top: var(--spacing-00); } + + .cookie-banner-content a, + .cookie-banner-actions .btn-link { + color: var(--link-ui); + } } } From b525a80d281e4ec62271a457218fb2f65d9d73f2 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 28 May 2025 15:22:23 +0100 Subject: [PATCH 022/595] Merge pull request #25470 from overleaf/bg-history-redis-downgrade-job-related-errors downgrade expected job errors in scanAndProcessDueItems GitOrigin-RevId: 0a2689699bfc6512c5017c7f5e51ac4f80c409fe --- services/history-v1/storage/lib/scan.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/services/history-v1/storage/lib/scan.js b/services/history-v1/storage/lib/scan.js index fe4b8d514e..2d9a0fd445 100644 --- a/services/history-v1/storage/lib/scan.js +++ b/services/history-v1/storage/lib/scan.js @@ -1,5 +1,5 @@ const logger = require('@overleaf/logger') - +const { JobNotFoundError, JobNotReadyError } = require('./errors') const BATCH_SIZE = 1000 // Default batch size for SCAN /** @@ -147,10 +147,24 @@ async function scanAndProcessDueItems( `Successfully performed ${taskName} for project` ) } catch (err) { - logger.error( - { ...logContext, projectId, err }, - `Error performing ${taskName} for project` - ) + if (err instanceof JobNotReadyError) { + // the project has been touched since the job was created + logger.info( + { ...logContext, projectId }, + `Job not ready for ${taskName} for project` + ) + } else if (err instanceof JobNotFoundError) { + // the project has been expired already by another worker + logger.info( + { ...logContext, projectId }, + `Job not found for ${taskName} for project` + ) + } else { + logger.error( + { ...logContext, projectId, err }, + `Error performing ${taskName} for project` + ) + } continue } } From 3296fc15da3957243a6a26b2c56a3d98ce4caa53 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Wed, 28 May 2025 15:22:32 +0100 Subject: [PATCH 023/595] Merge pull request #25905 from overleaf/bg-history-redis-fix-import-path fix import path for Job errors in history-v1 GitOrigin-RevId: f5f88bd34e713cd2ed78185ed4ce917e10d09caf --- services/history-v1/storage/lib/scan.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/history-v1/storage/lib/scan.js b/services/history-v1/storage/lib/scan.js index 2d9a0fd445..1f2a335254 100644 --- a/services/history-v1/storage/lib/scan.js +++ b/services/history-v1/storage/lib/scan.js @@ -1,5 +1,5 @@ const logger = require('@overleaf/logger') -const { JobNotFoundError, JobNotReadyError } = require('./errors') +const { JobNotFoundError, JobNotReadyError } = require('./chunk_store/errors') const BATCH_SIZE = 1000 // Default batch size for SCAN /** From d49a8f83df9d44fa1003da22d02cab696239bbd4 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Wed, 28 May 2025 10:38:36 -0400 Subject: [PATCH 024/595] Revert Recurly based subscription upgrades on failed payments (#25824) * feat: add ability to set restore point for subscriptions * feat: update recurly client with ability to get past due invoices and fail invoices * utility to retrieve last valid subscription * create revert requests and fail invoices, revert subscriptions to previous valid states on failed upgrade payments * add restore point and call to revert plans on failed payments * code style for PaymentProviderEntities * moving subs restore point check to SubscriptionController, and removing unecessary error * adding ability to stop sub restores without a deploy * ensure that subs restore point is set before changing plan * changing reverted flag on subscription to count, and only reverting automatic invoices * updating tests with restorepoint functions * rethrow error after voiding restore point, and ensure that recurly failed_payment always gets a 200 response * only void restore point if the changeRequest fails GitOrigin-RevId: cf3074c13db22d1cf680b59c4d57817c390db23e --- .../web/app/src/Features/Errors/Errors.js | 3 + .../Subscription/PaymentProviderEntities.js | 38 +++++++++ .../Features/Subscription/RecurlyClient.js | 36 ++++++++ .../Subscription/SubscriptionController.js | 32 +++++++- .../Subscription/SubscriptionHandler.js | 82 ++++++++++++++++++- .../Subscription/SubscriptionLocator.js | 28 ++++++- .../Subscription/SubscriptionUpdater.js | 54 ++++++++++++ services/web/app/src/models/Subscription.js | 7 ++ 8 files changed, 277 insertions(+), 3 deletions(-) diff --git a/services/web/app/src/Features/Errors/Errors.js b/services/web/app/src/Features/Errors/Errors.js index 4b1b7dd064..487b8cbd03 100644 --- a/services/web/app/src/Features/Errors/Errors.js +++ b/services/web/app/src/Features/Errors/Errors.js @@ -47,6 +47,8 @@ class DuplicateNameError extends OError {} class InvalidNameError extends BackwardCompatibleError {} +class IndeterminateInvoiceError extends OError {} + class UnsupportedFileTypeError extends BackwardCompatibleError {} class FileTooLargeError extends BackwardCompatibleError {} @@ -333,6 +335,7 @@ module.exports = { UnconfirmedEmailError, EmailExistsError, InvalidError, + IndeterminateInvoiceError, NotInV2Error, OutputFileFetchFailedError, SAMLAssertionAudienceMismatch, diff --git a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js index f6a8af4aa5..6fe8638389 100644 --- a/services/web/app/src/Features/Subscription/PaymentProviderEntities.js +++ b/services/web/app/src/Features/Subscription/PaymentProviderEntities.js @@ -2,6 +2,7 @@ /** * @import { PaymentProvider } from '../../../../types/subscription/dashboard/subscription' + * @import { AddOn } from '../../../../types/subscription/plan' */ const OError = require('@overleaf/o-error') @@ -254,6 +255,43 @@ class PaymentProviderSubscription { }) } + /** + * Form a request to revert the plan to it's last saved backup state + * + * @param {string} previousPlanCode + * @param {Array | null} previousAddOns + * @return {PaymentProviderSubscriptionChangeRequest} + * + * @throws {OError} if the restore point plan doesnt exist + */ + getRequestForPlanRevert(previousPlanCode, previousAddOns) { + const lastSuccessfulPlan = + PlansLocator.findLocalPlanInSettings(previousPlanCode) + if (lastSuccessfulPlan == null) { + throw new OError('Unable to find plan in settings', { previousPlanCode }) + } + const changeRequest = new PaymentProviderSubscriptionChangeRequest({ + subscription: this, + timeframe: 'now', + planCode: previousPlanCode, + }) + + // defaulting to empty array is important, as that will wipe away any add-ons that were added in the failed payment + // but were not part of the last successful subscription + const addOns = [] + for (const previousAddon of previousAddOns || []) { + const addOnUpdate = new PaymentProviderSubscriptionAddOnUpdate({ + code: previousAddon.addOnCode, + quantity: previousAddon.quantity, + unitPrice: previousAddon.unitAmountInCents / 100, + }) + addOns.push(addOnUpdate) + } + changeRequest.addOnUpdates = addOns + + return changeRequest + } + /** * Upgrade group plan with the plan code provided * diff --git a/services/web/app/src/Features/Subscription/RecurlyClient.js b/services/web/app/src/Features/Subscription/RecurlyClient.js index fdb3b023e6..753d49ba0f 100644 --- a/services/web/app/src/Features/Subscription/RecurlyClient.js +++ b/services/web/app/src/Features/Subscription/RecurlyClient.js @@ -685,6 +685,38 @@ function subscriptionUpdateRequestToApi(updateRequest) { return requestBody } +/** + * Retrieves a list of failed invoices for a given Recurly subscription ID. + * + * @async + * @function + * @param {string} subscriptionId - The ID of the Recurly subscription to fetch failed invoices for. + * @returns {Promise>} A promise that resolves to an array of failed invoice objects. + */ +async function getPastDueInvoices(subscriptionId) { + const failed = [] + const invoices = client.listSubscriptionInvoices(`uuid-${subscriptionId}`, { + params: { state: 'past_due' }, + }) + + for await (const invoice of invoices.each()) { + failed.push(invoice) + } + return failed +} + +/** + * Marks an invoice as failed using the Recurly client. + * + * @async + * @function failInvoice + * @param {string} invoiceId - The ID of the invoice to be marked as failed. + * @returns {Promise} Resolves when the invoice has been successfully marked as failed. + */ +async function failInvoice(invoiceId) { + await client.markInvoiceFailed(invoiceId) +} + module.exports = { errors: recurly.errors, @@ -706,6 +738,8 @@ module.exports = { subscriptionIsCanceledOrExpired, pauseSubscriptionByUuid: callbackify(pauseSubscriptionByUuid), resumeSubscriptionByUuid: callbackify(resumeSubscriptionByUuid), + getPastDueInvoices: callbackify(getPastDueInvoices), + failInvoice: callbackify(failInvoice), promises: { getSubscription, @@ -726,5 +760,7 @@ module.exports = { getPaymentMethod, getAddOn, getPlan, + getPastDueInvoices, + failInvoice, }, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index db278b23c0..1cc2ad0094 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -410,6 +410,8 @@ async function purchaseAddon(req, res, next) { logger.debug({ userId: user._id, addOnCode }, 'purchasing add-ons') try { + // set a restore point in the case of a failed payment for the upgrade (Recurly only) + await SubscriptionHandler.promises.setSubscriptionRestorePoint(user._id) await SubscriptionHandler.promises.purchaseAddon( user._id, addOnCode, @@ -574,7 +576,35 @@ function recurlyCallback(req, res, next) { ) ) - if ( + // this is a recurly only case which is required since Recurly does not have a reliable way to check credit info pre-upgrade purchase + if (event === 'failed_payment_notification') { + if (!Settings.planReverts?.enabled) { + return res.sendStatus(200) + } + + SubscriptionHandler.getSubscriptionRestorePoint( + eventData.transaction.subscription_id, + function (err, lastSubscription) { + if (err) { + return next(err) + } + // if theres no restore point it could be a failed renewal, or no restore set. Either way it will be handled through dunning automatically + if (!lastSubscription || !lastSubscription?.planCode) { + res.sendStatus(200) + } + SubscriptionHandler.revertPlanChange( + eventData.transaction.subscription_id, + lastSubscription, + function (err) { + if (err) { + return next(err) + } + res.sendStatus(200) + } + ) + } + ) + } else if ( [ 'new_subscription_notification', 'updated_subscription_notification', diff --git a/services/web/app/src/Features/Subscription/SubscriptionHandler.js b/services/web/app/src/Features/Subscription/SubscriptionHandler.js index 39a44f305f..1296a2a7de 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionHandler.js @@ -11,7 +11,7 @@ const LimitationsManager = require('./LimitationsManager') const EmailHandler = require('../Email/EmailHandler') const { callbackify } = require('@overleaf/promise-utils') const UserUpdater = require('../User/UserUpdater') -const { NotFoundError } = require('../Errors/Errors') +const { NotFoundError, IndeterminateInvoiceError } = require('../Errors/Errors') const Modules = require('../../infrastructure/Modules') /** @@ -387,6 +387,80 @@ async function resumeSubscription(user) { ) } +/** + * @param recurlySubscriptionId + */ +async function getSubscriptionRestorePoint(recurlySubscriptionId) { + const lastSubscription = + await SubscriptionLocator.promises.getLastSuccessfulSubscription( + recurlySubscriptionId + ) + return lastSubscription +} + +/** + * @param recurlySubscriptionId + * @param subscriptionRestorePoint + */ +async function revertPlanChange( + recurlySubscriptionId, + subscriptionRestorePoint +) { + const subscription = await RecurlyClient.promises.getSubscription( + recurlySubscriptionId + ) + + const changeRequest = subscription.getRequestForPlanRevert( + subscriptionRestorePoint.planCode, + subscriptionRestorePoint.addOns + ) + + const pastDue = await RecurlyClient.promises.getPastDueInvoices( + recurlySubscriptionId + ) + + // only process revert requests within the past 24 hours, as we dont want to restore plans at the end of their dunning cycle + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + if ( + pastDue.length !== 1 || + !pastDue[0].id || + !pastDue[0].dueAt || + pastDue[0].dueAt < yesterday || + pastDue[0].collectionMethod !== 'automatic' + ) { + throw new IndeterminateInvoiceError( + 'cant determine invoice to fail for plan revert', + { + info: { recurlySubscriptionId }, + } + ) + } + + await RecurlyClient.promises.failInvoice(pastDue[0].id) + await SubscriptionUpdater.promises.setSubscriptionWasReverted( + subscriptionRestorePoint._id + ) + await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest) + await syncSubscription({ uuid: recurlySubscriptionId }, {}) +} + +async function setSubscriptionRestorePoint(userId) { + const subscription = + await SubscriptionLocator.promises.getUsersSubscription(userId) + // if the subscription is not a recurly one, we can return early as we dont allow for failed payments on other payment providers + // we need to deal with it for recurly, because we cant verify payment in advance + if (!subscription?.recurlySubscription_id || !subscription.planCode) { + return + } + await SubscriptionUpdater.promises.setRestorePoint( + subscription.id, + subscription.planCode, + subscription.addOns, + false + ) +} + module.exports = { validateNoSubscriptionInRecurly: callbackify(validateNoSubscriptionInRecurly), createSubscription: callbackify(createSubscription), @@ -403,6 +477,9 @@ module.exports = { removeAddon: callbackify(removeAddon), pauseSubscription: callbackify(pauseSubscription), resumeSubscription: callbackify(resumeSubscription), + revertPlanChange: callbackify(revertPlanChange), + setSubscriptionRestorePoint: callbackify(setSubscriptionRestorePoint), + getSubscriptionRestorePoint: callbackify(getSubscriptionRestorePoint), promises: { validateNoSubscriptionInRecurly, createSubscription, @@ -419,5 +496,8 @@ module.exports = { removeAddon, pauseSubscription, resumeSubscription, + revertPlanChange, + setSubscriptionRestorePoint, + getSubscriptionRestorePoint, }, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.js b/services/web/app/src/Features/Subscription/SubscriptionLocator.js index 8526ad0fb2..978f4d41b7 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionLocator.js +++ b/services/web/app/src/Features/Subscription/SubscriptionLocator.js @@ -1,3 +1,7 @@ +/** + * @import { AddOn } from '../../../../types/subscription/plan' + */ + const { callbackifyAll } = require('@overleaf/promise-utils') const { Subscription } = require('../../models/Subscription') const { DeletedSubscription } = require('../../models/DeletedSubscription') @@ -124,7 +128,8 @@ const SubscriptionLocator = { // todo: as opposed to recurlyEntities which use addon.code, subscription model uses addon.addOnCode // which we hope to align via https://github.com/overleaf/internal/issues/25494 return Boolean( - isStandaloneAiAddOnPlanCode(subscription?.planCode) || + (subscription?.planCode && + isStandaloneAiAddOnPlanCode(subscription?.planCode)) || subscription?.addOns?.some(addOn => addOn.addOnCode === AI_ADD_ON_CODE) ) }, @@ -136,6 +141,27 @@ const SubscriptionLocator = { return userOrId } }, + + /** + * Retrieves the last successful subscription for a given user. + * + * @async + * @function + * @param {string} recurlyId - The ID of the recurly subscription tied to the mongo subscription to check for a previous successful state. + * @returns {Promise<{_id: ObjectId, planCode: string, addOns: [AddOn]}|null>} A promise that resolves to the last successful planCode and addon state, + * or null if we havent stored a previous + */ + async getLastSuccessfulSubscription(recurlyId) { + const subscription = await Subscription.findOne({ + recurlySubscription_id: recurlyId, + }).exec() + return subscription && subscription.lastSuccesfulSubscription + ? { + ...subscription.lastSuccesfulSubscription, + _id: subscription._id, + } + : null + }, } module.exports = { diff --git a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js index 15f61b6160..b0e24ce5ad 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionUpdater.js +++ b/services/web/app/src/Features/Subscription/SubscriptionUpdater.js @@ -19,6 +19,7 @@ const Modules = require('../../infrastructure/Modules') * @typedef {import('../../../../types/subscription/dashboard/subscription').Subscription} Subscription * @typedef {import('../../../../types/subscription/dashboard/subscription').PaymentProvider} PaymentProvider * @typedef {import('../../../../types/group-management/group-audit-log').GroupAuditLog} GroupAuditLog + * @import { AddOn } from '../../../../types/subscription/plan' */ /** @@ -486,6 +487,53 @@ async function _sendSubscriptionEventForAllMembers(subscriptionId, event) { } } +/** + * Sets the plan code and addon state to revert the plan to in case of failed upgrades, or clears the last restore point if it was used/ voided + * @param {ObjectId} subscriptionId the mongo ID of the subscription to set the restore point for + * @param {string} planCode the plan code to revert to + * @param {Array} addOns the addOns to revert to + * @param {Boolean} consumed whether the restore point was used to revert a subscription + */ +async function setRestorePoint(subscriptionId, planCode, addOns, consumed) { + const update = { + $set: { + 'lastSuccesfulSubscription.planCode': planCode, + 'lastSuccesfulSubscription.addOns': addOns, + }, + } + + if (consumed) { + update.$inc = { revertedDueToFailedPayment: 1 } + } + + await Subscription.updateOne({ _id: subscriptionId }, update).exec() +} + +/** + * Clears the restore point for a given subscription, and signals that the subscription was sucessfully reverted. + * + * @async + * @function setSubscriptionWasReverted + * @param {ObjectId} subscriptionId the mongo ID of the subscription to set the restore point for + * @returns {Promise} Resolves when the restore point has been cleared. + */ +async function setSubscriptionWasReverted(subscriptionId) { + // consume the backup and flag that the subscription was reverted due to failed payment + await setRestorePoint(subscriptionId, null, null, true) +} + +/** + * Clears the restore point for a given subscription, and signals that the subscription was not reverted. + * + * @async + * @function voidRestorePoint + * @param {string} subscriptionId - The unique identifier of the subscription. + * @returns {Promise} Resolves when the restore point has been cleared. + */ +async function voidRestorePoint(subscriptionId) { + await setRestorePoint(subscriptionId, null, null, false) +} + module.exports = { updateAdmin: callbackify(updateAdmin), syncSubscription: callbackify(syncSubscription), @@ -500,6 +548,9 @@ module.exports = { restoreSubscription: callbackify(restoreSubscription), updateSubscriptionFromRecurly: callbackify(updateSubscriptionFromRecurly), scheduleRefreshFeatures: callbackify(scheduleRefreshFeatures), + setSubscriptionRestorePoint: callbackify(setRestorePoint), + setSubscriptionWasReverted: callbackify(setSubscriptionWasReverted), + voidRestorePoint: callbackify(voidRestorePoint), promises: { updateAdmin, syncSubscription, @@ -514,5 +565,8 @@ module.exports = { restoreSubscription, updateSubscriptionFromRecurly, scheduleRefreshFeatures, + setRestorePoint, + setSubscriptionWasReverted, + voidRestorePoint, }, } diff --git a/services/web/app/src/models/Subscription.js b/services/web/app/src/models/Subscription.js index 92a7739515..4a5fed6f1f 100644 --- a/services/web/app/src/models/Subscription.js +++ b/services/web/app/src/models/Subscription.js @@ -25,6 +25,13 @@ const SubscriptionSchema = new Schema( invited_emails: [String], teamInvites: [TeamInviteSchema], recurlySubscription_id: String, + lastSuccesfulSubscription: { + planCode: { + type: String, + }, + addOns: Schema.Types.Mixed, + }, + timesRevertedDueToFailedPayment: { type: Number, default: 0 }, teamName: { type: String }, teamNotice: { type: String }, planCode: { type: String }, From de4a80ef935bcd3f372ff29211188100114ba623 Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Wed, 28 May 2025 14:24:59 +0100 Subject: [PATCH 025/595] Update unit test script to remove mocha module tests GitOrigin-RevId: 3bcc265e32486a179dd473233bed27ed798fba47 --- services/web/bin/test_unit_run_dir | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/services/web/bin/test_unit_run_dir b/services/web/bin/test_unit_run_dir index 50ab911b33..4d5d5ecb9a 100755 --- a/services/web/bin/test_unit_run_dir +++ b/services/web/bin/test_unit_run_dir @@ -3,25 +3,16 @@ declare -a vitest_args=("$@") has_mocha_test=0 -has_mocha_mjs_test=0 has_vitest_test=0 -mocha_mjs_dirs=() - for dir_path in "$@"; do if [ -n "$(find "$dir_path" -name "*.js" -type f -print -quit 2>/dev/null)" ]; then has_mocha_test=1 fi - if [ -n "$(find "$dir_path" -name "*Tests.mjs" -type f -print -quit 2>/dev/null)" ]; then - has_mocha_mjs_test=1 - mocha_mjs_dirs+=("$dir_path/**/*Tests.mjs") - fi - if [ -n "$(find "$dir_path" -name "*.test.mjs" -type f -print -quit 2>/dev/null)" ]; then has_vitest_test=1 fi - done if [[ -n "$MOCHA_GREP" ]]; then @@ -47,14 +38,7 @@ fi if (( has_mocha_test == 1 )); then mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=js "$@" mocha_status=$? -fi -# Remove this if/else when we have converted all module tests to vitest. -if (( has_mocha_mjs_test == 1)); then - mocha --recursive --timeout 25000 --exit --grep="$MOCHA_GREP" --require test/unit/bootstrap.js --extension=mjs "${mocha_mjs_dirs[@]}" - mocha_status=$((mocha_status != 0 ? mocha_status : $?)) -fi - -if (( has_mocha_mjs_test == 0 && has_mocha_test == 0 )); then +else echo "No mocha tests found in $TARGET_DIR, skipping mocha step." mocha_status=0 fi From a06ae82b56c103a726b3f6175d26ae36eac55edd Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Wed, 28 May 2025 16:25:21 +0100 Subject: [PATCH 026/595] Remove esmock from web GitOrigin-RevId: 32aa3f23da8bb135d41f2e305662f157094d4936 --- package-lock.json | 10 ---------- services/web/package.json | 1 - 2 files changed, 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 146ba3255d..73b722b1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44892,7 +44892,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": "^56.0.0", - "esmock": "^2.6.7", "events": "^3.3.0", "fake-indexeddb": "^6.0.0", "fetch-mock": "^12.5.2", @@ -45801,15 +45800,6 @@ "url": "https://opencollective.com/eslint" } }, - "services/web/node_modules/esmock": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.6.7.tgz", - "integrity": "sha512-4DmjZ0qQIG+NQV1njHvWrua/cZEuJq56A3pSELT2BjNuol1aads7BluofCbLErdO41Ic1XCd2UMepVLpjL64YQ==", - "dev": true, - "engines": { - "node": ">=14.16.0" - } - }, "services/web/node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", diff --git a/services/web/package.json b/services/web/package.json index ee5f81d4f8..609d24c0a3 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -294,7 +294,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-unicorn": "^56.0.0", - "esmock": "^2.6.7", "events": "^3.3.0", "fake-indexeddb": "^6.0.0", "fetch-mock": "^12.5.2", From aee3909a5f363bc6ab1f54a3480758c663dc5857 Mon Sep 17 00:00:00 2001 From: Jimmy Domagala-Tang Date: Wed, 28 May 2025 13:34:58 -0400 Subject: [PATCH 027/595] prevent attempting to set headers after we already sent respone (#25994) GitOrigin-RevId: be9f63f4c6d86ccd7f55850d71f5f2564eab2f12 --- .../app/src/Features/Subscription/SubscriptionController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js index 1cc2ad0094..7aa345e7a8 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionController.js +++ b/services/web/app/src/Features/Subscription/SubscriptionController.js @@ -590,7 +590,7 @@ function recurlyCallback(req, res, next) { } // if theres no restore point it could be a failed renewal, or no restore set. Either way it will be handled through dunning automatically if (!lastSubscription || !lastSubscription?.planCode) { - res.sendStatus(200) + return res.sendStatus(200) } SubscriptionHandler.revertPlanChange( eventData.transaction.subscription_id, @@ -599,7 +599,7 @@ function recurlyCallback(req, res, next) { if (err) { return next(err) } - res.sendStatus(200) + return res.sendStatus(200) } ) } From 1c499496c694bfc6949c3450167fed508e97169e Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 3 Dec 2024 16:40:27 +0100 Subject: [PATCH 028/595] Redirect non-existing links to Overleaf page --- services/web/app/src/router.mjs | 4 ++++ .../hotkeys-modal/components/hotkeys-modal-bottom-text.jsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs index f87297c35c..5f6286726a 100644 --- a/services/web/app/src/router.mjs +++ b/services/web/app/src/router.mjs @@ -1265,6 +1265,10 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) { TokenAccessController.grantTokenAccessReadOnly ) + webRouter.get(['/learn*', '/blog*', '/latex*', '/for/*', '/contact*'], (req, res) => { + res.redirect(301, `https://www.overleaf.com${req.originalUrl}`) + }) + webRouter.get('/unsupported-browser', renderUnsupportedBrowserPage) webRouter.get('*', ErrorController.notFound) diff --git a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx index db6507e823..0a840931bc 100644 --- a/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx +++ b/services/web/frontend/js/features/hotkeys-modal/components/hotkeys-modal-bottom-text.jsx @@ -10,7 +10,7 @@ export default function HotkeysModalBottomText() { // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key eventTracking.sendMB('left-menu-hotkeys-template')} - href="/articles/overleaf-keyboard-shortcuts/qykqfvmxdnjf" + href="https://www.overleaf.com/latex/templates/overleaf-keyboard-shortcuts/pphdnzrwmttk" target="_blank" />, ]} From 884e7d81c83f85d3f4d522df1638cc955e0e4d7c Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 3 Dec 2024 01:07:49 +0100 Subject: [PATCH 029/595] Enable track changes and comments feature --- .../Features/Project/ProjectEditorHandler.js | 2 +- services/web/config/settings.defaults.js | 1 + .../hooks/use-codemirror-scope.ts | 2 +- .../app/src/TrackChangesController.js | 194 ++++++++++++++++++ .../app/src/TrackChangesRouter.js | 72 +++++++ services/web/modules/track-changes/index.js | 2 + 6 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 services/web/modules/track-changes/app/src/TrackChangesController.js create mode 100644 services/web/modules/track-changes/app/src/TrackChangesRouter.js create mode 100644 services/web/modules/track-changes/index.js diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js index 05e5beba09..cd37c0f6d0 100644 --- a/services/web/app/src/Features/Project/ProjectEditorHandler.js +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -4,7 +4,7 @@ const Path = require('path') const Features = require('../../infrastructure/Features') module.exports = ProjectEditorHandler = { - trackChangesAvailable: false, + trackChangesAvailable: true, buildProjectModelView(project, members, invites) { let owner, ownerFeatures diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index a7ff970ef0..72db6cef36 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1005,6 +1005,7 @@ module.exports = { 'launchpad', 'server-ce-scripts', 'user-activate', + 'track-changes', ], viewIncludes: {}, diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index a4e2862e1f..bc7a99050d 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -185,7 +185,7 @@ function useCodeMirrorScope(view: EditorView) { if (currentDocument) { if (trackChanges) { - currentDocument.track_changes_as = userId || 'anonymous' + currentDocument.track_changes_as = userId || 'anonymous-user' } else { currentDocument.track_changes_as = null } diff --git a/services/web/modules/track-changes/app/src/TrackChangesController.js b/services/web/modules/track-changes/app/src/TrackChangesController.js new file mode 100644 index 0000000000..12cbb57da4 --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesController.js @@ -0,0 +1,194 @@ +const ChatApiHandler = require('../../../../app/src/Features/Chat/ChatApiHandler') +const ChatManager = require('../../../../app/src/Features/Chat/ChatManager') +const EditorRealTimeController = require('../../../../app/src/Features/Editor/EditorRealTimeController') +const SessionManager = require('../../../../app/src/Features/Authentication/SessionManager') +const UserInfoManager = require('../../../../app/src/Features/User/UserInfoManager') +const DocstoreManager = require('../../../../app/src/Features/Docstore/DocstoreManager') +const DocumentUpdaterHandler = require('../../../../app/src/Features/DocumentUpdater/DocumentUpdaterHandler') +const CollaboratorsGetter = require('../../../../app/src/Features/Collaborators/CollaboratorsGetter') +const { Project } = require('../../../../app/src/models/Project') +const pLimit = require('p-limit') + +function _transformId(doc) { + if (doc._id) { + doc.id = doc._id; + delete doc._id; + } + return doc; +} + +const TrackChangesController = { + async trackChanges(req, res, next) { + try { + const { project_id } = req.params + let state = req.body.on || req.body.on_for + if (req.body.on_for_guests && !req.body.on) state.__guests__ = true + await Project.updateOne({_id: project_id}, {track_changes: state}).exec() //do not wait? + EditorRealTimeController.emitToRoom(project_id, 'toggle-track-changes', state) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async acceptChanges(req, res, next) { + try { + const { project_id, doc_id } = req.params + const change_ids = req.body.change_ids + EditorRealTimeController.emitToRoom(project_id, 'accept-changes', doc_id, change_ids) + await DocumentUpdaterHandler.promises.acceptChanges(project_id, doc_id, change_ids) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async getAllRanges(req, res, next) { + try { + const { project_id } = req.params + // Flushing the project to mongo is not ideal. Is it possible to fetch the ranges from redis? + await DocumentUpdaterHandler.promises.flushProjectToMongo(project_id) + const ranges = await DocstoreManager.promises.getAllRanges(project_id) + res.json(ranges.map(_transformId)) + } catch (err) { + next(err) + } + }, + async getChangesUsers(req, res, next) { + try { + const { project_id } = req.params + const memberIds = await CollaboratorsGetter.promises.getMemberIds(project_id) + // FIXME: Fails to display names in changes made by former project collaborators. + // See the alternative below. However, it requires flushing the project to mongo, which is not ideal. + const limit = pLimit(3) + const users = await Promise.all( + memberIds.map(memberId => + limit(async () => { + const user = await UserInfoManager.promises.getPersonalInfo(memberId) + return user + }) + ) + ) + users.push({_id: null}) // An anonymous user won't cause any harm + res.json(users.map(_transformId)) + } catch (err) { + next(err) + } + }, +/* + async getChangesUsers(req, res, next) { + try { + const { project_id } = req.params + await DocumentUpdaterHandler.promises.flushProjectToMongo(project_id) + const memberIds = new Set() + const ranges = await DocstoreManager.promises.getAllRanges(project_id) + ranges.forEach(range => { + ;[...range.ranges?.changes || [], ...range.ranges?.comments || []].forEach(item => { + memberIds.add(item.metadata?.user_id) + }) + }) + const limit = pLimit(3) + const users = await Promise.all( + [...memberIds].map(memberId => + limit(async () => { + if( memberId !== "anonymous-user") { + return await UserInfoManager.promises.getPersonalInfo(memberId) + } else { + return {_id: null} + } + }) + ) + ) + res.json(users.map(_transformId)) + } catch (err) { + next(err) + } + }, +*/ + async getThreads(req, res, next) { + try { + const { project_id } = req.params + const messages = await ChatApiHandler.promises.getThreads(project_id) + await ChatManager.promises.injectUserInfoIntoThreads(messages) + res.json(messages) + } catch (err) { + next(err) + } + }, + async sendComment(req, res, next) { + try { + const { project_id, thread_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + const message = await ChatApiHandler.promises.sendComment(project_id, thread_id, user_id, content) + message.user = await UserInfoManager.promises.getPersonalInfo(user_id) + EditorRealTimeController.emitToRoom(project_id, 'new-comment', thread_id, message) + res.sendStatus(204) + } catch (err) { + next(err); + } + }, + async editMessage(req, res, next) { + try { + const { project_id, thread_id, message_id } = req.params + const { content } = req.body + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.editMessage(project_id, thread_id, message_id, user_id, content) + EditorRealTimeController.emitToRoom(project_id, 'edit-message', thread_id, message_id, content) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async deleteMessage(req, res, next) { + try { + const { project_id, thread_id, message_id } = req.params + await ChatApiHandler.promises.deleteMessage(project_id, thread_id, message_id) + EditorRealTimeController.emitToRoom(project_id, 'delete-message', thread_id, message_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async resolveThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + const user = await UserInfoManager.promises.getPersonalInfo(user_id) + await ChatApiHandler.promises.resolveThread(project_id, thread_id, user_id) + EditorRealTimeController.emitToRoom(project_id, 'resolve-thread', thread_id, user) + await DocumentUpdaterHandler.promises.resolveThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204); + } catch (err) { + next(err); + } + }, + async reopenThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.reopenThread(project_id, thread_id) + EditorRealTimeController.emitToRoom(project_id, 'reopen-thread', thread_id) + await DocumentUpdaterHandler.promises.reopenThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, + async deleteThread(req, res, next) { + try { + const { project_id, doc_id, thread_id } = req.params + const user_id = SessionManager.getLoggedInUserId(req.session) + if (!user_id) throw new Error('no logged-in user') + await ChatApiHandler.promises.deleteThread(project_id, thread_id) + EditorRealTimeController.emitToRoom(project_id, 'delete-thread', thread_id) + await DocumentUpdaterHandler.promises.deleteThread(project_id, doc_id, thread_id, user_id) + res.sendStatus(204) + } catch (err) { + next(err) + } + }, +} +module.exports = TrackChangesController diff --git a/services/web/modules/track-changes/app/src/TrackChangesRouter.js b/services/web/modules/track-changes/app/src/TrackChangesRouter.js new file mode 100644 index 0000000000..3791e251a1 --- /dev/null +++ b/services/web/modules/track-changes/app/src/TrackChangesRouter.js @@ -0,0 +1,72 @@ +const logger = require('@overleaf/logger') +const AuthorizationMiddleware = require('../../../../app/src/Features/Authorization/AuthorizationMiddleware') +const TrackChangesController = require('./TrackChangesController') + +module.exports = { + apply(webRouter) { + logger.debug({}, 'Init track-changes router') + + webRouter.post('/project/:project_id/track_changes', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.trackChanges + ) + webRouter.post('/project/:project_id/doc/:doc_id/changes/accept', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.acceptChanges + ) + webRouter.get('/project/:project_id/ranges', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getAllRanges + ) + webRouter.get('/project/:project_id/changes/users', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getChangesUsers + ) + webRouter.get( + '/project/:project_id/threads', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.getThreads + ) + webRouter.post( + '/project/:project_id/thread/:thread_id/messages', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.sendComment + ) + webRouter.post( + '/project/:project_id/thread/:thread_id/messages/:message_id/edit', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.editMessage + ) + webRouter.delete( + '/project/:project_id/thread/:thread_id/messages/:message_id', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.deleteMessage + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/thread/:thread_id/resolve', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.resolveThread + ) + webRouter.post( + '/project/:project_id/doc/:doc_id/thread/:thread_id/reopen', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.reopenThread + ) + webRouter.delete( + '/project/:project_id/doc/:doc_id/thread/:thread_id', + AuthorizationMiddleware.blockRestrictedUserFromProject, + AuthorizationMiddleware.ensureUserCanReadProject, + TrackChangesController.deleteThread + ) + }, +} diff --git a/services/web/modules/track-changes/index.js b/services/web/modules/track-changes/index.js new file mode 100644 index 0000000000..aa9e6a73da --- /dev/null +++ b/services/web/modules/track-changes/index.js @@ -0,0 +1,2 @@ +const TrackChangesRouter = require('./app/src/TrackChangesRouter') +module.exports = { router : TrackChangesRouter } From 928a514705254e0cf59ff9aa2c8aeed92dee0678 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 3 Dec 2024 01:14:41 +0100 Subject: [PATCH 030/595] Enable autocomplete of reference keys feature --- develop/README.md | 1 + develop/dev.env | 1 + develop/docker-compose.dev.yml | 11 + develop/docker-compose.yml | 8 + server-ce/config/env.sh | 1 + server-ce/runit/references-overleaf/run | 12 + server-ce/services.js | 3 + services/references/.eslintrc | 6 + services/references/.gitignore | 5 + services/references/.mocharc.json | 3 + services/references/.nvmrc | 1 + services/references/Dockerfile | 27 + services/references/LICENSE | 662 ++++++ services/references/Makefile | 156 ++ services/references/README.md | 10 + services/references/app.js | 40 + .../app/js/ReferencesAPIController.js | 42 + services/references/app/js/bib2json.js | 1967 +++++++++++++++++ services/references/buildscript.txt | 9 + .../references/config/settings.defaults.cjs | 9 + services/references/docker-compose.ci.yml | 52 + services/references/docker-compose.yml | 56 + services/references/package.json | 26 + services/references/tsconfig.json | 12 + services/web/config/settings.defaults.js | 3 + 25 files changed, 3123 insertions(+) create mode 100755 server-ce/runit/references-overleaf/run create mode 100644 services/references/.eslintrc create mode 100644 services/references/.gitignore create mode 100644 services/references/.mocharc.json create mode 100644 services/references/.nvmrc create mode 100644 services/references/Dockerfile create mode 100644 services/references/LICENSE create mode 100644 services/references/Makefile create mode 100644 services/references/README.md create mode 100644 services/references/app.js create mode 100644 services/references/app/js/ReferencesAPIController.js create mode 100644 services/references/app/js/bib2json.js create mode 100644 services/references/buildscript.txt create mode 100644 services/references/config/settings.defaults.cjs create mode 100644 services/references/docker-compose.ci.yml create mode 100644 services/references/docker-compose.yml create mode 100644 services/references/package.json create mode 100644 services/references/tsconfig.json diff --git a/develop/README.md b/develop/README.md index 568259c4e3..14f7354572 100644 --- a/develop/README.md +++ b/develop/README.md @@ -77,6 +77,7 @@ each service: | `filestore` | 9235 | | `notifications` | 9236 | | `real-time` | 9237 | +| `references` | 9238 | | `history-v1` | 9239 | | `project-history` | 9240 | diff --git a/develop/dev.env b/develop/dev.env index aae91497db..2ccfef7052 100644 --- a/develop/dev.env +++ b/develop/dev.env @@ -13,6 +13,7 @@ NOTIFICATIONS_HOST=notifications PROJECT_HISTORY_HOST=project-history REALTIME_HOST=real-time REDIS_HOST=redis +REFERENCES_HOST=references SESSION_SECRET=foo WEBPACK_HOST=webpack WEB_API_PASSWORD=overleaf diff --git a/develop/docker-compose.dev.yml b/develop/docker-compose.dev.yml index 4432a24162..05844136a0 100644 --- a/develop/docker-compose.dev.yml +++ b/develop/docker-compose.dev.yml @@ -112,6 +112,17 @@ services: - ../services/real-time/app.js:/overleaf/services/real-time/app.js - ../services/real-time/config:/overleaf/services/real-time/config + references: + command: ["node", "--watch", "app.js"] + environment: + - NODE_OPTIONS=--inspect=0.0.0.0:9229 + ports: + - "127.0.0.1:9238:9229" + volumes: + - ../services/references/app:/overleaf/services/references/app + - ../services/references/config:/overleaf/services/references/config + - ../services/references/app.js:/overleaf/services/references/app.js + web: command: ["node", "--watch", "app.js", "--watch-locales"] environment: diff --git a/develop/docker-compose.yml b/develop/docker-compose.yml index 750e11ac87..cccd37c018 100644 --- a/develop/docker-compose.yml +++ b/develop/docker-compose.yml @@ -131,6 +131,13 @@ services: volumes: - redis-data:/data + references: + build: + context: .. + dockerfile: services/references/Dockerfile + env_file: + - dev.env + web: build: context: .. @@ -161,6 +168,7 @@ services: - notifications - project-history - real-time + - references webpack: build: diff --git a/server-ce/config/env.sh b/server-ce/config/env.sh index b12ca242d3..81cebe4caa 100644 --- a/server-ce/config/env.sh +++ b/server-ce/config/env.sh @@ -9,5 +9,6 @@ export HISTORY_V1_HOST=127.0.0.1 export NOTIFICATIONS_HOST=127.0.0.1 export PROJECT_HISTORY_HOST=127.0.0.1 export REALTIME_HOST=127.0.0.1 +export REFERENCES_HOST=127.0.0.1 export WEB_HOST=127.0.0.1 export WEB_API_HOST=127.0.0.1 diff --git a/server-ce/runit/references-overleaf/run b/server-ce/runit/references-overleaf/run new file mode 100755 index 0000000000..875023df9f --- /dev/null +++ b/server-ce/runit/references-overleaf/run @@ -0,0 +1,12 @@ +#!/bin/bash + +NODE_PARAMS="" +if [ "$DEBUG_NODE" == "true" ]; then + echo "running debug - references" + NODE_PARAMS="--inspect=0.0.0.0:30560" +fi + +source /etc/overleaf/env.sh +export LISTEN_ADDRESS=127.0.0.1 + +exec /sbin/setuser www-data /usr/bin/node $NODE_PARAMS /overleaf/services/references/app.js >> /var/log/overleaf/references.log 2>&1 diff --git a/server-ce/services.js b/server-ce/services.js index d0b0a9c076..e0282f3bad 100644 --- a/server-ce/services.js +++ b/server-ce/services.js @@ -29,6 +29,9 @@ module.exports = [ { name: 'project-history', }, + { + name: 'references', + }, { name: 'history-v1', }, diff --git a/services/references/.eslintrc b/services/references/.eslintrc new file mode 100644 index 0000000000..cc68024d9d --- /dev/null +++ b/services/references/.eslintrc @@ -0,0 +1,6 @@ +{ + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + } +} diff --git a/services/references/.gitignore b/services/references/.gitignore new file mode 100644 index 0000000000..80bac793a7 --- /dev/null +++ b/services/references/.gitignore @@ -0,0 +1,5 @@ +node_modules +forever + +# managed by dev-environment$ bin/update_build_scripts +.npmrc diff --git a/services/references/.mocharc.json b/services/references/.mocharc.json new file mode 100644 index 0000000000..dc3280aa96 --- /dev/null +++ b/services/references/.mocharc.json @@ -0,0 +1,3 @@ +{ + "require": "test/setup.js" +} diff --git a/services/references/.nvmrc b/services/references/.nvmrc new file mode 100644 index 0000000000..0254b1e633 --- /dev/null +++ b/services/references/.nvmrc @@ -0,0 +1 @@ +20.18.2 diff --git a/services/references/Dockerfile b/services/references/Dockerfile new file mode 100644 index 0000000000..caa6e2a31c --- /dev/null +++ b/services/references/Dockerfile @@ -0,0 +1,27 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +FROM node:20.18.2 AS base + +WORKDIR /overleaf/services/references + +# Google Cloud Storage needs a writable $HOME/.config for resumable uploads +# (see https://googleapis.dev/nodejs/storage/latest/File.html#createWriteStream) +RUN mkdir /home/node/.config && chown node:node /home/node/.config + +FROM base AS app + +COPY package.json package-lock.json /overleaf/ +COPY services/references/package.json /overleaf/services/references/ +COPY libraries/ /overleaf/libraries/ +COPY patches/ /overleaf/patches/ + +RUN cd /overleaf && npm ci --quiet + +COPY services/references/ /overleaf/services/references/ + +FROM app +USER node + +CMD ["node", "--expose-gc", "app.js"] diff --git a/services/references/LICENSE b/services/references/LICENSE new file mode 100644 index 0000000000..ac8619dcb9 --- /dev/null +++ b/services/references/LICENSE @@ -0,0 +1,662 @@ + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/services/references/Makefile b/services/references/Makefile new file mode 100644 index 0000000000..e5181b46f3 --- /dev/null +++ b/services/references/Makefile @@ -0,0 +1,156 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +BUILD_NUMBER ?= local +BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) +PROJECT_NAME = references +BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') + +DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml +DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ + BRANCH_NAME=$(BRANCH_NAME) \ + PROJECT_NAME=$(PROJECT_NAME) \ + MOCHA_GREP=${MOCHA_GREP} \ + docker compose ${DOCKER_COMPOSE_FLAGS} + +COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE ?= test_acceptance_$(BUILD_DIR_NAME) +DOCKER_COMPOSE_TEST_ACCEPTANCE = \ + COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_ACCEPTANCE) $(DOCKER_COMPOSE) + +COMPOSE_PROJECT_NAME_TEST_UNIT ?= test_unit_$(BUILD_DIR_NAME) +DOCKER_COMPOSE_TEST_UNIT = \ + COMPOSE_PROJECT_NAME=$(COMPOSE_PROJECT_NAME_TEST_UNIT) $(DOCKER_COMPOSE) + +clean: + -docker rmi ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -docker rmi us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + -$(DOCKER_COMPOSE_TEST_UNIT) down --rmi local + -$(DOCKER_COMPOSE_TEST_ACCEPTANCE) down --rmi local + +HERE=$(shell pwd) +MONOREPO=$(shell cd ../../ && pwd) +# Run the linting commands in the scope of the monorepo. +# Eslint and prettier (plus some configs) are on the root. +RUN_LINTING = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(HERE) node:20.18.2 npm run --silent + +RUN_LINTING_CI = docker run --rm --volume $(MONOREPO)/.editorconfig:/overleaf/.editorconfig --volume $(MONOREPO)/.eslintignore:/overleaf/.eslintignore --volume $(MONOREPO)/.eslintrc:/overleaf/.eslintrc --volume $(MONOREPO)/.prettierignore:/overleaf/.prettierignore --volume $(MONOREPO)/.prettierrc:/overleaf/.prettierrc --volume $(MONOREPO)/tsconfig.backend.json:/overleaf/tsconfig.backend.json ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) npm run --silent + +# Same but from the top of the monorepo +RUN_LINTING_MONOREPO = docker run --rm -v $(MONOREPO):$(MONOREPO) -w $(MONOREPO) node:20.18.2 npm run --silent + +SHELLCHECK_OPTS = \ + --shell=bash \ + --external-sources +SHELLCHECK_COLOR := $(if $(CI),--color=never,--color) +SHELLCHECK_FILES := { git ls-files "*.sh" -z; git grep -Plz "\A\#\!.*bash"; } | sort -zu + +shellcheck: + @$(SHELLCHECK_FILES) | xargs -0 -r docker run --rm -v $(HERE):/mnt -w /mnt \ + koalaman/shellcheck:stable $(SHELLCHECK_OPTS) $(SHELLCHECK_COLOR) + +shellcheck_fix: + @$(SHELLCHECK_FILES) | while IFS= read -r -d '' file; do \ + diff=$$(docker run --rm -v $(HERE):/mnt -w /mnt koalaman/shellcheck:stable $(SHELLCHECK_OPTS) --format=diff "$$file" 2>/dev/null); \ + if [ -n "$$diff" ] && ! echo "$$diff" | patch -p1 >/dev/null 2>&1; then echo "\033[31m$$file\033[0m"; \ + elif [ -n "$$diff" ]; then echo "$$file"; \ + else echo "\033[2m$$file\033[0m"; fi \ + done + +format: + $(RUN_LINTING) format + +format_ci: + $(RUN_LINTING_CI) format + +format_fix: + $(RUN_LINTING) format:fix + +lint: + $(RUN_LINTING) lint + +lint_ci: + $(RUN_LINTING_CI) lint + +lint_fix: + $(RUN_LINTING) lint:fix + +typecheck: + $(RUN_LINTING) types:check + +typecheck_ci: + $(RUN_LINTING_CI) types:check + +test: format lint typecheck shellcheck test_unit test_acceptance + +test_unit: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) run --rm test_unit + $(MAKE) test_unit_clean +endif + +test_clean: test_unit_clean +test_unit_clean: +ifneq (,$(wildcard test/unit)) + $(DOCKER_COMPOSE_TEST_UNIT) down -v -t 0 +endif + +test_acceptance: test_acceptance_clean test_acceptance_pre_run test_acceptance_run + $(MAKE) test_acceptance_clean + +test_acceptance_debug: test_acceptance_clean test_acceptance_pre_run test_acceptance_run_debug + $(MAKE) test_acceptance_clean + +test_acceptance_run: +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance +endif + +test_acceptance_run_debug: +ifneq (,$(wildcard test/acceptance)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run -p 127.0.0.9:19999:19999 --rm test_acceptance npm run test:acceptance -- --inspect=0.0.0.0:19999 --inspect-brk +endif + +test_clean: test_acceptance_clean +test_acceptance_clean: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) down -v -t 0 + +test_acceptance_pre_run: +ifneq (,$(wildcard test/acceptance/js/scripts/pre-run)) + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance test/acceptance/js/scripts/pre-run +endif + +benchmarks: + $(DOCKER_COMPOSE_TEST_ACCEPTANCE) run --rm test_acceptance npm run benchmarks + +build: + docker build \ + --pull \ + --build-arg BUILDKIT_INLINE_CACHE=1 \ + --tag ci/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) \ + --tag us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME) \ + --cache-from us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):$(BRANCH_NAME) \ + --cache-from us-east1-docker.pkg.dev/overleaf-ops/ol-docker/$(PROJECT_NAME):main \ + --file Dockerfile \ + ../.. + +tar: + $(DOCKER_COMPOSE) up tar + +publish: + + docker push $(DOCKER_REPO)/$(PROJECT_NAME):$(BRANCH_NAME)-$(BUILD_NUMBER) + + +.PHONY: clean \ + format format_fix \ + lint lint_fix \ + build_types typecheck \ + lint_ci format_ci typecheck_ci \ + shellcheck shellcheck_fix \ + test test_clean test_unit test_unit_clean \ + test_acceptance test_acceptance_debug test_acceptance_pre_run \ + test_acceptance_run test_acceptance_run_debug test_acceptance_clean \ + benchmarks \ + build tar publish \ diff --git a/services/references/README.md b/services/references/README.md new file mode 100644 index 0000000000..41844d259a --- /dev/null +++ b/services/references/README.md @@ -0,0 +1,10 @@ +overleaf/references +=============== + +An API for providing citation-keys from user bib-files + +License +======= +The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. + +Based on https://github.com/overleaf/overleaf/commit/9964aebc794f9fd7ce1373ab3484f6b33b061af3 diff --git a/services/references/app.js b/services/references/app.js new file mode 100644 index 0000000000..a7da8720ed --- /dev/null +++ b/services/references/app.js @@ -0,0 +1,40 @@ +import '@overleaf/metrics/initialize.js' + +import express from 'express' +import Settings from '@overleaf/settings' +import logger from '@overleaf/logger' +import metrics from '@overleaf/metrics' +import ReferencesAPIController from './app/js/ReferencesAPIController.js' +import bodyParser from 'body-parser' + +const app = express() +metrics.injectMetricsRoute(app) + +app.use(bodyParser.json({ limit: '2mb' })) +app.use(metrics.http.monitor(logger)) + +app.post('/project/:project_id/index', ReferencesAPIController.index) +app.get('/status', (req, res) => res.send({ status: 'references api is up' })) + +const settings = + Settings.internal && Settings.internal.references + ? Settings.internal.references + : undefined +const host = settings && settings.host ? settings.host : 'localhost' +const port = settings && settings.port ? settings.port : 3056 + +logger.debug('Listening at', { host, port }) + +const server = app.listen(port, host, function (error) { + if (error) { + throw error + } + logger.info({ host, port }, 'references HTTP server starting up') +}) + +process.on('SIGTERM', () => { + server.close(() => { + logger.info({ host, port }, 'references HTTP server closed') + metrics.close() + }) +}) diff --git a/services/references/app/js/ReferencesAPIController.js b/services/references/app/js/ReferencesAPIController.js new file mode 100644 index 0000000000..ac51ca6bbd --- /dev/null +++ b/services/references/app/js/ReferencesAPIController.js @@ -0,0 +1,42 @@ +import logger from '@overleaf/logger' +import BibtexParser from './bib2json.js' + +export default { + async index(req, res) { + const { docUrls, fullIndex } = req.body + try { + const responses = await Promise.all( + docUrls.map(async (docUrl) => { + try { + const response = await fetch(docUrl) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return response.text() + } catch (error) { + logger.error({ error }, "Failed to fetch document from URL: " + docUrl) + return null + } + }) + ) + const keys = [] + for (const body of responses) { + if (!body) continue + + try { + const parsedEntries = BibtexParser(body).entries + const ks = parsedEntries + .filter(entry => entry.EntryKey) + .map(entry => entry.EntryKey) + keys.push(...ks) + } catch (error) { + logger.error({ error }, "bib file skipped.") + } + } + res.status(200).json({ keys }) + } catch (error) { + logger.error({ error }, "Unexpected error during indexing process.") + res.status(500).json({ error: "Failed to process bib files." }) + } + } +} diff --git a/services/references/app/js/bib2json.js b/services/references/app/js/bib2json.js new file mode 100644 index 0000000000..99cfcf70ee --- /dev/null +++ b/services/references/app/js/bib2json.js @@ -0,0 +1,1967 @@ +/* eslint-disable */ +/** + * Parser.js + * Copyright 2012-13 Mayank Lahiri + * mlahiri@gmail.com + * Released under the BSD License. + * + * Modifications 2016 Sharelatex + * Modifications 2017-2020 Overleaf + * + * A forgiving Bibtex parser that can: + * + * (1) operate in streaming or block mode, extracting entries as dictionaries. + * (2) convert Latex special characters to UTF-8. + * (3) best-effort parse malformed entries. + * (4) run in a CommonJS environment or a browser, without any dependencies. + * (5) be advanced-compiled by Google Closure Compiler. + * + * Handwritten as a labor of love, not auto-generated from a grammar. + * + * Modes of usage: + * + * (1) Synchronous, string + * + * var entries = BibtexParser(text); + * console.log(entries); + * + * (2) Asynchronous, stream + * + * function entryCallback(entry) { console.log(entry); } + * var parser = new BibtexParser(entryCallback); + * parser.parse(chunk1); + * parser.parse(chunk2); + * ... + * + * @param {text|function(Object)} arg0 Either a Bibtex string or callback + * function for processing parsed entries. + * @param {array} allowedKeys optimization: do not output key/value pairs that are not on this allowlist + * @constructor + */ +function BibtexParser(arg0, allowedKeys) { + // Determine how this function is to be used + if (typeof arg0 === 'string') { + // Passed a string, synchronous call without 'new' + const entries = [] + function accumulator(entry) { + entries.push(entry) + } + const parser = new BibtexParser(accumulator, allowedKeys) + parser.parse(arg0) + return { + entries, + errors: parser.getErrors(), + } + } + if (typeof arg0 !== 'function') { + throw 'Invalid parser construction.' + } + this.ALLOWEDKEYS_ = allowedKeys || [] + this.reset_(arg0) + this.initMacros_() + return this +} + +/** @enum {number} */ +BibtexParser.prototype.STATES_ = { + ENTRY_OR_JUNK: 0, + OBJECT_TYPE: 1, + ENTRY_KEY: 2, + KV_KEY: 3, + EQUALS: 4, + KV_VALUE: 5, +} +BibtexParser.prototype.reset_ = function (arg0) { + /** @private */ this.DATA_ = {} + /** @private */ this.CALLBACK_ = arg0 + /** @private */ this.CHAR_ = 0 + /** @private */ this.LINE_ = 1 + /** @private */ this.CHAR_IN_LINE_ = 0 + /** @private */ this.SKIPWS_ = true + /** @private */ this.SKIPCOMMENT_ = true + /** @private */ this.SKIPKVPAIR_ = false + /** @private */ this.PARSETMP_ = {} + /** @private */ this.SKIPTILLEOL_ = false + /** @private */ this.VALBRACES_ = null + /** @private */ this.BRACETYPE_ = null + /** @private */ this.BRACECOUNT_ = 0 + /** @private */ this.STATE_ = this.STATES_.ENTRY_OR_JUNK + /** @private */ this.ERRORS_ = [] +} +/** @private */ BibtexParser.prototype.ENTRY_TYPES_ = { + inproceedings: 1, + proceedings: 2, + article: 3, + techreport: 4, + misc: 5, + mastersthesis: 6, + book: 7, + phdthesis: 8, + incollection: 9, + unpublished: 10, + inbook: 11, + manual: 12, + periodical: 13, + booklet: 14, + masterthesis: 15, + conference: 16, + /* additional fields from biblatex */ + artwork: 17, + audio: 18, + bibnote: 19, + bookinbook: 20, + collection: 21, + commentary: 22, + customa: 23, + customb: 24, + customc: 25, + customd: 26, + custome: 27, + customf: 28, + image: 29, + inreference: 30, + jurisdiction: 31, + legal: 32, + legislation: 33, + letter: 34, + movie: 35, + music: 36, + mvbook: 37, + mvcollection: 38, + mvproceedings: 39, + mvreference: 40, + online: 41, + patent: 42, + performance: 43, + reference: 44, + report: 45, + review: 46, + set: 47, + software: 48, + standard: 49, + suppbook: 50, + suppcollection: 51, + thesis: 52, + video: 53, +} +BibtexParser.prototype.initMacros_ = function () { + // macros can be extended by the user via + // @string { macroName = "macroValue" } + /** @private */ this.MACROS_ = { + jan: 'January', + feb: 'February', + mar: 'March', + apr: 'April', + may: 'May', + jun: 'June', + jul: 'July', + aug: 'August', + sep: 'September', + oct: 'October', + nov: 'November', + dec: 'December', + Jan: 'January', + Feb: 'February', + Mar: 'March', + Apr: 'April', + May: 'May', + Jun: 'June', + Jul: 'July', + Aug: 'August', + Sep: 'September', + Oct: 'October', + Nov: 'November', + Dec: 'December', + } +} + +/** + * Gets an array of all errors encountered during parsing. + * Array entries are of the format: + * [ line number, character in line, character in stream, error text ] + * + * @returns Array + * @public + */ +BibtexParser.prototype.getErrors = function () { + return this.ERRORS_ +} + +/** + * Processes a chunk of data + * @public + */ +BibtexParser.prototype.parse = function (chunk) { + for (let i = 0; i < chunk.length; i++) this.processCharacter_(chunk[i]) +} + +/** + * Logs error at current stream position. + * + * @private + */ +BibtexParser.prototype.error_ = function (text) { + this.ERRORS_.push([this.LINE_, this.CHAR_IN_LINE_, this.CHAR_, text]) +} + +/** + * Called after an entire entry has been parsed from the stream. + * Performs post-processing and invokes the entry callback pointed to by + * this.CALLBACK_. Parsed (but unprocessed) entry data is in this.DATA_. + */ +BibtexParser.prototype.processEntry_ = function () { + const data = this.DATA_ + if (data.Fields) + for (const f in data.Fields) { + let raw = data.Fields[f] + + // Convert Latex/Bibtex special characters to UTF-8 equivalents + for (let i = 0; i < this.CHARCONV_.length; i++) { + const re = this.CHARCONV_[i][0] + const rep = this.CHARCONV_[i][1] + raw = raw.replace(re, rep) + } + + // Basic substitutions + raw = raw + .replace(/[\n\r\t]/g, ' ') + .replace(/\s\s+/g, ' ') + .replace(/^\s+|\s+$/g, '') + + // Remove braces and backslashes + const len = raw.length + let processedArr = [] + for (let i = 0; i < len; i++) { + let c = raw[i] + let skip = false + if (c == '\\' && i < len - 1) c = raw[++i] + else { + if (c == '{' || c == '}') skip = true + } + if (!skip) processedArr.push(c) + } + data.Fields[f] = processedArr.join('') + processedArr = null + } + + if (data.ObjectType == 'string') { + for (const f in data.Fields) { + this.MACROS_[f] = data.Fields[f] + } + } else { + // Parsed a new Bibtex entry + this.CALLBACK_(data) + } +} + +/** + * Processes next character in the stream, invoking the callback after + * each entry has been found and processed. + * + * @private + * @param {string} c Next character in input stream + */ +BibtexParser.prototype.processCharacter_ = function (c) { + // Housekeeping + this.CHAR_++ + this.CHAR_IN_LINE_++ + if (c == '\n') { + this.LINE_++ + this.CHAR_IN_LINE_ = 1 + } + + // Convenience states for skipping whitespace when needed + if (this.SKIPTILLEOL_) { + if (c == '\n') this.SKIPTILLEOL_ = false + return + } + if (this.SKIPCOMMENT_ && c == '%') { + this.SKIPTILLEOL_ = true + return + } + if (this.SKIPWS_ && /\s/.test(c)) return + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + this.SKIPTILLEOL_ = false + + // Main state machine + let AnotherIteration = true + while (AnotherIteration) { + // console.log(this.LINE_, this.CHAR_IN_LINE_, this.STATE_, c) + AnotherIteration = false + switch (this.STATE_) { + // -- Scan for an object marker ('@') + // -- Reset temporary data structure in case previous entry was garbled + case this.STATES_.ENTRY_OR_JUNK: + if (c == '@') { + // SUCCESS: Parsed a valid start-of-object marker. + // NEXT_STATE: OBJECT_TYPE + this.STATE_ = this.STATES_.OBJECT_TYPE + this.DATA_ = { + ObjectType: '', + } + } + this.BRACETYPE_ = null + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + break + + // Start at first non-whitespace character after start-of-object '@' + // -- Accept [A-Za-z], break on non-matching character + // -- Populate this.DATA_.EntryType and this.DATA_.ObjectType + case this.STATES_.OBJECT_TYPE: + if (/[A-Za-z]/.test(c)) { + this.DATA_.ObjectType += c.toLowerCase() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + // Break from state and validate object type + const ot = this.DATA_.ObjectType + if (ot == 'comment') { + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + if (ot == 'string') { + this.DATA_.ObjectType = ot + this.DATA_.Fields = {} + this.BRACETYPE_ = c + this.BRACECOUNT_ = 1 + this.STATE_ = this.STATES_.KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.PARSETMP_ = { + Key: '', + } + } else { + if (ot == 'preamble') { + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + if (ot in this.ENTRY_TYPES_) { + // SUCCESS: Parsed a valid object type. + // NEXT_STATE: ENTRY_KEY + this.DATA_.ObjectType = 'entry' + this.DATA_.EntryType = ot + this.DATA_.EntryKey = '' + this.STATE_ = this.STATES_.ENTRY_KEY + AnotherIteration = true + } else { + // ERROR: Unrecognized object type. + // NEXT_STATE: ENTRY_OR_JUNK + this.error_( + 'Unrecognized object type: "' + this.DATA_.ObjectType + '"' + ) + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } + } + } + } + } + break + + // Start at first non-alphabetic character after an entry type + // -- Populate this.DATA_.EntryKey + case this.STATES_.ENTRY_KEY: + if ((c === '{' || c === '(') && this.BRACETYPE_ == null) { + this.BRACETYPE_ = c + this.BRACECOUNT_ = 1 + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + break + } + if (/[,%\s]/.test(c)) { + if (this.DATA_.EntryKey.length < 1) { + // Skip comments and whitespace before entry key + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + if (this.BRACETYPE_ == null) { + // ERROR: No opening brace for object + // NEXT_STATE: ENTRY_OR_JUNK + this.error_('No opening brace for object.') + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + } else { + // SUCCESS: Parsed an entry key + // NEXT_STATE: KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + AnotherIteration = true + this.STATE_ = this.STATES_.KV_KEY + this.PARSETMP_.Key = '' + this.DATA_.Fields = {} + } + } + } else { + this.DATA_.EntryKey += c + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } + break + + // Start at first non-whitespace/comment character after entry key. + // -- Populate this.PARSETMP_.Key + case this.STATES_.KV_KEY: + // Test for end of entry + if ( + (c == '}' && this.BRACETYPE_ == '{') || + (c == ')' && this.BRACETYPE_ == '(') + ) { + // SUCCESS: Parsed an entry, possible incomplete + // NEXT_STATE: ENTRY_OR_JUNK + this.processEntry_() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + break + } + if (/[\-A-Za-z:]/.test(c)) { + // Add to key + this.PARSETMP_.Key += c + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } else { + // Either end of key or we haven't encountered start of key + if (this.PARSETMP_.Key.length < 1) { + // Keep going till we see a key + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } else { + // SUCCESS: Found full key in K/V pair + // NEXT_STATE: EQUALS + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.EQUALS + AnotherIteration = true + + if (this.DATA_.ObjectType !== 'string') { + // this entry is not a macro + // normalize the key to lower case + this.PARSETMP_.Key = this.PARSETMP_.Key.toLowerCase() + + // optimization: skip key/value pairs that are not on the allowlist + this.SKIPKVPAIR_ = + // has allowedKeys set + this.ALLOWEDKEYS_.length && + // key is not on the allowlist + this.ALLOWEDKEYS_.indexOf(this.PARSETMP_.Key) === -1 + } else { + this.SKIPKVPAIR_ = false + } + } + } + break + + // Start at first non-alphabetic character after K/V pair key. + case this.STATES_.EQUALS: + if ( + (c == '}' && this.BRACETYPE_ == '{') || + (c == ')' && this.BRACETYPE_ == '(') + ) { + // ERROR: K/V pair with key but no value + // NEXT_STATE: ENTRY_OR_JUNK + this.error_( + 'Key-value pair has key "' + this.PARSETMP_.Key + '", but no value.' + ) + this.processEntry_() + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + break + } + if (c == '=') { + // SUCCESS: found an equal signs separating key and value + // NEXT_STATE: KV_VALUE + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.KV_VALUE + this.PARSETMP_.Value = [] + this.VALBRACES_ = { '"': [], '{': [] } + } + break + + // Start at first non-whitespace/comment character after '=' + // -- Populate this.PARSETMP_.Value + case this.STATES_.KV_VALUE: + const delim = this.VALBRACES_ + // valueCharsArray is the list of characters that make up the + // current value + const valueCharsArray = this.PARSETMP_.Value + let doneParsingValue = false + + // Test for special characters + if (c == '"' || c == '{' || c == '}' || c == ',') { + if (c == ',') { + // This comma can mean: + // (1) just another comma literal + // (2) end of a macro reference + if (delim['"'].length + delim['{'].length === 0) { + // end of a macro reference + const macro = this.PARSETMP_.Value.join('').trim() + if (macro in this.MACROS_) { + // Successful macro reference + this.PARSETMP_.Value = [this.MACROS_[macro]] + } else { + // Reference to an undefined macro + this.error_('Reference to an undefined macro: ' + macro) + } + doneParsingValue = true + } + } + if (c == '"') { + // This quote can mean: + // (1) opening delimiter + // (2) closing delimiter + // (3) literal, if we have a '{' on the stack + if (delim['"'].length + delim['{'].length === 0) { + // opening delimiter + delim['"'].push(this.CHAR_) + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + break + } + if ( + delim['"'].length == 1 && + delim['{'].length == 0 && + (valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\') + ) { + // closing delimiter + doneParsingValue = true + } else { + // literal, add to value + } + } + if (c == '{') { + // This brace can mean: + // (1) opening delimiter + // (2) stacked verbatim delimiter + if ( + valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\' + ) { + delim['{'].push(this.CHAR_) + this.SKIPWS_ = false + this.SKIPCOMMENT_ = false + } else { + // literal, add to value + } + } + if (c == '}') { + // This brace can mean: + // (1) closing delimiter + // (2) closing stacked verbatim delimiter + // (3) end of object definition if value was a macro + if (delim['"'].length + delim['{'].length === 0) { + // end of object definition, after macro + const macro = this.PARSETMP_.Value.join('').trim() + if (macro in this.MACROS_) { + // Successful macro reference + this.PARSETMP_.Value = [this.MACROS_[macro]] + } else { + // Reference to an undefined macro + this.error_('Reference to an undefined macro: ' + macro) + } + AnotherIteration = true + doneParsingValue = true + } else { + // sometimes imported bibs will have {\},{\\}, {\\\}, {\\\\}, etc for whitespace, + // which would otherwise break the parsing. we watch for these occurences of + // 1+ backslashes in an empty bracket pair to gracefully handle the malformed bib file + const doubleSlash = + valueCharsArray.length >= 2 && + valueCharsArray[valueCharsArray.length - 1] === '\\' && // for \\} + valueCharsArray[valueCharsArray.length - 2] === '\\' + const singleSlash = + valueCharsArray.length >= 2 && + valueCharsArray[valueCharsArray.length - 1] === '\\' && // for {\} + valueCharsArray[valueCharsArray.length - 2] === '{' + + if ( + valueCharsArray.length == 0 || + valueCharsArray[valueCharsArray.length - 1] != '\\' || // for } + doubleSlash || + singleSlash + ) { + if (delim['{'].length > 0) { + // pop stack for stacked verbatim delimiter + delim['{'].splice(delim['{'].length - 1, 1) + if (delim['{'].length + delim['"'].length == 0) { + // closing delimiter + doneParsingValue = true + } else { + // end verbatim block + } + } + } else { + // literal, add to value + } + } + } + } + + // If here, then we are either done parsing the value or + // have a literal that should be added to the value. + if (doneParsingValue) { + // SUCCESS: value parsed + // NEXT_STATE: KV_KEY + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + this.STATE_ = this.STATES_.KV_KEY + if (!this.SKIPKVPAIR_) { + this.DATA_.Fields[this.PARSETMP_.Key] = + this.PARSETMP_.Value.join('') + } + this.PARSETMP_ = { Key: '' } + this.VALBRACES_ = null + } else { + this.PARSETMP_.Value.push(c) + if (this.PARSETMP_.Value.length >= 1000 * 20) { + this.PARSETMP_.Value = [] + this.STATE_ = this.STATES_.ENTRY_OR_JUNK + this.DATA_ = { ObjectType: '' } + this.BRACETYPE_ = null + this.SKIPWS_ = true + this.SKIPCOMMENT_ = true + } + } + break + } // end switch (this.STATE_) + } // end while(AnotherIteration) +} // end function processCharacter + +/** @private */ BibtexParser.prototype.CHARCONV_ = [ + [/\\space /g, '\u0020'], + [/\\textdollar /g, '\u0024'], + [/\\textquotesingle /g, '\u0027'], + [/\\ast /g, '\u002A'], + [/\\textbackslash /g, '\u005C'], + [/\\\^\{\}/g, '\u005E'], + [/\\textasciigrave /g, '\u0060'], + [/\\lbrace /g, '\u007B'], + [/\\vert /g, '\u007C'], + [/\\rbrace /g, '\u007D'], + [/\\textasciitilde /g, '\u007E'], + [/\\textexclamdown /g, '\u00A1'], + [/\\textcent /g, '\u00A2'], + [/\\textsterling /g, '\u00A3'], + [/\\textcurrency /g, '\u00A4'], + [/\\textyen /g, '\u00A5'], + [/\\textbrokenbar /g, '\u00A6'], + [/\\textsection /g, '\u00A7'], + [/\\textasciidieresis /g, '\u00A8'], + [/\\textcopyright /g, '\u00A9'], + [/\\textordfeminine /g, '\u00AA'], + [/\\guillemotleft /g, '\u00AB'], + [/\\lnot /g, '\u00AC'], + [/\\textregistered /g, '\u00AE'], + [/\\textasciimacron /g, '\u00AF'], + [/\\textdegree /g, '\u00B0'], + [/\\pm /g, '\u00B1'], + [/\\textasciiacute /g, '\u00B4'], + [/\\mathrm\{\\mu\}/g, '\u00B5'], + [/\\textparagraph /g, '\u00B6'], + [/\\cdot /g, '\u00B7'], + [/\\c\{\}/g, '\u00B8'], + [/\\textordmasculine /g, '\u00BA'], + [/\\guillemotright /g, '\u00BB'], + [/\\textonequarter /g, '\u00BC'], + [/\\textonehalf /g, '\u00BD'], + [/\\textthreequarters /g, '\u00BE'], + [/\\textquestiondown /g, '\u00BF'], + [/\\`\{A\}/g, '\u00C0'], + [/\\'\{A\}/g, '\u00C1'], + [/\\\^\{A\}/g, '\u00C2'], + [/\\~\{A\}/g, '\u00C3'], + [/\\"\{A\}/g, '\u00C4'], + [/\\AA /g, '\u00C5'], + [/\\AE /g, '\u00C6'], + [/\\c\{C\}/g, '\u00C7'], + [/\\`\{E\}/g, '\u00C8'], + [/\\'\{E\}/g, '\u00C9'], + [/\\\^\{E\}/g, '\u00CA'], + [/\\"\{E\}/g, '\u00CB'], + [/\\`\{I\}/g, '\u00CC'], + [/\\'\{I\}/g, '\u00CD'], + [/\\\^\{I\}/g, '\u00CE'], + [/\\"\{I\}/g, '\u00CF'], + [/\\DH /g, '\u00D0'], + [/\\~\{N\}/g, '\u00D1'], + [/\\`\{O\}/g, '\u00D2'], + [/\\'\{O\}/g, '\u00D3'], + [/\\\^\{O\}/g, '\u00D4'], + [/\\~\{O\}/g, '\u00D5'], + [/\\"\{O\}/g, '\u00D6'], + [/\\texttimes /g, '\u00D7'], + [/\\O /g, '\u00D8'], + [/\\`\{U\}/g, '\u00D9'], + [/\\'\{U\}/g, '\u00DA'], + [/\\\^\{U\}/g, '\u00DB'], + [/\\"\{U\}/g, '\u00DC'], + [/\\'\{Y\}/g, '\u00DD'], + [/\\TH /g, '\u00DE'], + [/\\ss /g, '\u00DF'], + [/\\`\{a\}/g, '\u00E0'], + [/\\'\{a\}/g, '\u00E1'], + [/\\\^\{a\}/g, '\u00E2'], + [/\\~\{a\}/g, '\u00E3'], + [/\\"\{a\}/g, '\u00E4'], + [/\\aa /g, '\u00E5'], + [/\\ae /g, '\u00E6'], + [/\\c\{c\}/g, '\u00E7'], + [/\\`\{e\}/g, '\u00E8'], + [/\\'\{e\}/g, '\u00E9'], + [/\\\^\{e\}/g, '\u00EA'], + [/\\"\{e\}/g, '\u00EB'], + [/\\`\{\\i\}/g, '\u00EC'], + [/\\'\{\\i\}/g, '\u00ED'], + [/\\\^\{\\i\}/g, '\u00EE'], + [/\\"\{\\i\}/g, '\u00EF'], + [/\\dh /g, '\u00F0'], + [/\\~\{n\}/g, '\u00F1'], + [/\\`\{o\}/g, '\u00F2'], + [/\\'\{o\}/g, '\u00F3'], + [/\\\^\{o\}/g, '\u00F4'], + [/\\~\{o\}/g, '\u00F5'], + [/\\"\{o\}/g, '\u00F6'], + [/\\div /g, '\u00F7'], + [/\\o /g, '\u00F8'], + [/\\`\{u\}/g, '\u00F9'], + [/\\'\{u\}/g, '\u00FA'], + [/\\\^\{u\}/g, '\u00FB'], + [/\\"\{u\}/g, '\u00FC'], + [/\\'\{y\}/g, '\u00FD'], + [/\\th /g, '\u00FE'], + [/\\"\{y\}/g, '\u00FF'], + [/\\=\{A\}/g, '\u0100'], + [/\\=\{a\}/g, '\u0101'], + [/\\u\{A\}/g, '\u0102'], + [/\\u\{a\}/g, '\u0103'], + [/\\k\{A\}/g, '\u0104'], + [/\\k\{a\}/g, '\u0105'], + [/\\'\{C\}/g, '\u0106'], + [/\\'\{c\}/g, '\u0107'], + [/\\\^\{C\}/g, '\u0108'], + [/\\\^\{c\}/g, '\u0109'], + [/\\.\{C\}/g, '\u010A'], + [/\\.\{c\}/g, '\u010B'], + [/\\v\{C\}/g, '\u010C'], + [/\\v\{c\}/g, '\u010D'], + [/\\v\{D\}/g, '\u010E'], + [/\\v\{d\}/g, '\u010F'], + [/\\DJ /g, '\u0110'], + [/\\dj /g, '\u0111'], + [/\\=\{E\}/g, '\u0112'], + [/\\=\{e\}/g, '\u0113'], + [/\\u\{E\}/g, '\u0114'], + [/\\u\{e\}/g, '\u0115'], + [/\\.\{E\}/g, '\u0116'], + [/\\.\{e\}/g, '\u0117'], + [/\\k\{E\}/g, '\u0118'], + [/\\k\{e\}/g, '\u0119'], + [/\\v\{E\}/g, '\u011A'], + [/\\v\{e\}/g, '\u011B'], + [/\\\^\{G\}/g, '\u011C'], + [/\\\^\{g\}/g, '\u011D'], + [/\\u\{G\}/g, '\u011E'], + [/\\u\{g\}/g, '\u011F'], + [/\\.\{G\}/g, '\u0120'], + [/\\.\{g\}/g, '\u0121'], + [/\\c\{G\}/g, '\u0122'], + [/\\c\{g\}/g, '\u0123'], + [/\\\^\{H\}/g, '\u0124'], + [/\\\^\{h\}/g, '\u0125'], + [/\\Elzxh /g, '\u0127'], + [/\\~\{I\}/g, '\u0128'], + [/\\~\{\\i\}/g, '\u0129'], + [/\\=\{I\}/g, '\u012A'], + [/\\=\{\\i\}/g, '\u012B'], + [/\\u\{I\}/g, '\u012C'], + [/\\u\{\\i\}/g, '\u012D'], + [/\\k\{I\}/g, '\u012E'], + [/\\k\{i\}/g, '\u012F'], + [/\\.\{I\}/g, '\u0130'], + [/\\i /g, '\u0131'], + [/\\\^\{J\}/g, '\u0134'], + [/\\\^\{\\j\}/g, '\u0135'], + [/\\c\{K\}/g, '\u0136'], + [/\\c\{k\}/g, '\u0137'], + [/\\'\{L\}/g, '\u0139'], + [/\\'\{l\}/g, '\u013A'], + [/\\c\{L\}/g, '\u013B'], + [/\\c\{l\}/g, '\u013C'], + [/\\v\{L\}/g, '\u013D'], + [/\\v\{l\}/g, '\u013E'], + [/\\L /g, '\u0141'], + [/\\l /g, '\u0142'], + [/\\'\{N\}/g, '\u0143'], + [/\\'\{n\}/g, '\u0144'], + [/\\c\{N\}/g, '\u0145'], + [/\\c\{n\}/g, '\u0146'], + [/\\v\{N\}/g, '\u0147'], + [/\\v\{n\}/g, '\u0148'], + [/\\NG /g, '\u014A'], + [/\\ng /g, '\u014B'], + [/\\=\{O\}/g, '\u014C'], + [/\\=\{o\}/g, '\u014D'], + [/\\u\{O\}/g, '\u014E'], + [/\\u\{o\}/g, '\u014F'], + [/\\H\{O\}/g, '\u0150'], + [/\\H\{o\}/g, '\u0151'], + [/\\OE /g, '\u0152'], + [/\\oe /g, '\u0153'], + [/\\'\{R\}/g, '\u0154'], + [/\\'\{r\}/g, '\u0155'], + [/\\c\{R\}/g, '\u0156'], + [/\\c\{r\}/g, '\u0157'], + [/\\v\{R\}/g, '\u0158'], + [/\\v\{r\}/g, '\u0159'], + [/\\'\{S\}/g, '\u015A'], + [/\\'\{s\}/g, '\u015B'], + [/\\\^\{S\}/g, '\u015C'], + [/\\\^\{s\}/g, '\u015D'], + [/\\c\{S\}/g, '\u015E'], + [/\\c\{s\}/g, '\u015F'], + [/\\v\{S\}/g, '\u0160'], + [/\\v\{s\}/g, '\u0161'], + [/\\c\{T\}/g, '\u0162'], + [/\\c\{t\}/g, '\u0163'], + [/\\v\{T\}/g, '\u0164'], + [/\\v\{t\}/g, '\u0165'], + [/\\~\{U\}/g, '\u0168'], + [/\\~\{u\}/g, '\u0169'], + [/\\=\{U\}/g, '\u016A'], + [/\\=\{u\}/g, '\u016B'], + [/\\u\{U\}/g, '\u016C'], + [/\\u\{u\}/g, '\u016D'], + [/\\r\{U\}/g, '\u016E'], + [/\\r\{u\}/g, '\u016F'], + [/\\H\{U\}/g, '\u0170'], + [/\\H\{u\}/g, '\u0171'], + [/\\k\{U\}/g, '\u0172'], + [/\\k\{u\}/g, '\u0173'], + [/\\\^\{W\}/g, '\u0174'], + [/\\\^\{w\}/g, '\u0175'], + [/\\\^\{Y\}/g, '\u0176'], + [/\\\^\{y\}/g, '\u0177'], + [/\\"\{Y\}/g, '\u0178'], + [/\\'\{Z\}/g, '\u0179'], + [/\\'\{z\}/g, '\u017A'], + [/\\.\{Z\}/g, '\u017B'], + [/\\.\{z\}/g, '\u017C'], + [/\\v\{Z\}/g, '\u017D'], + [/\\v\{z\}/g, '\u017E'], + [/\\texthvlig /g, '\u0195'], + [/\\textnrleg /g, '\u019E'], + [/\\eth /g, '\u01AA'], + [/\\textdoublepipe /g, '\u01C2'], + [/\\'\{g\}/g, '\u01F5'], + [/\\Elztrna /g, '\u0250'], + [/\\Elztrnsa /g, '\u0252'], + [/\\Elzopeno /g, '\u0254'], + [/\\Elzrtld /g, '\u0256'], + [/\\Elzschwa /g, '\u0259'], + [/\\varepsilon /g, '\u025B'], + [/\\Elzpgamma /g, '\u0263'], + [/\\Elzpbgam /g, '\u0264'], + [/\\Elztrnh /g, '\u0265'], + [/\\Elzbtdl /g, '\u026C'], + [/\\Elzrtll /g, '\u026D'], + [/\\Elztrnm /g, '\u026F'], + [/\\Elztrnmlr /g, '\u0270'], + [/\\Elzltlmr /g, '\u0271'], + [/\\Elzltln /g, '\u0272'], + [/\\Elzrtln /g, '\u0273'], + [/\\Elzclomeg /g, '\u0277'], + [/\\textphi /g, '\u0278'], + [/\\Elztrnr /g, '\u0279'], + [/\\Elztrnrl /g, '\u027A'], + [/\\Elzrttrnr /g, '\u027B'], + [/\\Elzrl /g, '\u027C'], + [/\\Elzrtlr /g, '\u027D'], + [/\\Elzfhr /g, '\u027E'], + [/\\Elzrtls /g, '\u0282'], + [/\\Elzesh /g, '\u0283'], + [/\\Elztrnt /g, '\u0287'], + [/\\Elzrtlt /g, '\u0288'], + [/\\Elzpupsil /g, '\u028A'], + [/\\Elzpscrv /g, '\u028B'], + [/\\Elzinvv /g, '\u028C'], + [/\\Elzinvw /g, '\u028D'], + [/\\Elztrny /g, '\u028E'], + [/\\Elzrtlz /g, '\u0290'], + [/\\Elzyogh /g, '\u0292'], + [/\\Elzglst /g, '\u0294'], + [/\\Elzreglst /g, '\u0295'], + [/\\Elzinglst /g, '\u0296'], + [/\\textturnk /g, '\u029E'], + [/\\Elzdyogh /g, '\u02A4'], + [/\\Elztesh /g, '\u02A7'], + [/\\textasciicaron /g, '\u02C7'], + [/\\Elzverts /g, '\u02C8'], + [/\\Elzverti /g, '\u02CC'], + [/\\Elzlmrk /g, '\u02D0'], + [/\\Elzhlmrk /g, '\u02D1'], + [/\\Elzsbrhr /g, '\u02D2'], + [/\\Elzsblhr /g, '\u02D3'], + [/\\Elzrais /g, '\u02D4'], + [/\\Elzlow /g, '\u02D5'], + [/\\textasciibreve /g, '\u02D8'], + [/\\textperiodcentered /g, '\u02D9'], + [/\\r\{\}/g, '\u02DA'], + [/\\k\{\}/g, '\u02DB'], + [/\\texttildelow /g, '\u02DC'], + [/\\H\{\}/g, '\u02DD'], + [/\\tone\{55\}/g, '\u02E5'], + [/\\tone\{44\}/g, '\u02E6'], + [/\\tone\{33\}/g, '\u02E7'], + [/\\tone\{22\}/g, '\u02E8'], + [/\\tone\{11\}/g, '\u02E9'], + [/\\cyrchar\\C/g, '\u030F'], + [/\\Elzpalh /g, '\u0321'], + [/\\Elzrh /g, '\u0322'], + [/\\Elzsbbrg /g, '\u032A'], + [/\\Elzxl /g, '\u0335'], + [/\\Elzbar /g, '\u0336'], + [/\\'\{A\}/g, '\u0386'], + [/\\'\{E\}/g, '\u0388'], + [/\\'\{H\}/g, '\u0389'], + [/\\'\{\}\{I\}/g, '\u038A'], + [/\\'\{\}O/g, '\u038C'], + [/\\mathrm\{'Y\}/g, '\u038E'], + [/\\mathrm\{'\\Omega\}/g, '\u038F'], + [/\\acute\{\\ddot\{\\iota\}\}/g, '\u0390'], + [/\\Alpha /g, '\u0391'], + [/\\Beta /g, '\u0392'], + [/\\Gamma /g, '\u0393'], + [/\\Delta /g, '\u0394'], + [/\\Epsilon /g, '\u0395'], + [/\\Zeta /g, '\u0396'], + [/\\Eta /g, '\u0397'], + [/\\Theta /g, '\u0398'], + [/\\Iota /g, '\u0399'], + [/\\Kappa /g, '\u039A'], + [/\\Lambda /g, '\u039B'], + [/\\Xi /g, '\u039E'], + [/\\Pi /g, '\u03A0'], + [/\\Rho /g, '\u03A1'], + [/\\Sigma /g, '\u03A3'], + [/\\Tau /g, '\u03A4'], + [/\\Upsilon /g, '\u03A5'], + [/\\Phi /g, '\u03A6'], + [/\\Chi /g, '\u03A7'], + [/\\Psi /g, '\u03A8'], + [/\\Omega /g, '\u03A9'], + [/\\mathrm\{\\ddot\{I\}\}/g, '\u03AA'], + [/\\mathrm\{\\ddot\{Y\}\}/g, '\u03AB'], + [/\\'\{\$\\alpha\$\}/g, '\u03AC'], + [/\\acute\{\\epsilon\}/g, '\u03AD'], + [/\\acute\{\\eta\}/g, '\u03AE'], + [/\\acute\{\\iota\}/g, '\u03AF'], + [/\\acute\{\\ddot\{\\upsilon\}\}/g, '\u03B0'], + [/\\alpha /g, '\u03B1'], + [/\\beta /g, '\u03B2'], + [/\\gamma /g, '\u03B3'], + [/\\delta /g, '\u03B4'], + [/\\epsilon /g, '\u03B5'], + [/\\zeta /g, '\u03B6'], + [/\\eta /g, '\u03B7'], + [/\\texttheta /g, '\u03B8'], + [/\\iota /g, '\u03B9'], + [/\\kappa /g, '\u03BA'], + [/\\lambda /g, '\u03BB'], + [/\\mu /g, '\u03BC'], + [/\\nu /g, '\u03BD'], + [/\\xi /g, '\u03BE'], + [/\\pi /g, '\u03C0'], + [/\\rho /g, '\u03C1'], + [/\\varsigma /g, '\u03C2'], + [/\\sigma /g, '\u03C3'], + [/\\tau /g, '\u03C4'], + [/\\upsilon /g, '\u03C5'], + [/\\varphi /g, '\u03C6'], + [/\\chi /g, '\u03C7'], + [/\\psi /g, '\u03C8'], + [/\\omega /g, '\u03C9'], + [/\\ddot\{\\iota\}/g, '\u03CA'], + [/\\ddot\{\\upsilon\}/g, '\u03CB'], + [/\\'\{o\}/g, '\u03CC'], + [/\\acute\{\\upsilon\}/g, '\u03CD'], + [/\\acute\{\\omega\}/g, '\u03CE'], + [/\\Pisymbol\{ppi022\}\{87\}/g, '\u03D0'], + [/\\textvartheta /g, '\u03D1'], + [/\\Upsilon /g, '\u03D2'], + [/\\phi /g, '\u03D5'], + [/\\varpi /g, '\u03D6'], + [/\\Stigma /g, '\u03DA'], + [/\\Digamma /g, '\u03DC'], + [/\\digamma /g, '\u03DD'], + [/\\Koppa /g, '\u03DE'], + [/\\Sampi /g, '\u03E0'], + [/\\varkappa /g, '\u03F0'], + [/\\varrho /g, '\u03F1'], + [/\\textTheta /g, '\u03F4'], + [/\\backepsilon /g, '\u03F6'], + [/\\cyrchar\\CYRYO /g, '\u0401'], + [/\\cyrchar\\CYRDJE /g, '\u0402'], + [/\\cyrchar\{\\'\\CYRG\}/g, '\u0403'], + [/\\cyrchar\\CYRIE /g, '\u0404'], + [/\\cyrchar\\CYRDZE /g, '\u0405'], + [/\\cyrchar\\CYRII /g, '\u0406'], + [/\\cyrchar\\CYRYI /g, '\u0407'], + [/\\cyrchar\\CYRJE /g, '\u0408'], + [/\\cyrchar\\CYRLJE /g, '\u0409'], + [/\\cyrchar\\CYRNJE /g, '\u040A'], + [/\\cyrchar\\CYRTSHE /g, '\u040B'], + [/\\cyrchar\{\\'\\CYRK\}/g, '\u040C'], + [/\\cyrchar\\CYRUSHRT /g, '\u040E'], + [/\\cyrchar\\CYRDZHE /g, '\u040F'], + [/\\cyrchar\\CYRA /g, '\u0410'], + [/\\cyrchar\\CYRB /g, '\u0411'], + [/\\cyrchar\\CYRV /g, '\u0412'], + [/\\cyrchar\\CYRG /g, '\u0413'], + [/\\cyrchar\\CYRD /g, '\u0414'], + [/\\cyrchar\\CYRE /g, '\u0415'], + [/\\cyrchar\\CYRZH /g, '\u0416'], + [/\\cyrchar\\CYRZ /g, '\u0417'], + [/\\cyrchar\\CYRI /g, '\u0418'], + [/\\cyrchar\\CYRISHRT /g, '\u0419'], + [/\\cyrchar\\CYRK /g, '\u041A'], + [/\\cyrchar\\CYRL /g, '\u041B'], + [/\\cyrchar\\CYRM /g, '\u041C'], + [/\\cyrchar\\CYRN /g, '\u041D'], + [/\\cyrchar\\CYRO /g, '\u041E'], + [/\\cyrchar\\CYRP /g, '\u041F'], + [/\\cyrchar\\CYRR /g, '\u0420'], + [/\\cyrchar\\CYRS /g, '\u0421'], + [/\\cyrchar\\CYRT /g, '\u0422'], + [/\\cyrchar\\CYRU /g, '\u0423'], + [/\\cyrchar\\CYRF /g, '\u0424'], + [/\\cyrchar\\CYRH /g, '\u0425'], + [/\\cyrchar\\CYRC /g, '\u0426'], + [/\\cyrchar\\CYRCH /g, '\u0427'], + [/\\cyrchar\\CYRSH /g, '\u0428'], + [/\\cyrchar\\CYRSHCH /g, '\u0429'], + [/\\cyrchar\\CYRHRDSN /g, '\u042A'], + [/\\cyrchar\\CYRERY /g, '\u042B'], + [/\\cyrchar\\CYRSFTSN /g, '\u042C'], + [/\\cyrchar\\CYREREV /g, '\u042D'], + [/\\cyrchar\\CYRYU /g, '\u042E'], + [/\\cyrchar\\CYRYA /g, '\u042F'], + [/\\cyrchar\\cyra /g, '\u0430'], + [/\\cyrchar\\cyrb /g, '\u0431'], + [/\\cyrchar\\cyrv /g, '\u0432'], + [/\\cyrchar\\cyrg /g, '\u0433'], + [/\\cyrchar\\cyrd /g, '\u0434'], + [/\\cyrchar\\cyre /g, '\u0435'], + [/\\cyrchar\\cyrzh /g, '\u0436'], + [/\\cyrchar\\cyrz /g, '\u0437'], + [/\\cyrchar\\cyri /g, '\u0438'], + [/\\cyrchar\\cyrishrt /g, '\u0439'], + [/\\cyrchar\\cyrk /g, '\u043A'], + [/\\cyrchar\\cyrl /g, '\u043B'], + [/\\cyrchar\\cyrm /g, '\u043C'], + [/\\cyrchar\\cyrn /g, '\u043D'], + [/\\cyrchar\\cyro /g, '\u043E'], + [/\\cyrchar\\cyrp /g, '\u043F'], + [/\\cyrchar\\cyrr /g, '\u0440'], + [/\\cyrchar\\cyrs /g, '\u0441'], + [/\\cyrchar\\cyrt /g, '\u0442'], + [/\\cyrchar\\cyru /g, '\u0443'], + [/\\cyrchar\\cyrf /g, '\u0444'], + [/\\cyrchar\\cyrh /g, '\u0445'], + [/\\cyrchar\\cyrc /g, '\u0446'], + [/\\cyrchar\\cyrch /g, '\u0447'], + [/\\cyrchar\\cyrsh /g, '\u0448'], + [/\\cyrchar\\cyrshch /g, '\u0449'], + [/\\cyrchar\\cyrhrdsn /g, '\u044A'], + [/\\cyrchar\\cyrery /g, '\u044B'], + [/\\cyrchar\\cyrsftsn /g, '\u044C'], + [/\\cyrchar\\cyrerev /g, '\u044D'], + [/\\cyrchar\\cyryu /g, '\u044E'], + [/\\cyrchar\\cyrya /g, '\u044F'], + [/\\cyrchar\\cyryo /g, '\u0451'], + [/\\cyrchar\\cyrdje /g, '\u0452'], + [/\\cyrchar\{\\'\\cyrg\}/g, '\u0453'], + [/\\cyrchar\\cyrie /g, '\u0454'], + [/\\cyrchar\\cyrdze /g, '\u0455'], + [/\\cyrchar\\cyrii /g, '\u0456'], + [/\\cyrchar\\cyryi /g, '\u0457'], + [/\\cyrchar\\cyrje /g, '\u0458'], + [/\\cyrchar\\cyrlje /g, '\u0459'], + [/\\cyrchar\\cyrnje /g, '\u045A'], + [/\\cyrchar\\cyrtshe /g, '\u045B'], + [/\\cyrchar\{\\'\\cyrk\}/g, '\u045C'], + [/\\cyrchar\\cyrushrt /g, '\u045E'], + [/\\cyrchar\\cyrdzhe /g, '\u045F'], + [/\\cyrchar\\CYROMEGA /g, '\u0460'], + [/\\cyrchar\\cyromega /g, '\u0461'], + [/\\cyrchar\\CYRYAT /g, '\u0462'], + [/\\cyrchar\\CYRIOTE /g, '\u0464'], + [/\\cyrchar\\cyriote /g, '\u0465'], + [/\\cyrchar\\CYRLYUS /g, '\u0466'], + [/\\cyrchar\\cyrlyus /g, '\u0467'], + [/\\cyrchar\\CYRIOTLYUS /g, '\u0468'], + [/\\cyrchar\\cyriotlyus /g, '\u0469'], + [/\\cyrchar\\CYRBYUS /g, '\u046A'], + [/\\cyrchar\\CYRIOTBYUS /g, '\u046C'], + [/\\cyrchar\\cyriotbyus /g, '\u046D'], + [/\\cyrchar\\CYRKSI /g, '\u046E'], + [/\\cyrchar\\cyrksi /g, '\u046F'], + [/\\cyrchar\\CYRPSI /g, '\u0470'], + [/\\cyrchar\\cyrpsi /g, '\u0471'], + [/\\cyrchar\\CYRFITA /g, '\u0472'], + [/\\cyrchar\\CYRIZH /g, '\u0474'], + [/\\cyrchar\\CYRUK /g, '\u0478'], + [/\\cyrchar\\cyruk /g, '\u0479'], + [/\\cyrchar\\CYROMEGARND /g, '\u047A'], + [/\\cyrchar\\cyromegarnd /g, '\u047B'], + [/\\cyrchar\\CYROMEGATITLO /g, '\u047C'], + [/\\cyrchar\\cyromegatitlo /g, '\u047D'], + [/\\cyrchar\\CYROT /g, '\u047E'], + [/\\cyrchar\\cyrot /g, '\u047F'], + [/\\cyrchar\\CYRKOPPA /g, '\u0480'], + [/\\cyrchar\\cyrkoppa /g, '\u0481'], + [/\\cyrchar\\cyrthousands /g, '\u0482'], + [/\\cyrchar\\cyrhundredthousands /g, '\u0488'], + [/\\cyrchar\\cyrmillions /g, '\u0489'], + [/\\cyrchar\\CYRSEMISFTSN /g, '\u048C'], + [/\\cyrchar\\cyrsemisftsn /g, '\u048D'], + [/\\cyrchar\\CYRRTICK /g, '\u048E'], + [/\\cyrchar\\cyrrtick /g, '\u048F'], + [/\\cyrchar\\CYRGUP /g, '\u0490'], + [/\\cyrchar\\cyrgup /g, '\u0491'], + [/\\cyrchar\\CYRGHCRS /g, '\u0492'], + [/\\cyrchar\\cyrghcrs /g, '\u0493'], + [/\\cyrchar\\CYRGHK /g, '\u0494'], + [/\\cyrchar\\cyrghk /g, '\u0495'], + [/\\cyrchar\\CYRZHDSC /g, '\u0496'], + [/\\cyrchar\\cyrzhdsc /g, '\u0497'], + [/\\cyrchar\\CYRZDSC /g, '\u0498'], + [/\\cyrchar\\cyrzdsc /g, '\u0499'], + [/\\cyrchar\\CYRKDSC /g, '\u049A'], + [/\\cyrchar\\cyrkdsc /g, '\u049B'], + [/\\cyrchar\\CYRKVCRS /g, '\u049C'], + [/\\cyrchar\\cyrkvcrs /g, '\u049D'], + [/\\cyrchar\\CYRKHCRS /g, '\u049E'], + [/\\cyrchar\\cyrkhcrs /g, '\u049F'], + [/\\cyrchar\\CYRKBEAK /g, '\u04A0'], + [/\\cyrchar\\cyrkbeak /g, '\u04A1'], + [/\\cyrchar\\CYRNDSC /g, '\u04A2'], + [/\\cyrchar\\cyrndsc /g, '\u04A3'], + [/\\cyrchar\\CYRNG /g, '\u04A4'], + [/\\cyrchar\\cyrng /g, '\u04A5'], + [/\\cyrchar\\CYRPHK /g, '\u04A6'], + [/\\cyrchar\\cyrphk /g, '\u04A7'], + [/\\cyrchar\\CYRABHHA /g, '\u04A8'], + [/\\cyrchar\\cyrabhha /g, '\u04A9'], + [/\\cyrchar\\CYRSDSC /g, '\u04AA'], + [/\\cyrchar\\cyrsdsc /g, '\u04AB'], + [/\\cyrchar\\CYRTDSC /g, '\u04AC'], + [/\\cyrchar\\cyrtdsc /g, '\u04AD'], + [/\\cyrchar\\CYRY /g, '\u04AE'], + [/\\cyrchar\\cyry /g, '\u04AF'], + [/\\cyrchar\\CYRYHCRS /g, '\u04B0'], + [/\\cyrchar\\cyryhcrs /g, '\u04B1'], + [/\\cyrchar\\CYRHDSC /g, '\u04B2'], + [/\\cyrchar\\cyrhdsc /g, '\u04B3'], + [/\\cyrchar\\CYRTETSE /g, '\u04B4'], + [/\\cyrchar\\cyrtetse /g, '\u04B5'], + [/\\cyrchar\\CYRCHRDSC /g, '\u04B6'], + [/\\cyrchar\\cyrchrdsc /g, '\u04B7'], + [/\\cyrchar\\CYRCHVCRS /g, '\u04B8'], + [/\\cyrchar\\cyrchvcrs /g, '\u04B9'], + [/\\cyrchar\\CYRSHHA /g, '\u04BA'], + [/\\cyrchar\\cyrshha /g, '\u04BB'], + [/\\cyrchar\\CYRABHCH /g, '\u04BC'], + [/\\cyrchar\\cyrabhch /g, '\u04BD'], + [/\\cyrchar\\CYRABHCHDSC /g, '\u04BE'], + [/\\cyrchar\\cyrabhchdsc /g, '\u04BF'], + [/\\cyrchar\\CYRpalochka /g, '\u04C0'], + [/\\cyrchar\\CYRKHK /g, '\u04C3'], + [/\\cyrchar\\cyrkhk /g, '\u04C4'], + [/\\cyrchar\\CYRNHK /g, '\u04C7'], + [/\\cyrchar\\cyrnhk /g, '\u04C8'], + [/\\cyrchar\\CYRCHLDSC /g, '\u04CB'], + [/\\cyrchar\\cyrchldsc /g, '\u04CC'], + [/\\cyrchar\\CYRAE /g, '\u04D4'], + [/\\cyrchar\\cyrae /g, '\u04D5'], + [/\\cyrchar\\CYRSCHWA /g, '\u04D8'], + [/\\cyrchar\\cyrschwa /g, '\u04D9'], + [/\\cyrchar\\CYRABHDZE /g, '\u04E0'], + [/\\cyrchar\\cyrabhdze /g, '\u04E1'], + [/\\cyrchar\\CYROTLD /g, '\u04E8'], + [/\\cyrchar\\cyrotld /g, '\u04E9'], + [/\\hspace\{0.6em\}/g, '\u2002'], + [/\\hspace\{1em\}/g, '\u2003'], + [/\\hspace\{0.33em\}/g, '\u2004'], + [/\\hspace\{0.25em\}/g, '\u2005'], + [/\\hspace\{0.166em\}/g, '\u2006'], + [/\\hphantom\{0\}/g, '\u2007'], + [/\\hphantom\{,\}/g, '\u2008'], + [/\\hspace\{0.167em\}/g, '\u2009'], + [/\\mkern1mu /g, '\u200A'], + [/\\textendash /g, '\u2013'], + [/\\textemdash /g, '\u2014'], + [/\\rule\{1em\}\{1pt\}/g, '\u2015'], + [/\\Vert /g, '\u2016'], + [/\\Elzreapos /g, '\u201B'], + [/\\textquotedblleft /g, '\u201C'], + [/\\textquotedblright /g, '\u201D'], + [/\\textdagger /g, '\u2020'], + [/\\textdaggerdbl /g, '\u2021'], + [/\\textbullet /g, '\u2022'], + [/\\ldots /g, '\u2026'], + [/\\textperthousand /g, '\u2030'], + [/\\textpertenthousand /g, '\u2031'], + [/\\backprime /g, '\u2035'], + [/\\guilsinglleft /g, '\u2039'], + [/\\guilsinglright /g, '\u203A'], + [/\\mkern4mu /g, '\u205F'], + [/\\nolinebreak /g, '\u2060'], + [/\\ensuremath\{\\Elzpes\}/g, '\u20A7'], + [/\\mbox\{\\texteuro\} /g, '\u20AC'], + [/\\dddot /g, '\u20DB'], + [/\\ddddot /g, '\u20DC'], + [/\\mathbb\{C\}/g, '\u2102'], + [/\\mathscr\{g\}/g, '\u210A'], + [/\\mathscr\{H\}/g, '\u210B'], + [/\\mathfrak\{H\}/g, '\u210C'], + [/\\mathbb\{H\}/g, '\u210D'], + [/\\hslash /g, '\u210F'], + [/\\mathscr\{I\}/g, '\u2110'], + [/\\mathfrak\{I\}/g, '\u2111'], + [/\\mathscr\{L\}/g, '\u2112'], + [/\\mathscr\{l\}/g, '\u2113'], + [/\\mathbb\{N\}/g, '\u2115'], + [/\\cyrchar\\textnumero /g, '\u2116'], + [/\\wp /g, '\u2118'], + [/\\mathbb\{P\}/g, '\u2119'], + [/\\mathbb\{Q\}/g, '\u211A'], + [/\\mathscr\{R\}/g, '\u211B'], + [/\\mathfrak\{R\}/g, '\u211C'], + [/\\mathbb\{R\}/g, '\u211D'], + [/\\Elzxrat /g, '\u211E'], + [/\\texttrademark /g, '\u2122'], + [/\\mathbb\{Z\}/g, '\u2124'], + [/\\Omega /g, '\u2126'], + [/\\mho /g, '\u2127'], + [/\\mathfrak\{Z\}/g, '\u2128'], + [/\\ElsevierGlyph\{2129\}/g, '\u2129'], + [/\\AA /g, '\u212B'], + [/\\mathscr\{B\}/g, '\u212C'], + [/\\mathfrak\{C\}/g, '\u212D'], + [/\\mathscr\{e\}/g, '\u212F'], + [/\\mathscr\{E\}/g, '\u2130'], + [/\\mathscr\{F\}/g, '\u2131'], + [/\\mathscr\{M\}/g, '\u2133'], + [/\\mathscr\{o\}/g, '\u2134'], + [/\\aleph /g, '\u2135'], + [/\\beth /g, '\u2136'], + [/\\gimel /g, '\u2137'], + [/\\daleth /g, '\u2138'], + [/\\textfrac\{1\}\{3\}/g, '\u2153'], + [/\\textfrac\{2\}\{3\}/g, '\u2154'], + [/\\textfrac\{1\}\{5\}/g, '\u2155'], + [/\\textfrac\{2\}\{5\}/g, '\u2156'], + [/\\textfrac\{3\}\{5\}/g, '\u2157'], + [/\\textfrac\{4\}\{5\}/g, '\u2158'], + [/\\textfrac\{1\}\{6\}/g, '\u2159'], + [/\\textfrac\{5\}\{6\}/g, '\u215A'], + [/\\textfrac\{1\}\{8\}/g, '\u215B'], + [/\\textfrac\{3\}\{8\}/g, '\u215C'], + [/\\textfrac\{5\}\{8\}/g, '\u215D'], + [/\\textfrac\{7\}\{8\}/g, '\u215E'], + [/\\leftarrow /g, '\u2190'], + [/\\uparrow /g, '\u2191'], + [/\\rightarrow /g, '\u2192'], + [/\\downarrow /g, '\u2193'], + [/\\leftrightarrow /g, '\u2194'], + [/\\updownarrow /g, '\u2195'], + [/\\nwarrow /g, '\u2196'], + [/\\nearrow /g, '\u2197'], + [/\\searrow /g, '\u2198'], + [/\\swarrow /g, '\u2199'], + [/\\nleftarrow /g, '\u219A'], + [/\\nrightarrow /g, '\u219B'], + [/\\arrowwaveright /g, '\u219C'], + [/\\arrowwaveright /g, '\u219D'], + [/\\twoheadleftarrow /g, '\u219E'], + [/\\twoheadrightarrow /g, '\u21A0'], + [/\\leftarrowtail /g, '\u21A2'], + [/\\rightarrowtail /g, '\u21A3'], + [/\\mapsto /g, '\u21A6'], + [/\\hookleftarrow /g, '\u21A9'], + [/\\hookrightarrow /g, '\u21AA'], + [/\\looparrowleft /g, '\u21AB'], + [/\\looparrowright /g, '\u21AC'], + [/\\leftrightsquigarrow /g, '\u21AD'], + [/\\nleftrightarrow /g, '\u21AE'], + [/\\Lsh /g, '\u21B0'], + [/\\Rsh /g, '\u21B1'], + [/\\ElsevierGlyph\{21B3\}/g, '\u21B3'], + [/\\curvearrowleft /g, '\u21B6'], + [/\\curvearrowright /g, '\u21B7'], + [/\\circlearrowleft /g, '\u21BA'], + [/\\circlearrowright /g, '\u21BB'], + [/\\leftharpoonup /g, '\u21BC'], + [/\\leftharpoondown /g, '\u21BD'], + [/\\upharpoonright /g, '\u21BE'], + [/\\upharpoonleft /g, '\u21BF'], + [/\\rightharpoonup /g, '\u21C0'], + [/\\rightharpoondown /g, '\u21C1'], + [/\\downharpoonright /g, '\u21C2'], + [/\\downharpoonleft /g, '\u21C3'], + [/\\rightleftarrows /g, '\u21C4'], + [/\\dblarrowupdown /g, '\u21C5'], + [/\\leftrightarrows /g, '\u21C6'], + [/\\leftleftarrows /g, '\u21C7'], + [/\\upuparrows /g, '\u21C8'], + [/\\rightrightarrows /g, '\u21C9'], + [/\\downdownarrows /g, '\u21CA'], + [/\\leftrightharpoons /g, '\u21CB'], + [/\\rightleftharpoons /g, '\u21CC'], + [/\\nLeftarrow /g, '\u21CD'], + [/\\nLeftrightarrow /g, '\u21CE'], + [/\\nRightarrow /g, '\u21CF'], + [/\\Leftarrow /g, '\u21D0'], + [/\\Uparrow /g, '\u21D1'], + [/\\Rightarrow /g, '\u21D2'], + [/\\Downarrow /g, '\u21D3'], + [/\\Leftrightarrow /g, '\u21D4'], + [/\\Updownarrow /g, '\u21D5'], + [/\\Lleftarrow /g, '\u21DA'], + [/\\Rrightarrow /g, '\u21DB'], + [/\\rightsquigarrow /g, '\u21DD'], + [/\\DownArrowUpArrow /g, '\u21F5'], + [/\\forall /g, '\u2200'], + [/\\complement /g, '\u2201'], + [/\\partial /g, '\u2202'], + [/\\exists /g, '\u2203'], + [/\\nexists /g, '\u2204'], + [/\\varnothing /g, '\u2205'], + [/\\nabla /g, '\u2207'], + [/\\in /g, '\u2208'], + [/\\not\\in /g, '\u2209'], + [/\\ni /g, '\u220B'], + [/\\not\\ni /g, '\u220C'], + [/\\prod /g, '\u220F'], + [/\\coprod /g, '\u2210'], + [/\\sum /g, '\u2211'], + [/\\mp /g, '\u2213'], + [/\\dotplus /g, '\u2214'], + [/\\setminus /g, '\u2216'], + [/\\circ /g, '\u2218'], + [/\\bullet /g, '\u2219'], + [/\\surd /g, '\u221A'], + [/\\propto /g, '\u221D'], + [/\\infty /g, '\u221E'], + [/\\rightangle /g, '\u221F'], + [/\\angle /g, '\u2220'], + [/\\measuredangle /g, '\u2221'], + [/\\sphericalangle /g, '\u2222'], + [/\\mid /g, '\u2223'], + [/\\nmid /g, '\u2224'], + [/\\parallel /g, '\u2225'], + [/\\nparallel /g, '\u2226'], + [/\\wedge /g, '\u2227'], + [/\\vee /g, '\u2228'], + [/\\cap /g, '\u2229'], + [/\\cup /g, '\u222A'], + [/\\int /g, '\u222B'], + [/\\int\\!\\int /g, '\u222C'], + [/\\int\\!\\int\\!\\int /g, '\u222D'], + [/\\oint /g, '\u222E'], + [/\\surfintegral /g, '\u222F'], + [/\\volintegral /g, '\u2230'], + [/\\clwintegral /g, '\u2231'], + [/\\ElsevierGlyph\{2232\}/g, '\u2232'], + [/\\ElsevierGlyph\{2233\}/g, '\u2233'], + [/\\therefore /g, '\u2234'], + [/\\because /g, '\u2235'], + [/\\Colon /g, '\u2237'], + [/\\ElsevierGlyph\{2238\}/g, '\u2238'], + [/\\mathbin\{\{:\}\\!\\!\{\-\}\\!\\!\{:\}\}/g, '\u223A'], + [/\\homothetic /g, '\u223B'], + [/\\sim /g, '\u223C'], + [/\\backsim /g, '\u223D'], + [/\\lazysinv /g, '\u223E'], + [/\\wr /g, '\u2240'], + [/\\not\\sim /g, '\u2241'], + [/\\ElsevierGlyph\{2242\}/g, '\u2242'], + [/\\NotEqualTilde /g, '\u2242-00338'], + [/\\simeq /g, '\u2243'], + [/\\not\\simeq /g, '\u2244'], + [/\\cong /g, '\u2245'], + [/\\approxnotequal /g, '\u2246'], + [/\\not\\cong /g, '\u2247'], + [/\\approx /g, '\u2248'], + [/\\not\\approx /g, '\u2249'], + [/\\approxeq /g, '\u224A'], + [/\\tildetrpl /g, '\u224B'], + [/\\not\\apid /g, '\u224B-00338'], + [/\\allequal /g, '\u224C'], + [/\\asymp /g, '\u224D'], + [/\\Bumpeq /g, '\u224E'], + [/\\NotHumpDownHump /g, '\u224E-00338'], + [/\\bumpeq /g, '\u224F'], + [/\\NotHumpEqual /g, '\u224F-00338'], + [/\\doteq /g, '\u2250'], + [/\\not\\doteq/g, '\u2250-00338'], + [/\\doteqdot /g, '\u2251'], + [/\\fallingdotseq /g, '\u2252'], + [/\\risingdotseq /g, '\u2253'], + [/\\eqcirc /g, '\u2256'], + [/\\circeq /g, '\u2257'], + [/\\estimates /g, '\u2259'], + [/\\ElsevierGlyph\{225A\}/g, '\u225A'], + [/\\starequal /g, '\u225B'], + [/\\triangleq /g, '\u225C'], + [/\\ElsevierGlyph\{225F\}/g, '\u225F'], + [/\\not =/g, '\u2260'], + [/\\equiv /g, '\u2261'], + [/\\not\\equiv /g, '\u2262'], + [/\\leq /g, '\u2264'], + [/\\geq /g, '\u2265'], + [/\\leqq /g, '\u2266'], + [/\\geqq /g, '\u2267'], + [/\\lneqq /g, '\u2268'], + [/\\lvertneqq /g, '\u2268-0FE00'], + [/\\gneqq /g, '\u2269'], + [/\\gvertneqq /g, '\u2269-0FE00'], + [/\\ll /g, '\u226A'], + [/\\NotLessLess /g, '\u226A-00338'], + [/\\gg /g, '\u226B'], + [/\\NotGreaterGreater /g, '\u226B-00338'], + [/\\between /g, '\u226C'], + [/\\not\\kern\-0.3em\\times /g, '\u226D'], + [/\\not/g, '\u226F'], + [/\\not\\leq /g, '\u2270'], + [/\\not\\geq /g, '\u2271'], + [/\\lessequivlnt /g, '\u2272'], + [/\\greaterequivlnt /g, '\u2273'], + [/\\ElsevierGlyph\{2274\}/g, '\u2274'], + [/\\ElsevierGlyph\{2275\}/g, '\u2275'], + [/\\lessgtr /g, '\u2276'], + [/\\gtrless /g, '\u2277'], + [/\\notlessgreater /g, '\u2278'], + [/\\notgreaterless /g, '\u2279'], + [/\\prec /g, '\u227A'], + [/\\succ /g, '\u227B'], + [/\\preccurlyeq /g, '\u227C'], + [/\\succcurlyeq /g, '\u227D'], + [/\\precapprox /g, '\u227E'], + [/\\NotPrecedesTilde /g, '\u227E-00338'], + [/\\succapprox /g, '\u227F'], + [/\\NotSucceedsTilde /g, '\u227F-00338'], + [/\\not\\prec /g, '\u2280'], + [/\\not\\succ /g, '\u2281'], + [/\\subset /g, '\u2282'], + [/\\supset /g, '\u2283'], + [/\\not\\subset /g, '\u2284'], + [/\\not\\supset /g, '\u2285'], + [/\\subseteq /g, '\u2286'], + [/\\supseteq /g, '\u2287'], + [/\\not\\subseteq /g, '\u2288'], + [/\\not\\supseteq /g, '\u2289'], + [/\\subsetneq /g, '\u228A'], + [/\\varsubsetneqq /g, '\u228A-0FE00'], + [/\\supsetneq /g, '\u228B'], + [/\\varsupsetneq /g, '\u228B-0FE00'], + [/\\uplus /g, '\u228E'], + [/\\sqsubset /g, '\u228F'], + [/\\NotSquareSubset /g, '\u228F-00338'], + [/\\sqsupset /g, '\u2290'], + [/\\NotSquareSuperset /g, '\u2290-00338'], + [/\\sqsubseteq /g, '\u2291'], + [/\\sqsupseteq /g, '\u2292'], + [/\\sqcap /g, '\u2293'], + [/\\sqcup /g, '\u2294'], + [/\\oplus /g, '\u2295'], + [/\\ominus /g, '\u2296'], + [/\\otimes /g, '\u2297'], + [/\\oslash /g, '\u2298'], + [/\\odot /g, '\u2299'], + [/\\circledcirc /g, '\u229A'], + [/\\circledast /g, '\u229B'], + [/\\circleddash /g, '\u229D'], + [/\\boxplus /g, '\u229E'], + [/\\boxminus /g, '\u229F'], + [/\\boxtimes /g, '\u22A0'], + [/\\boxdot /g, '\u22A1'], + [/\\vdash /g, '\u22A2'], + [/\\dashv /g, '\u22A3'], + [/\\top /g, '\u22A4'], + [/\\perp /g, '\u22A5'], + [/\\truestate /g, '\u22A7'], + [/\\forcesextra /g, '\u22A8'], + [/\\Vdash /g, '\u22A9'], + [/\\Vvdash /g, '\u22AA'], + [/\\VDash /g, '\u22AB'], + [/\\nvdash /g, '\u22AC'], + [/\\nvDash /g, '\u22AD'], + [/\\nVdash /g, '\u22AE'], + [/\\nVDash /g, '\u22AF'], + [/\\vartriangleleft /g, '\u22B2'], + [/\\vartriangleright /g, '\u22B3'], + [/\\trianglelefteq /g, '\u22B4'], + [/\\trianglerighteq /g, '\u22B5'], + [/\\original /g, '\u22B6'], + [/\\image /g, '\u22B7'], + [/\\multimap /g, '\u22B8'], + [/\\hermitconjmatrix /g, '\u22B9'], + [/\\intercal /g, '\u22BA'], + [/\\veebar /g, '\u22BB'], + [/\\rightanglearc /g, '\u22BE'], + [/\\ElsevierGlyph\{22C0\}/g, '\u22C0'], + [/\\ElsevierGlyph\{22C1\}/g, '\u22C1'], + [/\\bigcap /g, '\u22C2'], + [/\\bigcup /g, '\u22C3'], + [/\\diamond /g, '\u22C4'], + [/\\cdot /g, '\u22C5'], + [/\\star /g, '\u22C6'], + [/\\divideontimes /g, '\u22C7'], + [/\\bowtie /g, '\u22C8'], + [/\\ltimes /g, '\u22C9'], + [/\\rtimes /g, '\u22CA'], + [/\\leftthreetimes /g, '\u22CB'], + [/\\rightthreetimes /g, '\u22CC'], + [/\\backsimeq /g, '\u22CD'], + [/\\curlyvee /g, '\u22CE'], + [/\\curlywedge /g, '\u22CF'], + [/\\Subset /g, '\u22D0'], + [/\\Supset /g, '\u22D1'], + [/\\Cap /g, '\u22D2'], + [/\\Cup /g, '\u22D3'], + [/\\pitchfork /g, '\u22D4'], + [/\\lessdot /g, '\u22D6'], + [/\\gtrdot /g, '\u22D7'], + [/\\verymuchless /g, '\u22D8'], + [/\\verymuchgreater /g, '\u22D9'], + [/\\lesseqgtr /g, '\u22DA'], + [/\\gtreqless /g, '\u22DB'], + [/\\curlyeqprec /g, '\u22DE'], + [/\\curlyeqsucc /g, '\u22DF'], + [/\\not\\sqsubseteq /g, '\u22E2'], + [/\\not\\sqsupseteq /g, '\u22E3'], + [/\\Elzsqspne /g, '\u22E5'], + [/\\lnsim /g, '\u22E6'], + [/\\gnsim /g, '\u22E7'], + [/\\precedesnotsimilar /g, '\u22E8'], + [/\\succnsim /g, '\u22E9'], + [/\\ntriangleleft /g, '\u22EA'], + [/\\ntriangleright /g, '\u22EB'], + [/\\ntrianglelefteq /g, '\u22EC'], + [/\\ntrianglerighteq /g, '\u22ED'], + [/\\vdots /g, '\u22EE'], + [/\\cdots /g, '\u22EF'], + [/\\upslopeellipsis /g, '\u22F0'], + [/\\downslopeellipsis /g, '\u22F1'], + [/\\barwedge /g, '\u2305'], + [/\\perspcorrespond /g, '\u2306'], + [/\\lceil /g, '\u2308'], + [/\\rceil /g, '\u2309'], + [/\\lfloor /g, '\u230A'], + [/\\rfloor /g, '\u230B'], + [/\\recorder /g, '\u2315'], + [/\\mathchar"2208/g, '\u2316'], + [/\\ulcorner /g, '\u231C'], + [/\\urcorner /g, '\u231D'], + [/\\llcorner /g, '\u231E'], + [/\\lrcorner /g, '\u231F'], + [/\\frown /g, '\u2322'], + [/\\smile /g, '\u2323'], + [/\\langle /g, '\u2329'], + [/\\rangle /g, '\u232A'], + [/\\ElsevierGlyph\{E838\}/g, '\u233D'], + [/\\Elzdlcorn /g, '\u23A3'], + [/\\lmoustache /g, '\u23B0'], + [/\\rmoustache /g, '\u23B1'], + [/\\textvisiblespace /g, '\u2423'], + [/\\ding\{172\}/g, '\u2460'], + [/\\ding\{173\}/g, '\u2461'], + [/\\ding\{174\}/g, '\u2462'], + [/\\ding\{175\}/g, '\u2463'], + [/\\ding\{176\}/g, '\u2464'], + [/\\ding\{177\}/g, '\u2465'], + [/\\ding\{178\}/g, '\u2466'], + [/\\ding\{179\}/g, '\u2467'], + [/\\ding\{180\}/g, '\u2468'], + [/\\ding\{181\}/g, '\u2469'], + [/\\circledS /g, '\u24C8'], + [/\\Elzdshfnc /g, '\u2506'], + [/\\Elzsqfnw /g, '\u2519'], + [/\\diagup /g, '\u2571'], + [/\\ding\{110\}/g, '\u25A0'], + [/\\square /g, '\u25A1'], + [/\\blacksquare /g, '\u25AA'], + [/\\fbox\{~~\}/g, '\u25AD'], + [/\\Elzvrecto /g, '\u25AF'], + [/\\ElsevierGlyph\{E381\}/g, '\u25B1'], + [/\\ding\{115\}/g, '\u25B2'], + [/\\bigtriangleup /g, '\u25B3'], + [/\\blacktriangle /g, '\u25B4'], + [/\\vartriangle /g, '\u25B5'], + [/\\blacktriangleright /g, '\u25B8'], + [/\\triangleright /g, '\u25B9'], + [/\\ding\{116\}/g, '\u25BC'], + [/\\bigtriangledown /g, '\u25BD'], + [/\\blacktriangledown /g, '\u25BE'], + [/\\triangledown /g, '\u25BF'], + [/\\blacktriangleleft /g, '\u25C2'], + [/\\triangleleft /g, '\u25C3'], + [/\\ding\{117\}/g, '\u25C6'], + [/\\lozenge /g, '\u25CA'], + [/\\bigcirc /g, '\u25CB'], + [/\\ding\{108\}/g, '\u25CF'], + [/\\Elzcirfl /g, '\u25D0'], + [/\\Elzcirfr /g, '\u25D1'], + [/\\Elzcirfb /g, '\u25D2'], + [/\\ding\{119\}/g, '\u25D7'], + [/\\Elzrvbull /g, '\u25D8'], + [/\\Elzsqfl /g, '\u25E7'], + [/\\Elzsqfr /g, '\u25E8'], + [/\\Elzsqfse /g, '\u25EA'], + [/\\bigcirc /g, '\u25EF'], + [/\\ding\{72\}/g, '\u2605'], + [/\\ding\{73\}/g, '\u2606'], + [/\\ding\{37\}/g, '\u260E'], + [/\\ding\{42\}/g, '\u261B'], + [/\\ding\{43\}/g, '\u261E'], + [/\\rightmoon /g, '\u263E'], + [/\\mercury /g, '\u263F'], + [/\\venus /g, '\u2640'], + [/\\male /g, '\u2642'], + [/\\jupiter /g, '\u2643'], + [/\\saturn /g, '\u2644'], + [/\\uranus /g, '\u2645'], + [/\\neptune /g, '\u2646'], + [/\\pluto /g, '\u2647'], + [/\\aries /g, '\u2648'], + [/\\taurus /g, '\u2649'], + [/\\gemini /g, '\u264A'], + [/\\cancer /g, '\u264B'], + [/\\leo /g, '\u264C'], + [/\\virgo /g, '\u264D'], + [/\\libra /g, '\u264E'], + [/\\scorpio /g, '\u264F'], + [/\\sagittarius /g, '\u2650'], + [/\\capricornus /g, '\u2651'], + [/\\aquarius /g, '\u2652'], + [/\\pisces /g, '\u2653'], + [/\\ding\{171\}/g, '\u2660'], + [/\\diamond /g, '\u2662'], + [/\\ding\{168\}/g, '\u2663'], + [/\\ding\{170\}/g, '\u2665'], + [/\\ding\{169\}/g, '\u2666'], + [/\\quarternote /g, '\u2669'], + [/\\eighthnote /g, '\u266A'], + [/\\flat /g, '\u266D'], + [/\\natural /g, '\u266E'], + [/\\sharp /g, '\u266F'], + [/\\ding\{33\}/g, '\u2701'], + [/\\ding\{34\}/g, '\u2702'], + [/\\ding\{35\}/g, '\u2703'], + [/\\ding\{36\}/g, '\u2704'], + [/\\ding\{38\}/g, '\u2706'], + [/\\ding\{39\}/g, '\u2707'], + [/\\ding\{40\}/g, '\u2708'], + [/\\ding\{41\}/g, '\u2709'], + [/\\ding\{44\}/g, '\u270C'], + [/\\ding\{45\}/g, '\u270D'], + [/\\ding\{46\}/g, '\u270E'], + [/\\ding\{47\}/g, '\u270F'], + [/\\ding\{48\}/g, '\u2710'], + [/\\ding\{49\}/g, '\u2711'], + [/\\ding\{50\}/g, '\u2712'], + [/\\ding\{51\}/g, '\u2713'], + [/\\ding\{52\}/g, '\u2714'], + [/\\ding\{53\}/g, '\u2715'], + [/\\ding\{54\}/g, '\u2716'], + [/\\ding\{55\}/g, '\u2717'], + [/\\ding\{56\}/g, '\u2718'], + [/\\ding\{57\}/g, '\u2719'], + [/\\ding\{58\}/g, '\u271A'], + [/\\ding\{59\}/g, '\u271B'], + [/\\ding\{60\}/g, '\u271C'], + [/\\ding\{61\}/g, '\u271D'], + [/\\ding\{62\}/g, '\u271E'], + [/\\ding\{63\}/g, '\u271F'], + [/\\ding\{64\}/g, '\u2720'], + [/\\ding\{65\}/g, '\u2721'], + [/\\ding\{66\}/g, '\u2722'], + [/\\ding\{67\}/g, '\u2723'], + [/\\ding\{68\}/g, '\u2724'], + [/\\ding\{69\}/g, '\u2725'], + [/\\ding\{70\}/g, '\u2726'], + [/\\ding\{71\}/g, '\u2727'], + [/\\ding\{73\}/g, '\u2729'], + [/\\ding\{74\}/g, '\u272A'], + [/\\ding\{75\}/g, '\u272B'], + [/\\ding\{76\}/g, '\u272C'], + [/\\ding\{77\}/g, '\u272D'], + [/\\ding\{78\}/g, '\u272E'], + [/\\ding\{79\}/g, '\u272F'], + [/\\ding\{80\}/g, '\u2730'], + [/\\ding\{81\}/g, '\u2731'], + [/\\ding\{82\}/g, '\u2732'], + [/\\ding\{83\}/g, '\u2733'], + [/\\ding\{84\}/g, '\u2734'], + [/\\ding\{85\}/g, '\u2735'], + [/\\ding\{86\}/g, '\u2736'], + [/\\ding\{87\}/g, '\u2737'], + [/\\ding\{88\}/g, '\u2738'], + [/\\ding\{89\}/g, '\u2739'], + [/\\ding\{90\}/g, '\u273A'], + [/\\ding\{91\}/g, '\u273B'], + [/\\ding\{92\}/g, '\u273C'], + [/\\ding\{93\}/g, '\u273D'], + [/\\ding\{94\}/g, '\u273E'], + [/\\ding\{95\}/g, '\u273F'], + [/\\ding\{96\}/g, '\u2740'], + [/\\ding\{97\}/g, '\u2741'], + [/\\ding\{98\}/g, '\u2742'], + [/\\ding\{99\}/g, '\u2743'], + [/\\ding\{100\}/g, '\u2744'], + [/\\ding\{101\}/g, '\u2745'], + [/\\ding\{102\}/g, '\u2746'], + [/\\ding\{103\}/g, '\u2747'], + [/\\ding\{104\}/g, '\u2748'], + [/\\ding\{105\}/g, '\u2749'], + [/\\ding\{106\}/g, '\u274A'], + [/\\ding\{107\}/g, '\u274B'], + [/\\ding\{109\}/g, '\u274D'], + [/\\ding\{111\}/g, '\u274F'], + [/\\ding\{112\}/g, '\u2750'], + [/\\ding\{113\}/g, '\u2751'], + [/\\ding\{114\}/g, '\u2752'], + [/\\ding\{118\}/g, '\u2756'], + [/\\ding\{120\}/g, '\u2758'], + [/\\ding\{121\}/g, '\u2759'], + [/\\ding\{122\}/g, '\u275A'], + [/\\ding\{123\}/g, '\u275B'], + [/\\ding\{124\}/g, '\u275C'], + [/\\ding\{125\}/g, '\u275D'], + [/\\ding\{126\}/g, '\u275E'], + [/\\ding\{161\}/g, '\u2761'], + [/\\ding\{162\}/g, '\u2762'], + [/\\ding\{163\}/g, '\u2763'], + [/\\ding\{164\}/g, '\u2764'], + [/\\ding\{165\}/g, '\u2765'], + [/\\ding\{166\}/g, '\u2766'], + [/\\ding\{167\}/g, '\u2767'], + [/\\ding\{182\}/g, '\u2776'], + [/\\ding\{183\}/g, '\u2777'], + [/\\ding\{184\}/g, '\u2778'], + [/\\ding\{185\}/g, '\u2779'], + [/\\ding\{186\}/g, '\u277A'], + [/\\ding\{187\}/g, '\u277B'], + [/\\ding\{188\}/g, '\u277C'], + [/\\ding\{189\}/g, '\u277D'], + [/\\ding\{190\}/g, '\u277E'], + [/\\ding\{191\}/g, '\u277F'], + [/\\ding\{192\}/g, '\u2780'], + [/\\ding\{193\}/g, '\u2781'], + [/\\ding\{194\}/g, '\u2782'], + [/\\ding\{195\}/g, '\u2783'], + [/\\ding\{196\}/g, '\u2784'], + [/\\ding\{197\}/g, '\u2785'], + [/\\ding\{198\}/g, '\u2786'], + [/\\ding\{199\}/g, '\u2787'], + [/\\ding\{200\}/g, '\u2788'], + [/\\ding\{201\}/g, '\u2789'], + [/\\ding\{202\}/g, '\u278A'], + [/\\ding\{203\}/g, '\u278B'], + [/\\ding\{204\}/g, '\u278C'], + [/\\ding\{205\}/g, '\u278D'], + [/\\ding\{206\}/g, '\u278E'], + [/\\ding\{207\}/g, '\u278F'], + [/\\ding\{208\}/g, '\u2790'], + [/\\ding\{209\}/g, '\u2791'], + [/\\ding\{210\}/g, '\u2792'], + [/\\ding\{211\}/g, '\u2793'], + [/\\ding\{212\}/g, '\u2794'], + [/\\ding\{216\}/g, '\u2798'], + [/\\ding\{217\}/g, '\u2799'], + [/\\ding\{218\}/g, '\u279A'], + [/\\ding\{219\}/g, '\u279B'], + [/\\ding\{220\}/g, '\u279C'], + [/\\ding\{221\}/g, '\u279D'], + [/\\ding\{222\}/g, '\u279E'], + [/\\ding\{223\}/g, '\u279F'], + [/\\ding\{224\}/g, '\u27A0'], + [/\\ding\{225\}/g, '\u27A1'], + [/\\ding\{226\}/g, '\u27A2'], + [/\\ding\{227\}/g, '\u27A3'], + [/\\ding\{228\}/g, '\u27A4'], + [/\\ding\{229\}/g, '\u27A5'], + [/\\ding\{230\}/g, '\u27A6'], + [/\\ding\{231\}/g, '\u27A7'], + [/\\ding\{232\}/g, '\u27A8'], + [/\\ding\{233\}/g, '\u27A9'], + [/\\ding\{234\}/g, '\u27AA'], + [/\\ding\{235\}/g, '\u27AB'], + [/\\ding\{236\}/g, '\u27AC'], + [/\\ding\{237\}/g, '\u27AD'], + [/\\ding\{238\}/g, '\u27AE'], + [/\\ding\{239\}/g, '\u27AF'], + [/\\ding\{241\}/g, '\u27B1'], + [/\\ding\{242\}/g, '\u27B2'], + [/\\ding\{243\}/g, '\u27B3'], + [/\\ding\{244\}/g, '\u27B4'], + [/\\ding\{245\}/g, '\u27B5'], + [/\\ding\{246\}/g, '\u27B6'], + [/\\ding\{247\}/g, '\u27B7'], + [/\\ding\{248\}/g, '\u27B8'], + [/\\ding\{249\}/g, '\u27B9'], + [/\\ding\{250\}/g, '\u27BA'], + [/\\ding\{251\}/g, '\u27BB'], + [/\\ding\{252\}/g, '\u27BC'], + [/\\ding\{253\}/g, '\u27BD'], + [/\\ding\{254\}/g, '\u27BE'], + [/\\longleftarrow /g, '\u27F5'], + [/\\longrightarrow /g, '\u27F6'], + [/\\longleftrightarrow /g, '\u27F7'], + [/\\Longleftarrow /g, '\u27F8'], + [/\\Longrightarrow /g, '\u27F9'], + [/\\Longleftrightarrow /g, '\u27FA'], + [/\\longmapsto /g, '\u27FC'], + [/\\sim\\joinrel\\leadsto/g, '\u27FF'], + [/\\ElsevierGlyph\{E212\}/g, '\u2905'], + [/\\UpArrowBar /g, '\u2912'], + [/\\DownArrowBar /g, '\u2913'], + [/\\ElsevierGlyph\{E20C\}/g, '\u2923'], + [/\\ElsevierGlyph\{E20D\}/g, '\u2924'], + [/\\ElsevierGlyph\{E20B\}/g, '\u2925'], + [/\\ElsevierGlyph\{E20A\}/g, '\u2926'], + [/\\ElsevierGlyph\{E211\}/g, '\u2927'], + [/\\ElsevierGlyph\{E20E\}/g, '\u2928'], + [/\\ElsevierGlyph\{E20F\}/g, '\u2929'], + [/\\ElsevierGlyph\{E210\}/g, '\u292A'], + [/\\ElsevierGlyph\{E21C\}/g, '\u2933'], + [/\\ElsevierGlyph\{E21D\}/g, '\u2933-00338'], + [/\\ElsevierGlyph\{E21A\}/g, '\u2936'], + [/\\ElsevierGlyph\{E219\}/g, '\u2937'], + [/\\Elolarr /g, '\u2940'], + [/\\Elorarr /g, '\u2941'], + [/\\ElzRlarr /g, '\u2942'], + [/\\ElzrLarr /g, '\u2944'], + [/\\Elzrarrx /g, '\u2947'], + [/\\LeftRightVector /g, '\u294E'], + [/\\RightUpDownVector /g, '\u294F'], + [/\\DownLeftRightVector /g, '\u2950'], + [/\\LeftUpDownVector /g, '\u2951'], + [/\\LeftVectorBar /g, '\u2952'], + [/\\RightVectorBar /g, '\u2953'], + [/\\RightUpVectorBar /g, '\u2954'], + [/\\RightDownVectorBar /g, '\u2955'], + [/\\DownLeftVectorBar /g, '\u2956'], + [/\\DownRightVectorBar /g, '\u2957'], + [/\\LeftUpVectorBar /g, '\u2958'], + [/\\LeftDownVectorBar /g, '\u2959'], + [/\\LeftTeeVector /g, '\u295A'], + [/\\RightTeeVector /g, '\u295B'], + [/\\RightUpTeeVector /g, '\u295C'], + [/\\RightDownTeeVector /g, '\u295D'], + [/\\DownLeftTeeVector /g, '\u295E'], + [/\\DownRightTeeVector /g, '\u295F'], + [/\\LeftUpTeeVector /g, '\u2960'], + [/\\LeftDownTeeVector /g, '\u2961'], + [/\\UpEquilibrium /g, '\u296E'], + [/\\ReverseUpEquilibrium /g, '\u296F'], + [/\\RoundImplies /g, '\u2970'], + [/\\ElsevierGlyph\{E214\}/g, '\u297C'], + [/\\ElsevierGlyph\{E215\}/g, '\u297D'], + [/\\Elztfnc /g, '\u2980'], + [/\\ElsevierGlyph\{3018\}/g, '\u2985'], + [/\\Elroang /g, '\u2986'], + [/\\ElsevierGlyph\{E291\}/g, '\u2994'], + [/\\Elzddfnc /g, '\u2999'], + [/\\Angle /g, '\u299C'], + [/\\Elzlpargt /g, '\u29A0'], + [/\\ElsevierGlyph\{E260\}/g, '\u29B5'], + [/\\ElsevierGlyph\{E61B\}/g, '\u29B6'], + [/\\ElzLap /g, '\u29CA'], + [/\\Elzdefas /g, '\u29CB'], + [/\\LeftTriangleBar /g, '\u29CF'], + [/\\NotLeftTriangleBar /g, '\u29CF-00338'], + [/\\RightTriangleBar /g, '\u29D0'], + [/\\NotRightTriangleBar /g, '\u29D0-00338'], + [/\\ElsevierGlyph\{E372\}/g, '\u29DC'], + [/\\blacklozenge /g, '\u29EB'], + [/\\RuleDelayed /g, '\u29F4'], + [/\\Elxuplus /g, '\u2A04'], + [/\\ElzThr /g, '\u2A05'], + [/\\Elxsqcup /g, '\u2A06'], + [/\\ElzInf /g, '\u2A07'], + [/\\ElzSup /g, '\u2A08'], + [/\\ElzCint /g, '\u2A0D'], + [/\\clockoint /g, '\u2A0F'], + [/\\ElsevierGlyph\{E395\}/g, '\u2A10'], + [/\\sqrint /g, '\u2A16'], + [/\\ElsevierGlyph\{E25A\}/g, '\u2A25'], + [/\\ElsevierGlyph\{E25B\}/g, '\u2A2A'], + [/\\ElsevierGlyph\{E25C\}/g, '\u2A2D'], + [/\\ElsevierGlyph\{E25D\}/g, '\u2A2E'], + [/\\ElzTimes /g, '\u2A2F'], + [/\\ElsevierGlyph\{E25E\}/g, '\u2A34'], + [/\\ElsevierGlyph\{E25E\}/g, '\u2A35'], + [/\\ElsevierGlyph\{E259\}/g, '\u2A3C'], + [/\\amalg /g, '\u2A3F'], + [/\\ElzAnd /g, '\u2A53'], + [/\\ElzOr /g, '\u2A54'], + [/\\ElsevierGlyph\{E36E\}/g, '\u2A55'], + [/\\ElOr /g, '\u2A56'], + [/\\perspcorrespond /g, '\u2A5E'], + [/\\Elzminhat /g, '\u2A5F'], + [/\\ElsevierGlyph\{225A\}/g, '\u2A63'], + [/\\stackrel\{*\}\{=\}/g, '\u2A6E'], + [/\\Equal /g, '\u2A75'], + [/\\leqslant /g, '\u2A7D'], + [/\\nleqslant /g, '\u2A7D-00338'], + [/\\geqslant /g, '\u2A7E'], + [/\\ngeqslant /g, '\u2A7E-00338'], + [/\\lessapprox /g, '\u2A85'], + [/\\gtrapprox /g, '\u2A86'], + [/\\lneq /g, '\u2A87'], + [/\\gneq /g, '\u2A88'], + [/\\lnapprox /g, '\u2A89'], + [/\\gnapprox /g, '\u2A8A'], + [/\\lesseqqgtr /g, '\u2A8B'], + [/\\gtreqqless /g, '\u2A8C'], + [/\\eqslantless /g, '\u2A95'], + [/\\eqslantgtr /g, '\u2A96'], + [/\\Pisymbol\{ppi020\}\{117\}/g, '\u2A9D'], + [/\\Pisymbol\{ppi020\}\{105\}/g, '\u2A9E'], + [/\\NestedLessLess /g, '\u2AA1'], + [/\\NotNestedLessLess /g, '\u2AA1-00338'], + [/\\NestedGreaterGreater /g, '\u2AA2'], + [/\\NotNestedGreaterGreater /g, '\u2AA2-00338'], + [/\\preceq /g, '\u2AAF'], + [/\\not\\preceq /g, '\u2AAF-00338'], + [/\\succeq /g, '\u2AB0'], + [/\\not\\succeq /g, '\u2AB0-00338'], + [/\\precneqq /g, '\u2AB5'], + [/\\succneqq /g, '\u2AB6'], + [/\\precapprox /g, '\u2AB7'], + [/\\succapprox /g, '\u2AB8'], + [/\\precnapprox /g, '\u2AB9'], + [/\\succnapprox /g, '\u2ABA'], + [/\\subseteqq /g, '\u2AC5'], + [/\\nsubseteqq /g, '\u2AC5-00338'], + [/\\supseteqq /g, '\u2AC6'], + [/\\nsupseteqq/g, '\u2AC6-00338'], + [/\\subsetneqq /g, '\u2ACB'], + [/\\supsetneqq /g, '\u2ACC'], + [/\\ElsevierGlyph\{E30D\}/g, '\u2AEB'], + [/\\Elztdcol /g, '\u2AF6'], + [/\\ElsevierGlyph\{300A\}/g, '\u300A'], + [/\\ElsevierGlyph\{300B\}/g, '\u300B'], + [/\\ElsevierGlyph\{3018\}/g, '\u3018'], + [/\\ElsevierGlyph\{3019\}/g, '\u3019'], + [/\\openbracketleft /g, '\u301A'], + [/\\openbracketright /g, '\u301B'], +] + +export default BibtexParser +if (typeof module !== 'undefined' && module.exports) { + module.exports = BibtexParser +} diff --git a/services/references/buildscript.txt b/services/references/buildscript.txt new file mode 100644 index 0000000000..05771cd85a --- /dev/null +++ b/services/references/buildscript.txt @@ -0,0 +1,9 @@ +references +--dependencies=mongo +--docker-repos=us-east1-docker.pkg.dev/overleaf-ops/ol-docker +--env-add= +--env-pass-through= +--esmock-loader=True +--node-version=20.18.2 +--public-repo=False +--script-version=4.5.0 diff --git a/services/references/config/settings.defaults.cjs b/services/references/config/settings.defaults.cjs new file mode 100644 index 0000000000..2551f99f09 --- /dev/null +++ b/services/references/config/settings.defaults.cjs @@ -0,0 +1,9 @@ +module.exports = { + internal: { + references: { + port: 3056, + host: process.env.REFERENCES_HOST || '127.0.0.1', + }, + }, +} + diff --git a/services/references/docker-compose.ci.yml b/services/references/docker-compose.ci.yml new file mode 100644 index 0000000000..51eb64d126 --- /dev/null +++ b/services/references/docker-compose.ci.yml @@ -0,0 +1,52 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +version: "2.3" + +services: + test_unit: + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + user: node + command: npm run test:unit:_run + environment: + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + + + test_acceptance: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + environment: + ELASTIC_SEARCH_DSN: es:9200 + MONGO_HOST: mongo + POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + depends_on: + mongo: + condition: service_started + user: node + command: npm run test:acceptance + + + tar: + build: . + image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER + volumes: + - ./:/tmp/build/ + command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . + user: root + mongo: + image: mongo:6.0.13 + command: --replSet overleaf + volumes: + - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js + environment: + MONGO_INITDB_DATABASE: sharelatex + extra_hosts: + # Required when using the automatic database setup for initializing the + # replica set. This override is not needed when running the setup after + # starting up mongo. + - mongo:127.0.0.1 diff --git a/services/references/docker-compose.yml b/services/references/docker-compose.yml new file mode 100644 index 0000000000..ad71431768 --- /dev/null +++ b/services/references/docker-compose.yml @@ -0,0 +1,56 @@ +# This file was auto-generated, do not edit it directly. +# Instead run bin/update_build_scripts from +# https://github.com/overleaf/internal/ + +version: "2.3" + +services: + test_unit: + image: node:20.18.2 + volumes: + - .:/overleaf/services/references + - ../../node_modules:/overleaf/node_modules + - ../../libraries:/overleaf/libraries + working_dir: /overleaf/services/references + environment: + MOCHA_GREP: ${MOCHA_GREP} + LOG_LEVEL: ${LOG_LEVEL:-} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + command: npm run --silent test:unit + user: node + + test_acceptance: + image: node:20.18.2 + volumes: + - .:/overleaf/services/references + - ../../node_modules:/overleaf/node_modules + - ../../libraries:/overleaf/libraries + working_dir: /overleaf/services/references + environment: + ELASTIC_SEARCH_DSN: es:9200 + MONGO_HOST: mongo + POSTGRES_HOST: postgres + MOCHA_GREP: ${MOCHA_GREP} + LOG_LEVEL: ${LOG_LEVEL:-} + NODE_ENV: test + NODE_OPTIONS: "--unhandled-rejections=strict" + user: node + depends_on: + mongo: + condition: service_started + command: npm run --silent test:acceptance + + mongo: + image: mongo:6.0.13 + command: --replSet overleaf + volumes: + - ../../bin/shared/mongodb-init-replica-set.js:/docker-entrypoint-initdb.d/mongodb-init-replica-set.js + environment: + MONGO_INITDB_DATABASE: sharelatex + extra_hosts: + # Required when using the automatic database setup for initializing the + # replica set. This override is not needed when running the setup after + # starting up mongo. + - mongo:127.0.0.1 + diff --git a/services/references/package.json b/services/references/package.json new file mode 100644 index 0000000000..9b0988e7ac --- /dev/null +++ b/services/references/package.json @@ -0,0 +1,26 @@ +{ + "name": "@overleaf/references", + "description": "An API for providing citation-keys", + "private": true, + "type": "module", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "version": "0.1.0", + "dependencies": { + "@overleaf/settings": "*", + "@overleaf/logger": "*", + "@overleaf/metrics": "*", + "async": "^3.2.5", + "express": "^4.21.2" + }, + "devDependencies": { + "chai": "^4.3.6", + "chai-as-promised": "^7.1.1", + "esmock": "^2.6.9", + "mocha": "^11.1.0", + "sinon": "^9.2.4", + "typescript": "^5.0.4" + } +} diff --git a/services/references/tsconfig.json b/services/references/tsconfig.json new file mode 100644 index 0000000000..d3fdd3022a --- /dev/null +++ b/services/references/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.backend.json", + "include": [ + "app.js", + "app/js/**/*", + "benchmarks/**/*", + "config/**/*", + "scripts/**/*", + "test/**/*", + "types" + ] +} diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 72db6cef36..219635f532 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -264,6 +264,9 @@ module.exports = { notifications: { url: `http://${process.env.NOTIFICATIONS_HOST || '127.0.0.1'}:3042`, }, + references: { + url: `http://${process.env.REFERENCES_HOST || '127.0.0.1'}:3056`, + }, webpack: { url: `http://${process.env.WEBPACK_HOST || '127.0.0.1'}:3808`, }, From 6282e4b0eb73ef3b509bfb38bbf3ddca503e4e0e Mon Sep 17 00:00:00 2001 From: Sam Van den Vonder Date: Wed, 4 Dec 2024 08:01:22 +0100 Subject: [PATCH 031/595] Enable Sandboxed Compiles feature --- server-ce/config/settings.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/server-ce/config/settings.js b/server-ce/config/settings.js index a7e8219858..c95d2b4fb4 100644 --- a/server-ce/config/settings.js +++ b/server-ce/config/settings.js @@ -464,6 +464,41 @@ switch (process.env.OVERLEAF_FILESTORE_BACKEND) { } } +// Overleaf Extended CE Compiler options to enable sandboxed compiles. +// ----------- +if (process.env.SANDBOXED_COMPILES === 'true') { + settings.clsi = { + ...settings.clsi, + dockerRunner: true, + docker: { + image: process.env.TEX_LIVE_DOCKER_IMAGE, + env: { + HOME: '/tmp', + PATH: + process.env.COMPILER_PATH || + '/usr/local/bin:/usr/bin:/bin', + }, + user: 'www-data', + } + } + + if (settings.path == null) { + settings.path = {} + } + settings.path.synctexBaseDir = () => '/compile' + if (process.env.SANDBOXED_COMPILES_SIBLING_CONTAINERS === 'true') { + console.log('Using sibling containers for sandboxed compiles') + if (process.env.SANDBOXED_COMPILES_HOST_DIR) { + settings.path.sandboxedCompilesHostDir = + process.env.SANDBOXED_COMPILES_HOST_DIR + } else { + console.error( + 'Sibling containers, but SANDBOXED_COMPILES_HOST_DIR not set' + ) + } + } +} + // With lots of incoming and outgoing HTTP connections to different services, // sometimes long running, it is a good idea to increase the default number // of sockets that Node will hold open. From 6f8c951b7da3b6e0431216e8936848ee6933c745 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Fri, 6 Dec 2024 12:45:15 +0100 Subject: [PATCH 032/595] Allow selecting a TeX Live image for a project --- server-ce/config/settings.js | 8 +------- services/clsi/app/js/DockerRunner.js | 4 ++-- .../web/app/src/Features/Project/ProjectEditorHandler.js | 5 +---- .../app/src/Features/Project/ProjectOptionsHandler.js | 3 +-- services/web/config/settings.defaults.js | 9 +++++++++ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/server-ce/config/settings.js b/server-ce/config/settings.js index c95d2b4fb4..02e11483dc 100644 --- a/server-ce/config/settings.js +++ b/server-ce/config/settings.js @@ -472,13 +472,7 @@ if (process.env.SANDBOXED_COMPILES === 'true') { dockerRunner: true, docker: { image: process.env.TEX_LIVE_DOCKER_IMAGE, - env: { - HOME: '/tmp', - PATH: - process.env.COMPILER_PATH || - '/usr/local/bin:/usr/bin:/bin', - }, - user: 'www-data', + user: process.env.TEX_LIVE_DOCKER_USER || 'www-data', } } diff --git a/services/clsi/app/js/DockerRunner.js b/services/clsi/app/js/DockerRunner.js index def02eaf5b..97053c1875 100644 --- a/services/clsi/app/js/DockerRunner.js +++ b/services/clsi/app/js/DockerRunner.js @@ -232,8 +232,8 @@ const DockerRunner = { } } // set the path based on the image year - const match = image.match(/:([0-9]+)\.[0-9]+/) - const year = match ? match[1] : '2014' + const match = image.match(/:([0-9]+)\.[0-9]+|:TL([0-9]+)/) + const year = match ? match[1] || match[2] : '2014' env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/` const options = { Cmd: command, diff --git a/services/web/app/src/Features/Project/ProjectEditorHandler.js b/services/web/app/src/Features/Project/ProjectEditorHandler.js index cd37c0f6d0..40fd787e71 100644 --- a/services/web/app/src/Features/Project/ProjectEditorHandler.js +++ b/services/web/app/src/Features/Project/ProjectEditorHandler.js @@ -22,10 +22,7 @@ module.exports = ProjectEditorHandler = { deletedByExternalDataSource: project.deletedByExternalDataSource || false, members: [], invites: this.buildInvitesView(invites), - imageName: - project.imageName != null - ? Path.basename(project.imageName) - : undefined, + imageName: project.imageName, } ;({ owner, ownerFeatures, members } = diff --git a/services/web/app/src/Features/Project/ProjectOptionsHandler.js b/services/web/app/src/Features/Project/ProjectOptionsHandler.js index c0c11c396c..5d0001bcf4 100644 --- a/services/web/app/src/Features/Project/ProjectOptionsHandler.js +++ b/services/web/app/src/Features/Project/ProjectOptionsHandler.js @@ -24,7 +24,6 @@ const ProjectOptionsHandler = { if (!imageName || !Array.isArray(settings.allowedImageNames)) { return } - imageName = imageName.toLowerCase() const isAllowed = settings.allowedImageNames.find( allowed => imageName === allowed.imageName ) @@ -32,7 +31,7 @@ const ProjectOptionsHandler = { throw new Error(`invalid imageName: ${imageName}`) } const conditions = { _id: projectId } - const update = { imageName: settings.imageRoot + '/' + imageName } + const update = { imageName: imageName } return Project.updateOne(conditions, update, {}) }, diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 219635f532..a4175f8b36 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1035,6 +1035,15 @@ module.exports = { managedUsers: { enabled: false, }, + + allowedImageNames: process.env.SANDBOXED_COMPILES === 'true' + ? parseTextExtensions(process.env.ALL_TEX_LIVE_DOCKER_IMAGES) + .map((imageName, index) => ({ + imageName, + imageDesc: parseTextExtensions(process.env.ALL_TEX_LIVE_DOCKER_IMAGE_NAMES)[index] + || imageName.split(':')[1], + })) + : undefined, } module.exports.mergeWith = function (overrides) { From 504590d1298da48a60ff39680639a013ea6b0bee Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Wed, 11 Dec 2024 03:55:41 +0100 Subject: [PATCH 033/595] Enable Symbol Palette --- services/web/config/settings.defaults.js | 3 +- .../components/symbol-palette-body.js | 61 ++ .../components/symbol-palette-close-button.js | 18 + .../components/symbol-palette-content.js | 94 ++ .../components/symbol-palette-info-link.js | 29 + .../components/symbol-palette-item.js | 67 ++ .../components/symbol-palette-items.js | 86 ++ .../components/symbol-palette-search.js | 44 + .../components/symbol-palette-tabs.js | 22 + .../components/symbol-palette.js | 8 + .../features/symbol-palette/data/symbols.json | 872 ++++++++++++++++++ .../symbol-palette/utils/categories.js | 44 + services/web/modules/symbol-palette/index.mjs | 2 + services/web/package.json | 1 + 14 files changed, 1350 insertions(+), 1 deletion(-) create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-info-link.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js create mode 100644 services/web/frontend/js/features/symbol-palette/components/symbol-palette.js create mode 100644 services/web/frontend/js/features/symbol-palette/data/symbols.json create mode 100644 services/web/frontend/js/features/symbol-palette/utils/categories.js create mode 100644 services/web/modules/symbol-palette/index.mjs diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index a4175f8b36..7617f23127 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -974,7 +974,7 @@ module.exports = { pdfPreviewPromotions: [], diagnosticActions: [], sourceEditorCompletionSources: [], - sourceEditorSymbolPalette: [], + sourceEditorSymbolPalette: ['@/features/symbol-palette/components/symbol-palette'], sourceEditorToolbarComponents: [], mainEditorLayoutModals: [], langFeedbackLinkingWidgets: [], @@ -1008,6 +1008,7 @@ module.exports = { 'launchpad', 'server-ce-scripts', 'user-activate', + 'symbol-palette', 'track-changes', ], viewIncludes: {}, diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js new file mode 100644 index 0000000000..c4f47e325d --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js @@ -0,0 +1,61 @@ +import { TabPanels, TabPanel } from '@reach/tabs' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import SymbolPaletteItems from './symbol-palette-items' + +export default function SymbolPaletteBody({ + categories, + categorisedSymbols, + filteredSymbols, + handleSelect, + focusInput, +}) { + const { t } = useTranslation() + + // searching with matches: show the matched symbols + // searching with no matches: show a message + // note: include empty tab panels so that aria-controls on tabs can still reference the panel ids + if (filteredSymbols) { + return ( + <> + {filteredSymbols.length ? ( + + ) : ( +
{t('no_symbols_found')}
+ )} + + + {categories.map(category => ( + + ))} + + + ) + } + + // not searching: show the symbols grouped by category + return ( + + {categories.map(category => ( + + + + ))} + + ) +} +SymbolPaletteBody.propTypes = { + categories: PropTypes.arrayOf(PropTypes.object).isRequired, + categorisedSymbols: PropTypes.object, + filteredSymbols: PropTypes.arrayOf(PropTypes.object), + handleSelect: PropTypes.func.isRequired, + focusInput: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js new file mode 100644 index 0000000000..c472c31586 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js @@ -0,0 +1,18 @@ +import { Button } from 'react-bootstrap' +import { useEditorContext } from '../../../shared/context/editor-context' + +export default function SymbolPaletteCloseButton() { + const { toggleSymbolPalette } = useEditorContext() + + return ( + + ) +} + diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js new file mode 100644 index 0000000000..8537e14585 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js @@ -0,0 +1,94 @@ +import { Tabs } from '@reach/tabs' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import { matchSorter } from 'match-sorter' + +import symbols from '../data/symbols.json' +import { buildCategorisedSymbols, createCategories } from '../utils/categories' +import SymbolPaletteSearch from './symbol-palette-search' +import SymbolPaletteBody from './symbol-palette-body' +import SymbolPaletteTabs from './symbol-palette-tabs' +// import SymbolPaletteInfoLink from './symbol-palette-info-link' +import SymbolPaletteCloseButton from './symbol-palette-close-button' + +import '@reach/tabs/styles.css' + +export default function SymbolPaletteContent({ handleSelect }) { + const [input, setInput] = useState('') + + const { t } = useTranslation() + + // build the list of categories with translated labels + const categories = useMemo(() => createCategories(t), [t]) + + // group the symbols by category + const categorisedSymbols = useMemo( + () => buildCategorisedSymbols(categories), + [categories] + ) + + // select symbols which match the input + const filteredSymbols = useMemo(() => { + if (input === '') { + return null + } + + const words = input.trim().split(/\s+/) + + return words.reduceRight( + (symbols, word) => + matchSorter(symbols, word, { + keys: ['command', 'description', 'character', 'aliases'], + threshold: matchSorter.rankings.CONTAINS, + }), + symbols + ) + }, [input]) + + const inputRef = useRef(null) + + // allow the input to be focused + const focusInput = useCallback(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + // focus the input when the symbol palette is opened + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + } + }, []) + + return ( + +
+
+
+ +
+ {/* Useless button (uncomment if you see any sense in it) */} + {/* */} + +
+
+ +
+
+ +
+
+
+ ) +} +SymbolPaletteContent.propTypes = { + handleSelect: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-info-link.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-info-link.js new file mode 100644 index 0000000000..ba56cf2b10 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-info-link.js @@ -0,0 +1,29 @@ +import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' + +export default function SymbolPaletteInfoLink() { + const { t } = useTranslation() + + return ( + + {t('find_out_more_about_latex_symbols')} + + } + > + + + ) +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js new file mode 100644 index 0000000000..a892f33cf8 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js @@ -0,0 +1,67 @@ +import { useEffect, useRef } from 'react' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import PropTypes from 'prop-types' + +export default function SymbolPaletteItem({ + focused, + handleSelect, + handleKeyDown, + symbol, +}) { + const buttonRef = useRef(null) + + // call focus() on this item when appropriate + useEffect(() => { + if ( + focused && + buttonRef.current && + document.activeElement?.closest('.symbol-palette-items') + ) { + buttonRef.current.focus() + } + }, [focused]) + + return ( + +
+ {symbol.description} +
+
{symbol.command}
+ {symbol.notes && ( +
{symbol.notes}
+ )} + + } + > + +
+ ) +} +SymbolPaletteItem.propTypes = { + symbol: PropTypes.shape({ + codepoint: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + command: PropTypes.string.isRequired, + character: PropTypes.string.isRequired, + notes: PropTypes.string, + }), + handleKeyDown: PropTypes.func.isRequired, + handleSelect: PropTypes.func.isRequired, + focused: PropTypes.bool, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js new file mode 100644 index 0000000000..44835261f5 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js @@ -0,0 +1,86 @@ +import { useCallback, useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import SymbolPaletteItem from './symbol-palette-item' + +export default function SymbolPaletteItems({ + items, + handleSelect, + focusInput, +}) { + const [focusedIndex, setFocusedIndex] = useState(0) + + // reset the focused item when the list of items changes + useEffect(() => { + setFocusedIndex(0) + }, [items]) + + // navigate through items with left and right arrows + const handleKeyDown = useCallback( + event => { + if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) { + return + } + + switch (event.key) { + // focus previous item + case 'ArrowLeft': + case 'ArrowUp': + setFocusedIndex(index => (index > 0 ? index - 1 : items.length - 1)) + break + + // focus next item + case 'ArrowRight': + case 'ArrowDown': + setFocusedIndex(index => (index < items.length - 1 ? index + 1 : 0)) + break + + // focus first item + case 'Home': + setFocusedIndex(0) + break + + // focus last item + case 'End': + setFocusedIndex(items.length - 1) + break + + // allow the default action + case 'Enter': + case ' ': + break + + // any other key returns focus to the input + default: + focusInput() + break + } + }, + [focusInput, items.length] + ) + + return ( +
+ {items.map((symbol, index) => ( + { + handleSelect(symbol) + setFocusedIndex(index) + }} + handleKeyDown={handleKeyDown} + focused={index === focusedIndex} + /> + ))} +
+ ) +} +SymbolPaletteItems.propTypes = { + items: PropTypes.arrayOf( + PropTypes.shape({ + codepoint: PropTypes.string.isRequired, + }) + ).isRequired, + handleSelect: PropTypes.func.isRequired, + focusInput: PropTypes.func.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js new file mode 100644 index 0000000000..cf5a1eb2a7 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js @@ -0,0 +1,44 @@ +import { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import { FormControl } from 'react-bootstrap' +import useDebounce from '../../../shared/hooks/use-debounce' + +export default function SymbolPaletteSearch({ setInput, inputRef }) { + const [localInput, setLocalInput] = useState('') + + // debounce the search input until a typing delay + const debouncedLocalInput = useDebounce(localInput, 250) + + useEffect(() => { + setInput(debouncedLocalInput) + }, [debouncedLocalInput, setInput]) + + const { t } = useTranslation() + + const inputRefCallback = useCallback( + element => { + inputRef.current = element + }, + [inputRef] + ) + + return ( + { + setLocalInput(event.target.value) + }} + /> + ) +} +SymbolPaletteSearch.propTypes = { + setInput: PropTypes.func.isRequired, + inputRef: PropTypes.object.isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js new file mode 100644 index 0000000000..d53cd93ac0 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js @@ -0,0 +1,22 @@ +import { TabList, Tab } from '@reach/tabs' +import PropTypes from 'prop-types' + +export default function SymbolPaletteTabs({ categories }) { + return ( + + {categories.map(category => ( + + {category.label} + + ))} + + ) +} +SymbolPaletteTabs.propTypes = { + categories: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, +} diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js new file mode 100644 index 0000000000..2f1cc5e8c8 --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js @@ -0,0 +1,8 @@ +import SymbolPaletteContent from './symbol-palette-content' + +export default function SymbolPalette() { + const handleSelect = (symbol) => { + window.dispatchEvent(new CustomEvent('editor:insert-symbol', { detail: symbol })) + } + return +} diff --git a/services/web/frontend/js/features/symbol-palette/data/symbols.json b/services/web/frontend/js/features/symbol-palette/data/symbols.json new file mode 100644 index 0000000000..af160b3eed --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/data/symbols.json @@ -0,0 +1,872 @@ +[ + { + "category": "Greek", + "command": "\\alpha", + "codepoint": "U+1D6FC", + "description": "Lowercase Greek letter alpha", + "aliases": ["a", "α"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\beta", + "codepoint": "U+1D6FD", + "description": "Lowercase Greek letter beta", + "aliases": ["b", "β"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\gamma", + "codepoint": "U+1D6FE", + "description": "Lowercase Greek letter gamma", + "aliases": ["γ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\delta", + "codepoint": "U+1D6FF", + "description": "Lowercase Greek letter delta", + "aliases": ["δ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\varepsilon", + "codepoint": "U+1D700", + "description": "Lowercase Greek letter epsilon, varepsilon", + "aliases": ["ε"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\epsilon", + "codepoint": "U+1D716", + "description": "Lowercase Greek letter epsilon lunate", + "aliases": ["ε"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\zeta", + "codepoint": "U+1D701", + "description": "Lowercase Greek letter zeta", + "aliases": ["ζ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\eta", + "codepoint": "U+1D702", + "description": "Lowercase Greek letter eta", + "aliases": ["η"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\vartheta", + "codepoint": "U+1D717", + "description": "Lowercase Greek letter curly theta, vartheta", + "aliases": ["θ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\theta", + "codepoint": "U+1D703", + "description": "Lowercase Greek letter theta", + "aliases": ["θ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\iota", + "codepoint": "U+1D704", + "description": "Lowercase Greek letter iota", + "aliases": ["ι"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\kappa", + "codepoint": "U+1D705", + "description": "Lowercase Greek letter kappa", + "aliases": ["κ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\lambda", + "codepoint": "U+1D706", + "description": "Lowercase Greek letter lambda", + "aliases": ["λ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\mu", + "codepoint": "U+1D707", + "description": "Lowercase Greek letter mu", + "aliases": ["μ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\nu", + "codepoint": "U+1D708", + "description": "Lowercase Greek letter nu", + "aliases": ["ν"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\xi", + "codepoint": "U+1D709", + "description": "Lowercase Greek letter xi", + "aliases": ["ξ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\pi", + "codepoint": "U+1D70B", + "description": "Lowercase Greek letter pi", + "aliases": ["π"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\varrho", + "codepoint": "U+1D71A", + "description": "Lowercase Greek letter rho, varrho", + "aliases": ["ρ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\rho", + "codepoint": "U+1D70C", + "description": "Lowercase Greek letter rho", + "aliases": ["ρ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\sigma", + "codepoint": "U+1D70E", + "description": "Lowercase Greek letter sigma", + "aliases": ["σ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\varsigma", + "codepoint": "U+1D70D", + "description": "Lowercase Greek letter final sigma, varsigma", + "aliases": ["ς"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\tau", + "codepoint": "U+1D70F", + "description": "Lowercase Greek letter tau", + "aliases": ["τ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\upsilon", + "codepoint": "U+1D710", + "description": "Lowercase Greek letter upsilon", + "aliases": ["υ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\phi", + "codepoint": "U+1D719", + "description": "Lowercase Greek letter phi", + "aliases": ["φ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\varphi", + "codepoint": "U+1D711", + "description": "Lowercase Greek letter phi, varphi", + "aliases": ["φ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\chi", + "codepoint": "U+1D712", + "description": "Lowercase Greek letter chi", + "aliases": ["χ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\psi", + "codepoint": "U+1D713", + "description": "Lowercase Greek letter psi", + "aliases": ["ψ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\omega", + "codepoint": "U+1D714", + "description": "Lowercase Greek letter omega", + "aliases": ["ω"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Gamma", + "codepoint": "U+00393", + "description": "Uppercase Greek letter Gamma", + "aliases": ["Γ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Delta", + "codepoint": "U+00394", + "description": "Uppercase Greek letter Delta", + "aliases": ["Δ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Theta", + "codepoint": "U+00398", + "description": "Uppercase Greek letter Theta", + "aliases": ["Θ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Lambda", + "codepoint": "U+0039B", + "description": "Uppercase Greek letter Lambda", + "aliases": ["Λ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Xi", + "codepoint": "U+0039E", + "description": "Uppercase Greek letter Xi", + "aliases": ["Ξ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Pi", + "codepoint": "U+003A0", + "description": "Uppercase Greek letter Pi", + "aliases": ["Π"], + "notes": "Use \\prod for the product." + }, + { + "category": "Greek", + "command": "\\Sigma", + "codepoint": "U+003A3", + "description": "Uppercase Greek letter Sigma", + "aliases": ["Σ"], + "notes": "Use \\sum for the sum." + }, + { + "category": "Greek", + "command": "\\Upsilon", + "codepoint": "U+003A5", + "description": "Uppercase Greek letter Upsilon", + "aliases": ["Υ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Phi", + "codepoint": "U+003A6", + "description": "Uppercase Greek letter Phi", + "aliases": ["Φ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Psi", + "codepoint": "U+003A8", + "description": "Uppercase Greek letter Psi", + "aliases": ["Ψ"], + "notes": "" + }, + { + "category": "Greek", + "command": "\\Omega", + "codepoint": "U+003A9", + "description": "Uppercase Greek letter Omega", + "aliases": ["Ω"], + "notes": "" + }, + { + "category": "Relations", + "command": "\\neq", + "codepoint": "U+02260", + "description": "Not equal", + "aliases": ["!="], + "notes": "" + }, + { + "category": "Relations", + "command": "\\leq", + "codepoint": "U+02264", + "description": "Less than or equal", + "aliases": ["<="], + "notes": "" + }, + { + "category": "Relations", + "command": "\\geq", + "codepoint": "U+02265", + "description": "Greater than or equal", + "aliases": [">="], + "notes": "" + }, + { + "category": "Relations", + "command": "\\ll", + "codepoint": "U+0226A", + "description": "Much less than", + "aliases": ["<<"], + "notes": "" + }, + { + "category": "Relations", + "command": "\\gg", + "codepoint": "U+0226B", + "description": "Much greater than", + "aliases": [">>"], + "notes": "" + }, + { + "category": "Relations", + "command": "\\prec", + "codepoint": "U+0227A", + "description": "Precedes", + "notes": "" + }, + { + "category": "Relations", + "command": "\\succ", + "codepoint": "U+0227B", + "description": "Succeeds", + "notes": "" + }, + { + "category": "Relations", + "command": "\\in", + "codepoint": "U+02208", + "description": "Set membership", + "notes": "" + }, + { + "category": "Relations", + "command": "\\notin", + "codepoint": "U+02209", + "description": "Negated set membership", + "notes": "" + }, + { + "category": "Relations", + "command": "\\ni", + "codepoint": "U+0220B", + "description": "Contains", + "notes": "" + }, + { + "category": "Relations", + "command": "\\subset", + "codepoint": "U+02282", + "description": "Subset", + "notes": "" + }, + { + "category": "Relations", + "command": "\\subseteq", + "codepoint": "U+02286", + "description": "Subset or equals", + "notes": "" + }, + { + "category": "Relations", + "command": "\\supset", + "codepoint": "U+02283", + "description": "Superset", + "notes": "" + }, + { + "category": "Relations", + "command": "\\simeq", + "codepoint": "U+02243", + "description": "Similar", + "notes": "" + }, + { + "category": "Relations", + "command": "\\approx", + "codepoint": "U+02248", + "description": "Approximate", + "notes": "" + }, + { + "category": "Relations", + "command": "\\equiv", + "codepoint": "U+02261", + "description": "Identical with", + "notes": "" + }, + { + "category": "Relations", + "command": "\\cong", + "codepoint": "U+02245", + "description": "Congruent with", + "notes": "" + }, + { + "category": "Relations", + "command": "\\mid", + "codepoint": "U+02223", + "description": "Mid, divides, vertical bar, modulus, absolute value", + "notes": "Use \\lvert...\\rvert for the absolute value." + }, + { + "category": "Relations", + "command": "\\nmid", + "codepoint": "U+02224", + "description": "Negated mid, not divides", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Relations", + "command": "\\parallel", + "codepoint": "U+02225", + "description": "Parallel, double vertical bar, norm", + "notes": "Use \\lVert...\\rVert for the norm." + }, + { + "category": "Relations", + "command": "\\perp", + "codepoint": "U+027C2", + "description": "Perpendicular", + "notes": "" + }, + { + "category": "Operators", + "command": "\\times", + "codepoint": "U+000D7", + "description": "Cross product, multiplication", + "aliases": ["x"], + "notes": "" + }, + { + "category": "Operators", + "command": "\\div", + "codepoint": "U+000F7", + "description": "Division", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cap", + "codepoint": "U+02229", + "description": "Intersection", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cup", + "codepoint": "U+0222A", + "description": "Union", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cdot", + "codepoint": "U+022C5", + "description": "Dot product, multiplication", + "notes": "" + }, + { + "category": "Operators", + "command": "\\cdots", + "codepoint": "U+022EF", + "description": "Centered dots", + "notes": "" + }, + { + "category": "Operators", + "command": "\\bullet", + "codepoint": "U+02219", + "description": "Bullet", + "notes": "" + }, + { + "category": "Operators", + "command": "\\circ", + "codepoint": "U+025E6", + "description": "Circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\wedge", + "codepoint": "U+02227", + "description": "Wedge, logical and", + "notes": "" + }, + { + "category": "Operators", + "command": "\\vee", + "codepoint": "U+02228", + "description": "Vee, logical or", + "notes": "" + }, + { + "category": "Operators", + "command": "\\setminus", + "codepoint": "U+0005C", + "description": "Set minus, backslash", + "notes": "Use \\backslash for a backslash." + }, + { + "category": "Operators", + "command": "\\oplus", + "codepoint": "U+02295", + "description": "Plus sign in circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\otimes", + "codepoint": "U+02297", + "description": "Multiply sign in circle", + "notes": "" + }, + { + "category": "Operators", + "command": "\\sum", + "codepoint": "U+02211", + "description": "Summation operator", + "notes": "Use \\Sigma for the letter Sigma." + }, + { + "category": "Operators", + "command": "\\prod", + "codepoint": "U+0220F", + "description": "Product operator", + "notes": "Use \\Pi for the letter Pi." + }, + { + "category": "Operators", + "command": "\\bigcap", + "codepoint": "U+022C2", + "description": "Intersection operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\bigcup", + "codepoint": "U+022C3", + "description": "Union operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\int", + "codepoint": "U+0222B", + "description": "Integral operator", + "notes": "" + }, + { + "category": "Operators", + "command": "\\iint", + "codepoint": "U+0222C", + "description": "Double integral operator", + "notes": "Requires \\usepackage{amsmath}." + }, + { + "category": "Operators", + "command": "\\iiint", + "codepoint": "U+0222D", + "description": "Triple integral operator", + "notes": "Requires \\usepackage{amsmath}." + }, + { + "category": "Arrows", + "command": "\\leftarrow", + "codepoint": "U+02190", + "description": "Leftward arrow", + "aliases": ["<-"], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightarrow", + "codepoint": "U+02192", + "description": "Rightward arrow", + "aliases": ["->"], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftrightarrow", + "codepoint": "U+02194", + "description": "Left and right arrow", + "aliases": ["<->"], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\uparrow", + "codepoint": "U+02191", + "description": "Upward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\downarrow", + "codepoint": "U+02193", + "description": "Downward arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Leftarrow", + "codepoint": "U+021D0", + "description": "Is implied by", + "aliases": ["<="], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Rightarrow", + "codepoint": "U+021D2", + "description": "Implies", + "aliases": ["=>"], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\Leftrightarrow", + "codepoint": "U+021D4", + "description": "Left and right double arrow", + "aliases": ["<=>"], + "notes": "" + }, + { + "category": "Arrows", + "command": "\\mapsto", + "codepoint": "U+021A6", + "description": "Maps to, rightward", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\nearrow", + "codepoint": "U+02197", + "description": "NE pointing arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\searrow", + "codepoint": "U+02198", + "description": "SE pointing arrow", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightleftharpoons", + "codepoint": "U+021CC", + "description": "Right harpoon over left", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftharpoonup", + "codepoint": "U+021BC", + "description": "Left harpoon up", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightharpoonup", + "codepoint": "U+021C0", + "description": "Right harpoon up", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\leftharpoondown", + "codepoint": "U+021BD", + "description": "Left harpoon down", + "notes": "" + }, + { + "category": "Arrows", + "command": "\\rightharpoondown", + "codepoint": "U+021C1", + "description": "Right harpoon down", + "notes": "" + }, + { + "category": "Misc", + "command": "\\infty", + "codepoint": "U+0221E", + "description": "Infinity", + "notes": "" + }, + { + "category": "Misc", + "command": "\\partial", + "codepoint": "U+1D715", + "description": "Partial differential", + "notes": "" + }, + { + "category": "Misc", + "command": "\\nabla", + "codepoint": "U+02207", + "description": "Nabla, del, hamilton operator", + "notes": "" + }, + { + "category": "Misc", + "command": "\\varnothing", + "codepoint": "U+02300", + "description": "Empty set", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Misc", + "command": "\\forall", + "codepoint": "U+02200", + "description": "For all", + "notes": "" + }, + { + "category": "Misc", + "command": "\\exists", + "codepoint": "U+02203", + "description": "There exists", + "notes": "" + }, + { + "category": "Misc", + "command": "\\neg", + "codepoint": "U+000AC", + "description": "Not sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Re", + "codepoint": "U+0211C", + "description": "Real part", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Im", + "codepoint": "U+02111", + "description": "Imaginary part", + "notes": "" + }, + { + "category": "Misc", + "command": "\\Box", + "codepoint": "U+025A1", + "description": "Square", + "notes": "Requires \\usepackage{amssymb}." + }, + { + "category": "Misc", + "command": "\\triangle", + "codepoint": "U+025B3", + "description": "Triangle", + "notes": "" + }, + { + "category": "Misc", + "command": "\\aleph", + "codepoint": "U+02135", + "description": "Hebrew letter aleph", + "notes": "" + }, + { + "category": "Misc", + "command": "\\wp", + "codepoint": "U+02118", + "description": "Weierstrass letter p", + "notes": "" + }, + { + "category": "Misc", + "command": "\\#", + "codepoint": "U+00023", + "description": "Number sign, hashtag", + "notes": "" + }, + { + "category": "Misc", + "command": "\\$", + "codepoint": "U+00024", + "description": "Dollar sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\%", + "codepoint": "U+00025", + "description": "Percent sign", + "notes": "" + }, + { + "category": "Misc", + "command": "\\&", + "codepoint": "U+00026", + "description": "Et sign, and, ampersand", + "notes": "" + }, + { + "category": "Misc", + "command": "\\{", + "codepoint": "U+0007B", + "description": "Left curly brace", + "notes": "" + }, + { + "category": "Misc", + "command": "\\}", + "codepoint": "U+0007D", + "description": "Right curly brace", + "notes": "" + }, + { + "category": "Misc", + "command": "\\langle", + "codepoint": "U+027E8", + "description": "Left angle bracket, bra", + "notes": "" + }, + { + "category": "Misc", + "command": "\\rangle", + "codepoint": "U+027E9", + "description": "Right angle bracket, ket", + "notes": "" + } +] diff --git a/services/web/frontend/js/features/symbol-palette/utils/categories.js b/services/web/frontend/js/features/symbol-palette/utils/categories.js new file mode 100644 index 0000000000..872534771f --- /dev/null +++ b/services/web/frontend/js/features/symbol-palette/utils/categories.js @@ -0,0 +1,44 @@ +import symbols from '../data/symbols.json' +export function createCategories(t) { + return [ + { + id: 'Greek', + label: t('category_greek'), + }, + { + id: 'Arrows', + label: t('category_arrows'), + }, + { + id: 'Operators', + label: t('category_operators'), + }, + { + id: 'Relations', + label: t('category_relations'), + }, + { + id: 'Misc', + label: t('category_misc'), + }, + ] +} + +export function buildCategorisedSymbols(categories) { + const output = {} + + for (const category of categories) { + output[category.id] = [] + } + + for (const item of symbols) { + if (item.category in output) { + item.character = String.fromCodePoint( + parseInt(item.codepoint.replace(/^U\+0*/, ''), 16) + ) + output[item.category].push(item) + } + } + + return output +} diff --git a/services/web/modules/symbol-palette/index.mjs b/services/web/modules/symbol-palette/index.mjs new file mode 100644 index 0000000000..3a412c2eec --- /dev/null +++ b/services/web/modules/symbol-palette/index.mjs @@ -0,0 +1,2 @@ +import logger from '@overleaf/logger' +logger.debug({}, 'Enable Symbol Palette') diff --git a/services/web/package.json b/services/web/package.json index 5080813d55..23737b34e3 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -206,6 +206,7 @@ "@pollyjs/adapter-node-http": "^6.0.6", "@pollyjs/core": "^6.0.6", "@pollyjs/persister-fs": "^6.0.6", + "@reach/tabs": "0.18.0", "@replit/codemirror-emacs": "overleaf/codemirror-emacs#4394c03858f27053f8768258e9493866e06e938e", "@replit/codemirror-indentation-markers": "overleaf/codemirror-indentation-markers#78264032eb286bc47871569ae87bff5ca1c6c161", "@replit/codemirror-vim": "overleaf/codemirror-vim#1bef138382d948018f3f9b8a4d7a70ab61774e4b", From 4df5135936a52889a0939f47e94321cb9f496147 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Tue, 3 Dec 2024 01:18:19 +0100 Subject: [PATCH 034/595] Enable LDAP and SAML authentication support --- patches/@node-saml+node-saml+4.0.5.patch | 23 +++ patches/ldapauth-fork+4.3.3.patch | 64 +++++++ .../AuthenticationController.js | 5 +- .../PasswordReset/PasswordResetController.mjs | 4 + .../PasswordReset/PasswordResetHandler.mjs | 4 + .../app/src/Features/User/UserController.js | 3 +- services/web/app/views/user/login.pug | 15 +- services/web/app/views/user/settings.pug | 4 +- services/web/config/settings.defaults.js | 2 + services/web/locales/en.json | 2 + .../launchpad/app/src/LaunchpadController.mjs | 3 +- .../modules/launchpad/app/views/launchpad.pug | 81 ++++++++- .../app/src/AuthenticationControllerLdap.mjs | 64 +++++++ .../app/src/AuthenticationManagerLdap.mjs | 80 +++++++++ .../app/src/InitLdapSettings.mjs | 17 ++ .../app/src/LdapContacts.mjs | 136 +++++++++++++++ .../app/src/LdapStrategy.mjs | 78 +++++++++ .../web/modules/ldap-authentication/index.mjs | 30 ++++ .../app/src/AuthenticationControllerSaml.mjs | 160 ++++++++++++++++++ .../app/src/AuthenticationManagerSaml.mjs | 60 +++++++ .../app/src/InitSamlSettings.mjs | 16 ++ .../app/src/SamlNonCsrfRouter.mjs | 12 ++ .../app/src/SamlRouter.mjs | 14 ++ .../app/src/SamlStrategy.mjs | 62 +++++++ .../web/modules/saml-authentication/index.mjs | 26 +++ 25 files changed, 954 insertions(+), 11 deletions(-) create mode 100644 patches/@node-saml+node-saml+4.0.5.patch create mode 100644 patches/ldapauth-fork+4.3.3.patch create mode 100644 services/web/modules/ldap-authentication/app/src/AuthenticationControllerLdap.mjs create mode 100644 services/web/modules/ldap-authentication/app/src/AuthenticationManagerLdap.mjs create mode 100644 services/web/modules/ldap-authentication/app/src/InitLdapSettings.mjs create mode 100644 services/web/modules/ldap-authentication/app/src/LdapContacts.mjs create mode 100644 services/web/modules/ldap-authentication/app/src/LdapStrategy.mjs create mode 100644 services/web/modules/ldap-authentication/index.mjs create mode 100644 services/web/modules/saml-authentication/app/src/AuthenticationControllerSaml.mjs create mode 100644 services/web/modules/saml-authentication/app/src/AuthenticationManagerSaml.mjs create mode 100644 services/web/modules/saml-authentication/app/src/InitSamlSettings.mjs create mode 100644 services/web/modules/saml-authentication/app/src/SamlNonCsrfRouter.mjs create mode 100644 services/web/modules/saml-authentication/app/src/SamlRouter.mjs create mode 100644 services/web/modules/saml-authentication/app/src/SamlStrategy.mjs create mode 100644 services/web/modules/saml-authentication/index.mjs diff --git a/patches/@node-saml+node-saml+4.0.5.patch b/patches/@node-saml+node-saml+4.0.5.patch new file mode 100644 index 0000000000..81fd700b31 --- /dev/null +++ b/patches/@node-saml+node-saml+4.0.5.patch @@ -0,0 +1,23 @@ +diff --git a/node_modules/@node-saml/node-saml/lib/saml.js b/node_modules/@node-saml/node-saml/lib/saml.js +index fba15b9..a5778cb 100644 +--- a/node_modules/@node-saml/node-saml/lib/saml.js ++++ b/node_modules/@node-saml/node-saml/lib/saml.js +@@ -336,7 +336,8 @@ class SAML { + const requestOrResponse = request || response; + (0, utility_1.assertRequired)(requestOrResponse, "either request or response is required"); + let buffer; +- if (this.options.skipRequestCompression) { ++ // logout requestOrResponse must be compressed anyway ++ if (this.options.skipRequestCompression && operation !== "logout") { + buffer = Buffer.from(requestOrResponse, "utf8"); + } + else { +@@ -495,7 +496,7 @@ class SAML { + try { + xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8"); + doc = await (0, xml_1.parseDomFromString)(xml); +- const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo"); ++ const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response' or local-name()='LogoutResponse']/@InResponseTo"); + if (inResponseToNodes) { + inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null; + await this.validateInResponseTo(inResponseTo); diff --git a/patches/ldapauth-fork+4.3.3.patch b/patches/ldapauth-fork+4.3.3.patch new file mode 100644 index 0000000000..4d31210c9d --- /dev/null +++ b/patches/ldapauth-fork+4.3.3.patch @@ -0,0 +1,64 @@ +diff --git a/node_modules/ldapauth-fork/lib/ldapauth.js b/node_modules/ldapauth-fork/lib/ldapauth.js +index 85ecf36a8b..a7d07e0f78 100644 +--- a/node_modules/ldapauth-fork/lib/ldapauth.js ++++ b/node_modules/ldapauth-fork/lib/ldapauth.js +@@ -69,6 +69,7 @@ function LdapAuth(opts) { + this.opts.bindProperty || (this.opts.bindProperty = 'dn'); + this.opts.groupSearchScope || (this.opts.groupSearchScope = 'sub'); + this.opts.groupDnProperty || (this.opts.groupDnProperty = 'dn'); ++ this.opts.tlsStarted = false; + + EventEmitter.call(this); + +@@ -108,21 +109,7 @@ function LdapAuth(opts) { + this._userClient.on('error', this._handleError.bind(this)); + + var self = this; +- if (this.opts.starttls) { +- // When starttls is enabled, this callback supplants the 'connect' callback +- this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function(err) { +- if (err) { +- self._handleError(err); +- } else { +- self._onConnectAdmin(); +- } +- }); +- this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function(err) { +- if (err) { +- self._handleError(err); +- } +- }); +- } else if (opts.reconnect) { ++ if (opts.reconnect && !this.opts.starttls) { + this.once('_installReconnectListener', function() { + self.log && self.log.trace('install reconnect listener'); + self._adminClient.on('connect', function() { +@@ -384,6 +371,28 @@ LdapAuth.prototype._findGroups = function(user, callback) { + */ + LdapAuth.prototype.authenticate = function(username, password, callback) { + var self = this; ++ if (this.opts.starttls && !this.opts.tlsStarted) { ++ // When starttls is enabled, this callback supplants the 'connect' callback ++ this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) { ++ if (err) { ++ self._handleError(err); ++ } else { ++ self._onConnectAdmin(function(){self._handleAuthenticate(username, password, callback);}); ++ } ++ }); ++ this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) { ++ if (err) { ++ self._handleError(err); ++ } ++ }); ++ } else { ++ self._handleAuthenticate(username, password, callback); ++ } ++}; ++ ++LdapAuth.prototype._handleAuthenticate = function (username, password, callback) { ++ this.opts.tlsStarted = true; ++ var self = this; + + if (typeof password === 'undefined' || password === null || password === '') { + return callback(new Error('no password given')); diff --git a/services/web/app/src/Features/Authentication/AuthenticationController.js b/services/web/app/src/Features/Authentication/AuthenticationController.js index 7a97d2ac9c..983526006e 100644 --- a/services/web/app/src/Features/Authentication/AuthenticationController.js +++ b/services/web/app/src/Features/Authentication/AuthenticationController.js @@ -102,9 +102,9 @@ const AuthenticationController = { // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, // and send a `{redir: ""}` response on success passport.authenticate( - 'local', + Settings.ldap?.enable ? ['custom-fail-ldapauth','local'] : ['local'], { keepSessionInfo: true }, - async function (err, user, info) { + async function (err, user, infoArray) { if (err) { return next(err) } @@ -126,6 +126,7 @@ const AuthenticationController = { return next(err) } } else { + let info = infoArray[0] if (info.redir != null) { return res.json({ redir: info.redir }) } else { diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs index b7fc2da9c8..a6baf1bdc6 100644 --- a/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs +++ b/services/web/app/src/Features/PasswordReset/PasswordResetController.mjs @@ -137,6 +137,10 @@ async function requestReset(req, res, next) { return res.status(404).json({ message: req.i18n.translate('secondary_email_password_reset'), }) + } else if (status === 'external') { + return res.status(403).json({ + message: req.i18n.translate('password_managed_externally'), + }) } else { return res.status(404).json({ message: req.i18n.translate('cant_find_email'), diff --git a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs index 094f18b95f..0ac203222c 100644 --- a/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs +++ b/services/web/app/src/Features/PasswordReset/PasswordResetHandler.mjs @@ -18,6 +18,10 @@ async function generateAndEmailResetToken(email) { return null } + if (!user.hashedPassword) { + return 'external' + } + if (user.email !== email) { return 'secondary' } diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js index e4186d39a8..bbf433cccb 100644 --- a/services/web/app/src/Features/User/UserController.js +++ b/services/web/app/src/Features/User/UserController.js @@ -401,7 +401,7 @@ async function updateUserSettings(req, res, next) { if ( newEmail == null || newEmail === user.email || - req.externalAuthenticationSystemUsed() + (req.externalAuthenticationSystemUsed() && !user.hashedPassword) ) { // end here, don't update email SessionManager.setInSessionUser(req.session, { @@ -478,6 +478,7 @@ async function doLogout(req) { } async function logout(req, res, next) { + if (req?.session.saml_extce) return res.redirect(308, '/saml/logout') const requestedRedirect = req.body.redirect ? UrlHelper.getSafeRedirectPath(req.body.redirect) : undefined diff --git a/services/web/app/views/user/login.pug b/services/web/app/views/user/login.pug index 1ad77cb8b4..dd2be9fa31 100644 --- a/services/web/app/views/user/login.pug +++ b/services/web/app/views/user/login.pug @@ -6,6 +6,7 @@ block content .row .col-lg-6.offset-lg-3.col-xl-4.offset-xl-4 .card +<<<<<<< HEAD .card-body .page-header if login_support_title @@ -23,10 +24,10 @@ block content | !{translate('password_compromised_try_again_or_use_known_device_or_reset', {}, [{name: 'a', attrs: {href: 'https://haveibeenpwned.com/passwords', rel: 'noopener noreferrer', target: '_blank'}}, {name: 'a', attrs: {href: '/user/password/reset', target: '_blank'}}])}. .form-group input.form-control( - type='email', + type=(settings.ldap && settings.ldap.enable) ? 'text' : 'email', name='email', required, - placeholder='email@example.com', + placeholder=(settings.ldap && settings.ldap.enable) ? settings.ldap.placeholder : 'email@example.com', autofocus="true" ) .form-group @@ -47,4 +48,12 @@ block content if login_support_text hr p.text-center !{login_support_text} - + if settings.saml && settings.saml.enable + form(data-ol-async-form, name="samlLoginForm") + .actions(style='margin-top: 30px;') + a.btn.btn-secondary.btn-block( + href='/saml/login', + data-ol-disabled-inflight + ) + span(data-ol-inflight="idle") #{settings.saml.identityServiceName} + span(hidden data-ol-inflight="pending") #{translate("logging_in")}… diff --git a/services/web/app/views/user/settings.pug b/services/web/app/views/user/settings.pug index 4f939a41ca..f441a911ca 100644 --- a/services/web/app/views/user/settings.pug +++ b/services/web/app/views/user/settings.pug @@ -8,7 +8,7 @@ block vars block append meta meta(name="ol-hasPassword" data-type="boolean" content=hasPassword) - meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails) + meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails || hasPassword) meta(name="ol-oauthProviders", data-type="json", content=oauthProviders) meta(name="ol-institutionLinked", data-type="json", content=institutionLinked) meta(name="ol-samlError", data-type="json", content=samlError) @@ -20,7 +20,7 @@ block append meta meta(name="ol-ssoErrorMessage", content=ssoErrorMessage) meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {}) meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {}) - meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed()) + meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed() && !hasPassword) meta(name="ol-user" data-type="json" content=user) meta(name="ol-labsExperiments" data-type="json" content=labsExperiments) meta(name="ol-dropbox" data-type="json" content=dropbox) diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 7617f23127..44aa73df18 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -1010,6 +1010,8 @@ module.exports = { 'user-activate', 'symbol-palette', 'track-changes', + 'ldap-authentication', + 'saml-authentication', ], viewIncludes: {}, diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 4729f54756..0e360b7259 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -156,6 +156,7 @@ "already_have_sl_account": "Already have an __appName__ account?", "also": "Also", "alternatively_create_new_institution_account": "Alternatively, you can create a new account with your institution email (__email__) by clicking __clickText__.", + "alternatively_create_local_admin_account": "Alternatively, you can create __appName__ local admin account.", "an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__. Please wait and try again later.", "an_error_occured_while_restoring_project": "An error occured while restoring the project", "an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code", @@ -1237,6 +1238,7 @@ "loading_prices": "loading prices", "loading_recent_github_commits": "Loading recent commits", "loading_writefull": "Loading Writefull", + "local_account": "Local account", "log_entry_description": "Log entry with level: __level__", "log_entry_maximum_entries": "Maximum log entries limit hit", "log_entry_maximum_entries_enable_stop_on_first_error": "Try to fix the first error and recompile. Often one error causes many later error messages. You can <0>Enable “Stop on first error” to focus on fixing errors. We recommend fixing errors as soon as possible; letting them accumulate may lead to hard-to-debug and fatal errors. <1>Learn more", diff --git a/services/web/modules/launchpad/app/src/LaunchpadController.mjs b/services/web/modules/launchpad/app/src/LaunchpadController.mjs index b626e0176e..49dd9ea9cb 100644 --- a/services/web/modules/launchpad/app/src/LaunchpadController.mjs +++ b/services/web/modules/launchpad/app/src/LaunchpadController.mjs @@ -154,7 +154,8 @@ function registerExternalAuthAdmin(authMethod) { await User.updateOne( { _id: user._id }, { - $set: { isAdmin: true, emails: [{ email, reversedHostname }] }, + $set: { isAdmin: true, emails: [{ email, reversedHostname, 'confirmedAt' : Date.now() }] }, + $unset: { 'hashedPassword': "" }, // external-auth user must not have a hashedPassword } ).exec() } catch (err) { diff --git a/services/web/modules/launchpad/app/views/launchpad.pug b/services/web/modules/launchpad/app/views/launchpad.pug index fdf0576c4a..80215cadf2 100644 --- a/services/web/modules/launchpad/app/views/launchpad.pug +++ b/services/web/modules/launchpad/app/views/launchpad.pug @@ -29,7 +29,7 @@ block vars block append meta meta(name="ol-adminUserExists" data-type="boolean" content=adminUserExists) - meta(name="ol-ideJsPath" content=buildJsPath('ide.js')) + meta(name="ol-ideJsPath" content=buildJsPath('ide-detached.js')) block content script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js') @@ -126,6 +126,45 @@ block content span(data-ol-inflight="idle") #{translate("register")} span(hidden data-ol-inflight="pending") #{translate("registering")}… + h3 #{translate('local_account')} + p + | #{translate('alternatively_create_local_admin_account')} + + form( + data-ol-async-form + data-ol-register-admin + action="/launchpad/register_admin" + method="POST" + ) + input(name='_csrf', type='hidden', value=csrfToken) + +formMessages() + .form-group + label(for='email') #{translate("email")} + input.form-control( + type='email', + name='email', + placeholder="email@example.com" + autocomplete="username" + required, + autofocus="true" + ) + .form-group + label(for='password') #{translate("password")} + input.form-control#passwordField( + type='password', + name='password', + placeholder="********", + autocomplete="new-password" + required, + ) + .actions + button.btn-primary.btn( + type='submit' + data-ol-disabled-inflight + ) + span(data-ol-inflight="idle") #{translate("register")} + span(hidden data-ol-inflight="pending") #{translate("registering")}… + // Saml Form if authMethod === 'saml' h3 #{translate('saml')} @@ -137,6 +176,35 @@ block content data-ol-register-admin action="/launchpad/register_saml_admin" method="POST" + ) + input(name='_csrf', type='hidden', value=csrfToken) + +formMessages() + .form-group + label(for='email') #{translate("email")} + input.form-control( + name='email', + placeholder="email@example.com" + autocomplete="username" + required, + autofocus="true" + ) + .actions + button.btn-primary.btn( + type='submit' + data-ol-disabled-inflight + ) + span(data-ol-inflight="idle") #{translate("register")} + span(hidden data-ol-inflight="pending") #{translate("registering")}… + + h3 #{translate('local_account')} + p + | #{translate('alternatively_create_local_admin_account')} + + form( + data-ol-async-form + data-ol-register-admin + action="/launchpad/register_admin" + method="POST" ) input(name='_csrf', type='hidden', value=csrfToken) +formMessages() @@ -150,6 +218,15 @@ block content required, autofocus="true" ) + .form-group + label(for='password') #{translate("password")} + input.form-control#passwordField( + type='password', + name='password', + placeholder="********", + autocomplete="new-password" + required, + ) .actions button.btn-primary.btn( type='submit' @@ -220,7 +297,7 @@ block content p a(href="/admin").btn.btn-info | Go To Admin Panel - |   + p a(href="/project").btn.btn-primary | Start Using #{settings.appName} br diff --git a/services/web/modules/ldap-authentication/app/src/AuthenticationControllerLdap.mjs b/services/web/modules/ldap-authentication/app/src/AuthenticationControllerLdap.mjs new file mode 100644 index 0000000000..64fa4f5a96 --- /dev/null +++ b/services/web/modules/ldap-authentication/app/src/AuthenticationControllerLdap.mjs @@ -0,0 +1,64 @@ +import logger from '@overleaf/logger' +import LoginRateLimiter from '../../../../app/src/Features/Security/LoginRateLimiter.js' +import { handleAuthenticateErrors } from '../../../../app/src/Features/Authentication/AuthenticationErrors.js' +import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js' +import AuthenticationManagerLdap from './AuthenticationManagerLdap.mjs' + +const AuthenticationControllerLdap = { + async doPassportLdapLogin(req, ldapUser, done) { + let user, info + try { + ;({ user, info } = await AuthenticationControllerLdap._doPassportLdapLogin( + req, + ldapUser + )) + } catch (error) { + return done(error) + } + return done(undefined, user, info) + }, + async _doPassportLdapLogin(req, ldapUser) { + const { fromKnownDevice } = AuthenticationController.getAuditInfo(req) + const auditLog = { + ipAddress: req.ip, + info: { method: 'LDAP password login', fromKnownDevice }, + } + + let user, isPasswordReused + try { + user = await AuthenticationManagerLdap.promises.findOrCreateLdapUser(ldapUser, auditLog) + } catch (error) { + return { + user: false, + info: handleAuthenticateErrors(error, req), + } + } + if (user && AuthenticationController.captchaRequiredForLogin(req, user)) { + return { + user: false, + info: { + text: req.i18n.translate('cannot_verify_user_not_robot'), + type: 'error', + errorReason: 'cannot_verify_user_not_robot', + status: 400, + }, + } + } else if (user) { + // async actions + return { user, info: undefined } + } else { //something wrong + logger.debug({ email : ldapUser.mail }, 'failed LDAP log in') + return { + user: false, + info: { + type: 'error', + status: 500, + }, + } + } + }, +} + +export const { + doPassportLdapLogin, +} = AuthenticationControllerLdap diff --git a/services/web/modules/ldap-authentication/app/src/AuthenticationManagerLdap.mjs b/services/web/modules/ldap-authentication/app/src/AuthenticationManagerLdap.mjs new file mode 100644 index 0000000000..1371f76d52 --- /dev/null +++ b/services/web/modules/ldap-authentication/app/src/AuthenticationManagerLdap.mjs @@ -0,0 +1,80 @@ +import Settings from '@overleaf/settings' +import { callbackify } from '@overleaf/promise-utils' +import UserCreator from '../../../../app/src/Features/User/UserCreator.js' +import { User } from '../../../../app/src/models/User.js' + +const AuthenticationManagerLdap = { + splitFullName(fullName) { + fullName = fullName.trim(); + let lastSpaceIndex = fullName.lastIndexOf(' '); + let firstNames = fullName.substring(0, lastSpaceIndex).trim(); + let lastName = fullName.substring(lastSpaceIndex + 1).trim(); + return [firstNames, lastName]; + }, + async findOrCreateLdapUser(profile, auditLog) { + //user is already authenticated in Ldap + const { + attEmail, + attFirstName, + attLastName, + attName, + attAdmin, + valAdmin, + updateUserDetailsOnLogin, + } = Settings.ldap + + const email = Array.isArray(profile[attEmail]) + ? profile[attEmail][0].toLowerCase() + : profile[attEmail].toLowerCase() + let nameParts = ["",""] + if ((!attFirstName || !attLastName) && attName) { + nameParts = this.splitFullName(profile[attName] || "") + } + const firstName = attFirstName ? (profile[attFirstName] || "") : nameParts[0] + let lastName = attLastName ? (profile[attLastName] || "") : nameParts[1] + if (!firstName && !lastName) lastName = email + let isAdmin = false + if( attAdmin && valAdmin ) { + isAdmin = (profile._groups?.length > 0) || + (Array.isArray(profile[attAdmin]) ? profile[attAdmin].includes(valAdmin) : + profile[attAdmin] === valAdmin) + } + let user = await User.findOne({ 'email': email }).exec() + if( !user ) { + user = await UserCreator.promises.createNewUser( + { + email: email, + first_name: firstName, + last_name: lastName, + isAdmin: isAdmin, + holdingAccount: false, + } + ) + await User.updateOne( + { _id: user._id }, + { $set : { 'emails.0.confirmedAt' : Date.now() } } + ).exec() //email of ldap user is confirmed + } + let userDetails = updateUserDetailsOnLogin ? { first_name : firstName, last_name: lastName } : {} + if( attAdmin && valAdmin ) { + user.isAdmin = isAdmin + userDetails.isAdmin = isAdmin + } + const result = await User.updateOne( + { _id: user._id, loginEpoch: user.loginEpoch }, { $inc: { loginEpoch: 1 }, $set: userDetails }, + {} + ).exec() + if (result.modifiedCount !== 1) { + throw new ParallelLoginError() + } + return user + }, +} + +export default { + findOrCreateLdapUser: callbackify(AuthenticationManagerLdap.findOrCreateLdapUser), + promises: AuthenticationManagerLdap, +} +export const { + splitFullName, +} = AuthenticationManagerLdap diff --git a/services/web/modules/ldap-authentication/app/src/InitLdapSettings.mjs b/services/web/modules/ldap-authentication/app/src/InitLdapSettings.mjs new file mode 100644 index 0000000000..e7f312fc11 --- /dev/null +++ b/services/web/modules/ldap-authentication/app/src/InitLdapSettings.mjs @@ -0,0 +1,17 @@ +import Settings from '@overleaf/settings' + +function initLdapSettings() { + Settings.ldap = { + enable: true, + placeholder: process.env.OVERLEAF_LDAP_PLACEHOLDER || 'Username', + attEmail: process.env.OVERLEAF_LDAP_EMAIL_ATT || 'mail', + attFirstName: process.env.OVERLEAF_LDAP_FIRST_NAME_ATT, + attLastName: process.env.OVERLEAF_LDAP_LAST_NAME_ATT, + attName: process.env.OVERLEAF_LDAP_NAME_ATT, + attAdmin: process.env.OVERLEAF_LDAP_IS_ADMIN_ATT, + valAdmin: process.env.OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE, + updateUserDetailsOnLogin: String(process.env.OVERLEAF_LDAP_UPDATE_USER_DETAILS_ON_LOGIN ).toLowerCase() === 'true', + } +} + +export default initLdapSettings diff --git a/services/web/modules/ldap-authentication/app/src/LdapContacts.mjs b/services/web/modules/ldap-authentication/app/src/LdapContacts.mjs new file mode 100644 index 0000000000..c4093b8684 --- /dev/null +++ b/services/web/modules/ldap-authentication/app/src/LdapContacts.mjs @@ -0,0 +1,136 @@ +import Settings from '@overleaf/settings' +import logger from '@overleaf/logger' +import passport from 'passport' +import ldapjs from 'ldapauth-fork/node_modules/ldapjs/lib/index.js' +import UserGetter from '../../../../app/src/Features/User/UserGetter.js' +import { splitFullName } from './AuthenticationManagerLdap.mjs' + +async function fetchLdapContacts(userId, contacts) { + if (!Settings.ldap?.enable || !process.env.OVERLEAF_LDAP_CONTACTS_FILTER) { + return [] + } + + const ldapOpts = passport._strategy('custom-fail-ldapauth').options.server + const { attEmail, attFirstName = "", attLastName = "", attName = "" } = Settings.ldap + const { + url, + timeout, + connectTimeout, + tlsOptions, + starttls, + bindDN, + bindCredentials, + } = ldapOpts + const searchBase = process.env.OVERLEAF_LDAP_CONTACTS_SEARCH_BASE || ldapOpts.searchBase + const searchScope = process.env.OVERLEAF_LDAP_CONTACTS_SEARCH_SCOPE || 'sub' + const ldapConfig = { url, timeout, connectTimeout, tlsOptions } + + let ldapUsers + const client = ldapjs.createClient(ldapConfig) + try { + if (starttls) { + await _upgradeToTLS(client, tlsOptions) + } + await _bindLdap(client, bindDN, bindCredentials) + + const filter = await _formContactsSearchFilter(client, ldapOpts, userId, process.env.OVERLEAF_LDAP_CONTACTS_FILTER) + const searchOptions = { scope: searchScope, attributes: [attEmail, attFirstName, attLastName, attName], filter } + + ldapUsers = await _searchLdap(client, searchBase, searchOptions) + } catch (err) { + logger.warn({ err }, 'error in fetchLdapContacts') + return [] + } finally { + client.unbind() + } + + const newLdapContacts = ldapUsers.reduce((acc, ldapUser) => { + const email = Array.isArray(ldapUser[attEmail]) + ? ldapUser[attEmail][0]?.toLowerCase() + : ldapUser[attEmail]?.toLowerCase() + if (!email) return acc + if (!contacts.some(contact => contact.email === email)) { + let nameParts = ["",""] + if ((!attFirstName || !attLastName) && attName) { + nameParts = splitFullName(ldapUser[attName] || "") + } + const firstName = attFirstName ? (ldapUser[attFirstName] || "") : nameParts[0] + const lastName = attLastName ? (ldapUser[attLastName] || "") : nameParts[1] + acc.push({ + first_name: firstName, + last_name: lastName, + email: email, + type: 'user', + }) + } + return acc + }, []) + + return newLdapContacts.sort((a, b) => + a.last_name.localeCompare(b.last_name) || + a.first_name.localeCompare(a.first_name) || + a.email.localeCompare(b.email) + ) +} + +function _upgradeToTLS(client, tlsOptions) { + return new Promise((resolve, reject) => { + client.on('error', error => reject(new Error(`LDAP client error: ${error}`))) + client.on('connect', () => { + client.starttls(tlsOptions, null, error => { + if (error) { + reject(new Error(`StartTLS error: ${error}`)) + } else { + resolve() + } + }) + }) + }) +} + +function _bindLdap(client, bindDN, bindCredentials) { + return new Promise((resolve, reject) => { + client.bind(bindDN, bindCredentials, error => { + if (error) { + reject(error) + } else { + resolve() + } + }) + }) +} + +function _searchLdap(client, baseDN, options) { + return new Promise((resolve, reject) => { + const searchEntries = [] + client.search(baseDN, options, (error, res) => { + if (error) { + reject(error) + } else { + res.on('searchEntry', entry => searchEntries.push(entry.object)) + res.on('error', reject) + res.on('end', () => resolve(searchEntries)) + } + }) + }) +} + +async function _formContactsSearchFilter(client, ldapOpts, userId, contactsFilter) { + const searchProperty = process.env.OVERLEAF_LDAP_CONTACTS_PROPERTY + if (!searchProperty) { + return contactsFilter + } + const email = await UserGetter.promises.getUserEmail(userId) + const searchOptions = { + scope: ldapOpts.searchScope, + attributes: [searchProperty], + filter: `(${Settings.ldap.attEmail}=${email})`, + } + const searchBase = ldapOpts.searchBase + const ldapUser = (await _searchLdap(client, searchBase, searchOptions))[0] + const searchPropertyValue = ldapUser ? ldapUser[searchProperty] + : process.env.OVERLEAF_LDAP_CONTACTS_NON_LDAP_VALUE || 'IMATCHNOTHING' + return contactsFilter.replace(/{{userProperty}}/g, searchPropertyValue) +} + +export default fetchLdapContacts diff --git a/services/web/modules/ldap-authentication/app/src/LdapStrategy.mjs b/services/web/modules/ldap-authentication/app/src/LdapStrategy.mjs new file mode 100644 index 0000000000..b07dc3f3bd --- /dev/null +++ b/services/web/modules/ldap-authentication/app/src/LdapStrategy.mjs @@ -0,0 +1,78 @@ +import fs from 'fs' +import passport from 'passport' +import Settings from '@overleaf/settings' +import { doPassportLdapLogin } from './AuthenticationControllerLdap.mjs' +import { Strategy as LdapStrategy } from 'passport-ldapauth' + +function _readFilesContentFromEnv(envVar) { +// envVar is either a file name: 'file.pem', or string with array: '["file.pem", "file2.pem"]' + if (!envVar) return undefined + try { + const parsedFileNames = JSON.parse(envVar) + return parsedFileNames.map(filename => fs.readFileSync(filename, 'utf8')) + } catch (error) { + if (error instanceof SyntaxError) { // failed to parse, envVar must be a file name + return fs.readFileSync(envVar, 'utf8') + } else { + throw error + } + } +} + +// custom responses on authentication failure +class CustomFailLdapStrategy extends LdapStrategy { + constructor(options, validate) { + super(options, validate); + this.name = 'custom-fail-ldapauth' + } + authenticate(req, options) { + const defaultFail = this.fail.bind(this) + this.fail = function(info, status) { + info.type = 'error' + info.key = 'invalid-password-retry-or-reset' + info.status = 401 + return defaultFail(info, status) + }.bind(this) + super.authenticate(req, options) + } +} + +const ldapServerOpts = { + url: process.env.OVERLEAF_LDAP_URL, + bindDN: process.env.OVERLEAF_LDAP_BIND_DN || "", + bindCredentials: process.env.OVERLEAF_LDAP_BIND_CREDENTIALS || "", + bindProperty: process.env.OVERLEAF_LDAP_BIND_PROPERTY, + searchBase: process.env.OVERLEAF_LDAP_SEARCH_BASE, + searchFilter: process.env.OVERLEAF_LDAP_SEARCH_FILTER, + searchScope: process.env.OVERLEAF_LDAP_SEARCH_SCOPE || 'sub', + searchAttributes: JSON.parse(process.env.OVERLEAF_LDAP_SEARCH_ATTRIBUTES || '[]'), + groupSearchBase: process.env.OVERLEAF_LDAP_ADMIN_SEARCH_BASE, + groupSearchFilter: process.env.OVERLEAF_LDAP_ADMIN_SEARCH_FILTER, + groupSearchScope: process.env.OVERLEAF_LDAP_ADMIN_SEARCH_SCOPE || 'sub', + groupSearchAttributes: ["dn"], + groupDnProperty: process.env.OVERLEAF_LDAP_ADMIN_DN_PROPERTY, + cache: String(process.env.OVERLEAF_LDAP_CACHE).toLowerCase() === 'true', + timeout: process.env.OVERLEAF_LDAP_TIMEOUT ? Number(process.env.OVERLEAF_LDAP_TIMEOUT) : undefined, + connectTimeout: process.env.OVERLEAF_LDAP_CONNECT_TIMEOUT ? Number(process.env.OVERLEAF_LDAP_CONNECT_TIMEOUT) : undefined, + starttls: String(process.env.OVERLEAF_LDAP_STARTTLS).toLowerCase() === 'true', + tlsOptions: { + ca: _readFilesContentFromEnv(process.env.OVERLEAF_LDAP_TLS_OPTS_CA_PATH), + rejectUnauthorized: String(process.env.OVERLEAF_LDAP_TLS_OPTS_REJECT_UNAUTH).toLowerCase() === 'true', + } +} + +function addLdapStrategy(passport) { + passport.use( + new CustomFailLdapStrategy( + { + server: ldapServerOpts, + passReqToCallback: true, + usernameField: 'email', + passwordField: 'password', + }, + doPassportLdapLogin + ) + ) +} + +export default addLdapStrategy diff --git a/services/web/modules/ldap-authentication/index.mjs b/services/web/modules/ldap-authentication/index.mjs new file mode 100644 index 0000000000..f56d7ffee0 --- /dev/null +++ b/services/web/modules/ldap-authentication/index.mjs @@ -0,0 +1,30 @@ +import initLdapSettings from './app/src/InitLdapSettings.mjs' +import addLdapStrategy from './app/src/LdapStrategy.mjs' +import fetchLdapContacts from './app/src/LdapContacts.mjs' + +let ldapModule = {}; +if (process.env.EXTERNAL_AUTH === 'ldap') { + initLdapSettings() + ldapModule = { + name: 'ldap-authentication', + hooks: { + passportSetup: function (passport, callback) { + try { + addLdapStrategy(passport) + callback(null) + } catch (error) { + callback(error) + } + }, + getContacts: async function (userId, contacts, callback) { + try { + const newLdapContacts = await fetchLdapContacts(userId, contacts) + callback(null, newLdapContacts) + } catch (error) { + callback(error) + } + }, + } + } +} +export default ldapModule diff --git a/services/web/modules/saml-authentication/app/src/AuthenticationControllerSaml.mjs b/services/web/modules/saml-authentication/app/src/AuthenticationControllerSaml.mjs new file mode 100644 index 0000000000..f5db3f738d --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/AuthenticationControllerSaml.mjs @@ -0,0 +1,160 @@ +import Settings from '@overleaf/settings' +import logger from '@overleaf/logger' +import passport from 'passport' +import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js' +import AuthenticationManagerSaml from './AuthenticationManagerSaml.mjs' +import UserController from '../../../../app/src/Features/User/UserController.js' +import UserSessionsManager from '../../../../app/src/Features/User/UserSessionsManager.js' +import { handleAuthenticateErrors } from '../../../../app/src/Features/Authentication/AuthenticationErrors.js' +import { xmlResponse } from '../../../../app/src/infrastructure/Response.js' + +const AuthenticationControllerSaml = { + passportSamlAuthWithIdP(req, res, next) { + if ( passport._strategy('saml')._saml.options.authnRequestBinding === 'HTTP-POST') { + const csp = res.getHeader('Content-Security-Policy') + if (csp) { + res.setHeader( + 'Content-Security-Policy', + csp.replace(/(?:^|\s)(default-src|form-action)[^;]*;?/g, '') + ) + } + } + passport.authenticate('saml')(req, res, next) + }, + passportSamlLogin(req, res, next) { + // This function is middleware which wraps the passport.authenticate middleware, + // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure, + // and send a `{redir: ""}` response on success + passport.authenticate( + 'saml', + { keepSessionInfo: true }, + async function (err, user, info) { + if (err) { + return next(err) + } + if (user) { + // `user` is either a user object or false + AuthenticationController.setAuditInfo(req, { + method: 'SAML login', + }) + try { + await AuthenticationController.promises.finishLogin(user, req, res) + } catch (err) { + return next(err) + } + } else { + if (info.redir != null) { + return res.json({ redir: info.redir }) + } else { + res.status(info.status || 200) + delete info.status + const body = { message: info } + const { errorReason } = info + if (errorReason) { + body.errorReason = errorReason + delete info.errorReason + } + return res.json(body) + } + } + } + )(req, res, next) + }, + async doPassportSamlLogin(req, profile, done) { + let user, info + try { + ;({ user, info } = await AuthenticationControllerSaml._doPassportSamlLogin( + req, + profile + )) + } catch (error) { + return done(error) + } + return done(undefined, user, info) + }, + async _doPassportSamlLogin(req, profile) { + const { fromKnownDevice } = AuthenticationController.getAuditInfo(req) + const auditLog = { + ipAddress: req.ip, + info: { method: 'SAML login', fromKnownDevice }, + } + + let user + try { + user = await AuthenticationManagerSaml.promises.findOrCreateSamlUser(profile, auditLog) + } catch (error) { + return { + user: false, + info: handleAuthenticateErrors(error, req), + } + } + if (user) { + req.session.saml_extce = {nameID : profile.nameID, sessionIndex : profile.sessionIndex} + return { user, info: undefined } + } else { //something wrong + logger.debug({ email : profile.mail }, 'failed SAML log in') + return { + user: false, + info: { + type: 'error', + text: 'Unknown error', + status: 500, + }, + } + } + }, + async passportSamlSPLogout(req, res, next) { + passport._strategy('saml').logout(req, async (err, url) => { + if (err) logger.error({ err }, 'can not generate logout url') + await UserController.promises.doLogout(req) + res.redirect(url) + }) + }, + passportSamlIdPLogout(req, res, next) { + passport.authenticate('saml')(req, res, (err) => { + if (err) return next(err) + res.redirect('/login'); + }) + }, + async doPassportSamlLogout(req, profile, done) { + let user, info + try { + ;({ user, info } = await AuthenticationControllerSaml._doPassportSamlLogout( + req, + profile + )) + } catch (error) { + return done(error) + } + return done(undefined, user, info) + }, + async _doPassportSamlLogout(req, profile) { + if (req?.session?.saml_extce?.nameID === profile.nameID && + req?.session?.saml_extce?.sessionIndex === profile.sessionIndex) { + profile = req.user + } + await UserSessionsManager.promises.untrackSession(req.user, req.sessionID).catch(err => { + logger.warn({ err, userId: req.user._id }, 'failed to untrack session') + }) + return { user: profile, info: undefined } + }, + passportSamlMetadata(req, res) { + const samlStratery = passport._strategy('saml') + res.setHeader('Content-Disposition', `attachment; filename="${samlStratery._saml.options.issuer}-meta.xml"`) + xmlResponse(res, + samlStratery.generateServiceProviderMetadata( + samlStratery._saml.options.decryptionCert, + samlStratery._saml.options.signingCert + ) + ) + }, +} +export const { + passportSamlAuthWithIdP, + passportSamlLogin, + passportSamlSPLogout, + passportSamlIdPLogout, + doPassportSamlLogin, + doPassportSamlLogout, + passportSamlMetadata, +} = AuthenticationControllerSaml diff --git a/services/web/modules/saml-authentication/app/src/AuthenticationManagerSaml.mjs b/services/web/modules/saml-authentication/app/src/AuthenticationManagerSaml.mjs new file mode 100644 index 0000000000..47d97f3019 --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/AuthenticationManagerSaml.mjs @@ -0,0 +1,60 @@ +import Settings from '@overleaf/settings' +import UserCreator from '../../../../app/src/Features/User/UserCreator.js' +import { User } from '../../../../app/src/models/User.js' + +const AuthenticationManagerSaml = { + async findOrCreateSamlUser(profile, auditLog) { + const { + attEmail, + attFirstName, + attLastName, + attAdmin, + valAdmin, + updateUserDetailsOnLogin, + } = Settings.saml + const email = Array.isArray(profile[attEmail]) + ? profile[attEmail][0].toLowerCase() + : profile[attEmail].toLowerCase() + const firstName = attFirstName ? profile[attFirstName] : "" + const lastName = attLastName ? profile[attLastName] : email + let isAdmin = false + if( attAdmin && valAdmin ) { + isAdmin = (Array.isArray(profile[attAdmin]) ? profile[attAdmin].includes(valAdmin) : + profile[attAdmin] === valAdmin) + } + let user = await User.findOne({ 'email': email }).exec() + if( !user ) { + user = await UserCreator.promises.createNewUser( + { + email: email, + first_name: firstName, + last_name: lastName, + isAdmin: isAdmin, + holdingAccount: false, + } + ) + await User.updateOne( + { _id: user._id }, + { $set : { 'emails.0.confirmedAt' : Date.now() } } + ).exec() //email of saml user is confirmed + } + let userDetails = updateUserDetailsOnLogin ? { first_name : firstName, last_name: lastName } : {} + if( attAdmin && valAdmin ) { + user.isAdmin = isAdmin + userDetails.isAdmin = isAdmin + } + const result = await User.updateOne( + { _id: user._id, loginEpoch: user.loginEpoch }, { $inc: { loginEpoch: 1 }, $set: userDetails }, + {} + ).exec() + + if (result.modifiedCount !== 1) { + throw new ParallelLoginError() + } + return user + }, +} + +export default { + promises: AuthenticationManagerSaml, +} diff --git a/services/web/modules/saml-authentication/app/src/InitSamlSettings.mjs b/services/web/modules/saml-authentication/app/src/InitSamlSettings.mjs new file mode 100644 index 0000000000..441f9033af --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/InitSamlSettings.mjs @@ -0,0 +1,16 @@ +import Settings from '@overleaf/settings' + +function initSamlSettings() { + Settings.saml = { + enable: true, + identityServiceName: process.env.OVERLEAF_SAML_IDENTITY_SERVICE_NAME || 'Login with SAML IdP', + attEmail: process.env.OVERLEAF_SAML_EMAIL_FIELD || 'nameID', + attFirstName: process.env.OVERLEAF_SAML_FIRST_NAME_FIELD || 'givenName', + attLastName: process.env.OVERLEAF_SAML_LAST_NAME_FIELD || 'lastName', + attAdmin: process.env.OVERLEAF_SAML_IS_ADMIN_FIELD, + valAdmin: process.env.OVERLEAF_SAML_IS_ADMIN_FIELD_VALUE, + updateUserDetailsOnLogin: String(process.env.OVERLEAF_SAML_UPDATE_USER_DETAILS_ON_LOGIN).toLowerCase() === 'true', + } +} + +export default initSamlSettings diff --git a/services/web/modules/saml-authentication/app/src/SamlNonCsrfRouter.mjs b/services/web/modules/saml-authentication/app/src/SamlNonCsrfRouter.mjs new file mode 100644 index 0000000000..65b42c92ae --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/SamlNonCsrfRouter.mjs @@ -0,0 +1,12 @@ +import logger from '@overleaf/logger' +import { passportSamlLogin, passportSamlIdPLogout } from './AuthenticationControllerSaml.mjs' + +export default { + apply(webRouter) { + logger.debug({}, 'Init SAML NonCsrfRouter') + webRouter.get('/saml/login/callback', passportSamlLogin) + webRouter.post('/saml/login/callback', passportSamlLogin) + webRouter.get('/saml/logout/callback', passportSamlIdPLogout) + webRouter.post('/saml/logout/callback', passportSamlIdPLogout) + }, +} diff --git a/services/web/modules/saml-authentication/app/src/SamlRouter.mjs b/services/web/modules/saml-authentication/app/src/SamlRouter.mjs new file mode 100644 index 0000000000..9ee3677901 --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/SamlRouter.mjs @@ -0,0 +1,14 @@ +import logger from '@overleaf/logger' +import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js' +import { passportSamlAuthWithIdP, passportSamlSPLogout, passportSamlMetadata} from './AuthenticationControllerSaml.mjs' + +export default { + apply(webRouter) { + logger.debug({}, 'Init SAML router') + webRouter.get('/saml/login', passportSamlAuthWithIdP) + AuthenticationController.addEndpointToLoginWhitelist('/saml/login') + webRouter.post('/saml/logout', AuthenticationController.requireLogin(), passportSamlSPLogout) + webRouter.get('/saml/meta', passportSamlMetadata) + AuthenticationController.addEndpointToLoginWhitelist('/saml/meta') + }, +} diff --git a/services/web/modules/saml-authentication/app/src/SamlStrategy.mjs b/services/web/modules/saml-authentication/app/src/SamlStrategy.mjs new file mode 100644 index 0000000000..3a16459f98 --- /dev/null +++ b/services/web/modules/saml-authentication/app/src/SamlStrategy.mjs @@ -0,0 +1,62 @@ +import fs from 'fs' +import passport from 'passport' +import Settings from '@overleaf/settings' +import { doPassportSamlLogin, doPassportSamlLogout } from './AuthenticationControllerSaml.mjs' +import { Strategy as SamlStrategy } from '@node-saml/passport-saml' + +function _readFilesContentFromEnv(envVar) { +// envVar is either a file name: 'file.pem', or string with array: '["file.pem", "file2.pem"]' + if (!envVar) return undefined + try { + const parsedFileNames = JSON.parse(envVar) + return parsedFileNames.map(filename => fs.readFileSync(filename, 'utf8')) + } catch (error) { + if (error instanceof SyntaxError) { // failed to parse, envVar must be a file name + return fs.readFileSync(envVar, 'utf8') + } else { + throw error + } + } +} + +const samlOptions = { + entryPoint: process.env.OVERLEAF_SAML_ENTRYPOINT, + callbackUrl: process.env.OVERLEAF_SAML_CALLBACK_URL, + issuer: process.env.OVERLEAF_SAML_ISSUER, + audience: process.env.OVERLEAF_SAML_AUDIENCE, + cert: _readFilesContentFromEnv(process.env.OVERLEAF_SAML_IDP_CERT), + signingCert: _readFilesContentFromEnv(process.env.OVERLEAF_SAML_PUBLIC_CERT), + privateKey: _readFilesContentFromEnv(process.env.OVERLEAF_SAML_PRIVATE_KEY), + decryptionCert: _readFilesContentFromEnv(process.env.OVERLEAF_SAML_DECRYPTION_CERT), + decryptionPvk: _readFilesContentFromEnv(process.env.OVERLEAF_SAML_DECRYPTION_PVK), + signatureAlgorithm: process.env.OVERLEAF_SAML_SIGNATURE_ALGORITHM, + additionalParams: JSON.parse(process.env.OVERLEAF_SAML_ADDITIONAL_PARAMS || '{}'), + additionalAuthorizeParams: JSON.parse(process.env.OVERLEAF_SAML_ADDITIONAL_AUTHORIZE_PARAMS || '{}'), + identifierFormat: process.env.OVERLEAF_SAML_IDENTIFIER_FORMAT, + acceptedClockSkewMs: process.env.OVERLEAF_SAML_ACCEPTED_CLOCK_SKEW_MS ? Number(process.env.OVERLEAF_SAML_ACCEPTED_CLOCK_SKEW_MS) : undefined, + attributeConsumingServiceIndex: process.env.OVERLEAF_SAML_ATTRIBUTE_CONSUMING_SERVICE_INDEX, + authnContext: process.env.OVERLEAF_SAML_AUTHN_CONTEXT ? JSON.parse(process.env.OVERLEAF_SAML_AUTHN_CONTEXT) : undefined, + forceAuthn: String(process.env.OVERLEAF_SAML_FORCE_AUTHN).toLowerCase() === 'true', + disableRequestedAuthnContext: String(process.env.OVERLEAF_SAML_DISABLE_REQUESTED_AUTHN_CONTEXT).toLowerCase() === 'true', + skipRequestCompression: process.env.OVERLEAF_SAML_AUTHN_REQUEST_BINDING === 'HTTP-POST', // compression should be skipped iff authnRequestBinding is POST + authnRequestBinding: process.env.OVERLEAF_SAML_AUTHN_REQUEST_BINDING, + validateInResponseTo: process.env.OVERLEAF_SAML_VALIDATE_IN_RESPONSE_TO, + requestIdExpirationPeriodMs: process.env.OVERLEAF_SAML_REQUEST_ID_EXPIRATION_PERIOD_MS ? Number(process.env.OVERLEAF_SAML_REQUEST_ID_EXPIRATION_PERIOD_MS) : undefined, +// cacheProvider: process.env.OVERLEAF_SAML_CACHE_PROVIDER, + logoutUrl: process.env.OVERLEAF_SAML_LOGOUT_URL, + logoutCallbackUrl: process.env.OVERLEAF_SAML_LOGOUT_CALLBACK_URL, + additionalLogoutParams: JSON.parse(process.env.OVERLEAF_SAML_ADDITIONAL_LOGOUT_PARAMS || '{}'), + passReqToCallback: true, +} + +function addSamlStrategy(passport) { + passport.use( + new SamlStrategy( + samlOptions, + doPassportSamlLogin, + doPassportSamlLogout + ) + ) +} + +export default addSamlStrategy diff --git a/services/web/modules/saml-authentication/index.mjs b/services/web/modules/saml-authentication/index.mjs new file mode 100644 index 0000000000..35ea70283f --- /dev/null +++ b/services/web/modules/saml-authentication/index.mjs @@ -0,0 +1,26 @@ +import initSamlSettings from './app/src/InitSamlSettings.mjs' +import addSamlStrategy from './app/src/SamlStrategy.mjs' +import SamlRouter from './app/src/SamlRouter.mjs' +import SamlNonCsrfRouter from './app/src/SamlNonCsrfRouter.mjs' + +let samlModule = {}; + +if (process.env.EXTERNAL_AUTH === 'saml') { + initSamlSettings() + samlModule = { + name: 'saml-authentication', + hooks: { + passportSetup: function (passport, callback) { + try { + addSamlStrategy(passport) + callback(null) + } catch (error) { + callback(error) + } + }, + }, + router: SamlRouter, + nonCsrfRouter: SamlNonCsrfRouter, + } +} +export default samlModule From 04e0acbce55e644b6c5175a4d9a62b4c0e56dd97 Mon Sep 17 00:00:00 2001 From: yu-i-i Date: Mon, 9 Dec 2024 06:18:51 +0100 Subject: [PATCH 035/595] Update README.md --- README.md | 675 +++++++++++++++++++++++++++++++++++++++++++-- doc/screenshot.png | Bin 601370 -> 1080606 bytes 2 files changed, 659 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4895254926..69fd26b2cc 100644 --- a/README.md +++ b/README.md @@ -14,30 +14,676 @@
License

-A screenshot of a project being edited in Overleaf Community Edition +A screenshot of a project being edited in Overleaf Extended Community Edition

- Figure 1: A screenshot of a project being edited in Overleaf Community Edition. + Figure 1: A screenshot of a project being edited in Overleaf Extended Community Edition.

## Community Edition -[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf. +[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. Overleaf runs a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf. + +## Extended Community Edition + +The present "extended" version of Overleaf CE includes: + +- Sandboxed Compiles with TeX Live image selection +- LDAP authentication +- SAML authentication +- Real-time track changes and comments +- Autocomplete of reference keys +- Symbol Palette ## Enterprise -If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises) - -## Keeping up to date - -Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development. +If you want help installing and maintaining Overleaf in your lab or workplace, Overleaf offers an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). ## Installation -We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/). +Detailed installation instructions can be found in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/). +To run a custom image, add a file named docker-compose.override.yml with the following or similar content into the `overleaf-toolkit/config directory`: -## Upgrading +```yml +--- +version: '2.2' +services: + sharelatex: + image: sharelatex/sharelatex:ext-ce + volumes: + - ../config/certs:/overleaf/certs +``` +Here, the attached volume provides convenient access for the container to the certificates needed for SAML or LDAP authentication. -If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to. +If you want to build a Docker image of the extended CE based on the upstream v5.2.1 codebase, you can check out the corresponding tag by running: +``` +git checkout v5.2.1-ext +``` +Alternatively, you can download a prebuilt image from Docker Hub: +``` +docker pull overleafcep/sharelatex:5.2.1-ext +``` +Make sure to update the image name in overleaf-toolkit/config/docker-compose.override.yml accordingly. + +## Sandboxed Compiles + +To enable sandboxed compiles (also known as "Sibling containers"), set the following configuration options in `overleaf-toolkit/config/overleaf.rc`: + +``` +SERVER_PRO=true +SIBLING_CONTAINERS_ENABLED=true +``` + +The following environment variables are used to specify which TeX Live images to use for sandboxed compiles: + +- `ALL_TEX_LIVE_DOCKER_IMAGES` **(required)** + * A comma-separated list of TeX Live images to use. These images will be downloaded or updated. + To skip downloading the images, set `SIBLING_CONTAINERS_PULL=false` in `config/overleaf.rc`. +- `ALL_TEX_LIVE_DOCKER_IMAGE_NAMES` + * A comma-separated list of friendly names for the images. If omitted, the version name will be used (e.g., `latest-full`). +- `TEX_LIVE_DOCKER_IMAGE` **(required)** + * The default TeX Live image that will be used for compiling new projects. The environment variable `ALL_TEX_LIVE_DOCKER_IMAGES` must include this image. + +Users can select the image for their project in the project menu. + +Here is an example where the default TeX Live image is `latest-full` from Docker Hub, but the `TL2023-historic` image can be used for older projects: +``` +ALL_TEX_LIVE_DOCKER_IMAGES=texlive/texlive:latest-full, texlive/texlive:TL2023-historic +ALL_TEX_LIVE_DOCKER_IMAGE_NAMES=TeXLive 2024, TeXLive 2023 +TEX_LIVE_DOCKER_IMAGE=texlive/texlive:latest-full +``` +For additional details refer to +[Server Pro: Sandboxed Compiles](https://github.com/overleaf/overleaf/wiki/Server-Pro:-Sandboxed-Compiles) and +[Toolkit: Sandboxed Compiles](https://github.com/overleaf/toolkit/blob/master/doc/sandboxed-compiles.md). + +
+

Sample variables.env file

+ +``` +OVERLEAF_APP_NAME="Our Overleaf Instance" + +ENABLED_LINKED_FILE_TYPES=project_file,project_output_file + +# Enables Thumbnail generation using ImageMagick +ENABLE_CONVERSIONS=true + +# Disables email confirmation requirement +EMAIL_CONFIRMATION_DISABLED=true + +## Nginx +# NGINX_WORKER_PROCESSES=4 +# NGINX_WORKER_CONNECTIONS=768 + +## Set for TLS via nginx-proxy +# OVERLEAF_BEHIND_PROXY=true +# OVERLEAF_SECURE_COOKIE=true + +OVERLEAF_SITE_URL=http://my-overleaf-instance.com +OVERLEAF_NAV_TITLE=Our Overleaf Instance +# OVERLEAF_HEADER_IMAGE_URL=http://somewhere.com/mylogo.png +OVERLEAF_ADMIN_EMAIL=support@example.com + +OVERLEAF_LEFT_FOOTER=[{"text": "Contact your support team", "url": "mailto:support@example.com"}] +OVERLEAF_RIGHT_FOOTER=[{"text":"Hello, I am on the Right", "url":"https://github.com/yu-i-i/overleaf-cep"}] + +OVERLEAF_EMAIL_FROM_ADDRESS=team@example.com +OVERLEAF_EMAIL_SMTP_HOST=smtp.example.com +OVERLEAF_EMAIL_SMTP_PORT=587 +OVERLEAF_EMAIL_SMTP_SECURE=false +# OVERLEAF_EMAIL_SMTP_USER= +# OVERLEAF_EMAIL_SMTP_PASS= +# OVERLEAF_EMAIL_SMTP_NAME= +OVERLEAF_EMAIL_SMTP_LOGGER=false +OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH=true +OVERLEAF_EMAIL_SMTP_IGNORE_TLS=false +OVERLEAF_CUSTOM_EMAIL_FOOTER=This system is run by department x + +OVERLEAF_PROXY_LEARN=true +NAV_HIDE_POWERED_BY=true + +######################## +## Sandboxed Compiles ## +######################## + +ALL_TEX_LIVE_DOCKER_IMAGES=texlive/texlive:latest-full, texlive/texlive:TL2023-historic +ALL_TEX_LIVE_DOCKER_IMAGE_NAMES=TeXLive 2024, TeXLive 2023 +TEX_LIVE_DOCKER_IMAGE=texlive/texlive:latest-full +``` +
+ +## Authentication Methods + +The following authentication methods are supported: Local authentication, LDAP authentication, and SAML authentication. Local authentication is always active. +To enable LDAP or SAML authentication, the environment variable `EXTERNAL_AUTH` must be set to `ldap` or `saml`, respectively. + +
+

Local Authentication

+ +Password of local users stored in the MongoDB database. An admin user can create a new local user. For details, visit the +[wiki of Overleaf project](https://github.com/overleaf/overleaf/wiki/Creating-and-managing-users). + +It is possible to enforce password restrictions on local users: + + +* `OVERLEAF_PASSWORD_VALIDATION_MIN_LENGTH`: The minimum length required + +* `OVERLEAF_PASSWORD_VALIDATION_MAX_LENGTH`: The maximum length allowed + +* `OVERLEAF_PASSWORD_VALIDATION_PATTERN`: is used to validate password strength + + - `abc123` – password requires 3 letters and 3 numbers and be at least 6 characters long + - `aA` – password requires lower and uppercase letters and be at least 2 characters long + - `ab$3` – it must contain letters, digits and symbols and be at least 4 characters long + - There are 4 groups of characters: letters, UPPERcase letters, digits, symbols. Anything that is neither a letter nor a digit is considered to be a symbol. + +
+ +
+

LDAP Authentication

+ +Internally, Overleaf LDAP uses the [passport-ldapauth](https://github.com/vesse/passport-ldapauth) library. Most of these configuration options are passed through to the `server` config object which is used to configure `passport-ldapauth`. If you are having issues configuring LDAP, it is worth reading the README for `passport-ldapauth` to understand the configuration it expects. + +#### Environment Variables + +- `OVERLEAF_LDAP_URL` **(required)** + * URL of the LDAP server. + + - Example: `ldaps://ldap.example.com:636` (LDAP over SSL) + - Example: `ldap://ldap.example.com:389` (unencrypted or STARTTLS, if configured). + +- `OVERLEAF_LDAP_EMAIL_ATT` + * The email attribute returned by the LDAP server, default `mail`. Each LDAP user must have at least one email address. + If multiple addresses are provided, only the first one will be used. + +- `OVERLEAF_LDAP_FIRST_NAME_ATT` + * The property name holding the first name of the user which is used in the application, usually `givenName`. + +- `OVERLEAF_LDAP_LAST_NAME_ATT` + * The property name holding the family name of the user which is used in the application, usually `sn`. + +- `OVERLEAF_LDAP_NAME_ATT` + * The property name holding the full name of the user, usually `cn`. If either of the two previous variables is not defined, + the first and/or last name of the user is extracted from this variable. Otherwise, it is not used. + +- `OVERLEAF_LDAP_PLACEHOLDER` + * The placeholder for the login form, defaults to `Username`. + +- `OVERLEAF_LDAP_UPDATE_USER_DETAILS_ON_LOGIN` + * If set to `true`, updates the LDAP user `first_name` and `last_name` field on login, and turn off the user details form on the `/user/settings` + page for LDAP users. Otherwise, details will be fetched only on first login. + +- `OVERLEAF_LDAP_BIND_DN` + * The distinguished name of the LDAP user that should be used for the LDAP connection + (this user should be able to search/list accounts on the LDAP server), + e.g., `cn=ldap_reader,dc=example,dc=com`. If not defined, anonymous binding is used. + +- `OVERLEAF_LDAP_BIND_CREDENTIALS` + * Password for `OVERLEAF_LDAP_BIND_DN`. + +- `OVERLEAF_LDAP_BIND_PROPERTY` + * Property of the user to bind against the client, defaults to `dn`. + +- `OVERLEAF_LDAP_SEARCH_BASE` **(required)** + * The base DN from which to search for users. E.g., `ou=people,dc=example,dc=com`. + +- `OVERLEAF_LDAP_SEARCH_FILTER` + * LDAP search filter with which to find a user. Use the literal '{{username}}' to have the given username be interpolated in for the LDAP search. + + - Example: `(|(uid={{username}})(mail={{username}}))` (user can login with email or with login name). + - Example: `(sAMAccountName={{username}})` (Active Directory). + +- `OVERLEAF_LDAP_SEARCH_SCOPE` + * The scope of the search can be `base`, `one`, or `sub` (default). + +- `OVERLEAF_LDAP_SEARCH_ATTRIBUTES` + * JSON array of attributes to fetch from the LDAP server, e.g., `["uid", "mail", "givenName", "sn"]`. + By default, all attributes are fetched. + +- `OVERLEAF_LDAP_STARTTLS` + * If `true`, LDAP over TLS is used. + +- `OVERLEAF_LDAP_TLS_OPTS_CA_PATH` + * Path to the file containing the CA certificate used to verify the LDAP server's SSL/TLS certificate. If there are multiple certificates, then + it can be a JSON array of paths to the certificates. The files must be accessible to the docker container. + + - Example (one certificate): `/overleaf/certs/ldap_ca_cert.pem` + - Example (multiple certificates): `["/overleaf/certs/ldap_ca_cert1.pem", "/overleaf/certs/ldap_ca_cert2.pem"]` + +- `OVERLEAF_LDAP_TLS_OPTS_REJECT_UNAUTH` + * If `true`, the server certificate is verified against the list of supplied CAs. + +- `OVERLEAF_LDAP_CACHE` + * If `true`, then up to 100 credentials at a time will be cached for 5 minutes. + +- `OVERLEAF_LDAP_TIMEOUT` + * How long the client should let operations live for before timing out, ms (Default: Infinity). + +- `OVERLEAF_LDAP_CONNECT_TIMEOUT` + * How long the client should wait before timing out on TCP connections, ms (Default: OS default). + +- `OVERLEAF_LDAP_IS_ADMIN_ATT` and `OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE` + * When both environment variables are set, the login process updates `user.isAdmin = true` if the LDAP profile contains the attribute specified by + `OVERLEAF_LDAP_IS_ADMIN_ATT` and its value either matches `OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE` or is an array containing `OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE`, + otherwise `user.isAdmin` is set to `false`. If either of these variables is not set, then the admin status is only set to `true` during admin user + creation in Launchpad. + +The following five variables are used to configure how user contacts are retrieved from the LDAP server. + +- `OVERLEAF_LDAP_CONTACTS_FILTER` + * The filter used to search for users in the LDAP server to be loaded into contacts. The placeholder '{{userProperty}}' within the filter is replaced with the value of + the property specified by `OVERLEAF_LDAP_CONTACTS_PROPERTY` from the LDAP user initiating the search. If not defined, no users are retrieved from the LDAP server into contacts. + +- `OVERLEAF_LDAP_CONTACTS_SEARCH_BASE` + * Specifies the base DN from which to start searching for the contacts. Defaults to `OVERLEAF_LDAP_SEARCH_BASE`. + +- `OVERLEAF_LDAP_CONTACTS_SEARCH_SCOPE` + * The scope of the search can be `base`, `one`, or `sub` (default). + +- `OVERLEAF_LDAP_CONTACTS_PROPERTY` + * Specifies the property of the user object that will replace the '{{userProperty}}' placeholder in the `OVERLEAF_LDAP_CONTACTS_FILTER`. + +- `OVERLEAF_LDAP_CONTACTS_NON_LDAP_VALUE` + * Specifies the value of the `OVERLEAF_LDAP_CONTACTS_PROPERTY` if the search is initiated by a non-LDAP user. If this variable is not defined, the resulting filter + will match nothing. The value `*` can be used as a wildcard. + +
+
Example
+ + OVERLEAF_LDAP_CONTACTS_FILTER=(gidNumber={{userProperty}}) + OVERLEAF_LDAP_CONTACTS_PROPERTY=gidNumber + OVERLEAF_LDAP_CONTACTS_NON_LDAP_VALUE=1000 + +The above example results in loading into the contacts of the current LDAP user all LDAP users who have the same UNIX `gid`. Non-LDAP users will have all LDAP users with UNIX `gid=1000` in their contacts. + +
+ + +
+

Sample variables.env file

+ +``` +OVERLEAF_APP_NAME="Our Overleaf Instance" + +ENABLED_LINKED_FILE_TYPES=project_file,project_output_file + +# Enables Thumbnail generation using ImageMagick +ENABLE_CONVERSIONS=true + +# Disables email confirmation requirement +EMAIL_CONFIRMATION_DISABLED=true + +## Nginx +# NGINX_WORKER_PROCESSES=4 +# NGINX_WORKER_CONNECTIONS=768 + +## Set for TLS via nginx-proxy +# OVERLEAF_BEHIND_PROXY=true +# OVERLEAF_SECURE_COOKIE=true + +OVERLEAF_SITE_URL=http://my-overleaf-instance.com +OVERLEAF_NAV_TITLE=Our Overleaf Instance +# OVERLEAF_HEADER_IMAGE_URL=http://somewhere.com/mylogo.png +OVERLEAF_ADMIN_EMAIL=support@example.com + +OVERLEAF_LEFT_FOOTER=[{"text": "Contact your support team", "url": "mailto:support@example.com"}] +OVERLEAF_RIGHT_FOOTER=[{"text":"Hello, I am on the Right", "url":"https://github.com/yu-i-i/overleaf-cep"}] + +OVERLEAF_EMAIL_FROM_ADDRESS=team@example.com +OVERLEAF_EMAIL_SMTP_HOST=smtp.example.com +OVERLEAF_EMAIL_SMTP_PORT=587 +OVERLEAF_EMAIL_SMTP_SECURE=false +# OVERLEAF_EMAIL_SMTP_USER= +# OVERLEAF_EMAIL_SMTP_PASS= +# OVERLEAF_EMAIL_SMTP_NAME= +OVERLEAF_EMAIL_SMTP_LOGGER=false +OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH=true +OVERLEAF_EMAIL_SMTP_IGNORE_TLS=false +OVERLEAF_CUSTOM_EMAIL_FOOTER=This system is run by department x + +OVERLEAF_PROXY_LEARN=true +NAV_HIDE_POWERED_BY=true + +################# +## LDAP for CE ## +################# + +EXTERNAL_AUTH=ldap +OVERLEAF_LDAP_URL=ldap://ldap.example.com:389 +OVERLEAF_LDAP_STARTTLS=true +OVERLEAF_LDAP_TLS_OPTS_CA_PATH=/overleaf/certs/ldap_ca_cert.pem +OVERLEAF_LDAP_SEARCH_BASE=ou=people,dc=example,dc=com +OVERLEAF_LDAP_SEARCH_FILTER=(|(uid={{username}})(mail={{username}})) +OVERLEAF_LDAP_BIND_DN=cn=ldap_reader,dc=example,dc=com +OVERLEAF_LDAP_BIND_CREDENTIALS=GoodNewsEveryone +OVERLEAF_LDAP_EMAIL_ATT=mail +OVERLEAF_LDAP_FIRST_NAME_ATT=givenName +OVERLEAF_LDAP_LAST_NAME_ATT=sn +# OVERLEAF_LDAP_NAME_ATT=cn +OVERLEAF_LDAP_SEARCH_ATTRIBUTES=["uid", "sn", "givenName", "mail"] + +OVERLEAF_LDAP_UPDATE_USER_DETAILS_ON_LOGIN=true + +OVERLEAF_LDAP_PLACEHOLDER='Username or email address' + +OVERLEAF_LDAP_IS_ADMIN_ATT=mail +OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE=admin@example.com + +OVERLEAF_LDAP_CONTACTS_FILTER=(gidNumber={{userProperty}}) +OVERLEAF_LDAP_CONTACTS_PROPERTY=gidNumber +OVERLEAF_LDAP_CONTACTS_NON_LDAP_VALUE='*' +``` +
+ +
+Deprecated variables + +**These variables will be removed soon**, use `OVERLEAF_LDAP_IS_ADMIN_ATT` and `OVERLEAF_LDAP_IS_ADMIN_ATT_VALUE` instead. + +The following variables are used to determine if the user has admin rights. +Please note: the user gains admin status if the search result is not empty, not when the user is explicitly included in the search results. + +- `OVERLEAF_LDAP_ADMIN_SEARCH_BASE` + * Specifies the base DN from which to start searching for the admin group. If this variable is defined, + `OVERLEAF_LDAP_ADMIN_SEARCH_FILTER` must also be defined for the search to function properly. + +- `OVERLEAF_LDAP_ADMIN_SEARCH_FILTER` + * Defines the LDAP search filter used to identify the admin group. The placeholder `{{dn}}` within the filter + is replaced with the value of the property specified by `OVERLEAF_LDAP_ADMIN_DN_PROPERTY`. The placeholder `{{username}}` is also supported. + +- `OVERLEAF_LDAP_ADMIN_DN_PROPERTY` + * Specifies the property of the user object that will replace the '{{dn}}' placeholder + in the `OVERLEAF_LDAP_ADMIN_SEARCH_FILTER`, defaults to `dn`. + +- `OVERLEAF_LDAP_ADMIN_SEARCH_SCOPE` + * The scope of the LDAP search can be `base`, `one`, or `sub` (default) + +
+
Example
+ +In the following example admins are members of a group `admins`, the objectClass of the entry `admins` is `groupOfNames`: + + OVERLEAF_LDAP_ADMIN_SEARCH_BASE='cn=admins,ou=group,dc=example,dc=com' + OVERLEAF_LDAP_ADMIN_SEARCH_FILTER='(member={{dn}})' + +In the following example admins are members of a group 'admins', the objectClass of the entry `admins` is `posixGroup`: + + OVERLEAF_LDAP_ADMIN_SEARCH_BASE='cn=admins,ou=group,dc=example,dc=com' + OVERLEAF_LDAP_ADMIN_SEARCH_FILTER='(memberUid={{username}})' + +In the following example admins are users with UNIX gid=1234: + + OVERLEAF_LDAP_ADMIN_SEARCH_BASE='ou=people,dc=example,dc=com' + OVERLEAF_LDAP_ADMIN_SEARCH_FILTER='(&(gidNumber=1234)(uid={{username}}))' + +In the following example admin is the user with `uid=someuser`: + + OVERLEAF_LDAP_ADMIN_SEARCH_BASE='ou=people,dc=example,dc=com' + OVERLEAF_LDAP_ADMIN_SEARCH_FILTER='(&(uid=someuser)(uid={{username}}))' + +The filter + + OVERLEAF_LDAP_ADMIN_SEARCH_FILTER='(uid=someuser)' + +where `someuser` is the uid of an existing user, will always produce a non-empty search result. +As a result, **every user will be granted admin rights**, not just `someuser`, as one might expect. + +
+
+
+ +
+

SAML Authentication

+ +Internally, Overleaf SAML module uses the [passport-saml](https://github.com/node-saml/passport-saml) library, most of the following +configuration options are passed through to `passport-saml`. If you are having issues configuring SAML, it is worth reading the README +for `passport-saml` to get a feel for the configuration it expects. + +#### Environment Variables + +- `OVERLEAF_SAML_IDENTITY_SERVICE_NAME` + * Display name for the identity service, used on the login page (default: `Login with SAML IdP`). + +- `OVERLEAF_SAML_EMAIL_FIELD` + * Name of the Email field in user profile, default to 'nameID'. + +- `OVERLEAF_SAML_FIRST_NAME_FIELD` + * Name of the firstName field in user profile, default to 'givenName'. + +- `OVERLEAF_SAML_LAST_NAME_FIELD` + * Name of the lastName field in user profile, default to 'lastName' + +- `OVERLEAF_SAML_UPDATE_USER_DETAILS_ON_LOGIN` + * If set to `true`, updates the user `first_name` and `last_name` field on login, + and turn off the user details form on `/user/settings` page. + +- `OVERLEAF_SAML_ENTRYPOINT` **(required)** + * Entrypoint URL for the SAML identity service. + + - Example: `https://idp.example.com/simplesaml/saml2/idp/SSOService.php` + - Azure Example: `https://login.microsoftonline.com/8b26b46a-6dd3-45c7-a104-f883f4db1f6b/saml2` + +- `OVERLEAF_SAML_CALLBACK_URL` **(required)** + * Callback URL for Overleaf service. Should be the full URL of the `/saml/login/callback` path. + + - Example: `https://my-overleaf-instance.com/saml/login/callback` + +- `OVERLEAF_SAML_ISSUER` **(required)** + * The Issuer name. + +- `OVERLEAF_SAML_AUDIENCE` + * Expected saml response Audience, defaults to value of `OVERLEAF_SAML_ISSUER`. + +- `OVERLEAF_SAML_IDP_CERT` **(required)** + * Path to a file containing the Identity Provider's public certificate, used to validate the signatures of incoming SAML responses. If the Identity Provider has multiple valid signing certificates, then + it can be a JSON array of paths to the certificates. + + - Example (one certificate): `/overleaf/certs/idp_cert.pem` + - Example (multiple certificates): `["/overleaf/certs/idp_cert.pem", "/overleaf/certs/idp_cert_old.pem"]` + +- `OVERLEAF_SAML_PUBLIC_CERT` + * Path to a file containing public signing certificate used to embed in auth requests in order for the IdP to validate the signatures of the incoming SAML Request. It's required when setting up the [metadata endpoint](#metadata-for-the-identity-provider) + when the strategy is configured with a `OVERLEAF_SAML_PRIVATE_KEY`. A JSON array of paths to certificates can be provided to support certificate rotation. When supplying an array of certificates, the first entry in the array should match the + current `OVERLEAF_SAML_PRIVATE_KEY`. Additional entries in the array can be used to publish upcoming certificates to IdPs before changing the `OVERLEAF_SAML_PRIVATE_KEY`. + +- `OVERLEAF_SAML_PRIVATE_KEY` + * Path to a file containing a PEM-formatted private key matching the `OVERLEAF_SAML_PUBLIC_CERT` used to sign auth requests sent by passport-saml. + +- `OVERLEAF_SAML_DECRYPTION_CERT` + * Path to a file containing public certificate, used for the [metadata endpoint](#metadata-for-the-identity-provider). + +- `OVERLEAF_SAML_DECRYPTION_PVK` + * Path to a file containing private key matching the `OVERLEAF_SAML_DECRYPTION_CERT` that will be used to attempt to decrypt any encrypted assertions that are received. + +- `OVERLEAF_SAML_SIGNATURE_ALGORITHM` + * Optionally set the signature algorithm for signing requests, + valid values are 'sha1' (default), 'sha256' (prefered), 'sha512' (most secure, check if your IdP supports it). + +- `OVERLEAF_SAML_ADDITIONAL_PARAMS` + * JSON dictionary of additional query params to add to all requests. + +- `OVERLEAF_SAML_ADDITIONAL_AUTHORIZE_PARAMS` + * JSON dictionary of additional query params to add to 'authorize' requests. + + - Example: `{"some_key": "some_value"}` + +- `OVERLEAF_SAML_IDENTIFIER_FORMAT` + * Name identifier format to request from identity provider (default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`). + +- `OVERLEAF_SAML_ACCEPTED_CLOCK_SKEW_MS` + * Time in milliseconds of skew that is acceptable between client and server when checking OnBefore and NotOnOrAfter assertion + condition validity timestamps. Setting to -1 will disable checking these conditions entirely. Default is 0. + +- `OVERLEAF_SAML_ATTRIBUTE_CONSUMING_SERVICE_INDEX` + * `AttributeConsumingServiceIndex` attribute to add to AuthnRequest to instruct the IdP which attribute set to attach + to the response ([link](http://blog.aniljohn.com/2014/01/data-minimization-front-channel-saml-attribute-requests.html)). + +- `OVERLEAF_SAML_AUTHN_CONTEXT` + * JSON array of name identifier format values to request auth context. Default: `["urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"]`. + +- `OVERLEAF_SAML_FORCE_AUTHN` + * If `true`, the initial SAML request from the service provider specifies that the IdP should force re-authentication of the user, + even if they possess a valid session. + +- `OVERLEAF_SAML_DISABLE_REQUESTED_AUTHN_CONTEXT` + * If `true`, do not request a specific auth context. For example, you can this this to `true` to allow additional contexts such as password-less logins (`urn:oasis:names:tc:SAML:2.0:ac:classes:X509`). Support for additional contexts is dependant on your IdP. + +- `OVERLEAF_SAML_AUTHN_REQUEST_BINDING` + * If set to `HTTP-POST`, will request authentication from IdP via HTTP POST binding, otherwise defaults to HTTP-Redirect. + +- `OVERLEAF_SAML_VALIDATE_IN_RESPONSE_TO` + * If `always`, then InResponseTo will be validated from incoming SAML responses. + * If `never`, then InResponseTo won't be validated (default). + * If `ifPresent`, then InResponseTo will only be validated if present in the incoming SAML response. + +- `OVERLEAF_SAML_REQUEST_ID_EXPIRATION_PERIOD_MS` + * Defines the expiration time when a Request ID generated for a SAML request will not be valid if seen + in a SAML response in the `InResponseTo` field. Default: 28800000 (8 hours). + +- `OVERLEAF_SAML_LOGOUT_URL` + * base address to call with logout requests (default: `entryPoint`). + + - Example: `https://idp.example.com/simplesaml/saml2/idp/SingleLogoutService.php` + +- `OVERLEAF_SAML_LOGOUT_CALLBACK_URL` + * Callback URL for IdP initiated logout. Should be the full URL of the `/saml/logot/callback` path. + With this value the `Location` attribute in the `SingleLogoutService` elements in the generated service provider metadata is populated with this value. + + - Example: `https://my-overleaf-instance.com/saml/logout/callback` + +- `OVERLEAF_SAML_ADDITIONAL_LOGOUT_PARAMS` + * JSON dictionary of additional query params to add to 'logout' requests. + + +- `OVERLEAF_SAML_IS_ADMIN_FIELD` and `OVERLEAF_SAML_IS_ADMIN_FIELD_VALUE` + * When both environment variables are set, the login process updates `user.isAdmin = true` if the profile returned by the SAML IdP contains the attribute specified by + `OVERLEAF_SAML_IS_ADMIN_FIELD` and its value either matches `OVERLEAF_SAML_IS_ADMIN_FIELD_VALUE` or is an array containing `OVERLEAF_SAML_IS_ADMIN_FIELD_VALUE`, + otherwise `user.isAdmin` is set to `false`. If either of these variables is not set, then the admin status is only set to `true` during admin user. + creation in Launchpad. + +#### Metadata for the Identity Provider + +The current version of Overleaf CE includes and endpoint to retrieve Service Provider Metadata: `http://my-overleaf-instance.com/saml/meta` + +The Identity Provider will need to be configured to recognize the Overleaf server as a "Service Provider". Consult the documentation for your SAML server for instructions on how to do this. + +Below is an example of appropriate Service Provider metadata: + +
+
ol-meta.xml
+ +``` + + + + + + + MII... +[skipped] + + + + + + + + MII... +[skipped] + + + + + + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + +``` +
+ +Note the certificates, `AssertionConsumerService.Location`, `SingleLogoutService.Location` and `EntityDescriptor.entityID` +and set as appropriate in your IdP configuration, or send the metadata file to the IdP admin. + +
+

Sample variables.env file

+ +``` +OVERLEAF_APP_NAME="Our Overleaf Instance" + +ENABLED_LINKED_FILE_TYPES=project_file,project_output_file + +# Enables Thumbnail generation using ImageMagick +ENABLE_CONVERSIONS=true + +# Disables email confirmation requirement +EMAIL_CONFIRMATION_DISABLED=true + +## Nginx +# NGINX_WORKER_PROCESSES=4 +# NGINX_WORKER_CONNECTIONS=768 + +## Set for TLS via nginx-proxy +# OVERLEAF_BEHIND_PROXY=true +# OVERLEAF_SECURE_COOKIE=true + +OVERLEAF_SITE_URL=http://my-overleaf-instance.com +OVERLEAF_NAV_TITLE=Our Overleaf Instance +# OVERLEAF_HEADER_IMAGE_URL=http://somewhere.com/mylogo.png +OVERLEAF_ADMIN_EMAIL=support@example.com + +OVERLEAF_LEFT_FOOTER=[{"text": "Contact your support team", "url": "mailto:support@example.com"}] +OVERLEAF_RIGHT_FOOTER=[{"text":"Hello, I am on the Right", "url":"https://github.com/yu-i-i/overleaf-cep"}] + +OVERLEAF_EMAIL_FROM_ADDRESS=team@example.com +OVERLEAF_EMAIL_SMTP_HOST=smtp.example.com +OVERLEAF_EMAIL_SMTP_PORT=587 +OVERLEAF_EMAIL_SMTP_SECURE=false +# OVERLEAF_EMAIL_SMTP_USER= +# OVERLEAF_EMAIL_SMTP_PASS= +# OVERLEAF_EMAIL_SMTP_NAME= +OVERLEAF_EMAIL_SMTP_LOGGER=false +OVERLEAF_EMAIL_SMTP_TLS_REJECT_UNAUTH=true +OVERLEAF_EMAIL_SMTP_IGNORE_TLS=false +OVERLEAF_CUSTOM_EMAIL_FOOTER=This system is run by department x + +OVERLEAF_PROXY_LEARN=true +NAV_HIDE_POWERED_BY=true + +################# +## SAML for CE ## +################# + +EXTERNAL_AUTH=saml +OVERLEAF_SAML_IDENTITY_SERVICE_NAME='Login with My IdP' +OVERLEAF_SAML_EMAIL_FIELD=mail +OVERLEAF_SAML_FIRST_NAME_FIELD=givenName +OVERLEAF_SAML_LAST_NAME_FIELD=sn +OVERLEAF_SAML_ENTRYPOINT=https://idp.example.com/simplesamlphp/saml2/idp/SSOService.php +OVERLEAF_SAML_CALLBACK_URL=https://my-overleaf-instance.com/saml/login/callback +OVERLEAF_SAML_LOGOUT_URL=https://idp.example.com/simplesamlphp/saml2/idp/SingleLogoutService.php +OVERLEAF_SAML_LOGOUT_CALLBACK_URL=https://my-overleaf-instance.com/saml/logout/callback +OVERLEAF_SAML_ISSUER=MyOverleaf +OVERLEAF_SAML_IDP_CERT=/overleaf/certs/idp_cert.pem +OVERLEAF_SAML_PUBLIC_CERT=/overleaf/certs/myol_cert.pem +OVERLEAF_SAML_PRIVATE_KEY=/overleaf/certs/myol_key.pem +OVERLEAF_SAML_DECRYPTION_CERT=/overleaf/certs/myol_decr_cert.pem +OVERLEAF_SAML_DECRYPTION_PVK=/overleaf/certs/myol_decr_key.pem +OVERLEAF_SAML_IS_ADMIN_FIELD=mail +OVERLEAF_SAML_IS_ADMIN_FIELD_VALUE=overleaf.admin@example.com +``` +
+
## Overleaf Docker Image @@ -60,14 +706,11 @@ in which to run the Overleaf services. Baseimage uses the `runit` service manager to manage services, and we add our init-scripts from the `server-ce/runit` folder. - -## Contributing - -Please see the [CONTRIBUTING](CONTRIBUTING.md) file for information on contributing to the development of Overleaf. - ## Authors [The Overleaf Team](https://www.overleaf.com/about) +
+Extensions for CE by: [yu-i-i](https://github.com/yu-i-i/overleaf-cep) ## License diff --git a/doc/screenshot.png b/doc/screenshot.png index 1c1f339630b048780aed1e692290dada8aa23307..92633192a51ec83a2e16a0b41d2f4f0d7a392cf9 100644 GIT binary patch literal 1080606 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¥5?kaZ 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 z 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 S|ENsy_k69r(Dp&{XDq-7zt1q^L3@S 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

j$ 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

Chb>7E3?M*}o!sNcD%hrl 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ᓨo(DHmiXoM1Ah4R<@7lR`8P!b?YR`Bcw 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È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