diff --git a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts index dac1d1393..c0c1fcf8d 100644 --- a/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/repositories/workspaceJoinRequests.ts @@ -23,7 +23,11 @@ const tables = { export const createWorkspaceJoinRequestFactory = ({ db }: { db: Knex }): CreateWorkspaceJoinRequest => async ({ workspaceJoinRequest }) => { - const res = await tables.workspaceJoinRequests(db).insert(workspaceJoinRequest, '*') + const res = await tables + .workspaceJoinRequests(db) + .insert(workspaceJoinRequest, '*') + .onConflict() + .ignore() return res[0] } diff --git a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts index 1be5235b3..ef6c97b22 100644 --- a/packages/server/modules/workspaces/services/workspaceJoinRequests.ts +++ b/packages/server/modules/workspaces/services/workspaceJoinRequests.ts @@ -84,7 +84,7 @@ export const requestToJoinWorkspaceFactory = throw new WorkspaceProtectedError() } - await createWorkspaceJoinRequest({ + const joinRequest = await createWorkspaceJoinRequest({ workspaceJoinRequest: { userId, workspaceId, @@ -92,6 +92,11 @@ export const requestToJoinWorkspaceFactory = } }) + if (!joinRequest || joinRequest.status !== 'pending') { + // The request was already created, so don't send the email again + return true + } + await sendWorkspaceJoinRequestReceivedEmail({ workspace, requester diff --git a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts index ada605204..0a936b5b2 100644 --- a/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspaceJoinRequests.spec.ts @@ -233,6 +233,95 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() ) expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].requester).to.equal(user) }) + it('duplicate request is idempotent', async () => { + const createWorkspaceJoinRequest = createWorkspaceJoinRequestFactory({ db }) + + const sendWorkspaceJoinRequestReceivedEmailCalls: Parameters[number][] = + [] + const sendWorkspaceJoinRequestReceivedEmail = async ( + args: Parameters[number] + ) => sendWorkspaceJoinRequestReceivedEmailCalls.push(args) + + const user: BasicTestUser = { + id: '', + name: 'John Speckle', + email: `${createRandomString()}@example.org`, + role: Roles.Server.Admin, + verified: true + } + + await createTestUser(user) + + const workspace: BasicTestWorkspace = { + id: '', + slug: '', + ownerId: '', + name: cryptoRandomString({ length: 6 }), + description: cryptoRandomString({ length: 12 }), + discoverabilityEnabled: true + } + await createTestWorkspace(workspace, user, { domain: 'example.org' }) + const domain = { + id: cryptoRandomString({ length: 10 }), + workspaceId: workspace.id, + domain: 'example.org', + verified: true, + createdAt: new Date(), + createdByUserId: user.id, + updatedAt: new Date() + } + + const requestToJoinWorkspace = await requestToJoinWorkspaceFactory({ + createWorkspaceJoinRequest, + sendWorkspaceJoinRequestReceivedEmail: + sendWorkspaceJoinRequestReceivedEmail as unknown as SendWorkspaceJoinRequestReceivedEmail, + getUserById: async () => user as unknown as UserWithOptionalRole, + getWorkspaceWithDomains: async () => + ({ + ...workspace, + domains: [domain] + } as unknown as WorkspaceWithDomains), + getUserEmails: async () => + [{ email: user.email, verified: true }] as unknown as UserEmail[] + }) + + expect( + await requestToJoinWorkspace({ workspaceId: workspace.id, userId: user.id }) + ).to.equal(true) + + expect( + (await db(WorkspaceJoinRequests.name) + .where({ + workspaceId: workspace.id, + userId: user.id + }) + .select('status') + .first())!.status + ).to.equal('pending') + + expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1) + expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].workspace.id).to.equal( + workspace.id + ) + expect(sendWorkspaceJoinRequestReceivedEmailCalls[0].requester).to.equal(user) + + // attempt to join again + expect( + await requestToJoinWorkspace({ workspaceId: workspace.id, userId: user.id }) + ).to.equal(true) + + expect( + (await db(WorkspaceJoinRequests.name) + .where({ + workspaceId: workspace.id, + userId: user.id + }) + .select('status') + .first())!.status + ).to.equal('pending') + + expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1) + }) }) describe('approveWorkspaceJoinRequestFactory, returns a function that ', () => {