fix(server): restrict /webhooks/server dispatch to server-route-exposed functions (#22469)
## What `ServerRouteTriggerService.findResolver` resolved a logic function purely by `universalIdentifier` and app-registration ownership, then executed it before its resolver result shape was validated. As a result the public `/webhooks/server/:universalIdentifier` route could dispatch any owner-workspace app function — including ones exposed only as authenticated HTTP routes, tools, or workflow actions — instead of only functions declared as server-route resolvers. ## Change `findResolver` now requires `serverRouteTriggerSettings`: - DB predicate `serverRouteTriggerSettings: Not(IsNull())`, so non-exposed functions are never fetched - in-memory `isDefined(...)` guard alongside the existing owner-workspace check A function that did not opt into server-route exposure is now rejected at `findResolver`, before any execution. A legitimately exposed resolver is unaffected. ## Tests - Unit (`server-route-trigger.service.spec.ts`): asserts the resolver query carries the exposure predicate, and that an owner-workspace function without `serverRouteTriggerSettings` is rejected and never handed to the executor. - Integration (`server-route-trigger-authorization.integration-spec.ts`): exercises the public endpoint end to end — a non-exposed owner-workspace function is rejected before execution, while a server-route-exposed resolver still passes the boundary. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22469?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+87
-111
@@ -46,35 +46,59 @@ const buildRequest = (body: object | null = {}): Request =>
|
||||
body,
|
||||
}) as unknown as Request;
|
||||
|
||||
type QueryBuilderMock = {
|
||||
innerJoinAndSelect: jest.Mock;
|
||||
where: jest.Mock;
|
||||
andWhere: jest.Mock;
|
||||
getOne: jest.Mock;
|
||||
};
|
||||
|
||||
const buildResolverRow = () => ({
|
||||
id: 'resolver-id',
|
||||
universalIdentifier: RESOLVER_UID,
|
||||
workspaceId: 'owner-ws',
|
||||
serverRouteTriggerSettings: { forwardedRequestHeaders: ['x-test'] },
|
||||
application: {
|
||||
applicationRegistration: { id: 'reg-1', ownerWorkspaceId: 'owner-ws' },
|
||||
},
|
||||
});
|
||||
|
||||
describe('ServerRouteTriggerService', () => {
|
||||
let service: ServerRouteTriggerService;
|
||||
let logicFunctionRepository: jest.Mocked<
|
||||
Pick<Repository<LogicFunctionEntity>, 'find' | 'findOne'>
|
||||
>;
|
||||
let logicFunctionRepository: {
|
||||
createQueryBuilder: jest.Mock;
|
||||
findOne: jest.Mock;
|
||||
};
|
||||
let logicFunctionExecutorService: jest.Mocked<
|
||||
Pick<LogicFunctionExecutorService, 'execute'>
|
||||
>;
|
||||
|
||||
let resolverRow: unknown;
|
||||
let queryBuilder: QueryBuilderMock;
|
||||
|
||||
const buildQueryBuilderMock = (): QueryBuilderMock => {
|
||||
const qb: QueryBuilderMock = {
|
||||
innerJoinAndSelect: jest.fn(() => qb),
|
||||
where: jest.fn(() => qb),
|
||||
andWhere: jest.fn(() => qb),
|
||||
getOne: jest.fn(() => Promise.resolve(resolverRow)),
|
||||
};
|
||||
|
||||
return qb;
|
||||
};
|
||||
|
||||
const handle = () =>
|
||||
service.handle({
|
||||
request: buildRequest(),
|
||||
resolverLogicFunctionUniversalIdentifier: RESOLVER_UID,
|
||||
});
|
||||
|
||||
const buildResolverRow = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: 'resolver-id',
|
||||
universalIdentifier: RESOLVER_UID,
|
||||
workspaceId: 'owner-ws',
|
||||
serverRouteTriggerSettings: { forwardedRequestHeaders: ['x-test'] },
|
||||
application: {
|
||||
applicationRegistration: { id: 'reg-1', ownerWorkspaceId: 'owner-ws' },
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resolverRow = buildResolverRow();
|
||||
queryBuilder = buildQueryBuilderMock();
|
||||
|
||||
logicFunctionRepository = {
|
||||
find: jest.fn().mockResolvedValue([buildResolverRow()]),
|
||||
createQueryBuilder: jest.fn(() => queryBuilder),
|
||||
findOne: jest
|
||||
.fn()
|
||||
// resolver lookup inside runFunction
|
||||
@@ -129,68 +153,56 @@ describe('ServerRouteTriggerService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws LOGIC_FUNCTION_NOT_FOUND when no row matches the universalIdentifier', async () => {
|
||||
logicFunctionRepository.find.mockResolvedValue([]);
|
||||
|
||||
await expect(handle()).rejects.toMatchObject({
|
||||
code: ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws LOGIC_FUNCTION_NOT_FOUND when only non-owner-workspace copies exist', async () => {
|
||||
logicFunctionRepository.find.mockResolvedValue([
|
||||
buildResolverRow({
|
||||
workspaceId: 'other-ws',
|
||||
application: {
|
||||
applicationRegistration: { ownerWorkspaceId: 'owner-ws' },
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
] as any);
|
||||
|
||||
await expect(handle()).rejects.toMatchObject({
|
||||
code: ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
|
||||
});
|
||||
});
|
||||
|
||||
it('picks the owner-workspace copy when multiple workspaces installed the app', async () => {
|
||||
logicFunctionRepository.find.mockResolvedValue([
|
||||
buildResolverRow({
|
||||
id: 'tenant-copy',
|
||||
workspaceId: 'tenant-ws',
|
||||
application: {
|
||||
applicationRegistration: {
|
||||
id: 'reg-1',
|
||||
ownerWorkspaceId: 'owner-ws',
|
||||
},
|
||||
},
|
||||
}),
|
||||
buildResolverRow({
|
||||
id: 'owner-copy',
|
||||
workspaceId: 'owner-ws',
|
||||
application: {
|
||||
applicationRegistration: {
|
||||
id: 'reg-1',
|
||||
ownerWorkspaceId: 'owner-ws',
|
||||
},
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
] as any);
|
||||
|
||||
it('queries the resolver by universalIdentifier and joins the application registration chain', async () => {
|
||||
await handle();
|
||||
|
||||
// runFunction's internal findOne is called with the
|
||||
// (universalIdentifier, workspaceId) of the owner-workspace copy.
|
||||
expect(logicFunctionRepository.findOne).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
universalIdentifier: RESOLVER_UID,
|
||||
workspaceId: 'owner-ws',
|
||||
}),
|
||||
}),
|
||||
expect(logicFunctionRepository.createQueryBuilder).toHaveBeenCalledWith(
|
||||
'logicFunction',
|
||||
);
|
||||
expect(queryBuilder.innerJoinAndSelect).toHaveBeenCalledWith(
|
||||
'logicFunction.application',
|
||||
'application',
|
||||
);
|
||||
expect(queryBuilder.innerJoinAndSelect).toHaveBeenCalledWith(
|
||||
'application.applicationRegistration',
|
||||
'applicationRegistration',
|
||||
);
|
||||
expect(queryBuilder.where).toHaveBeenCalledWith(
|
||||
'logicFunction.universalIdentifier = :universalIdentifier',
|
||||
{ universalIdentifier: RESOLVER_UID },
|
||||
);
|
||||
});
|
||||
|
||||
it('restricts the resolver query to server-route-exposed, owner-workspace functions', async () => {
|
||||
await handle();
|
||||
|
||||
expect(queryBuilder.andWhere).toHaveBeenCalledWith(
|
||||
'logicFunction.serverRouteTriggerSettings IS NOT NULL',
|
||||
);
|
||||
expect(queryBuilder.andWhere).toHaveBeenCalledWith(
|
||||
'logicFunction.workspaceId = applicationRegistration.ownerWorkspaceId',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws LOGIC_FUNCTION_NOT_FOUND and executes nothing when the query returns no server-route resolver', async () => {
|
||||
resolverRow = null;
|
||||
|
||||
await expect(handle()).rejects.toMatchObject({
|
||||
code: ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
|
||||
});
|
||||
expect(logicFunctionExecutorService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects and executes nothing when the resolver requires authentication', async () => {
|
||||
resolverRow = {
|
||||
...buildResolverRow(),
|
||||
httpRouteTriggerSettings: { isAuthRequired: true },
|
||||
};
|
||||
|
||||
await expect(handle()).rejects.toMatchObject({
|
||||
code: ServerRouteTriggerExceptionCode.RESOLVER_REQUIRES_AUTHENTICATION,
|
||||
});
|
||||
expect(logicFunctionExecutorService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws RESOLVER_INVALID_RESULT when the resolver does not return a workspaceId', async () => {
|
||||
@@ -232,11 +244,7 @@ describe('ServerRouteTriggerService', () => {
|
||||
logicFunctionRepository.findOne.mockReset();
|
||||
logicFunctionRepository.findOne
|
||||
// resolver lookup succeeds
|
||||
.mockResolvedValueOnce({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
id: 'resolver-id',
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any)
|
||||
.mockResolvedValueOnce({ id: 'resolver-id' })
|
||||
// target lookup returns null
|
||||
.mockResolvedValueOnce(null);
|
||||
|
||||
@@ -286,23 +294,6 @@ describe('ServerRouteTriggerService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('looks up the resolver by universalIdentifier and loads the application registration chain', async () => {
|
||||
await handle();
|
||||
|
||||
const findArgs = logicFunctionRepository.find.mock.calls[0][0];
|
||||
|
||||
expect(findArgs?.where).toEqual(
|
||||
expect.objectContaining({ universalIdentifier: RESOLVER_UID }),
|
||||
);
|
||||
expect(findArgs?.relations).toEqual(
|
||||
expect.objectContaining({
|
||||
application: expect.objectContaining({
|
||||
applicationRegistration: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps a LogicFunctionExecutionException(RATE_LIMIT_EXCEEDED) to the server-route rate-limit code', async () => {
|
||||
logicFunctionExecutorService.execute.mockReset();
|
||||
logicFunctionExecutorService.execute.mockRejectedValue(
|
||||
@@ -332,21 +323,6 @@ describe('ServerRouteTriggerService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('throws LOGIC_FUNCTION_NOT_FOUND when the resolver is not linked to an application registration', async () => {
|
||||
logicFunctionRepository.find.mockResolvedValue([
|
||||
buildResolverRow({
|
||||
application: {
|
||||
applicationRegistration: { ownerWorkspaceId: 'owner-ws' },
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
] as any);
|
||||
|
||||
await expect(handle()).rejects.toMatchObject({
|
||||
code: ServerRouteTriggerExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not leak the raw executor error message to the caller', async () => {
|
||||
logicFunctionExecutorService.execute.mockReset();
|
||||
logicFunctionExecutorService.execute
|
||||
|
||||
+9
@@ -63,6 +63,15 @@ export class ServerRouteTriggerRestApiExceptionFilter implements ExceptionFilter
|
||||
undefined,
|
||||
{ shouldBeCapturedBySentry: false },
|
||||
);
|
||||
case ServerRouteTriggerExceptionCode.RESOLVER_REQUIRES_AUTHENTICATION:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception as CustomException,
|
||||
response,
|
||||
403,
|
||||
undefined,
|
||||
undefined,
|
||||
{ shouldBeCapturedBySentry: false },
|
||||
);
|
||||
default: {
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception as CustomException,
|
||||
|
||||
+3
@@ -10,6 +10,7 @@ export enum ServerRouteTriggerExceptionCode {
|
||||
SERVER_ROUTE_USER_UNCAUGHT_ERROR = 'SERVER_ROUTE_USER_UNCAUGHT_ERROR',
|
||||
SERVER_ROUTE_PLATFORM_ERROR = 'SERVER_ROUTE_PLATFORM_ERROR',
|
||||
RESOLVER_INVALID_RESULT = 'RESOLVER_INVALID_RESULT',
|
||||
RESOLVER_REQUIRES_AUTHENTICATION = 'RESOLVER_REQUIRES_AUTHENTICATION',
|
||||
}
|
||||
|
||||
const getServerRouteTriggerExceptionUserFriendlyMessage = (
|
||||
@@ -26,6 +27,8 @@ const getServerRouteTriggerExceptionUserFriendlyMessage = (
|
||||
return msg`An unexpected error occurred while handling the server route.`;
|
||||
case ServerRouteTriggerExceptionCode.RESOLVER_INVALID_RESULT:
|
||||
return msg`Resolver logic function returned an invalid result.`;
|
||||
case ServerRouteTriggerExceptionCode.RESOLVER_REQUIRES_AUTHENTICATION:
|
||||
return msg`Server logic function requires authentication.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+22
-11
@@ -57,6 +57,13 @@ export class ServerRouteTriggerService {
|
||||
);
|
||||
}
|
||||
|
||||
if (resolver.httpRouteTriggerSettings?.isAuthRequired === true) {
|
||||
throw new ServerRouteTriggerException(
|
||||
`Server resolver function ${resolverLogicFunctionUniversalIdentifier} requires authentication and cannot be dispatched through the public server route`,
|
||||
ServerRouteTriggerExceptionCode.RESOLVER_REQUIRES_AUTHENTICATION,
|
||||
);
|
||||
}
|
||||
|
||||
const applicationRegistrationId =
|
||||
resolver.application?.applicationRegistration?.id;
|
||||
|
||||
@@ -105,18 +112,22 @@ export class ServerRouteTriggerService {
|
||||
}: {
|
||||
logicFunctionUniversalIdentifier: string;
|
||||
}): Promise<LogicFunctionEntity | null> {
|
||||
const candidates = await this.logicFunctionRepository.find({
|
||||
where: { universalIdentifier: logicFunctionUniversalIdentifier },
|
||||
relations: { application: { applicationRegistration: true } },
|
||||
});
|
||||
|
||||
return (
|
||||
candidates.find(
|
||||
(candidate) =>
|
||||
isDefined(candidate.application?.applicationRegistration) &&
|
||||
candidate.workspaceId ===
|
||||
candidate.application.applicationRegistration.ownerWorkspaceId,
|
||||
) ?? null
|
||||
(await this.logicFunctionRepository
|
||||
.createQueryBuilder('logicFunction')
|
||||
.innerJoinAndSelect('logicFunction.application', 'application')
|
||||
.innerJoinAndSelect(
|
||||
'application.applicationRegistration',
|
||||
'applicationRegistration',
|
||||
)
|
||||
.where('logicFunction.universalIdentifier = :universalIdentifier', {
|
||||
universalIdentifier: logicFunctionUniversalIdentifier,
|
||||
})
|
||||
.andWhere('logicFunction.serverRouteTriggerSettings IS NOT NULL')
|
||||
.andWhere(
|
||||
'logicFunction.workspaceId = applicationRegistration.ownerWorkspaceId',
|
||||
)
|
||||
.getOne()) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ServerRouteTrigger authorization (integration) POST /webhooks/server/:universalIdentifier (public, unauthenticated) rejects a server-route-exposed resolver that requires authentication before executing it 1`] = `
|
||||
{
|
||||
"body": {
|
||||
"code": "RESOLVER_REQUIRES_AUTHENTICATION",
|
||||
"error": "Error",
|
||||
"messages": [
|
||||
"Server resolver function 3c3f983f-5c1a-4c60-a3c8-7d0e2a4a33c3 requires authentication and cannot be dispatched through the public server route",
|
||||
],
|
||||
"statusCode": 403,
|
||||
},
|
||||
"status": 403,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ServerRouteTrigger authorization (integration) POST /webhooks/server/:universalIdentifier (public, unauthenticated) rejects an owner-workspace function without serverRouteTriggerSettings before executing it 1`] = `
|
||||
{
|
||||
"body": {
|
||||
"code": "LOGIC_FUNCTION_NOT_FOUND",
|
||||
"error": "Error",
|
||||
"messages": [
|
||||
"Server resolver function 1a1f983f-5c1a-4c60-a3c8-7d0e2a4a11a1 not found",
|
||||
],
|
||||
"statusCode": 404,
|
||||
},
|
||||
"status": 404,
|
||||
}
|
||||
`;
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
import request from 'supertest';
|
||||
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
||||
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
||||
import { setupApplicationForSync } from 'test/integration/metadata/suites/application/utils/setup-application-for-sync.util';
|
||||
import { syncApplication } from 'test/integration/metadata/suites/application/utils/sync-application.util';
|
||||
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
|
||||
import { expectOneNotInternalServerErrorHttpResponseSnapshot } from 'test/integration/utils/expect-one-not-internal-server-error-http-response-snapshot.util';
|
||||
import { type LogicFunctionManifest } from 'twenty-shared/application';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
|
||||
const OWNER_WORKSPACE_ID = SEED_APPLE_WORKSPACE_ID;
|
||||
|
||||
const APP_UNIVERSAL_IDENTIFIER = 'd41340eb-6cc9-4383-8b04-9be7dc794bb1';
|
||||
const ROLE_UNIVERSAL_IDENTIFIER = 'e5b19f77-3e1c-4a10-9c2e-56d6b0f8a3d2';
|
||||
|
||||
const NON_EXPOSED_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'1a1f983f-5c1a-4c60-a3c8-7d0e2a4a11a1';
|
||||
const EXPOSED_RESOLVER_UNIVERSAL_IDENTIFIER =
|
||||
'2b2f983f-5c1a-4c60-a3c8-7d0e2a4a22b2';
|
||||
const AUTH_REQUIRED_RESOLVER_UNIVERSAL_IDENTIFIER =
|
||||
'3c3f983f-5c1a-4c60-a3c8-7d0e2a4a33c3';
|
||||
const TARGET_FUNCTION_UNIVERSAL_IDENTIFIER =
|
||||
'4d4f983f-5c1a-4c60-a3c8-7d0e2a4a44d4';
|
||||
|
||||
const TARGET_FUNCTION_RESPONSE = { greeting: 'hello from target function' };
|
||||
|
||||
// Built (ESM) handler code executed by the logic function driver. The resolver
|
||||
// routes the public request to the target function in the owner workspace.
|
||||
const RESOLVER_BUILT_HANDLER_CODE = `export const main = async () => ({
|
||||
workspaceId: '${OWNER_WORKSPACE_ID}',
|
||||
targetLogicFunctionUniversalIdentifier: '${TARGET_FUNCTION_UNIVERSAL_IDENTIFIER}',
|
||||
});
|
||||
`;
|
||||
|
||||
const TARGET_BUILT_HANDLER_CODE = `export const main = async () => (${JSON.stringify(
|
||||
TARGET_FUNCTION_RESPONSE,
|
||||
)});
|
||||
`;
|
||||
|
||||
const buildLogicFunctionManifest = ({
|
||||
universalIdentifier,
|
||||
name,
|
||||
serverRouteExposed,
|
||||
authRequired,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
serverRouteExposed: boolean;
|
||||
authRequired: boolean;
|
||||
}): LogicFunctionManifest => ({
|
||||
universalIdentifier,
|
||||
name,
|
||||
handlerName: 'main',
|
||||
sourceHandlerPath: `src/${name}.ts`,
|
||||
builtHandlerPath: `dist/${name}.mjs`,
|
||||
builtHandlerChecksum: `checksum-${name}`,
|
||||
...(authRequired
|
||||
? {
|
||||
httpRouteTriggerSettings: {
|
||||
path: `/${name}`,
|
||||
httpMethod: 'POST',
|
||||
isAuthRequired: true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(serverRouteExposed
|
||||
? { serverRouteTriggerSettings: { forwardedRequestHeaders: [] } }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const uploadBuiltHandlerFile = async ({
|
||||
builtHandlerPath,
|
||||
builtHandlerCode,
|
||||
}: {
|
||||
builtHandlerPath: string;
|
||||
builtHandlerCode: string;
|
||||
}) => {
|
||||
jest.useRealTimers();
|
||||
|
||||
await uploadApplicationFile({
|
||||
applicationUniversalIdentifier: APP_UNIVERSAL_IDENTIFIER,
|
||||
fileFolder: 'BuiltLogicFunction',
|
||||
filePath: builtHandlerPath,
|
||||
fileBuffer: Buffer.from(builtHandlerCode),
|
||||
filename: builtHandlerPath.split('/').pop() as string,
|
||||
contentType: 'application/javascript',
|
||||
expectToFail: false,
|
||||
});
|
||||
|
||||
jest.useFakeTimers();
|
||||
};
|
||||
|
||||
describe('ServerRouteTrigger authorization (integration)', () => {
|
||||
const baseUrl = `http://localhost:${APP_PORT}`;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupApplicationForSync({
|
||||
applicationUniversalIdentifier: APP_UNIVERSAL_IDENTIFIER,
|
||||
name: 'Server Route Auth Test App',
|
||||
description: 'App for testing server route trigger authorization',
|
||||
sourcePath: 'server-route-auth-test-app',
|
||||
});
|
||||
|
||||
await uploadBuiltHandlerFile({
|
||||
builtHandlerPath: 'dist/exposed-resolver.mjs',
|
||||
builtHandlerCode: RESOLVER_BUILT_HANDLER_CODE,
|
||||
});
|
||||
|
||||
await uploadBuiltHandlerFile({
|
||||
builtHandlerPath: 'dist/target-function.mjs',
|
||||
builtHandlerCode: TARGET_BUILT_HANDLER_CODE,
|
||||
});
|
||||
|
||||
await syncApplication({
|
||||
manifest: buildBaseManifest({
|
||||
appId: APP_UNIVERSAL_IDENTIFIER,
|
||||
roleId: ROLE_UNIVERSAL_IDENTIFIER,
|
||||
overrides: {
|
||||
logicFunctions: [
|
||||
buildLogicFunctionManifest({
|
||||
universalIdentifier: NON_EXPOSED_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'non-exposed-function',
|
||||
serverRouteExposed: false,
|
||||
authRequired: true,
|
||||
}),
|
||||
buildLogicFunctionManifest({
|
||||
universalIdentifier: EXPOSED_RESOLVER_UNIVERSAL_IDENTIFIER,
|
||||
name: 'exposed-resolver',
|
||||
serverRouteExposed: true,
|
||||
authRequired: false,
|
||||
}),
|
||||
buildLogicFunctionManifest({
|
||||
universalIdentifier: AUTH_REQUIRED_RESOLVER_UNIVERSAL_IDENTIFIER,
|
||||
name: 'auth-required-resolver',
|
||||
serverRouteExposed: true,
|
||||
authRequired: true,
|
||||
}),
|
||||
buildLogicFunctionManifest({
|
||||
universalIdentifier: TARGET_FUNCTION_UNIVERSAL_IDENTIFIER,
|
||||
name: 'target-function',
|
||||
serverRouteExposed: false,
|
||||
authRequired: false,
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
expectToFail: false,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupApplicationAndAppRegistration({
|
||||
applicationUniversalIdentifier: APP_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
describe('POST /webhooks/server/:universalIdentifier (public, unauthenticated)', () => {
|
||||
it('rejects an owner-workspace function without serverRouteTriggerSettings before executing it', async () => {
|
||||
const response = await request(baseUrl)
|
||||
.post(`/webhooks/server/${NON_EXPOSED_FUNCTION_UNIVERSAL_IDENTIFIER}`)
|
||||
.send({ any: 'payload' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expectOneNotInternalServerErrorHttpResponseSnapshot({
|
||||
status: response.status,
|
||||
body: response.body,
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches a server-route-exposed resolver and returns the target function response', async () => {
|
||||
const response = await request(baseUrl)
|
||||
.post(`/webhooks/server/${EXPOSED_RESOLVER_UNIVERSAL_IDENTIFIER}`)
|
||||
.send({ any: 'payload' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toEqual(TARGET_FUNCTION_RESPONSE);
|
||||
}, 60000);
|
||||
|
||||
it('rejects a server-route-exposed resolver that requires authentication before executing it', async () => {
|
||||
const response = await request(baseUrl)
|
||||
.post(`/webhooks/server/${AUTH_REQUIRED_RESOLVER_UNIVERSAL_IDENTIFIER}`)
|
||||
.send({ any: 'payload' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expectOneNotInternalServerErrorHttpResponseSnapshot({
|
||||
status: response.status,
|
||||
body: response.body,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user