import { assignToWorkspaces, BasicTestWorkspace, createTestWorkspaces, createWorkspaceInviteDirectly, unassignFromWorkspace } from '@/modules/workspaces/tests/helpers/creation' import { BasicTestUser, createTestUsers } from '@/test/authHelper' import { createTestContext, testApolloServer, TestApolloServer } from '@/test/graphqlHelper' import { beforeEachContext, truncateTables } from '@/test/hooks' import { WorkspaceRole } from '@/modules/core/graph/generated/graphql' import { expect } from 'chai' import { captureCreatedInvite, validateInviteExistanceFromEmail } from '@/test/speckle-helpers/inviteHelper' import { Roles, StreamRoles, WorkspaceRoles } from '@speckle/shared' import { itEach } from '@/test/assertionHelper' import { ServerInvites } from '@/modules/core/dbSchema' import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql' import { times } from 'lodash-es' import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites' import { db } from '@/db/knex' import { BasicTestStream, createTestStreams, leaveStream } from '@/test/speckle-helpers/streamHelper' import { Workspaces } from '@/modules/workspaces/helpers/db' import { generateRegistrationParams, localAuthRestApi, LocalAuthRestApiHelpers } from '@/modules/auth/tests/helpers/registration' import type { Express } from 'express' import { AllScopes } from '@/modules/core/helpers/mainConstants' import { createUserEmailFactory, deleteUserEmailFactory, findEmailFactory, findVerifiedEmailsByUserIdFactory, updateUserEmailFactory } from '@/modules/core/repositories/userEmails' import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification' import { createRandomPassword } from '@/modules/core/helpers/testHelpers' import { WorkspaceInvalidRoleError, WorkspaceProtectedError } from '@/modules/workspaces/errors/workspace' import cryptoRandomString from 'crypto-random-string' import { getStreamRolesFactory, grantStreamPermissionsFactory } from '@/modules/core/repositories/streams' import { addOrUpdateStreamCollaboratorFactory, validateStreamAccessFactory } from '@/modules/core/services/streams/access' import { authorizeResolver } from '@/modules/shared' import { getUserFactory } from '@/modules/core/repositories/users' import { TestInvitesGraphQLOperations, buildInvitesGraphqlOperations } from '@/modules/workspaces/tests/helpers/invites' import { getEventBus } from '@/modules/shared/services/eventBus' import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' import { ProjectRecordVisibility } from '@/modules/core/helpers/types' import { getFeatureFlags } from '@/modules/shared/helpers/envHelper' import { createEmailListener, TestEmailListener } from '@/test/speckle-helpers/email' enum InviteByTarget { Email = 'email', Id = 'id' } const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags() const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver }) const getUser = getUserFactory({ db }) const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({ validateStreamAccess, getUser, grantStreamPermissions: grantStreamPermissionsFactory({ db }), getStreamRoles: getStreamRolesFactory({ db }), emitEvent: getEventBus().emit }) describe('Workspaces Invites GQL', () => { let app: Express const me: BasicTestUser = { name: 'Authenticated server invites guy', email: 'serverinvitesguy@example.org', id: '' } const otherGuy: BasicTestUser = { name: 'Some Other DUde', email: 'otherguy111@example.org', id: '' } const myWorkspaceFriend: BasicTestUser = { name: 'My Workspace Friend', email: 'myworkspacefriend@asdasd.com', id: '' } const myFirstWorkspace: BasicTestWorkspace = { name: 'My First Workspace', id: '', ownerId: '', slug: cryptoRandomString({ length: 10 }), domainBasedMembershipProtectionEnabled: false } const domainProtectedWorkspace: BasicTestWorkspace = { name: 'My Domain protected workspace', id: '', ownerId: '', slug: cryptoRandomString({ length: 10 }), domainBasedMembershipProtectionEnabled: true } const otherGuysWorkspace: BasicTestWorkspace = { name: 'Other Guy Workspace', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const workspaceDomain = 'example.org' let emailListener: TestEmailListener before(async () => { const ctx = await beforeEachContext() app = ctx.app await createTestUsers([me, otherGuy, myWorkspaceFriend]) const email = 'something@example.org' await createUserEmailFactory({ db })({ userEmail: { email, primary: false, userId: me.id } }) await markUserEmailAsVerifiedFactory({ updateUserEmail: updateUserEmailFactory({ db }) })({ email }) await createTestWorkspaces([ [myFirstWorkspace, me], [domainProtectedWorkspace, me, { domain: workspaceDomain }], [otherGuysWorkspace, otherGuy] ]) await assignToWorkspaces([ [otherGuysWorkspace, me, Roles.Workspace.Member], [myFirstWorkspace, myWorkspaceFriend, Roles.Workspace.Member] ]) emailListener = await createEmailListener() }) after(async () => { await emailListener.destroy() }) afterEach(() => { emailListener.reset() }) describe('when authenticated', () => { let apollo: TestApolloServer let gqlHelpers: TestInvitesGraphQLOperations before(async () => { apollo = await testApolloServer({ authUserId: me.id, context: { role: Roles.Server.User } }) gqlHelpers = buildInvitesGraphqlOperations({ apollo }) }) describe('and inviting to workspace', () => { afterEach(async () => { await truncateTables([ServerInvites.name]) }) it("doesn't work when inviting user to workspace that doesn't exist", async () => { const res = await gqlHelpers.createInvite({ workspaceId: 'a', input: { userId: otherGuy.id, role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors('You do not have access to the workspace') expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) it("doesn't work when inviting nonexistant user ID", async () => { const res = await gqlHelpers.createInvite({ workspaceId: myFirstWorkspace.id, input: { userId: 'a', role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors('Attempting to invite an invalid user') expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) it("doesn't work if neither email nor user id specified", async () => { const res = await gqlHelpers.createInvite({ workspaceId: myFirstWorkspace.id, input: { role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors('Either email or userId must be specified') expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) it("doesn't work if not workspace admin", async () => { const res = await gqlHelpers.createInvite({ workspaceId: otherGuysWorkspace.id, input: { userId: myWorkspaceFriend.id, role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors( 'You do not have enough permissions in the workspace to perform this action' ) expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) it('should throw an error when trying to invite a user as a member without email matching domain and domain protection is enabled', async () => { const res = await gqlHelpers.createInvite({ workspaceId: domainProtectedWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors( 'The target user has no verified emails matching the domain policies' ) expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) it('batch inviting fails if more than 200 invites', async () => { const res = await gqlHelpers.batchCreateInvites({ workspaceId: myFirstWorkspace.id, input: times(201, () => ({ email: `asdasasd${Math.random()}@example.org`, role: WorkspaceRole.Member })) }) expect(res).to.haveGraphQLErrors( 'Maximum 200 invites can be sent at once by non admins' ) expect(res.data?.workspaceMutations?.invites?.batchCreate).to.not.be.ok }) it('batch inviting fails if not workspace admin', async () => { const res = await gqlHelpers.batchCreateInvites({ workspaceId: otherGuysWorkspace.id, input: times(10, () => ({ email: `asdasasd${Math.random()}@example.org`, role: WorkspaceRole.Member })) }) expect(res).to.haveGraphQLErrors( 'You do not have enough permissions in the workspace to perform this action' ) expect(res.data?.workspaceMutations?.invites?.batchCreate).to.not.be.ok }) it('batch inviting fails if resourceAccessRules prevent workspace access', async () => { const res = await gqlHelpers.batchCreateInvites( { workspaceId: myFirstWorkspace.id, input: times(10, () => ({ email: `asdasasd${Math.random()}@${workspaceDomain}`, role: WorkspaceRole.Member })) }, { context: { resourceAccessRules: [ { id: otherGuysWorkspace.id, type: TokenResourceIdentifierType.Workspace } ] } } ) expect(res).to.haveGraphQLErrors( 'You are not authorized to access this resource' ) expect(res.data?.workspaceMutations?.invites?.batchCreate).to.not.be.ok }) it('batch inviting works', async () => { const count = 10 const { getSends } = emailListener.listen({ times: count }) const res = await gqlHelpers.batchCreateInvites({ workspaceId: myFirstWorkspace.id, input: times(count, () => ({ email: `asdasasd${Math.random()}@example.org`, role: WorkspaceRole.Member })) }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.batchCreate).to.be.ok expect( res.data?.workspaceMutations?.invites?.batchCreate?.invitedTeam ).to.have.length(count) expect(getSends()).to.have.lengthOf(count) }) it('works when inviting user by id', async () => { const { getSends } = emailListener.listen({ times: 2 }) const randomUnregisteredEmail = `${createRandomPassword()}@example.org` await createUserEmailFactory({ db })({ userEmail: { userId: otherGuy.id, email: randomUnregisteredEmail } }) await markUserEmailAsVerifiedFactory({ updateUserEmail: updateUserEmailFactory({ db }) })({ email: randomUnregisteredEmail }) const res = await gqlHelpers.createInvite({ workspaceId: myFirstWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member } }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.create).to.be.ok const workspace = res.data!.workspaceMutations!.invites!.create expect(workspace.invitedTeam).to.have.length(1) expect(workspace.invitedTeam![0].invitedBy.id).to.equal(me.id) expect(workspace.invitedTeam![0].token).to.be.not.ok expect(workspace.invitedTeam![0].user?.id).to.equal(otherGuy.id) const emailSends = getSends() expect(emailSends).to.have.lengthOf(1) const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(otherGuy.email) expect(emailParams.subject).to.be.ok // Validate that invite exists await validateInviteExistanceFromEmail(emailParams) }) it('works when inviting user by email', async () => { const { getSends } = emailListener.listen({ times: 2 }) const randomUnregisteredEmail = `${createRandomPassword()}@example.org` const res = await gqlHelpers.createInvite({ workspaceId: myFirstWorkspace.id, input: { email: randomUnregisteredEmail, role: WorkspaceRole.Member } }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.create).to.be.ok const workspace = res.data!.workspaceMutations!.invites!.create expect(workspace.invitedTeam).to.have.length(1) expect(workspace.invitedTeam![0].invitedBy.id).to.equal(me.id) expect(workspace.invitedTeam![0].token).to.be.not.ok expect(workspace.invitedTeam![0].user).to.be.not.ok expect(workspace.invitedTeam![0].title).to.equal(randomUnregisteredEmail) const emailSends = getSends() expect(emailSends).to.have.lengthOf(1) const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(randomUnregisteredEmail) expect(emailParams.subject).to.be.ok // Validate that invite exists await validateInviteExistanceFromEmail(emailParams) }) it("doesn't work if inviting to a workspace that the token doesn't have access to", async () => { const res = await gqlHelpers.createInvite( { workspaceId: myFirstWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member } }, { context: { resourceAccessRules: [ { id: otherGuysWorkspace.id, type: TokenResourceIdentifierType.Workspace } ] } } ) expect(res).to.haveGraphQLErrors( 'You are not authorized to access this resource' ) expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok }) itEach( [InviteByTarget.Email, InviteByTarget.Id], (type) => `fails when inviting user by ${type} that already has a role`, async (type) => { const res = await gqlHelpers.createInvite({ workspaceId: myFirstWorkspace.id, input: { ...(type === InviteByTarget.Email ? { email: myWorkspaceFriend.email } : {}), ...(type === InviteByTarget.Id ? { userId: myWorkspaceFriend.id } : {}), role: WorkspaceRole.Member } }) expect(res).to.haveGraphQLErrors( 'The target user is already a member of the specified workspace' ) expect(res.data?.workspaceMutations?.invites?.create).to.not.be.ok } ) }) describe('and inviting to project', () => { const myProjectInviteTargetWorkspace: BasicTestWorkspace = { name: 'My Project Invite Target Workspace #1', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const myProjectInviteTargetWorkspaceWithNewPlan: BasicTestWorkspace = { name: 'My Project Invite Target Workspace w/ New Plan #1', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const myProjectInviteTargetBasicProject: BasicTestStream = { name: 'My Project Invite Target Basic Project #1', id: '', ownerId: '', isPublic: false } const myProjectInviteTargetWorkspaceProject: BasicTestStream = { name: 'My Project Invite Target Workspace Project #1', id: '', ownerId: '', isPublic: false } const myProjectInviteTargetWorkspaceNewPlanProject: BasicTestStream = { name: 'My Project Invite Target Workspace New Plan Project #1', id: '', ownerId: '', isPublic: false } const workspaceMemberWithNoProjectAccess: BasicTestUser = { name: 'Workspace Member With No Project Access #1', email: 'workspaceMemberWithNoProjectAccess1@example.org', id: '' } const workspaceGuest: BasicTestUser = { name: 'Workspace Guest #1', email: 'workspaceGuest1@bababooey.com', id: '' } before(async () => { await createTestUsers([workspaceMemberWithNoProjectAccess, workspaceGuest]) await createTestWorkspaces([ [myProjectInviteTargetWorkspace, me], [ myProjectInviteTargetWorkspaceWithNewPlan, me, { addPlan: { name: 'teamUnlimited', status: 'valid' } } ] ]) await assignToWorkspaces([ [ myProjectInviteTargetWorkspace, myWorkspaceFriend, Roles.Workspace.Member, WorkspaceSeatType.Editor ], [ myProjectInviteTargetWorkspace, workspaceMemberWithNoProjectAccess, Roles.Workspace.Member ], [myProjectInviteTargetWorkspace, workspaceGuest, Roles.Workspace.Guest], [ myProjectInviteTargetWorkspaceWithNewPlan, workspaceGuest, Roles.Workspace.Guest ] ]) myProjectInviteTargetWorkspaceNewPlanProject.workspaceId = myProjectInviteTargetWorkspaceWithNewPlan.id myProjectInviteTargetWorkspaceProject.workspaceId = myProjectInviteTargetWorkspace.id await createTestStreams([ [myProjectInviteTargetWorkspaceProject, me], [myProjectInviteTargetWorkspaceNewPlanProject, me], [myProjectInviteTargetBasicProject, me] ]) // Make myworkspacefriend a project owner (but not workspace admin!) await addOrUpdateStreamCollaborator( myProjectInviteTargetWorkspaceProject.id, myWorkspaceFriend.id, Roles.Stream.Owner, me.id ) }) beforeEach(async () => { // Remove all project access from workspaceMemberWithNoProjectAccess await Promise.all([ leaveStream( myProjectInviteTargetWorkspaceProject, workspaceMemberWithNoProjectAccess ), leaveStream( myProjectInviteTargetBasicProject, workspaceMemberWithNoProjectAccess ), // Switch workspaceGuest back to Viewer/Guest assignToWorkspaces([ [ myProjectInviteTargetWorkspace, workspaceGuest, Roles.Workspace.Guest, WorkspaceSeatType.Viewer ], [ myProjectInviteTargetWorkspaceWithNewPlan, workspaceGuest, Roles.Workspace.Guest, WorkspaceSeatType.Viewer ] ]) ]) }) afterEach(async () => { await truncateTables([ServerInvites.name]) }) it("can't invite to workspace project through base project invite resolver", async () => { const res = await gqlHelpers.createDefaultProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, input: { userId: otherGuy.id, role: Roles.Stream.Owner } }) expect(res).to.haveGraphQLErrors('Target project belongs to a workspace') expect(res.data?.projectMutations.invites.create.id).to.not.be.ok }) ;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? it.skip : it)( 'can invite to non-workspace project through workspace project invite resolver', async () => { const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetBasicProject.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Owner, workspaceRole: Roles.Workspace.Admin // should be ignored } ] }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok } ) it("can't indirectly invite to workspace if not workspace admin", async () => { const res = await gqlHelpers.createWorkspaceProjectInvite( { projectId: myProjectInviteTargetWorkspaceProject.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer } ] }, { context: { userId: myWorkspaceFriend.id } } ) expect(res).to.haveGraphQLErrors( "Inviter doesn't have admin access to the workspace" ) expect(res.data?.projectMutations.invites.createForWorkspace.id).to.not.be.ok }) it('can invite to workspace project as admin, even if target doesnt belong to workspace', async () => { const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer } ] }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok // no auto-accept, since target is not a workspace member const emailSends = getSends() expect(emailSends).to.have.lengthOf(1) const emailParams = emailSends[0] await validateInviteExistanceFromEmail(emailParams) await gqlHelpers.validateResourceAccess({ shouldHaveAccess: false, userId: otherGuy.id, workspaceId: myProjectInviteTargetWorkspace.id, streamId: myProjectInviteTargetWorkspaceProject.id }) }) it('can invite to workspace project even if not workspace admin, if target already belongs to workspace', async () => { const res = await gqlHelpers.createWorkspaceProjectInvite( { projectId: myProjectInviteTargetWorkspaceProject.id, inputs: [ { userId: workspaceMemberWithNoProjectAccess.id, role: Roles.Stream.Reviewer } ] }, { context: { userId: myWorkspaceFriend.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok }) it('invite auto-accepted if both users already belong to the workspace', async () => { const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, inputs: [ { userId: workspaceMemberWithNoProjectAccess.id, role: Roles.Stream.Reviewer } ] }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok // No invite email should be sent out, due to auto-accept expect(getSends().length).to.eq(0) // Should have project role await gqlHelpers.validateResourceAccess({ shouldHaveAccess: true, userId: workspaceMemberWithNoProjectAccess.id, workspaceId: myProjectInviteTargetWorkspace.id, streamId: myProjectInviteTargetWorkspaceProject.id, expectedProjectRole: Roles.Stream.Reviewer }) }) it("can't invite a workspace guest to be a workspace project owner", async () => { const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceProject.id, inputs: [ { userId: workspaceGuest.id, role: Roles.Stream.Owner } ] }) expect(res).to.haveGraphQLErrors({ code: WorkspaceInvalidRoleError.code }) expect(res.data?.projectMutations.invites.createForWorkspace.id).to.not.be.ok }) it(`can't invite someone with a viewer seat to be a contributor`, async () => { const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceNewPlanProject.id, inputs: [ { userId: workspaceGuest.id, role: Roles.Stream.Contributor } ] }) expect(res).to.haveGraphQLErrors({ code: WorkspaceInvalidRoleError.code }) expect(res.data?.projectMutations.invites.createForWorkspace.id).to.not.be.ok }) it('can invite someone with a viewer seat to be a contributor if seatType set to editor', async () => { const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: myProjectInviteTargetWorkspaceNewPlanProject.id, inputs: [ { userId: workspaceGuest.id, role: Roles.Stream.Contributor, seatType: WorkspaceSeatType.Editor } ] }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok // invite should be auto-accepted await gqlHelpers.validateResourceAccess({ shouldHaveAccess: true, expectedWorkspaceRole: Roles.Workspace.Guest, expectedWorkspaceSeatType: WorkspaceSeatType.Editor, expectedProjectRole: Roles.Stream.Contributor, streamId: myProjectInviteTargetWorkspaceNewPlanProject.id, userId: workspaceGuest.id, workspaceId: myProjectInviteTargetWorkspaceWithNewPlan.id }) }) it("can't invite invalid domain email to domain protected workspace project", async () => { const project: BasicTestStream = { name: 'My Project Invite Target Workspace Project #2', id: '', ownerId: '', isPublic: false, workspaceId: domainProtectedWorkspace.id } await createTestStreams([[project, me]]) const invalidEmail = 'johnny123456@test.com' const res = await gqlHelpers.createWorkspaceProjectInvite({ projectId: project.id, inputs: [ { email: invalidEmail, role: Roles.Stream.Owner, workspaceRole: Roles.Workspace.Member } ] }) expect(res).to.haveGraphQLErrors({ code: WorkspaceProtectedError.code }) }) }) describe('and administrating invites', () => { const BATCH_INVITE_COUNT = 10 const EXPLICIT_INVITE_COUNT = BATCH_INVITE_COUNT + 1 // + 1 in beforeEach const IMPLICIT_INVITE_COUNT = EXPLICIT_INVITE_COUNT + 1 // 1 from project invite const unrelatedWorkspace: BasicTestWorkspace = { name: 'Unrelated Workspace', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const unrelatedWorkspaceStream: BasicTestStream = { name: 'Unrelated Workspace Stream', id: '', ownerId: '', isPublic: false } const myAdministrationWorkspace: BasicTestWorkspace = { name: 'My Administration Workspace', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const myAdministrationProject: BasicTestStream = { name: 'My Administration Project', id: '', ownerId: '', isPublic: false } const cancelableInvite = { workspaceId: '', inviteId: '' } before(async () => { await createTestWorkspaces([ [myAdministrationWorkspace, me], [unrelatedWorkspace, me] ]) await assignToWorkspaces([ [myAdministrationWorkspace, myWorkspaceFriend, Roles.Workspace.Guest] ]) await gqlHelpers.batchCreateInvites( { workspaceId: myAdministrationWorkspace.id, input: times(BATCH_INVITE_COUNT, () => ({ email: `aszzzdasasd${Math.random()}@example.org`, role: WorkspaceRole.Member })) }, { assertNoErrors: true } ) // Create project and an invite to it too myAdministrationProject.workspaceId = myAdministrationWorkspace.id await createTestStreams([[myAdministrationProject, me]]) await gqlHelpers.createWorkspaceProjectInvite( { projectId: myAdministrationProject.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer } ] }, { assertNoErrors: true } ) // Create unrelated invites, to ensure they dont show up where they shouldnt unrelatedWorkspaceStream.workspaceId = unrelatedWorkspace.id await createTestStreams([[unrelatedWorkspaceStream, me]]) await gqlHelpers.createWorkspaceProjectInvite( { projectId: unrelatedWorkspaceStream.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer } ] }, { assertNoErrors: true } ) await gqlHelpers.createInvite( { workspaceId: unrelatedWorkspace.id, input: { email: 'aaasdasjdhasdjasd@aaa.com', role: WorkspaceRole.Member } }, { assertNoErrors: true } ) }) beforeEach(async () => { const inviteData = await captureCreatedInvite( async () => await gqlHelpers.createInvite( { workspaceId: myAdministrationWorkspace.id, input: { email: 'someRandomCancelableInviteGuy@asdasd.com', role: WorkspaceRole.Member } }, { assertNoErrors: true } ) ) cancelableInvite.workspaceId = myAdministrationWorkspace.id cancelableInvite.inviteId = inviteData.id }) it("can't list invites, if not admin", async () => { const res = await gqlHelpers.getWorkspaceWithTeam( { workspaceId: myAdministrationWorkspace.id }, { context: { userId: myWorkspaceFriend.id } } ) expect(res).to.haveGraphQLErrors('You are not authorized') expect(res.data?.workspace).to.be.ok expect(res.data?.workspace.invitedTeam).to.be.not.ok }) it('can list invites, if admin', async () => { const res = await gqlHelpers.getWorkspaceWithTeam({ workspaceId: myAdministrationWorkspace.id }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspace).to.be.ok expect(res.data?.workspace.invitedTeam?.length || 0).to.be.equal( IMPLICIT_INVITE_COUNT ) }) it('invite list includes implicit one from project invite', async () => { const res = await gqlHelpers.getWorkspaceWithTeam({ workspaceId: myAdministrationWorkspace.id }) expect(res).to.not.haveGraphQLErrors() const otherGuyInvite = res.data?.workspace.invitedTeam?.find( (t) => t.user?.id === otherGuy.id ) expect(otherGuyInvite).to.be.ok }) it("invite list doesn't include unrelated invites", async () => { const res = await gqlHelpers.getWorkspaceWithTeam({ workspaceId: myAdministrationWorkspace.id }) expect(res).to.not.haveGraphQLErrors() const unrelatedInvite = res.data?.workspace.invitedTeam?.find( (t) => t.workspace.id === unrelatedWorkspace.id ) expect(unrelatedInvite).to.be.not.ok }) it("can't cancel invite, if not admin", async () => { const res = await gqlHelpers.cancelInvite(cancelableInvite, { context: { userId: myWorkspaceFriend.id } }) expect(res).to.haveGraphQLErrors( 'You do not have enough permissions in the workspace to perform this action' ) expect(res.data?.workspaceMutations?.invites?.cancel).to.not.be.ok const invite = await findInviteFactory({ db })({ inviteId: cancelableInvite.inviteId }) expect(invite).to.be.ok }) it('can cancel invite, if admin', async () => { const res = await gqlHelpers.cancelInvite(cancelableInvite) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.cancel).to.be.ok const invite = await findInviteFactory({ db })({ inviteId: cancelableInvite.inviteId }) expect(invite).to.be.not.ok }) it("can't cancel invite if resourceAccessRules prevent it", async () => { const res = await gqlHelpers.cancelInvite(cancelableInvite, { context: { resourceAccessRules: [ { id: otherGuysWorkspace.id, type: TokenResourceIdentifierType.Workspace } ] } }) expect(res).to.haveGraphQLErrors('You are not authorized') expect(res.data?.workspaceMutations?.invites?.cancel).to.not.be.ok const invite = await findInviteFactory({ db })({ inviteId: cancelableInvite.inviteId }) expect(invite).to.be.ok }) }) describe('and looking at a specific invite', () => { const myInviteTargetWorkspace: BasicTestWorkspace = { name: 'My Invite Target Workspace', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } const myInviteTargetWorkspaceStream1: BasicTestStream = { name: 'My Invite Target Workspace Stream 1', id: '', ownerId: '', visibility: ProjectRecordVisibility.Workspace } const myInviteTargetPrivateWorkspaceStream1: BasicTestStream = { name: 'My Invite Target Private Workspace Stream 1', id: '', ownerId: '', visibility: ProjectRecordVisibility.Private } const processableWorkspaceInvite = { workspaceId: '', inviteId: '', token: '' } const emailInviteEmail = 'imJustSomeRandomNewGuy@aaaaa.com' const adminEmailInviteEmail = 'admin-imJustSomeRandomNewGuy@aaaaa.com' const processableWorkspaceEmailInvite = { workspaceId: '', inviteId: '', token: '', email: emailInviteEmail } const processableWorkspaceEmailAdminInvite = { workspaceId: '', inviteId: '', token: '', email: adminEmailInviteEmail } const processableProjectInvite = { projectId: '', inviteId: '', token: '' } const validateResourceAccess = async (params: { shouldHaveAccess: boolean | { workspace: boolean; project: boolean } expectedWorkspaceRole?: WorkspaceRoles expectedProjectRole?: StreamRoles expectedWorkspaceSeatType?: WorkspaceSeatType streamId: string }) => { return gqlHelpers.validateResourceAccess({ ...params, userId: otherGuy.id, workspaceId: myInviteTargetWorkspace.id, streamId: params.streamId }) } before(async () => { await truncateTables([ServerInvites.name]) await createTestWorkspaces([[myInviteTargetWorkspace, me]]) myInviteTargetWorkspaceStream1.workspaceId = myInviteTargetWorkspace.id myInviteTargetPrivateWorkspaceStream1.workspaceId = myInviteTargetWorkspace.id await createTestStreams([ [myInviteTargetWorkspaceStream1, me], [myInviteTargetPrivateWorkspaceStream1, me] ]) }) beforeEach(async () => { const workspaceInvite = await captureCreatedInvite(async () => { await gqlHelpers.createInvite( { workspaceId: myInviteTargetWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member } }, { assertNoErrors: true } ) }) processableWorkspaceInvite.workspaceId = myInviteTargetWorkspace.id processableWorkspaceInvite.inviteId = workspaceInvite.id processableWorkspaceInvite.token = workspaceInvite.token const workspaceEmailInvite = await captureCreatedInvite(async () => { await gqlHelpers.createInvite( { workspaceId: myInviteTargetWorkspace.id, input: { email: processableWorkspaceEmailInvite.email, role: WorkspaceRole.Guest } }, { assertNoErrors: true } ) }) processableWorkspaceEmailInvite.workspaceId = myInviteTargetWorkspace.id processableWorkspaceEmailInvite.inviteId = workspaceEmailInvite.id processableWorkspaceEmailInvite.token = workspaceEmailInvite.token const workspaceEmailAdminInvite = await captureCreatedInvite(async () => { await gqlHelpers.createInvite( { workspaceId: myInviteTargetWorkspace.id, input: { email: processableWorkspaceEmailAdminInvite.email, role: WorkspaceRole.Admin } }, { assertNoErrors: true } ) }) processableWorkspaceEmailAdminInvite.workspaceId = myInviteTargetWorkspace.id processableWorkspaceEmailAdminInvite.inviteId = workspaceEmailAdminInvite.id processableWorkspaceEmailAdminInvite.token = workspaceEmailAdminInvite.token const projectInvite = await captureCreatedInvite( async () => await gqlHelpers.createWorkspaceProjectInvite( { projectId: myInviteTargetWorkspaceStream1.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer } ] }, { assertNoErrors: true } ) ) processableProjectInvite.projectId = myInviteTargetWorkspaceStream1.id processableProjectInvite.inviteId = projectInvite.id processableProjectInvite.token = projectInvite.token }) const deleteEmail = async (email: string) => { const emailEntity = await findEmailFactory({ db })({ email }) if (emailEntity) { await deleteUserEmailFactory({ db })({ userId: emailEntity.userId, id: emailEntity.id }) } } afterEach(async () => { // Serial execution to avoid race conditions await unassignFromWorkspace(myInviteTargetWorkspace, otherGuy) await leaveStream(myInviteTargetWorkspaceStream1, otherGuy) // Reset otherGuy's newly added verified email await Promise.all([ deleteEmail(emailInviteEmail), deleteEmail(adminEmailInviteEmail) ]) }) it("can't retrieve it if not the invitee and no token specified", async () => { const res = await gqlHelpers.getInvite({ workspaceId: myInviteTargetWorkspace.id }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceInvite).to.be.not.ok }) it('can retrieve it even if not the invitee, as long as the token is valid', async () => { const res = await gqlHelpers.getInvite({ workspaceId: myInviteTargetWorkspace.id, token: processableWorkspaceInvite.token }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceInvite).to.be.ok expect(res.data!.workspaceInvite?.user!.id).to.equal(otherGuy.id) }) it("can't retrieve it by passing in the slug, not workspace id", async () => { const res = await gqlHelpers.getInvite( { workspaceId: myInviteTargetWorkspace.slug }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceInvite).to.not.be.ok }) it('can retrieve it by passing in the slug, not workspace id, if explicit about it', async () => { const res = await gqlHelpers.getInvite( { workspaceId: myInviteTargetWorkspace.slug, options: { useSlug: true } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceInvite).to.be.ok expect(res.data!.workspaceInvite?.user!.id).to.equal(otherGuy.id) }) it('cant resend the invite email w/ mismatched workspaceId', async () => { const res = await gqlHelpers.resendWorkspaceInvite({ input: { workspaceId: myFirstWorkspace.id, inviteId: processableWorkspaceInvite.inviteId } }) expect(res).to.haveGraphQLErrors('Invite not found') expect(res.data?.workspaceMutations.invites.resend).to.not.be.ok }) it('can resend the invite email', async () => { const { getSends } = emailListener.listen({ times: 2 }) const res = await gqlHelpers.resendWorkspaceInvite({ input: { workspaceId: myInviteTargetWorkspace.id, inviteId: processableWorkspaceInvite.inviteId } }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.resend).to.be.ok const emailSends = getSends() expect(emailSends).to.have.lengthOf(1) const emailParams = emailSends[0] expect(emailParams).to.be.ok expect(emailParams.to).to.eq(otherGuy.email) }) it("can't retrieve broken invite with invalid workspaceIds", async () => { const brokenWorkspace: BasicTestWorkspace = { name: 'Broken Workspace', id: 'a', slug: cryptoRandomString({ length: 10 }), ownerId: '' } await createTestWorkspaces([[brokenWorkspace, me]]) // Doing direct invite to avoid workspace id validation checks const brokenInvite = await createWorkspaceInviteDirectly( { workspaceId: brokenWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member } }, me.id ) expect(brokenInvite.id).to.be.ok // Db query directly, cause this isn't a supported use case await Workspaces.knex() .where({ [Workspaces.col.id]: brokenWorkspace.id }) .del() const res1 = await gqlHelpers.getInvite( { workspaceId: brokenWorkspace.id }, { context: { userId: otherGuy.id } } ) expect(res1).to.not.haveGraphQLErrors('') expect(res1.data?.workspaceInvite).to.eq(null) const res2 = await gqlHelpers.getMyInvites({ context: { userId: otherGuy.id } }) expect(res2).to.not.haveGraphQLErrors() expect(res2.data?.activeUser?.workspaceInvites).to.be.ok expect( res2.data!.activeUser!.workspaceInvites.find( (i) => i.workspace.id === brokenWorkspace.id ) ).to.not.be.ok }) itEach( [{ withToken: true }, { withToken: false }], (test) => `can retrieve it if the invitee ${ test.withToken ? 'and specifying token' : 'and omitting token' }`, async (test) => { const res = await gqlHelpers.getInvite( { workspaceId: myInviteTargetWorkspace.id, token: test.withToken ? processableWorkspaceInvite.token : undefined }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors('') expect(res.data?.workspaceInvite).to.be.ok expect(res.data!.workspaceInvite!.inviteId).to.equal( processableWorkspaceInvite.inviteId ) expect(res.data!.workspaceInvite!.workspace.id).to.equal( myInviteTargetWorkspace.id ) expect(res.data!.workspaceInvite!.token).to.equal( processableWorkspaceInvite.token ) } ) itEach( [{ hasSome: true }, { hasSome: false }], (test) => `can get all of my invites ${ test.hasSome ? 'when there are some' : 'when there are none' }`, async (test) => { const res = await gqlHelpers.getMyInvites({ context: { userId: test.hasSome ? otherGuy.id : me.id } }) expect(res).to.not.haveGraphQLErrors() expect(res.data?.activeUser?.workspaceInvites).to.be.ok if (test.hasSome) { expect(res.data?.activeUser?.workspaceInvites).to.have.length(2) // 1 workspace + 1 workspace project (implicit workspace invite) expect(res.data?.activeUser?.workspaceInvites![0].inviteId).to.equal( processableWorkspaceInvite.inviteId ) expect(res.data?.activeUser?.workspaceInvites![0].workspace.id).to.equal( myInviteTargetWorkspace.id ) } else { expect(res.data?.activeUser?.workspaceInvites).to.have.length(0) } } ) itEach( [{ accept: true }, { accept: false }], ({ accept }) => `can ${accept ? 'accept' : 'decline'} the invite`, async ({ accept }) => { const res = await gqlHelpers.useInvite( { input: { accept, token: processableWorkspaceInvite.token } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.use).to.be.ok const invite = await findInviteFactory({ db })({ inviteId: processableWorkspaceInvite.inviteId }) expect(invite).to.be.not.ok // Should have access to workspace visibility stream, not the other one await validateResourceAccess({ shouldHaveAccess: accept, streamId: myInviteTargetWorkspaceStream1.id, expectedWorkspaceSeatType: WorkspaceSeatType.Viewer }) await validateResourceAccess({ shouldHaveAccess: { workspace: accept, project: false }, streamId: myInviteTargetPrivateWorkspaceStream1.id }) } ) itEach( [WorkspaceSeatType.Editor, WorkspaceSeatType.Viewer], (seatType) => `can specify ${seatType} seat type in workspace invite`, async (seatType) => { const workspaceInvite = await captureCreatedInvite(async () => { await gqlHelpers.createInvite( { workspaceId: myInviteTargetWorkspace.id, input: { userId: otherGuy.id, role: WorkspaceRole.Member, seatType } }, { assertNoErrors: true } ) }) expect(workspaceInvite.id).to.be.ok expect(workspaceInvite.resource.workspaceSeatType).to.equal(seatType) const res = await gqlHelpers.useInvite( { input: { accept: true, token: workspaceInvite.token } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.use).to.be.ok const invite = await findInviteFactory({ db })({ inviteId: processableWorkspaceInvite.inviteId }) expect(invite).to.be.not.ok // Should have access to workspace visibility stream, not the other one await validateResourceAccess({ shouldHaveAccess: true, streamId: myInviteTargetWorkspaceStream1.id, expectedWorkspaceSeatType: seatType }) await validateResourceAccess({ shouldHaveAccess: { workspace: true, project: false }, streamId: myInviteTargetPrivateWorkspaceStream1.id, expectedWorkspaceSeatType: seatType }) } ) itEach( [WorkspaceSeatType.Editor, WorkspaceSeatType.Viewer], (seatType) => `can specify ${seatType} seat type in workspace project invite`, async (seatType) => { const projectInvite = await captureCreatedInvite( async () => await gqlHelpers.createWorkspaceProjectInvite( { projectId: myInviteTargetWorkspaceStream1.id, inputs: [ { userId: otherGuy.id, role: Roles.Stream.Reviewer, seatType } ] }, { assertNoErrors: true } ) ) expect(projectInvite.id).to.be.ok expect(projectInvite.resource.workspaceSeatType).to.equal(seatType) const res = await gqlHelpers.useInvite( { input: { accept: true, token: projectInvite.token } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.use).to.be.ok await validateResourceAccess({ shouldHaveAccess: true, streamId: myInviteTargetWorkspaceStream1.id, expectedWorkspaceSeatType: seatType }) } ) it("can't accept a new email invite, if not explicitly doing so", async () => { const res = await gqlHelpers.useInvite( { input: { accept: true, token: processableWorkspaceEmailInvite.token } }, { context: { userId: otherGuy.id } } ) expect(res).to.haveGraphQLErrors( 'Attempted to finalize an invite for a mismatched e-mail address' ) expect(res.data?.workspaceMutations?.invites?.use).to.not.be.ok }) itEach( [{ accept: true }, { accept: false }], ({ accept }) => `can explicitly ${accept ? 'accept' : 'decline'} a new email invite`, async ({ accept }) => { const res = await gqlHelpers.useInvite( { input: { accept, token: processableWorkspaceEmailAdminInvite.token, addNewEmail: true } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.use).to.be.ok await validateResourceAccess({ shouldHaveAccess: accept, streamId: myInviteTargetWorkspaceStream1.id, expectedWorkspaceSeatType: WorkspaceSeatType.Editor // admin role }) const verifiedEmails = await findVerifiedEmailsByUserIdFactory({ db })({ userId: otherGuy.id }) const newVerifiedEmail = verifiedEmails.find( (e) => e.email.toLowerCase() === processableWorkspaceEmailAdminInvite.email.toLowerCase() ) if (accept) { expect(newVerifiedEmail).to.be.ok } else { expect(newVerifiedEmail).to.not.be.ok } } ) itEach( [{ roleChanged: true }, { roleChanged: false }], ({ roleChanged }) => `can accept an email invite, even if already a workspace member, and role ${ roleChanged ? 'upgraded' : 'not downgraded' }`, async ({ roleChanged }) => { const res1 = await gqlHelpers.useInvite( { input: { accept: true, token: processableWorkspaceInvite.token } }, { context: { userId: otherGuy.id } } ) expect(res1).to.not.haveGraphQLErrors() expect(res1.data?.workspaceMutations.invites.use).to.be.ok await validateResourceAccess({ shouldHaveAccess: true, expectedWorkspaceRole: Roles.Workspace.Member, streamId: myInviteTargetWorkspaceStream1.id }) const targetInvite = roleChanged ? processableWorkspaceEmailAdminInvite : processableWorkspaceEmailInvite const res2 = await gqlHelpers.useInvite( { input: { accept: true, token: targetInvite.token, addNewEmail: true } }, { context: { userId: otherGuy.id } } ) expect(res2).to.not.haveGraphQLErrors() expect(res2.data?.workspaceMutations.invites.use).to.be.ok await validateResourceAccess({ shouldHaveAccess: true, expectedWorkspaceRole: roleChanged ? Roles.Workspace.Admin : Roles.Workspace.Member, streamId: myInviteTargetWorkspaceStream1.id }) const email = targetInvite.email const verifiedEmails = await findVerifiedEmailsByUserIdFactory({ db })({ userId: otherGuy.id }) const newVerifiedEmail = verifiedEmails.find( (e) => e.email.toLowerCase() === email.toLowerCase() ) expect(newVerifiedEmail).to.be.ok } ) it("can't accept the invite, if it belongs to another user", async () => { const res = await gqlHelpers.useInvite( { input: { accept: true, token: processableWorkspaceInvite.token } }, { context: { userId: myWorkspaceFriend.id } } ) expect(res).to.haveGraphQLErrors() expect(res.data?.workspaceMutations?.invites?.use).to.not.be.ok }) it("can't accept invite, if token resource access rules prevent it", async () => { const res = await gqlHelpers.useInvite( { input: { accept: true, token: processableWorkspaceInvite.token } }, { context: { userId: otherGuy.id, resourceAccessRules: [ { id: otherGuysWorkspace.id, type: TokenResourceIdentifierType.Workspace } ] } } ) expect(res).to.haveGraphQLErrors( 'You are not allowed to process an invite for this workspace' ) expect(res.data?.workspaceMutations?.invites?.use).to.not.be.ok const invite = await findInviteFactory({ db })({ inviteId: processableWorkspaceInvite.inviteId }) expect(invite).to.be.ok }) it('accepting workspace project invite also adds user to workspace w/ guest role', async () => { const res = await gqlHelpers.useProjectInvite( { input: { token: processableProjectInvite.token, accept: true, projectId: processableProjectInvite.projectId } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations?.invites?.use).to.be.ok const invite = await findInviteFactory({ db })({ inviteId: processableProjectInvite.inviteId }) expect(invite).to.be.not.ok await validateResourceAccess({ shouldHaveAccess: true, expectedWorkspaceRole: Roles.Workspace.Guest, expectedWorkspaceSeatType: WorkspaceSeatType.Viewer, expectedProjectRole: Roles.Stream.Reviewer, streamId: myInviteTargetWorkspaceStream1.id }) }) itEach( [{ withRole: true }, { withRole: false }], ({ withRole }) => `accepting workspace project invite created w/ createForWorkspace also adds user to workspace w/ ${ withRole ? 'selected' : 'default (guest)' } role`, async ({ withRole }) => { const inviteData = await captureCreatedInvite( async () => await gqlHelpers.createWorkspaceProjectInvite( { projectId: myInviteTargetWorkspaceStream1.id, inputs: [ { userId: otherGuy.id, role: withRole ? Roles.Stream.Owner : Roles.Stream.Reviewer, workspaceRole: withRole ? Roles.Workspace.Admin : undefined } ] }, { assertNoErrors: true } ) ) const res = await gqlHelpers.useProjectInvite( { input: { token: inviteData.token, accept: true, projectId: myInviteTargetWorkspaceStream1.id } }, { context: { userId: otherGuy.id } } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.projectMutations?.invites?.use).to.be.ok const invite = await findInviteFactory({ db })({ inviteId: processableProjectInvite.inviteId }) expect(invite).to.be.not.ok await validateResourceAccess({ shouldHaveAccess: true, expectedWorkspaceRole: withRole ? Roles.Workspace.Admin : Roles.Workspace.Guest, expectedWorkspaceSeatType: withRole ? WorkspaceSeatType.Editor : WorkspaceSeatType.Viewer, expectedProjectRole: withRole ? Roles.Stream.Owner : Roles.Stream.Reviewer, streamId: myInviteTargetWorkspaceStream1.id }) // ws admin will have access to everything await validateResourceAccess({ shouldHaveAccess: { workspace: true, project: withRole ? true : false }, streamId: myInviteTargetPrivateWorkspaceStream1.id }) } ) }) }) describe('when unauthenticated', () => { let registrationRestApi: LocalAuthRestApiHelpers let apollo: TestApolloServer let gqlHelpers: TestInvitesGraphQLOperations const otherWorkspaceOwner: BasicTestUser = { name: 'Other Workspace Owner', email: 'otherworkspaceowner@example.org', id: '' } const otherWorkspace: BasicTestWorkspace = { name: 'Other Workspace', id: '', slug: cryptoRandomString({ length: 10 }), ownerId: '' } before(async () => { apollo = await testApolloServer() registrationRestApi = localAuthRestApi({ express: app }) gqlHelpers = buildInvitesGraphqlOperations({ apollo }) await createTestUsers([otherWorkspaceOwner]) await createTestWorkspaces([[otherWorkspace, otherWorkspaceOwner]]) }) it('can register with workspace invite and join workspace afterwards', async () => { const params = generateRegistrationParams() const invite = await createWorkspaceInviteDirectly( { workspaceId: otherWorkspace.id, input: { email: params.user.email, role: WorkspaceRole.Member } }, otherWorkspaceOwner.id ) expect(invite.token).to.be.ok params.inviteToken = invite.token const newUser = await registrationRestApi.register(params) const res = await gqlHelpers.useInvite( { input: { accept: true, token: invite.token } }, { context: await createTestContext({ userId: newUser.id, auth: true, role: Roles.Server.User, token: 'asd', scopes: AllScopes }) } ) expect(res).to.not.haveGraphQLErrors() expect(res.data?.workspaceMutations.invites.use).to.be.ok expect(await findInviteFactory({ db })({ inviteId: invite.id })).to.be.not.ok await gqlHelpers.validateResourceAccess({ shouldHaveAccess: true, userId: newUser.id, workspaceId: otherWorkspace.id, expectedWorkspaceSeatType: WorkspaceSeatType.Viewer }) }) }) })