diff --git a/packages/core/src/libraries/organization-invitation.ts b/packages/core/src/libraries/organization-invitation.ts index 6b6fc5ed9..5677c2fac 100644 --- a/packages/core/src/libraries/organization-invitation.ts +++ b/packages/core/src/libraries/organization-invitation.ts @@ -14,6 +14,8 @@ import type Queries from '#src/tenants/Queries.js'; import { type ConnectorLibrary } from './connector.js'; +const invitationLinkPath = '/invitation'; + /** * The ending statuses of an organization invitation per RFC 0003. It means that the invitation * status cannot be changed anymore. diff --git a/packages/integration-tests/src/__mocks__/connectors-mock.ts b/packages/integration-tests/src/__mocks__/connectors-mock.ts index 0bc30c0d4..02222ff19 100644 --- a/packages/integration-tests/src/__mocks__/connectors-mock.ts +++ b/packages/integration-tests/src/__mocks__/connectors-mock.ts @@ -157,6 +157,12 @@ export const mockEmailConnectorConfig = { subject: 'Logto Test Template', content: 'This is for testing purposes only. Your passcode is {{code}}.', }, + { + usageType: 'OrganizationInvitation', + type: 'text/plain', + subject: 'Logto Organization Invitation Template', + content: 'This is for organization invitation purposes only. Your link is {{link}}.', + }, ], }; diff --git a/packages/integration-tests/src/api/organization-invitation.ts b/packages/integration-tests/src/api/organization-invitation.ts index 521867992..b3d9b541c 100644 --- a/packages/integration-tests/src/api/organization-invitation.ts +++ b/packages/integration-tests/src/api/organization-invitation.ts @@ -13,7 +13,7 @@ export type PostOrganizationInvitationData = { organizationId: string; expiresAt: number; organizationRoleIds?: string[]; - emailPayload?: SendMessagePayload | false; + messagePayload?: SendMessagePayload | false; }; export class OrganizationInvitationApi extends ApiFactory< diff --git a/packages/integration-tests/src/helpers/index.ts b/packages/integration-tests/src/helpers/index.ts index 2fe9e7309..b5f93ca23 100644 --- a/packages/integration-tests/src/helpers/index.ts +++ b/packages/integration-tests/src/helpers/index.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import { createServer, type RequestListener } from 'node:http'; -import { mockConnectorFilePaths } from '@logto/connector-kit'; +import { mockConnectorFilePaths, type SendMessagePayload } from '@logto/connector-kit'; import { RequestError } from 'got'; import { createUser } from '#src/api/index.js'; @@ -28,6 +28,7 @@ type ConnectorMessageRecord = { address?: string; code: string; type: string; + payload: SendMessagePayload; }; /** diff --git a/packages/integration-tests/src/tests/api/organization/organization-invitation.creation.test.ts b/packages/integration-tests/src/tests/api/organization/organization-invitation.creation.test.ts index 3b8d7d572..a1489abd8 100644 --- a/packages/integration-tests/src/tests/api/organization/organization-invitation.creation.test.ts +++ b/packages/integration-tests/src/tests/api/organization/organization-invitation.creation.test.ts @@ -1,8 +1,11 @@ import assert from 'node:assert'; +import { ConnectorType } from '@logto/connector-kit'; import { generateStandardId } from '@logto/shared'; import { HTTPError } from 'got'; +import { clearConnectorsByTypes, setEmailConnector } from '#src/helpers/connector.js'; +import { readConnectorMessage } from '#src/helpers/index.js'; import { OrganizationApiTest, OrganizationInvitationApiTest } from '#src/helpers/organization.js'; const randomId = () => generateStandardId(4); @@ -36,6 +39,43 @@ describe('organization invitation creation', () => { }); }); + it('should be able to create an invitation with sending email', async () => { + await setEmailConnector(); + + const organization = await organizationApi.create({ name: 'test' }); + await invitationApi.create({ + organizationId: organization.id, + invitee: `${randomId()}@example.com`, + expiresAt: Date.now() + 1_000_000, + messagePayload: { + link: 'https://example.com', + }, + }); + expect(await readConnectorMessage('Email')).toMatchObject({ + type: 'OrganizationInvitation', + payload: { + link: 'https://example.com', + }, + }); + }); + + it('should throw error if email connector is not set', async () => { + await clearConnectorsByTypes([ConnectorType.Email]); + const organization = await organizationApi.create({ name: 'test' }); + const error = await invitationApi + .create({ + organizationId: organization.id, + invitee: `${randomId()}@example.com`, + expiresAt: Date.now() + 1_000_000, + messagePayload: { + link: 'https://example.com', + }, + }) + .catch((error: unknown) => error); + + expectErrorResponse(error, 501, 'connector.not_found'); + }); + it('should not be able to create invitations with the same email', async () => { const organization = await organizationApi.create({ name: 'test' }); const email = `${randomId()}@example.com`;