Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Restrict prompt to join org to a smaller set of orgs #9265

Merged
merged 1 commit into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 249 additions & 4 deletions packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ type TestOrganizationUser = Pick<
const addOrg = async (
activeDomain: string | null,
members: TestOrganizationUser[],
featureFlags?: string[]
rest?: {featureFlags?: string[]; tier?: string}
) => {
const {featureFlags, tier} = rest ?? {}
const orgId = generateUID()
const org = {
id: orgId,
activeDomain,
featureFlags: featureFlags ?? []
featureFlags: featureFlags ?? [],
tier: tier ?? 'starter'
}

const orgUsers = members.map((member) => ({
Expand Down Expand Up @@ -143,7 +145,7 @@ test('Org with noPromptToJoinOrg feature flag is ignored', async () => {
userId: 'user2'
}
],
['noPromptToJoinOrg']
{featureFlags: ['noPromptToJoinOrg']}
)

const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader)
Expand Down Expand Up @@ -285,7 +287,84 @@ test('Org matching the user are ignored', async () => {
expect(userLoader.loadMany).toHaveBeenCalledTimes(0)
})

test('All orgs with verified emails qualify', async () => {
test('Only the biggest org with verified emails qualify', async () => {
const org = await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder1'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member1'
}
])
const biggerOrg = await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
])
await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder3'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
])

userLoader.loadMany.mockImplementation((userIds) => {
const users = {
founder1: {
email: 'user1@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder2: {
email: 'user2@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder3: {
email: 'user3@parabol.co',
identities: [
{
isEmailVerified: false
}
]
}
}
return userIds.map((id) => ({
id,
...users[id]
}))
})

const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader)
expect(userLoader.loadMany).toHaveBeenCalledTimes(3)
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3'])
expect(orgIds).toIncludeSameMembers([biggerOrg])
})

test('All the biggest orgs with verified emails qualify', async () => {
const org1 = await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
Expand Down Expand Up @@ -358,6 +437,172 @@ test('All orgs with verified emails qualify', async () => {
expect(orgIds).toIncludeSameMembers([org1, org2])
})

test('Team trumps starter tier with more users org', async () => {
const teamOrg = await addOrg(
'parabol.co',
[
{
joinedAt: new Date('2023-09-06'),
userId: 'founder1'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member1'
}
],
{tier: 'team'}
)
const biggerStarterOrg = await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
])
await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder3'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
])

userLoader.loadMany.mockImplementation((userIds) => {
const users = {
founder1: {
email: 'user1@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder2: {
email: 'user2@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder3: {
email: 'user3@parabol.co',
identities: [
{
isEmailVerified: false
}
]
}
}
return userIds.map((id) => ({
id,
...users[id]
}))
})

const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader)
expect(userLoader.loadMany).toHaveBeenCalledTimes(3)
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3'])
expect(orgIds).toIncludeSameMembers([teamOrg])
})

test('Enterprise trumps team tier with more users org', async () => {
const enterpriseOrg = await addOrg(
'parabol.co',
[
{
joinedAt: new Date('2023-09-06'),
userId: 'founder1'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member1'
}
],
{tier: 'enterprise'}
)
const starterOrg = await addOrg(
'parabol.co',
[
{
joinedAt: new Date('2023-09-06'),
userId: 'founder2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member2'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
],
{tier: 'team'}
)
await addOrg('parabol.co', [
{
joinedAt: new Date('2023-09-06'),
userId: 'founder3'
},
{
joinedAt: new Date('2023-09-07'),
userId: 'member3'
}
])

userLoader.loadMany.mockImplementation((userIds) => {
const users = {
founder1: {
email: 'user1@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder2: {
email: 'user2@parabol.co',
identities: [
{
isEmailVerified: true
}
]
},
founder3: {
email: 'user3@parabol.co',
identities: [
{
isEmailVerified: false
}
]
}
}
return userIds.map((id) => ({
id,
...users[id]
}))
})

const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader)
expect(userLoader.loadMany).toHaveBeenCalledTimes(3)
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2'])
expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3'])
expect(orgIds).toIncludeSameMembers([enterpriseOrg])
})

test('Orgs with verified emails from different domains do not qualify', async () => {
const org1 = await addOrg('parabol.co', [
{
Expand Down
31 changes: 29 additions & 2 deletions packages/server/utils/isRequestToJoinDomainAllowed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import User from '../database/types/User'
import {DataLoaderWorker} from '../graphql/graphql'
import isValid from '../graphql/isValid'
import TeamMember from '../database/types/TeamMember'
import Organization from '../database/types/Organization'

export const getEligibleOrgIdsByDomain = async (
activeDomain: string,
Expand Down Expand Up @@ -39,7 +40,8 @@ export const getEligibleOrgIdsByDomain = async (
)
.run()

const eligibleOrgs = await Promise.all(
type OrgWithActiveMembers = Organization & {activeMembers: number}
const eligibleOrgs = (await Promise.all(
orgs.map(async (org) => {
const {founder} = org
const importantMembers = org.billingLeads.slice() as TeamMember[]
Expand All @@ -57,8 +59,33 @@ export const getEligibleOrgIdsByDomain = async (
}
return org
})
)) as OrgWithActiveMembers[]

const highestTierOrgs = eligibleOrgs.filter(isValid).reduce((acc, org) => {
if (acc.length === 0) {
return [org]
}
const highestTier = acc[0]!.tier
if (org.tier === highestTier) {
return [...acc, org]
}
if (org.tier === 'enterprise') {
return [org]
}
if (highestTier === 'starter' && org.tier === 'team') {
return [org]
}
return acc
}, [] as OrgWithActiveMembers[])

const biggestSize = highestTierOrgs.reduce(
(acc, org) => (org.activeMembers > acc ? org.activeMembers : acc),
0
)
return eligibleOrgs.filter(isValid).map(({id}) => id)

return highestTierOrgs
.filter(({activeMembers}) => activeMembers === biggestSize)
.map(({id}) => id)
}

const isRequestToJoinDomainAllowed = async (
Expand Down
Loading