0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(core): trigger organization membership updated hook from jit

This commit is contained in:
Gao Sun 2024-06-06 18:11:49 +08:00
parent d4cb91e6c9
commit ab4867d310
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
21 changed files with 234 additions and 105 deletions

View file

@ -1,12 +1,14 @@
import {
InteractionEvent,
InteractionHookEvent,
type User,
managementApiHooksRegistration,
type DataHookEvent,
type InteractionApiMetadata,
type ManagementApiContext,
userInfoSelectFields,
} from '@logto/schemas';
import { type Optional } from '@silverhand/essentials';
import { pick, type Optional } from '@silverhand/essentials';
import { type Context } from 'koa';
import { type IRouterParamContext } from 'koa-router';
@ -21,21 +23,41 @@ type DataHookMetadata = {
ip: string;
} & Partial<InteractionApiMetadata>;
type DataHookContext = {
event: DataHookEvent;
export type DataHookContext = {
/** Data details */
data?: unknown;
} & Partial<ManagementApiContext> &
Record<string, unknown>;
type UserContext = {
/**
* This user will be picked with {@link userInfoSelectFields} and set to the `data` field. The
* original user object will be discarded.
*
* @example
* const context = { user: { ... } };
*
* // The actual context to send will be:
* { data: pick(user, ...userInfoSelectFields) }
*/
user: User;
};
export type DataHookContextMap = {
'Organization.Membership.Updated': { organizationId: string };
'User.Created': UserContext;
'User.Data.Updated': UserContext;
'User.Deleted': UserContext;
};
export class DataHookContextManager {
contextArray: DataHookContext[] = [];
contextArray: Array<DataHookContext & { event: DataHookEvent }> = [];
constructor(public metadata: DataHookMetadata) {}
getRegisteredDataHookEventContext(
ctx: IRouterParamContext & Context
): DataHookContext | undefined {
): Readonly<[DataHookEvent, DataHookContext]> | undefined {
const { method, _matchedRoute: matchedRoute } = ctx;
const key = buildManagementApiDataHookRegistrationKey(method, matchedRoute);
@ -44,16 +66,29 @@ export class DataHookContextManager {
return;
}
return {
event: managementApiHooksRegistration[key],
...buildManagementApiContext(ctx),
data: ctx.response.body,
};
return Object.freeze([
managementApiHooksRegistration[key],
{
...buildManagementApiContext(ctx),
data: ctx.response.body,
},
]);
}
appendContext(context: DataHookContext) {
appendContext<Event extends DataHookEvent>(
event: Event,
context: Event extends keyof DataHookContextMap
? DataHookContextMap[Event] & Partial<ManagementApiContext> & Record<string, unknown>
: DataHookContext
) {
const { user, ...rest } = context;
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
this.contextArray.push(context);
this.contextArray.push({
event,
// eslint-disable-next-line no-restricted-syntax -- trust the input
...(user ? { data: pick(user as User, ...userInfoSelectFields) } : {}),
...rest,
});
}
}

View file

@ -107,7 +107,18 @@ export const createUserLibrary = (queries: Queries) => {
{ retries, factor: 0 } // No need for exponential backoff
);
const insertUser = async (data: OmitAutoSetFields<CreateUser>, additionalRoleNames: string[]) => {
type InsertUserResult = [
User,
{
/** The organization IDs that the user has been provisioned into. */
organizationsIds: readonly string[];
},
];
const insertUser = async (
data: OmitAutoSetFields<CreateUser>,
additionalRoleNames: string[]
): Promise<InsertUserResult> => {
const roleNames = [...EnvSet.values.userDefaultRoleNames, ...additionalRoleNames];
const [parameterRoles, defaultRoles] = await Promise.all([
findRolesByRoleNames(roleNames),
@ -131,23 +142,31 @@ export const createUserLibrary = (queries: Queries) => {
);
}
// Just-in-time organization provisioning
const userEmailDomain = data.primaryEmail?.split('@')[1];
// TODO: Remove this check when launching
if (EnvSet.values.isDevFeaturesEnabled && userEmailDomain) {
const organizationQueries = new OrganizationQueries(connection);
const organizationIds = await organizationQueries.emailDomains.getOrganizationIdsByDomain(
userEmailDomain
);
if (organizationIds.length > 0) {
await organizationQueries.relations.users.insert(
...organizationIds.map<[string, string]>((organizationId) => [organizationId, user.id])
const provisionOrganizations = async (): Promise<readonly string[]> => {
// Just-in-time organization provisioning
const userEmailDomain = data.primaryEmail?.split('@')[1];
// TODO: Remove this check when launching
if (EnvSet.values.isDevFeaturesEnabled && userEmailDomain) {
const organizationQueries = new OrganizationQueries(connection);
const organizationIds = await organizationQueries.emailDomains.getOrganizationIdsByDomain(
userEmailDomain
);
}
}
return user;
if (organizationIds.length > 0) {
await organizationQueries.relations.users.insert(
...organizationIds.map<[string, string]>((organizationId) => [
organizationId,
user.id,
])
);
return organizationIds;
}
}
return [];
};
return [user, { organizationsIds: await provisionOrganizations() }];
});
};

View file

@ -38,7 +38,7 @@ describe('koaManagementApiHooks', () => {
appendDataHookContext: notToBeCalled,
};
next.mockImplementation(() => {
ctx.appendDataHookContext({ event: 'Role.Created', data: { id: '123' } });
ctx.appendDataHookContext('Role.Created', { data: { id: '123' } });
});
await koaManagementApiHooks(mockHooksLibrary)(ctx, next);

View file

@ -37,11 +37,10 @@ export const koaManagementApiHooks = <StateT, ContextT extends IRouterParamConte
await next();
// Auto append pre-registered management API hooks if any
const registeredHookEventContext =
dataHooksContextManager.getRegisteredDataHookEventContext(ctx);
const registeredData = dataHooksContextManager.getRegisteredDataHookEventContext(ctx);
if (registeredHookEventContext) {
dataHooksContextManager.appendContext(registeredHookEventContext);
if (registeredData) {
dataHooksContextManager.appendContext(...registeredData);
}
// Trigger data hooks

View file

@ -200,7 +200,7 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
const id = await generateUserId();
const user = await insertUser(
const [user, { organizationsIds }] = await insertUser(
{
id,
primaryEmail,
@ -221,8 +221,14 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
[]
);
ctx.body = pick(user, ...userInfoSelectFields);
for (const organizationId of organizationsIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
...buildManagementApiContext(ctx),
organizationId,
});
}
ctx.body = pick(user, ...userInfoSelectFields);
return next();
}
);
@ -382,10 +388,9 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
ctx.status = 204;
// Manually trigger the `User.Deleted` hook since we need to send the user data in the payload
ctx.appendDataHookContext({
event: 'User.Deleted',
ctx.appendDataHookContext('User.Deleted', {
...buildManagementApiContext(ctx),
data: pick(user, ...userInfoSelectFields),
user,
});
return next();

View file

@ -135,7 +135,7 @@ async function handleSubmitRegister(
(invitation) => invitation.status === OrganizationInvitationStatus.Pending
);
const user = await insertUser(
const [user, { organizationsIds }] = await insertUser(
{
id,
...userProfile,
@ -188,7 +188,13 @@ async function handleSubmitRegister(
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
ctx.assignInteractionHookResult({ userId: id });
ctx.assignDataHookContext({ event: 'User.Created', user });
ctx.appendDataHookContext('User.Created', { user });
for (const organizationId of organizationsIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
organizationId,
});
}
log?.append({ userId: id });
appInsights.client?.trackEvent({
@ -242,10 +248,7 @@ async function handleSubmitSignIn(
ctx.assignInteractionHookResult({ userId: accountId });
// Trigger user.updated data hook event if the user profile or mfa data is updated
if (hasUpdatedProfile(updateUserProfile) || mfaVerifications.length > 0) {
ctx.assignDataHookContext({
event: 'User.Data.Updated',
user: updatedUser,
});
ctx.appendDataHookContext('User.Data.Updated', { user: updatedUser });
}
appInsights.client?.trackEvent({
@ -284,7 +287,7 @@ export default async function submitInteraction(
passwordEncryptionMethod,
});
ctx.assignInteractionHookResult({ userId: accountId });
ctx.assignDataHookContext({ event: 'User.Data.Updated', user });
ctx.appendDataHookContext('User.Data.Updated', { user });
await clearInteractionStorage(ctx, provider);
ctx.status = 204;

View file

@ -1,9 +1,10 @@
import { userInfoSelectFields, type DataHookEvent, type User } from '@logto/schemas';
import { conditional, conditionalString, pick, trySafe } from '@silverhand/essentials';
import { type User } from '@logto/schemas';
import { conditionalString, trySafe } from '@silverhand/essentials';
import type { MiddlewareType } from 'koa';
import type { IRouterParamContext } from 'koa-router';
import {
type DataHookContext,
DataHookContextManager,
InteractionHookContextManager,
} from '#src/libraries/hook/context-manager.js';
@ -14,17 +15,17 @@ import { getInteractionStorage } from '../utils/interaction.js';
import type { WithInteractionDetailsContext } from './koa-interaction-details.js';
type AssignDataHookContext = (payload: {
event: DataHookEvent;
user?: User;
data?: Record<string, unknown>;
}) => void;
type AppendDataHookContext = (
payload: DataHookContext & {
user?: User;
}
) => void;
export type WithInteractionHooksContext<
ContextT extends IRouterParamContext = IRouterParamContext,
> = ContextT & {
assignInteractionHookResult: InteractionHookContextManager['assignInteractionHookResult'];
assignDataHookContext: AssignDataHookContext;
appendDataHookContext: DataHookContextManager['appendContext'];
};
/**
@ -68,17 +69,7 @@ export default function koaInteractionHooks<
ip,
});
// Assign user and event data to the data hook context
ctx.assignDataHookContext = ({ event, user, data: extraData }) => {
dataHookContext.appendContext({
event,
data: {
// Only return the selected user fields
...conditional(user && pick(user, ...userInfoSelectFields)),
...extraData,
},
});
};
ctx.appendDataHookContext = dataHookContext.appendContext.bind(dataHookContext);
await next();

View file

@ -135,7 +135,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
async (ctx, next) => {
const {
assignInteractionHookResult,
assignDataHookContext,
appendDataHookContext,
guard: { params },
} = ctx;
const {
@ -161,7 +161,7 @@ export default function singleSignOnRoutes<T extends IRouterParamContext>(
// Trigger webhooks
assignInteractionHookResult({ userId: accountId });
assignDataHookContext({ event: 'User.Created', user });
appendDataHookContext('User.Created', { user });
return next();
}

View file

@ -18,6 +18,8 @@ import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';
import {
assignSingleSignOnAuthenticationResult,
getSingleSignOnSessionResult,
@ -289,7 +291,7 @@ const signInAndLinkWithSsoAuthentication = async (
};
export const registerWithSsoAuthentication = async (
ctx: WithLogContext,
ctx: WithInteractionHooksContext<WithLogContext>,
{
queries: { userSsoIdentities: userSsoIdentitiesQueries },
libraries: { users: usersLibrary },
@ -308,7 +310,7 @@ export const registerWithSsoAuthentication = async (
};
// Insert new user
const user = await usersLibrary.insertUser(
const [user, { organizationsIds }] = await usersLibrary.insertUser(
{
id: await usersLibrary.generateUserId(),
...syncingProfile,
@ -316,6 +318,11 @@ export const registerWithSsoAuthentication = async (
},
[]
);
for (const organizationId of organizationsIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
organizationId,
});
}
const { id: userId } = user;

View file

@ -1,14 +1,14 @@
import { OrganizationEmailDomains } from '@logto/schemas';
import { type IRouterParamContext } from 'koa-router';
import type Router from 'koa-router';
import { z } from 'zod';
import koaGuard from '#src/middleware/koa-guard.js';
import { type WithHookContext } from '#src/middleware/koa-management-api-hooks.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import type OrganizationQueries from '#src/queries/organization/index.js';
export default function emailDomainRoutes(
router: Router<unknown, IRouterParamContext>,
router: Router<unknown, WithHookContext>,
organizations: OrganizationQueries
) {
const params = Object.freeze({ id: z.string().min(1) });

View file

@ -83,7 +83,10 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
);
// MARK: Organization - user relation routes
router.addRelationRoutes(organizations.relations.users, undefined, { disabled: { get: true } });
router.addRelationRoutes(organizations.relations.users, undefined, {
disabled: { get: true },
hookEvent: 'Organization.Membership.Updated',
});
router.get(
'/:id/users',

View file

@ -1,16 +1,16 @@
import { OrganizationRoles, OrganizationScopes } from '@logto/schemas';
import type Router from 'koa-router';
import { type IRouterParamContext } from 'koa-router';
import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import { type WithHookContext } from '#src/middleware/koa-management-api-hooks.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import type OrganizationQueries from '#src/queries/organization/index.js';
// Manually add these routes since I don't want to over-engineer the `SchemaRouter`
export default function userRoleRelationRoutes(
router: Router<unknown, IRouterParamContext>,
router: Router<unknown, WithHookContext>,
organizations: OrganizationQueries
) {
// MARK: Organization - user - organization role relation routes

View file

@ -116,8 +116,7 @@ export default function organizationRoleRoutes<T extends ManagementApiRouter>(
// Trigger `OrganizationRole.Scope.Updated` event if organizationScopeIds or resourceScopeIds are provided.
if (organizationScopeIds.length > 0 || resourceScopeIds.length > 0) {
ctx.appendDataHookContext({
event: 'OrganizationRole.Scopes.Updated',
ctx.appendDataHookContext('OrganizationRole.Scopes.Updated', {
...buildManagementApiContext(ctx),
organizationRoleId: role.id,
});

View file

@ -180,8 +180,7 @@ export default function roleRoutes<T extends ManagementApiRouter>(
// Align the response type with POST /roles/:id/scopes
const newRolesScopes = await findScopesByIds(scopeIds);
ctx.appendDataHookContext({
event: 'Role.Scopes.Updated',
ctx.appendDataHookContext('Role.Scopes.Updated', {
...buildManagementApiContext(ctx),
roleId: role.id,
data: newRolesScopes,

View file

@ -1,14 +1,16 @@
import { type SchemaLike, type GeneratedSchema } from '@logto/schemas';
import { type SchemaLike, type GeneratedSchema, type DataHookEvent } from '@logto/schemas';
import { generateStandardId } from '@logto/shared';
import { type DeepPartial, isPlainObject } from '@silverhand/essentials';
import camelcase from 'camelcase';
import deepmerge from 'deepmerge';
import { type MiddlewareType } from 'koa';
import { type Context, type MiddlewareType } from 'koa';
import Router, { type IRouterParamContext } from 'koa-router';
import { z } from 'zod';
import { type SearchOptions } from '#src/database/utils.js';
import { buildManagementApiContext } from '#src/libraries/hook/utils.js';
import koaGuard from '#src/middleware/koa-guard.js';
import { type WithHookContext } from '#src/middleware/koa-management-api-hooks.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import { type TwoRelationsQueries } from './RelationQueries.js';
@ -87,6 +89,8 @@ type SchemaRouterConfig<Key extends string> = {
};
type RelationRoutesConfig = {
/** The event that should be triggered when the relation is modified. */
hookEvent?: DataHookEvent;
/** Disable certain routes for the relation. */
disabled: {
/** Disable `GET /:id/[pathname]` route. */
@ -112,7 +116,7 @@ export default class SchemaRouter<
CreateSchema extends Partial<SchemaLike<Key> & { id: string }>,
Schema extends SchemaLike<Key> & { id: string },
StateT = unknown,
CustomT extends IRouterParamContext = IRouterParamContext,
CustomT extends WithHookContext = WithHookContext,
> extends Router<StateT, CustomT> {
public readonly config: SchemaRouterConfig<Key>;
@ -180,7 +184,7 @@ export default class SchemaRouter<
GeneratedSchema<string, RelationCreateSchema, RelationSchema>
>,
pathname = tableToPathname(relationQueries.schemas[1].table),
{ disabled }: Partial<RelationRoutesConfig> = {}
{ disabled, hookEvent }: Partial<RelationRoutesConfig> = {}
) {
const relationSchema = relationQueries.schemas[1];
const relationSchemaId = camelCaseSchemaId(relationSchema);
@ -189,6 +193,14 @@ export default class SchemaRouter<
relationSchemaId,
relationSchemaIds: relationSchemaId + 's',
};
const appendHookContext = (ctx: WithHookContext<IRouterParamContext & Context>, id: string) => {
if (hookEvent) {
ctx.appendDataHookContext(hookEvent, {
...buildManagementApiContext(ctx),
[columns.schemaId]: id,
});
}
};
if (!disabled?.get) {
this.get(
@ -207,9 +219,7 @@ export default class SchemaRouter<
const [totalCount, entities] = await relationQueries.getEntities(
relationSchema,
{
[columns.schemaId]: id,
},
{ [columns.schemaId]: id },
ctx.pagination
);
@ -236,7 +246,7 @@ export default class SchemaRouter<
await relationQueries.insert(
...(relationIds?.map<[string, string]>((relationId) => [id, relationId]) ?? [])
);
appendHookContext(ctx, id);
ctx.status = 201;
return next();
}
@ -256,6 +266,7 @@ export default class SchemaRouter<
} = ctx.guard;
await relationQueries.replace(id, relationIds ?? []);
appendHookContext(ctx, id);
ctx.status = 204;
return next();
}
@ -278,7 +289,8 @@ export default class SchemaRouter<
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- `koaGuard()` ensures the value is not `undefined`
[columns.relationSchemaId]: relationId!,
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
appendHookContext(ctx, id!);
ctx.status = 204;
return next();
}

View file

@ -34,6 +34,10 @@ export class OrganizationApi extends ApiFactory<
await authedAdminApi.post(`${this.path}/${id}/users`, { json: { userIds } });
}
async replaceUsers(id: string, userIds: string[]): Promise<void> {
await authedAdminApi.put(`${this.path}/${id}/users`, { json: { userIds } });
}
async getUsers(
id: string,
query?: Query

View file

@ -1,21 +1,30 @@
import { hookEvents, userInfoSelectFields } from '@logto/schemas';
/**
* @fileoverview
* Tests for manual-triggered data hook events.
*/
import { SignInIdentifier, hookEvents, userInfoSelectFields } from '@logto/schemas';
import { pick } from '@silverhand/essentials';
import { createUser, deleteUser } from '#src/api/admin-user.js';
import { OrganizationRoleApi } from '#src/api/organization-role.js';
import { OrganizationScopeApi } from '#src/api/organization-scope.js';
import { deleteUser } from '#src/api/admin-user.js';
import { createResource, deleteResource } from '#src/api/resource.js';
import { createRole } from '#src/api/role.js';
import { createScope } from '#src/api/scope.js';
import { WebHookApiTest } from '#src/helpers/hook.js';
import { generateName, generateRoleName } from '#src/utils.js';
import { registerWithEmail } from '#src/helpers/interactions.js';
import { OrganizationApiTest } from '#src/helpers/organization.js';
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
import { UserApiTest } from '#src/helpers/user.js';
import { generateName, generateRoleName, randomString } from '#src/utils.js';
import WebhookMockServer from './WebhookMockServer.js';
import { assertHookLogResult } from './utils.js';
describe('trigger custom data hook events', () => {
describe('manual data hook tests', () => {
const webbHookMockServer = new WebhookMockServer(9999);
const webHookApi = new WebHookApiTest();
const userApi = new UserApiTest();
const organizationApi = new OrganizationApiTest();
const hookName = 'customDataHookEventListener';
beforeAll(async () => {
@ -35,7 +44,7 @@ describe('trigger custom data hook events', () => {
});
afterEach(async () => {
await webHookApi.cleanUp();
await Promise.all([webHookApi.cleanUp(), userApi.cleanUp(), organizationApi.cleanUp()]);
});
it('create roles with scopeIds should trigger Roles.Scopes.Updated event', async () => {
@ -67,12 +76,10 @@ describe('trigger custom data hook events', () => {
});
it('create organizationRoles with organizationScopeIds should trigger OrganizationRole.Scopes.Updated event', async () => {
const roleApi = new OrganizationRoleApi();
const organizationScopeApi = new OrganizationScopeApi();
const scope = await organizationScopeApi.create({ name: generateName() });
const scope = await organizationApi.scopeApi.create({ name: generateName() });
const hook = webHookApi.hooks.get(hookName)!;
const organizationRole = await roleApi.create({
const organizationRole = await organizationApi.roleApi.create({
name: generateRoleName(),
organizationScopeIds: [scope.id],
});
@ -92,13 +99,10 @@ describe('trigger custom data hook events', () => {
organizationRoleId: organizationRole.id,
},
});
await roleApi.delete(organizationRole.id);
await organizationScopeApi.delete(scope.id);
});
it('delete user should trigger User.Deleted event with selected user info', async () => {
const user = await createUser();
const user = await userApi.create({});
const hook = webHookApi.hooks.get(hookName)!;
await deleteUser(user.id);
@ -110,4 +114,40 @@ describe('trigger custom data hook events', () => {
},
});
});
const assertOrganizationMembershipUpdated = async (organizationId: string) =>
assertHookLogResult(webHookApi.hooks.get(hookName)!, 'Organization.Membership.Updated', {
hookPayload: {
event: 'Organization.Membership.Updated',
organizationId,
},
});
describe('organization membership update by just-in-time organization provisioning', () => {
it('should trigger `Organization.Membership.Updated` event when user is provisioned by Management API', async () => {
const organization = await organizationApi.create({ name: 'foo' });
const domain = 'example.com';
await organizationApi.addEmailDomain(organization.id, domain);
await userApi.create({ primaryEmail: `${randomString()}@${domain}` });
await assertOrganizationMembershipUpdated(organization.id);
});
it('should trigger `Organization.Membership.Updated` event when user is provisioned by experience', async () => {
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
});
const organization = await organizationApi.create({ name: 'foo' });
const domain = 'example.com';
await organizationApi.addEmailDomain(organization.id, domain);
await registerWithEmail(`${randomString()}@${domain}`);
await assertOrganizationMembershipUpdated(organization.id);
});
// TODO: Add SSO test case
});
});

View file

@ -1,3 +1,8 @@
/**
* @fileoverview
* Tests for automatic data hook events triggered by the Management API.
*/
import {
RoleType,
hookEventGuard,
@ -242,13 +247,16 @@ describe('organization data hook events', () => {
it.each(organizationDataHookTestCases)(
'test case %#: %p',
async ({ route, event, method, endpoint, payload }) => {
async ({ route, event, method, endpoint, payload, hookPayload }) => {
await authedAdminApi[method](
endpoint.replace('{organizationId}', organizationId).replace('{userId}', userId),
{ json: JSON.parse(JSON.stringify(payload).replace('{userId}', userId)) }
);
const hook = await getWebhookResult(route);
expect(hook?.payload.event).toBe(event);
if (hookPayload) {
expect(hook?.payload).toMatchObject(hookPayload);
}
}
);
});

View file

@ -5,7 +5,10 @@ type TestCase = {
event: string;
method: 'patch' | 'post' | 'delete' | 'put';
endpoint: string;
/** The payload that should be sent to the route. */
payload: Record<string, unknown>;
/** The payload that should be sent to the webhook. */
hookPayload?: Record<string, unknown>;
};
export const userDataHookTestCases: TestCase[] = [
@ -108,6 +111,7 @@ export const organizationDataHookTestCases: TestCase[] = [
method: 'post',
endpoint: `organizations/{organizationId}/users`,
payload: { userIds: ['{userId}'] },
hookPayload: { organizationId: expect.any(String) },
},
{
route: 'PUT /organizations/:id/users',
@ -115,6 +119,7 @@ export const organizationDataHookTestCases: TestCase[] = [
method: 'put',
endpoint: `organizations/{organizationId}/users`,
payload: { userIds: ['{userId}'] },
hookPayload: { organizationId: expect.any(String) },
},
{
route: 'DELETE /organizations/:id/users/:userId',
@ -122,6 +127,7 @@ export const organizationDataHookTestCases: TestCase[] = [
method: 'delete',
endpoint: `organizations/{organizationId}/users/{userId}`,
payload: {},
hookPayload: { organizationId: expect.any(String) },
},
{
route: 'DELETE /organizations/:id',

View file

@ -63,4 +63,6 @@ describe('organization just-in-time provisioning', () => {
await logoutClient(client);
await deleteUser(id);
});
// TODO: Add SSO test case
});

View file

@ -134,9 +134,6 @@ export const managementApiHooksRegistration = Object.freeze({
'POST /organizations': 'Organization.Created',
'DELETE /organizations/:id': 'Organization.Deleted',
'PATCH /organizations/:id': 'Organization.Data.Updated',
'PUT /organizations/:id/users': 'Organization.Membership.Updated',
'POST /organizations/:id/users': 'Organization.Membership.Updated',
'DELETE /organizations/:id/users/:userId': 'Organization.Membership.Updated',
'POST /organization-roles': 'OrganizationRole.Created',
'DELETE /organization-roles/:id': 'OrganizationRole.Deleted',
'PATCH /organization-roles/:id': 'OrganizationRole.Data.Updated',