import { RuleTester } from 'eslint'; import tseslint from 'typescript-eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/webhook-json-parse-must-have-try.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const tsRuleTester = new RuleTester({ languageOptions: { parser: tseslint.parser, ecmaVersion: 2022, sourceType: 'module', }, }); const WEBHOOK_FILE = '/repo/integrations/foo/src/webhook.ts'; const WEBHOOKS_FILE = '/repo/integrations/foo/src/webhooks.ts'; const PARSE_WEBHOOKS_FILE = '/repo/integrations/foo/src/parseWebhooks.ts'; const WEBHOOK_HELPER_FILE = '/repo/integrations/foo/src/webhookHelper.ts'; const WEBHOOKS_HELPER_FILE = '/repo/integrations/foo/src/helpers/webhooksHelper.ts'; const NON_WEBHOOK_FILE = '/repo/integrations/foo/src/handlers/item.ts'; const PARSE_EVENTS_FILE = '/repo/integrations/foo/src/handlers/parseEvents.ts'; const EVENT_DISPATCHER_FILE = '/repo/integrations/foo/src/handlers/eventDispatcher.ts'; test('webhook-json-parse-must-have-try', () => { ruleTester.run('webhook-json-parse-must-have-try', rule, { valid: [ { name: 'try-wrapped JSON.parse in webhook.ts', filename: WEBHOOK_FILE, code: `function f(raw) { try { return JSON.parse(raw); } catch (e) { return null; } }`, }, { name: 'try-wrapped JSON.parse with logger in webhooks.ts', filename: WEBHOOKS_FILE, code: `function f(raw) { try { const body = JSON.parse(raw); return body; } catch (e) { logger.warn('bad', { error: e }); return null; } }`, }, { name: 'JSON.parse inside catch handler is fine (already error-handling)', filename: WEBHOOK_FILE, code: `function f(raw) { try { doThing(); } catch (e) { const fallback = JSON.parse('{}'); return fallback; } }`, }, { name: 'JSON.parse inside arrow inside try.block is covered', filename: PARSE_WEBHOOKS_FILE, code: `function f(raw) { try { return [raw].map((r) => JSON.parse(r)); } catch (e) { return []; } }`, }, { name: 'JSON.parse in non-webhook file is skipped', filename: NON_WEBHOOK_FILE, code: `function f(raw) { return JSON.parse(raw); }`, }, { name: 'webhookHelper.ts with try-wrapped JSON.parse', filename: WEBHOOK_HELPER_FILE, code: `function parsePayload(raw) { try { return JSON.parse(raw); } catch (e) { logger.warn('x'); return null; } }`, }, { name: 'webhooksHelper.ts with try-wrapped JSON.parse', filename: WEBHOOKS_HELPER_FILE, code: `export function parse(s) { try { return JSON.parse(s); } catch { return undefined; } }`, }, { name: 'no JSON.parse at all in webhook file', filename: WEBHOOK_FILE, code: `function f(body) { return body.id; }`, }, { name: 'JSON.stringify is not flagged', filename: WEBHOOK_FILE, code: `function f(body) { return JSON.stringify(body); }`, }, { name: 'unrelated parse method on non-JSON object is not flagged', filename: WEBHOOK_FILE, code: `function f(s) { return Date.parse(s); }`, }, ], invalid: [ { name: 'bare JSON.parse in webhook.ts', filename: WEBHOOK_FILE, code: `function f(raw) { return JSON.parse(raw); }`, errors: [{ messageId: 'missingTry' }], }, { name: 'bare JSON.parse in webhooks.ts', filename: WEBHOOKS_FILE, code: `function f(raw) { const body = JSON.parse(raw); return body; }`, errors: [{ messageId: 'missingTry' }], }, { name: 'bare JSON.parse in parseWebhooks.ts', filename: PARSE_WEBHOOKS_FILE, code: `export function parse(raw) { return JSON.parse(raw); }`, errors: [{ messageId: 'missingTry' }], }, { name: 'bare JSON.parse in webhookHelper.ts', filename: WEBHOOK_HELPER_FILE, code: `function parsePayload(raw) { const data = JSON.parse(raw); return data; }`, errors: [{ messageId: 'missingTry' }], }, { name: 'bare JSON.parse in webhooksHelper.ts', filename: WEBHOOKS_HELPER_FILE, code: `export function parse(s) { return JSON.parse(s); }`, errors: [{ messageId: 'missingTry' }], }, { name: 'JSON.parse inside arrow function nested in webhook function — function boundary crosses before try', filename: WEBHOOK_FILE, code: `function f(items) { return items.map((r) => JSON.parse(r)); }`, errors: [{ messageId: 'missingTry' }], }, { name: 'JSON.parse before an unrelated try — try does not cover the parse', filename: WEBHOOK_FILE, code: `function f(raw) { const x = JSON.parse(raw); try { doIt(); } catch (e) { log(e); } return x; }`, errors: [{ messageId: 'missingTry' }], }, { name: 'JSON.parse at module top-level (no enclosing function or try)', filename: WEBHOOK_FILE, code: `const config = JSON.parse(process.env.CFG);`, errors: [{ messageId: 'missingTry' }], }, { name: 'bracket-access JSON["parse"](raw) is also flagged', filename: WEBHOOK_FILE, code: `function f(raw) { return JSON['parse'](raw); }`, errors: [{ messageId: 'missingTry' }], }, ], }); }); test('webhook-json-parse-must-have-try — handler-type AST detection', () => { tsRuleTester.run('webhook-json-parse-must-have-try', rule, { valid: [ { name: 'ParseWebhooksHandler in non-webhook file with try-wrapped JSON.parse', filename: PARSE_EVENTS_FILE, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseEvents: ParseWebhooksHandler = async (ctx) => { try { return JSON.parse(ctx.body.payload); } catch (e) { return []; } }; `, }, { name: 'AcknowledgeWebhooksHandler in non-webhook file with try-wrapped JSON.parse', filename: EVENT_DISPATCHER_FILE, code: ` import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk'; export const ack: AcknowledgeWebhooksHandler = async (ctx) => { try { const p = JSON.parse(ctx.body.payload); return { statusCode: 200 }; } catch { return { statusCode: 400 }; } }; `, }, { name: 'non-handler-typed function in non-webhook file with bare JSON.parse is skipped', filename: NON_WEBHOOK_FILE, code: ` export const handler = async (ctx: { body: { payload: string } }) => { return JSON.parse(ctx.body.payload); }; `, }, { name: 'unrelated typed export in non-webhook file with bare JSON.parse is skipped', filename: NON_WEBHOOK_FILE, code: ` type SomeOtherHandler = (s: string) => unknown; export const handler: SomeOtherHandler = (s) => JSON.parse(s); `, }, ], invalid: [ { name: 'ParseWebhooksHandler in non-webhook file with bare JSON.parse — flagged via AST signal', filename: PARSE_EVENTS_FILE, code: ` import { ParseWebhooksHandler } from '@unito/integration-sdk'; export const parseEvents: ParseWebhooksHandler = async (ctx) => { const body = JSON.parse(ctx.body.payload); return body; }; `, errors: [{ messageId: 'missingTry' }], }, { name: 'AcknowledgeWebhooksHandler in non-webhook file with bare JSON.parse — flagged via AST signal', filename: EVENT_DISPATCHER_FILE, code: ` import { AcknowledgeWebhooksHandler } from '@unito/integration-sdk'; export const ack: AcknowledgeWebhooksHandler = async (ctx) => { const p = JSON.parse(ctx.body.payload); return { statusCode: 200, payload: JSON.stringify(p) }; }; `, errors: [{ messageId: 'missingTry' }], }, { name: 'singular ParseWebhookHandler variant also flagged', filename: PARSE_EVENTS_FILE, code: ` import { ParseWebhookHandler } from '@unito/integration-sdk'; export const parseEvent: ParseWebhookHandler = async (ctx) => JSON.parse(ctx.body.payload); `, errors: [{ messageId: 'missingTry' }], }, ], }); });