@unito/integration-sdk
Version:
Integration SDK
202 lines (201 loc) • 8.92 kB
JavaScript
import { RuleTester } from 'eslint';
import tsParser from '@typescript-eslint/parser';
import test from 'node:test';
import rule from '../../src/eslint/rules/webhook-handler-requires-signature-verification.js';
const ruleTester = new RuleTester({
languageOptions: {
parser: tsParser,
ecmaVersion: 2022,
sourceType: 'module',
},
});
const PARSE_FILENAME = '/repo/integrations/foo/src/handlers/parseWebhooks.ts';
const PARSE_FILENAME_ALT = '/repo/integrations/foo/src/routes/webhookParse.ts';
const NON_WEBHOOK_FILENAME = '/repo/integrations/foo/src/handlers/task.ts';
test('webhook-handler-requires-signature-verification', () => {
ruleTester.run('webhook-handler-requires-signature-verification', rule, {
valid: [
{
name: 'parseWebhooks.ts importing node:crypto verifier helpers',
filename: PARSE_FILENAME,
code: `
import { createHmac, timingSafeEqual } from 'node:crypto';
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
return [];
};
`,
},
{
name: 'webhookParse.ts importing legacy `crypto` module',
filename: PARSE_FILENAME_ALT,
code: `
import * as crypto from 'crypto';
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
return [];
};
`,
},
{
name: 'parseWebhooks.ts with @webhook-no-signature opt-out',
filename: PARSE_FILENAME,
code: `
import { ParseWebhooksHandler } from '@unito/integration-sdk';
/** @webhook-no-signature: provider does not sign webhooks (legacy ServiceNow business rule) */
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
return [];
};
`,
},
{
name: 'AcknowledgeWebhooksHandler export is out of scope',
filename: '/repo/integrations/foo/src/handlers/webhookAcknowledge.ts',
code: `
import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk';
export const acknowledgeWebhooks: AcknowledgeWebhooksHandler = async (ctx) => {
return { handshake: 'ok' };
};
`,
},
{
name: 'non-webhook file is out of scope',
filename: NON_WEBHOOK_FILENAME,
code: `
import { GetItemHandler } from '@unito/integration-sdk';
export const getTask: GetItemHandler = async (ctx) => ({});
`,
},
{
name: 'arbitrary file that imports ParseWebhooksHandler with verifier (in-scope by import)',
filename: '/repo/integrations/foo/src/helpers/webhookHelper.ts',
code: `
import { createHmac } from 'node:crypto';
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => [];
`,
},
{
name: 'parseWebhooks.ts importing SDK verifier helper by name',
filename: PARSE_FILENAME,
code: `
import { ParseWebhooksHandler, verifyWebhookSignature } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
verifyWebhookSignature(ctx.body.headers, ctx.body.payload);
return [];
};
`,
},
{
name: 'webhookParse.ts only an Acknowledge export plus crypto-less helper imports',
filename: '/repo/integrations/foo/src/handlers/webhookSubscriptions.ts',
code: `
import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk';
export const acknowledgeWebhooks: AcknowledgeWebhooksHandler = async (ctx) => ({});
`,
},
],
invalid: [
{
name: 'parseWebhooks.ts exporting ParseWebhooksHandler with no crypto import',
filename: PARSE_FILENAME,
code: `
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
const payload = JSON.parse(ctx.body.payload);
return [];
};
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'webhookParse.ts (alternate name) with no verifier import',
filename: PARSE_FILENAME_ALT,
code: `
import { ParseWebhooksHandler, ParseWebhooksContext } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx: ParseWebhooksContext) => {
return [];
};
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'parseWebhooks.ts with raw JSON.parse and no signature check',
filename: PARSE_FILENAME,
code: `
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
const data = JSON.parse(ctx.body.payload);
return data.events.map((e) => ({ itemPath: '/x', date: '', impactedRelations: [] }));
};
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'arbitrary filename in-scope by ParseWebhooksHandler import only',
filename: '/repo/integrations/foo/src/helpers/somethingElse.ts',
code: `
import { ParseWebhooksHandler } from '@unito/integration-api';
export const parseWebhooks: ParseWebhooksHandler = async () => [];
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'parseWebhooks.ts opt-out without reason is rejected',
filename: PARSE_FILENAME,
code: `
import { ParseWebhooksHandler } from '@unito/integration-sdk';
/** @webhook-no-signature: */
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => [];
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'parseWebhooks.ts inferred from export name even without type annotation',
filename: PARSE_FILENAME,
code: `
export const parseWebhooks = async (ctx) => {
return [];
};
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'parseWebhooks.ts with non-crypto import still flagged',
filename: PARSE_FILENAME,
code: `
import { JSON5 } from 'some-other-pkg';
import { ParseWebhooksHandler } from '@unito/integration-sdk';
export const parseWebhooks: ParseWebhooksHandler = async (ctx) => {
return JSON5.parse(ctx.body.payload);
};
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'webhookParse.ts with separate const decl and `export { parseWebhooks }` form',
filename: PARSE_FILENAME_ALT,
code: `
import { ParseWebhooksHandler, ParseWebhooksContext } from '@unito/integration-sdk';
const parseWebhooks: ParseWebhooksHandler = async (ctx: ParseWebhooksContext) => {
return [];
};
export { parseWebhooks };
`,
errors: [{ messageId: 'missingVerification', data: { name: 'parseWebhooks' } }],
},
{
name: 'Express-route webhookParse.ts: only exports a `router` but file is in scope by filename',
filename: '/repo/integrations/foo/src/routes/webhookParse.ts',
code: `
import { Router } from 'express';
export const router = Router();
router.post('/', async (req, res) => {
res.json({});
});
`,
errors: [{ messageId: 'missingVerification', data: { name: '<webhook-parse-route>' } }],
},
],
});
});