Merge pull request #26968 from overleaf/em-revert-jul8

Revert bad deploy

GitOrigin-RevId: fd6227cf4fde7fd8053b47365154d59d15fa115e
This commit is contained in:
Eric Mc Sween
2025-07-08 11:40:04 -04:00
committed by Copybot
parent ff89d3b834
commit 855b7ca628
22 changed files with 177 additions and 292 deletions

View File

@@ -26,8 +26,6 @@ class SubtotalLimitExceededError extends OError {}
class HasPastDueInvoiceError extends OError {}
class HasNoAdditionalLicenseWhenManuallyCollectedError extends OError {}
class PaymentActionRequiredError extends OError {
constructor(info) {
super('Payment action required', info)
@@ -45,5 +43,4 @@ module.exports = {
InactiveError,
SubtotalLimitExceededError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
}

View File

@@ -8,6 +8,7 @@ import SessionManager from '../Authentication/SessionManager.js'
import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
import { expressify } from '@overleaf/promise-utils'
import Modules from '../../infrastructure/Modules.js'
import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
import UserGetter from '../User/UserGetter.js'
import { Subscription } from '../../models/Subscription.js'
import { isProfessionalGroupPlan } from './PlansHelper.mjs'
@@ -18,7 +19,6 @@ import {
InactiveError,
SubtotalLimitExceededError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
} from './Errors.js'
/**
@@ -151,13 +151,26 @@ async function addSeatsToGroupSubscription(req, res) {
await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice(
subscription
)
await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
paymentProviderSubscription,
userId
)
await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
paymentProviderSubscription
)
const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } =
await SplitTestHandler.promises.getAssignment(
req,
res,
'flexible-group-licensing-for-manually-billed-subscriptions'
)
if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') {
await SubscriptionGroupHandler.promises.checkBillingInfoExistence(
paymentProviderSubscription,
userId
)
} else {
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
paymentProviderSubscription
)
// Check if the user has missing billing details
await Modules.promises.hooks.fire('getPaymentMethod', userId)
}
res.render('subscriptions/add-seats', {
subscriptionId: subscription._id,
@@ -174,7 +187,7 @@ async function addSeatsToGroupSubscription(req, res) {
)
}
if (error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError) {
if (error instanceof ManuallyCollectedError) {
return res.redirect(
'/user/subscription/group/manually-collected-subscription'
)
@@ -215,10 +228,10 @@ async function previewAddSeatsSubscriptionChange(req, res) {
} catch (error) {
if (
error instanceof MissingBillingInfoError ||
error instanceof ManuallyCollectedError ||
error instanceof PendingChangeError ||
error instanceof InactiveError ||
error instanceof HasPastDueInvoiceError ||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
error instanceof HasPastDueInvoiceError
) {
return res.status(422).end()
}
@@ -258,10 +271,10 @@ async function createAddSeatsSubscriptionChange(req, res) {
} catch (error) {
if (
error instanceof MissingBillingInfoError ||
error instanceof ManuallyCollectedError ||
error instanceof PendingChangeError ||
error instanceof InactiveError ||
error instanceof HasPastDueInvoiceError ||
error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError
error instanceof HasPastDueInvoiceError
) {
return res.status(422).end()
}
@@ -398,6 +411,12 @@ async function manuallyCollectedSubscription(req, res) {
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
await SplitTestHandler.promises.getAssignment(
req,
res,
'flexible-group-licensing-for-manually-billed-subscriptions'
)
res.render('subscriptions/manually-collected-subscription', {
groupName: subscription.teamName,
})

View File

@@ -18,7 +18,6 @@ const {
PendingChangeError,
InactiveError,
HasPastDueInvoiceError,
HasNoAdditionalLicenseWhenManuallyCollectedError,
} = require('./Errors')
const EmailHelper = require('../Helpers/EmailHelper')
const { InvalidEmailError } = require('../Errors/Errors')
@@ -124,22 +123,6 @@ async function ensureSubscriptionHasNoPastDueInvoice(subscription) {
}
}
async function ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
paymentProviderSubscription
) {
if (
paymentProviderSubscription.isCollectionMethodManual &&
!paymentProviderSubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE)
) {
throw new HasNoAdditionalLicenseWhenManuallyCollectedError(
'This subscription is being collected manually has no "additional-license" add-on',
{
subscription_id: paymentProviderSubscription.id,
}
)
}
}
async function getUsersGroupSubscriptionDetails(userId) {
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
@@ -487,10 +470,6 @@ module.exports = {
ensureSubscriptionHasNoPastDueInvoice: callbackify(
ensureSubscriptionHasNoPastDueInvoice
),
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
callbackify(
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
),
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
isUserPartOfGroup: callbackify(isUserPartOfGroup),
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
@@ -505,7 +484,6 @@ module.exports = {
ensureSubscriptionCollectionMethodIsNotManual,
ensureSubscriptionHasNoPendingChanges,
ensureSubscriptionHasNoPastDueInvoice,
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual,
getTotalConfirmedUsersInGroup,
isUserPartOfGroup,
getUsersGroupSubscriptionDetails,

View File

@@ -4,7 +4,6 @@
const { callbackifyAll } = require('@overleaf/promise-utils')
const { Subscription } = require('../../models/Subscription')
const SubscriptionHelper = require('./SubscriptionHelper')
const { DeletedSubscription } = require('../../models/DeletedSubscription')
const logger = require('@overleaf/logger')
const {
@@ -176,8 +175,7 @@ const SubscriptionLocator = {
const hasActiveGroupSubscription = memberSubscriptions.some(
subscription =>
subscription.groupPlan &&
SubscriptionHelper.getPaidSubscriptionState(subscription) === 'active'
subscription.recurlyStatus?.state === 'active' && subscription.groupPlan
)
if (hasActiveGroupSubscription) {
// Member of a group plan
@@ -189,8 +187,7 @@ const SubscriptionLocator = {
if (personalSubscription) {
const hasActivePersonalSubscription =
SubscriptionHelper.getPaidSubscriptionState(personalSubscription) ===
'active'
personalSubscription.recurlyStatus?.state === 'active'
if (hasActivePersonalSubscription) {
if (personalSubscription.groupPlan) {
// Owner of a group plan

View File

@@ -79,7 +79,11 @@ async function getPopulatedListOfMembers(entity, attributes) {
}
}
const users = await UserMembershipViewModel.promises.buildAsync(userObjects)
const users = await Promise.all(
userObjects.map(userObject =>
UserMembershipViewModel.promises.buildAsync(userObject)
)
)
for (const user of users) {
if (

View File

@@ -23,59 +23,30 @@ const UserMembershipViewModel = {
}
},
buildAsync(userOrIdOrEmailArray, callback) {
buildAsync(userOrIdOrEmail, callback) {
if (callback == null) {
callback = function () {}
}
if (!isObjectIdInstance(userOrIdOrEmail)) {
// userOrIdOrEmail is a user or an email and can be parsed by #build
return callback(null, UserMembershipViewModel.build(userOrIdOrEmail))
}
const userObjectIds = userOrIdOrEmailArray.filter(isObjectIdInstance)
return UserGetter.getUsers(
userObjectIds,
{
email: 1,
first_name: 1,
last_name: 1,
lastLoggedIn: 1,
lastActive: 1,
enrollment: 1,
},
function (error, users) {
const results = []
if (error != null) {
userOrIdOrEmailArray.forEach(item => {
if (isObjectIdInstance(item)) {
results.push(buildUserViewModelWithId(item.toString()))
} else {
// `item` is a user or an email and can be parsed by #build
results.push(UserMembershipViewModel.build(item))
}
})
} else {
const usersMap = new Map()
for (const user of users) {
usersMap.set(user._id.toString(), user)
}
userOrIdOrEmailArray.forEach(item => {
if (isObjectIdInstance(item)) {
const user = usersMap.get(item.toString())
if (user == null) {
results.push(buildUserViewModelWithId(item.toString()))
} else {
results.push(buildUserViewModel(user))
}
} else {
// `item` is a user or an email and can be parsed by #build
results.push(UserMembershipViewModel.build(item))
}
})
}
callback(null, results)
const userId = userOrIdOrEmail
const projection = {
email: 1,
first_name: 1,
last_name: 1,
lastLoggedIn: 1,
lastActive: 1,
enrollment: 1,
}
return UserGetter.getUser(userId, projection, function (error, user) {
if (error != null || user == null) {
return callback(null, buildUserViewModelWithId(userId.toString()))
}
)
return callback(null, buildUserViewModel(user))
})
},
}

View File

@@ -872,7 +872,8 @@
"is_email_affiliated": "",
"issued_on": "",
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "",
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "",
"it_looks_like_your_account_is_billed_manually": "",
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "",
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "",
"italics": "",
"join_beta_program": "",
@@ -1025,7 +1026,6 @@
"math_inline": "",
"maximum_files_uploaded_together": "",
"maybe_later": "",
"members_added": "",
"members_management": "",
"mendeley_dynamic_sync_description": "",
"mendeley_groups_loading_error": "",
@@ -1146,10 +1146,10 @@
"on_free_plan_upgrade_to_access_features": "",
"one_step_away_from_professional_features": "",
"only_group_admin_or_managers_can_delete_your_account_1": "",
"only_group_admin_or_managers_can_delete_your_account_10": "",
"only_group_admin_or_managers_can_delete_your_account_3": "",
"only_group_admin_or_managers_can_delete_your_account_6": "",
"only_group_admin_or_managers_can_delete_your_account_7": "",
"only_group_admin_or_managers_can_delete_your_account_8": "",
"only_group_admin_or_managers_can_delete_your_account_9": "",
"only_importer_can_refresh": "",
"open_action_menu": "",

View File

@@ -30,6 +30,7 @@ import {
} from '../../../../../../types/subscription/subscription-change-preview'
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
import { sendMB } from '../../../../infrastructure/event-tracking'
import { useFeatureFlag } from '@/shared/context/split-test-context'
export const MAX_NUMBER_OF_USERS = 20
export const MAX_NUMBER_OF_PO_NUMBER_CHARACTERS = 50
@@ -49,6 +50,9 @@ function AddSeats() {
const [addSeatsInputError, setAddSeatsInputError] = useState<string>()
const [poNumberInputError, setPoNumberInputError] = useState<string>()
const [shouldContactSales, setShouldContactSales] = useState(false)
const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag(
'flexible-group-licensing-for-manually-billed-subscriptions'
)
const controller = useAbortController()
const { signal: addSeatsSignal } = useAbortController()
const { signal: contactSalesSignal } = useAbortController()
@@ -369,12 +373,13 @@ function AddSeats() {
<FormText type="error">{addSeatsInputError}</FormText>
)}
</FormGroup>
{isCollectionMethodManual && (
<PoNumber
error={poNumberInputError}
validate={validatePoNumber}
/>
)}
{isFlexibleGroupLicensingForManuallyBilledSubscriptions &&
isCollectionMethodManual && (
<PoNumber
error={poNumberInputError}
validate={validatePoNumber}
/>
)}
</div>
<CostSummarySection
isLoadingCostSummary={isLoadingCostSummary}

View File

@@ -13,7 +13,6 @@ import OLCard from '@/features/ui/components/ol/ol-card'
import OLButton from '@/features/ui/components/ol/ol-button'
import OLFormControl from '@/features/ui/components/ol/ol-form-control'
import OLFormText from '@/features/ui/components/ol/ol-form-text'
import OLNotification from '@/features/ui/components/ol/ol-notification'
export default function GroupMembers() {
const { isReady } = useWaitForI18n()
@@ -27,7 +26,6 @@ export default function GroupMembers() {
removeMemberError,
inviteMemberLoading,
inviteError,
memberAdded,
paths,
} = useGroupMembersContext()
const [emailString, setEmailString] = useState<string>('')
@@ -142,13 +140,6 @@ export default function GroupMembers() {
data-testid="add-more-members-form"
>
<p className="small">{t('invite_more_members')}</p>
{memberAdded && (
<OLNotification
content={t('members_added')}
type="success"
className="mt-2 mb-3"
/>
)}
<ErrorAlert error={inviteError} />
<form onSubmit={onAddMembersSubmit}>
<OLRow>

View File

@@ -2,9 +2,14 @@ import { Trans, useTranslation } from 'react-i18next'
import OLNotification from '@/features/ui/components/ol/ol-notification'
import Card from '@/features/group-management/components/card'
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import { useFeatureFlag } from '@/shared/context/split-test-context'
function ManuallyCollectedSubscription() {
const { t } = useTranslation()
const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag(
'flexible-group-licensing-for-manually-billed-subscriptions'
)
const { isReady } = useWaitForI18n()
if (!isReady) {
@@ -17,13 +22,23 @@ function ManuallyCollectedSubscription() {
type="error"
title={t('account_billed_manually')}
content={
<Trans
i18nKey="it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/contact" rel="noreferrer noopener" />,
]}
/>
isFlexibleGroupLicensingForManuallyBilledSubscriptions ? (
<Trans
i18nKey="it_looks_like_your_account_is_billed_manually_upgrading_subscription"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/contact" rel="noreferrer noopener" />,
]}
/>
) : (
<Trans
i18nKey="it_looks_like_your_account_is_billed_manually"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a href="/contact" rel="noreferrer noopener" />,
]}
/>
)
}
className="m-0"
/>

View File

@@ -30,6 +30,9 @@ export default function ManagedUserStatus({ user }: ManagedUserStatusProps) {
</span>
)
if (user.isEntityAdmin) {
return <span className="security-state-group-admin" />
}
if (user.invite) {
return managedUserInvite
}

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useMemo } from 'react'
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { User } from '../../../../../../types/group-management/user'
import { useGroupMembersContext } from '../../context/group-members-context'
@@ -13,9 +13,6 @@ import getMeta from '@/utils/meta'
import UnlinkUserModal from './unlink-user-modal'
import OLTable from '@/features/ui/components/ol/ol-table'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import Pagination from '@/shared/components/pagination'
const USERS_DISPLAY_LIMIT = 50
type ManagedUsersListProps = {
groupId: string
@@ -34,30 +31,13 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
const managedUsersActive = getMeta('ol-managedUsersActive')
const groupSSOActive = getMeta('ol-groupSSOActive')
const tHeadRowRef = useRef<HTMLTableRowElement>(null)
const [pagination, setPagination] = useState({ currPage: 1, totalPages: 1 })
const usersForCurrentPage = useMemo(
() =>
users.slice(
(pagination.currPage - 1) * USERS_DISPLAY_LIMIT,
pagination.currPage * USERS_DISPLAY_LIMIT
),
[users, pagination.currPage]
)
const handlePageClick = (
_e: React.MouseEvent<HTMLButtonElement>,
page: number
) => {
setPagination(p => ({ ...p, currPage: page }))
}
const [colSpan, setColSpan] = useState(0)
useEffect(() => {
setPagination(p => ({
...p,
totalPages: Math.ceil(users.length / USERS_DISPLAY_LIMIT),
}))
}, [users.length])
if (tHeadRowRef.current) {
setColSpan(tHeadRowRef.current.querySelectorAll('th').length)
}
}, [])
return (
<div>
@@ -113,17 +93,12 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
<tbody>
{users.length === 0 && (
<tr>
<td
className="text-center"
colSpan={
tHeadRowRef.current?.querySelectorAll('th').length ?? 0
}
>
<td className="text-center" colSpan={colSpan}>
<small>{t('no_members')}</small>
</td>
</tr>
)}
{usersForCurrentPage.map(user => (
{users.map(user => (
<MemberRow
key={user.email}
user={user}
@@ -136,15 +111,6 @@ export default function MembersList({ groupId }: ManagedUsersListProps) {
))}
</tbody>
</OLTable>
{pagination.totalPages > 1 && (
<div className="d-flex justify-content-center">
<Pagination
handlePageClick={handlePageClick}
currentPage={pagination.currPage}
totalPages={pagination.totalPages}
/>
</div>
)}
{userToOffboard && (
<OffboardManagedUserModal
user={userToOffboard}

View File

@@ -31,7 +31,6 @@ export type GroupMembersContextValue = {
updateMemberView: (userId: string, updatedUser: User) => void
inviteMemberLoading: boolean
inviteError?: APIError
memberAdded: boolean
paths: { [key: string]: string }
}
@@ -59,7 +58,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
const [inviteError, setInviteError] = useState<APIError>()
const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0)
const [removeMemberError, setRemoveMemberError] = useState<APIError>()
const [memberAdded, setMemberAdded] = useState(false)
const groupId = getMeta('ol-groupId')
@@ -76,9 +74,7 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
const addMembers = useCallback(
(emailString: string) => {
setInviteError(undefined)
setMemberAdded(false)
const emails = parseEmails(emailString)
let isError = false
mapSeries(emails, async email => {
setInviteUserInflightCount(count => count + 1)
try {
@@ -98,15 +94,8 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
} catch (error: unknown) {
debugConsole.error(error)
setInviteError((error as FetchError)?.data?.error || {})
isError = true
}
setInviteUserInflightCount(count => {
const newCount = count - 1
if (newCount === 0 && !isError) {
setMemberAdded(true)
}
return newCount
})
setInviteUserInflightCount(count => count - 1)
})
},
[paths.addMember, users, setUsers]
@@ -184,7 +173,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
removeMemberError,
inviteMemberLoading: inviteUserInflightCount > 0,
inviteError,
memberAdded,
paths,
}),
[
@@ -203,7 +191,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) {
removeMemberError,
inviteUserInflightCount,
inviteError,
memberAdded,
paths,
]
)

View File

@@ -247,7 +247,6 @@ export interface Meta {
'ol-splitTestVariants': { [name: string]: string }
'ol-ssoDisabled': boolean
'ol-ssoErrorMessage': string
'ol-stripeAccountId': string
'ol-stripeCustomerId': string
'ol-subscription': any // TODO: mixed types, split into two fields
'ol-subscriptionChangePreview': SubscriptionChangePreview

View File

@@ -1130,7 +1130,8 @@
"issued_on": "Issued: __date__",
"it": "Italian",
"it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "It looks like that didnt work. You can try again or <0>get in touch</0> with our Support team for more help.",
"it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "It looks like your account is being billed manually - purchasing additional licenses or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_account_is_billed_manually": "It looks like your account is being billed manually - adding seats or upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_account_is_billed_manually_upgrading_subscription": "It looks like your account is being billed manually - upgrading your subscription can only be done by the Support team. Please <0>get in touch</0> for help.",
"it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "It looks like your payment details are missing. Please <0>update your billing information</0>, or <1>get in touch</1> with our Support team for more help.",
"italics": "Italics",
"ja": "Japanese",
@@ -1341,7 +1342,6 @@
"may": "May",
"maybe_later": "Maybe later",
"member_picker": "Select number of users for group plan",
"members_added": "Member(s) added.",
"members_management": "Members management",
"mendeley": "Mendeley",
"mendeley_dynamic_sync_description": "With the Mendeley integration, you can import your references into __appName__. You can either import all your references at once or dynamically search your Mendeley library directly from __appName__.",
@@ -1504,10 +1504,10 @@
"ongoing_experiments": "Ongoing experiments",
"online_latex_editor": "Online LaTeX Editor",
"only_group_admin_or_managers_can_delete_your_account_1": "By becoming a managed user, your organization will have admin rights over your account and control over your stuff, including the right to close your account and access, delete and share your stuff. As a result:",
"only_group_admin_or_managers_can_delete_your_account_10": "If you have an individual subscription, well automatically terminate it and cancel its renewal when your account becomes managed. To request a pro-rata refund for the remainder, please contact Support.",
"only_group_admin_or_managers_can_delete_your_account_3": "Your group admin and group managers will be able to reassign ownership of your projects to another group member.",
"only_group_admin_or_managers_can_delete_your_account_6": "Only your group admin or group managers will be able to delete your account or change your account back into an unmanaged account.",
"only_group_admin_or_managers_can_delete_your_account_7": "Only your group admin or group managers will be able to delete your account or change your account into an unmanaged account.",
"only_group_admin_or_managers_can_delete_your_account_8": "Well cancel the renewal of your subscription, reach out to Support to request a pro-rata refund. Your individual subscription will be terminated when your account becomes managed.",
"only_group_admin_or_managers_can_delete_your_account_9": "Once you have become a managed user, <0>you yourself cannot change it back to an unmanaged account</0>. <1>Learn more about managed Overleaf accounts.</1>",
"only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.",
"open_action_menu": "Open __name__ action menu",

View File

@@ -14,6 +14,9 @@ describe('<AddSeats />', function () {
win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses)
win.metaAttributesCache.set('ol-isProfessional', false)
win.metaAttributesCache.set('ol-isCollectionMethodManual', true)
win.metaAttributesCache.set('ol-splitTestVariants', {
'flexible-group-licensing-for-manually-billed-subscriptions': 'enabled',
})
})
cy.mount(

View File

@@ -61,4 +61,26 @@ describe('MemberStatus', function () {
cy.get('.security-state-not-managed').contains('Managed')
})
})
describe('with the group admin', function () {
const user: User = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: false,
last_active_at: new Date(),
enrollment: undefined,
isEntityAdmin: true,
}
beforeEach(function () {
cy.mount(<ManagedUserStatus user={user} />)
})
it('should render no state indicator', function () {
cy.get('.security-state-group-admin')
.contains('Managed')
.should('not.exist')
})
})
})

View File

@@ -121,16 +121,6 @@ describe('MembersList', function () {
users[1].last_name
)
})
it('should render the pagination navigation', function () {
cy.window().then(win => {
win.metaAttributesCache.set(
'ol-users',
Array.from({ length: 50 }).flatMap(() => users.flat())
)
})
mountManagedUsersList()
cy.findByRole('navigation', { name: /pagination navigation/i })
})
})
describe('empty user list', function () {

View File

@@ -73,8 +73,6 @@ describe('SubscriptionGroupController', function () {
.resolves(ctx.previewSubscriptionChangeData),
checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod),
updateSubscriptionPaymentTerms: sinon.stub().resolves(),
ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual:
sinon.stub().resolves(),
},
}
@@ -107,6 +105,12 @@ describe('SubscriptionGroupController', function () {
},
}
ctx.SplitTestHandler = {
promises: {
getAssignment: sinon.stub().resolves({ variant: 'enabled' }),
},
}
ctx.UserGetter = {
promises: {
getUserEmail: sinon.stub().resolves(ctx.user.email),
@@ -136,7 +140,6 @@ describe('SubscriptionGroupController', function () {
InactiveError: class extends Error {},
SubtotalLimitExceededError: class extends Error {},
HasPastDueInvoiceError: class extends Error {},
HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {},
}
vi.doMock(
@@ -168,6 +171,13 @@ describe('SubscriptionGroupController', function () {
default: ctx.Modules,
}))
vi.doMock(
'../../../../app/src/Features/SplitTests/SplitTestHandler',
() => ({
default: ctx.SplitTestHandler,
})
)
vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
default: ctx.UserGetter,
}))
@@ -446,9 +456,6 @@ describe('SubscriptionGroupController', function () {
ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence
.calledWith(ctx.recurlySubscription, ctx.adminUserId)
.should.equal(true)
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual
.calledWith(ctx.recurlySubscription)
.should.equal(true)
page.should.equal('subscriptions/add-seats')
props.subscriptionId.should.equal(ctx.subscriptionId)
props.groupName.should.equal(ctx.subscription.teamName)
@@ -514,28 +521,6 @@ describe('SubscriptionGroupController', function () {
})
})
it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) {
await new Promise(resolve => {
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual =
sinon
.stub()
.throws(
new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError()
)
const res = {
redirect: url => {
url.should.equal(
'/user/subscription/group/manually-collected-subscription'
)
resolve()
},
}
ctx.Controller.addSeatsToGroupSubscription(ctx.req, res)
})
})
it('should redirect to subscription page when there is a pending change', async function (ctx) {
await new Promise(resolve => {
ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges =

View File

@@ -806,49 +806,6 @@ describe('SubscriptionGroupHandler', function () {
})
})
describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () {
it('should throw if the subscription is manually collected and has no additional license add-on', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: true,
hasAddOn: sinon
.stub()
.withArgs('additional-license')
.returns(false),
}
)
).to.be.rejectedWith(
'This subscription is being collected manually has no "additional-license" add-on'
)
})
it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: false,
hasAddOn: sinon
.stub()
.withArgs('additional-license')
.returns(false),
}
)
).to.not.be.rejected
})
it('should not throw if the subscription is not manually collected and has additional license add-on', async function () {
await expect(
this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual(
{
isCollectionMethodManual: true,
hasAddOn: sinon.stub().withArgs('additional-license').returns(true),
}
)
).to.not.be.rejected
})
})
describe('getGroupPlanUpgradePreview', function () {
it('should generate preview for subscription upgrade', async function () {
const result = await this.Handler.promises.getGroupPlanUpgradePreview(

View File

@@ -48,7 +48,7 @@ describe('UserMembershipHandler', function () {
this.UserMembershipViewModel = {
promises: {
buildAsync: sinon.stub().resolves([{ _id: 'mock-member-id' }]),
buildAsync: sinon.stub().resolves({ _id: 'mock-member-id' }),
},
build: sinon.stub().returns(this.newUser),
}
@@ -118,26 +118,26 @@ describe('UserMembershipHandler', function () {
this.subscription,
EntityConfigs.group
)
const expectedCallcount =
this.subscription.member_ids.length +
this.subscription.invited_emails.length +
this.subscription.teamInvites.length
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(
this.subscription.invited_emails.concat(
this.subscription.teamInvites[0].email,
this.subscription.member_ids
)
)
this.UserMembershipViewModel.promises.buildAsync.callCount
).to.equal(expectedCallcount)
})
})
describe('group managers', function () {
describe('group mamagers', function () {
it('build view model for all managers', async function () {
await this.UserMembershipHandler.promises.getUsers(
this.subscription,
EntityConfigs.groupManagers
)
const expectedCallcount = this.subscription.manager_ids.length
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(this.subscription.manager_ids)
this.UserMembershipViewModel.promises.buildAsync.callCount
).to.equal(expectedCallcount)
})
})
@@ -147,9 +147,11 @@ describe('UserMembershipHandler', function () {
this.institution,
EntityConfigs.institution
)
const expectedCallcount = this.institution.managerIds.length
expect(
this.UserMembershipViewModel.promises.buildAsync
).to.be.calledOnceWith(this.institution.managerIds)
this.UserMembershipViewModel.promises.buildAsync.callCount
).to.equal(expectedCallcount)
})
})
})

View File

@@ -25,7 +25,7 @@ const {
describe('UserMembershipViewModel', function () {
beforeEach(function () {
this.UserGetter = { getUsers: sinon.stub() }
this.UserGetter = { getUser: sinon.stub() }
this.UserMembershipViewModel = SandboxedModule.require(modulePath, {
requires: {
'mongodb-legacy': { ObjectId },
@@ -87,10 +87,9 @@ describe('UserMembershipViewModel', function () {
})
it('build email', function (done) {
this.UserGetter.getUsers.yields(null, [])
return this.UserMembershipViewModel.buildAsync(
[this.email],
(error, [viewModel]) => {
this.email,
(error, viewModel) => {
assertCalledWith(this.UserMembershipViewModel.build, this.email)
return done()
}
@@ -98,10 +97,9 @@ describe('UserMembershipViewModel', function () {
})
it('build user', function (done) {
this.UserGetter.getUsers.yields(null, [])
return this.UserMembershipViewModel.buildAsync(
[this.user],
(error, [viewModel]) => {
this.user,
(error, viewModel) => {
assertCalledWith(this.UserMembershipViewModel.build, this.user)
return done()
}
@@ -109,34 +107,30 @@ describe('UserMembershipViewModel', function () {
})
it('build user id', function (done) {
const user = {
...this.user,
_id: new ObjectId(),
}
this.UserGetter.getUsers.yields(null, [user])
this.UserGetter.getUser.yields(null, this.user)
return this.UserMembershipViewModel.buildAsync(
[user._id],
(error, [viewModel]) => {
new ObjectId(),
(error, viewModel) => {
expect(error).not.to.exist
assertNotCalled(this.UserMembershipViewModel.build)
expect(viewModel._id.toString()).to.equal(user._id.toString())
expect(viewModel.email).to.equal(user.email)
expect(viewModel.first_name).to.equal(user.first_name)
expect(viewModel._id).to.equal(this.user._id)
expect(viewModel.email).to.equal(this.user.email)
expect(viewModel.first_name).to.equal(this.user.first_name)
expect(viewModel.invite).to.equal(false)
expect(viewModel.email).to.exist
expect(viewModel.enrollment).to.exist
expect(viewModel.enrollment).to.deep.equal(user.enrollment)
expect(viewModel.enrollment).to.deep.equal(this.user.enrollment)
return done()
}
)
})
it('build user id with error', function (done) {
this.UserGetter.getUsers.yields(new Error('nope'), [])
this.UserGetter.getUser.yields(new Error('nope'))
const userId = new ObjectId()
return this.UserMembershipViewModel.buildAsync(
[userId],
(error, [viewModel]) => {
userId,
(error, viewModel) => {
expect(error).not.to.exist
assertNotCalled(this.UserMembershipViewModel.build)
expect(viewModel._id).to.equal(userId.toString())