import { RuleTester } from 'eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/bad-request-only-for-input-validation.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const HANDLER_FILE = '/repo/integrations/foo/src/handlers/item.ts'; test('bad-request-only-for-input-validation', () => { ruleTester.run('bad-request-only-for-input-validation', rule, { valid: [ { name: 'typed UnprocessableEntityError in catch is fine', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.UnprocessableEntityError('boom'); } }`, }, { name: 'BadRequestError in provider-catch but message starts with "Missing"', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('Missing required field "title"'); } }`, }, { name: 'BadRequestError in provider-catch but message starts with "Invalid"', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('Invalid email format'); } }`, }, { name: 'BadRequestError in provider-catch but message contains "required"', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('Field "title" is required'); } }`, }, { name: 'BadRequestError in provider-catch but message contains "must be"', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('Status must be one of: open, closed'); } }`, }, { name: 'BadRequestError thrown outside catch and not after provider await', filename: HANDLER_FILE, code: `function f(input) { if (!input) { throw new HttpErrors.BadRequestError('no good'); } }`, }, { name: 'BadRequestError in provider.ts is allowlisted', filename: '/repo/integrations/foo/src/provider.ts', code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('boom'); } }`, }, { name: 'BadRequestError in credentials.ts is allowlisted', filename: '/repo/integrations/foo/src/credentials.ts', code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('bad credentials'); } }`, }, { name: 'BadRequestError in helpers/credentials.ts is allowlisted', filename: '/repo/integrations/foo/src/helpers/credentials.ts', code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('bad credentials'); } }`, }, { name: 'BadRequestError after non-provider await is fine', filename: HANDLER_FILE, code: `async function f() { await db.query(); throw new HttpErrors.BadRequestError('boom'); }`, }, { name: 'plain throw new Error is out of scope', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new Error('boom'); } }`, }, { name: 'BadRequestError in catch wrapping JSON.parse is fine — sync input validation', filename: HANDLER_FILE, code: `function f(raw) { try { return JSON.parse(raw); } catch (e) { throw new HttpErrors.BadRequestError('Could not parse input'); } }`, }, { name: 'BadRequestError in catch wrapping schema.validate is fine — sync input validation', filename: HANDLER_FILE, code: `function f(input) { try { schema.validate(input); } catch (e) { throw new HttpErrors.BadRequestError('Schema mismatch'); } }`, }, { name: 'BadRequestError in catch wrapping a CallExpression on a non-provider helper', filename: HANDLER_FILE, code: `function f(s) { try { return parseInt(s, 10); } catch (e) { throw new HttpErrors.BadRequestError('Could not parse number'); } }`, }, ], invalid: [ { name: 'BadRequestError in catch wrapping `await provider.x()` with non-validation message', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError('Upload failed'); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError in provider-catch with non-literal arg (cannot allowlist)', filename: HANDLER_FILE, code: `async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError(buildMsg(e)); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError immediately after `await provider.foo()`', filename: HANDLER_FILE, code: `async function f() { const r = await provider.fetchItem(); throw new HttpErrors.BadRequestError('something happened'); }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError 2 statements after `await provider.bar()`', filename: HANDLER_FILE, code: `async function f() { await provider.bar(); const x = 1; throw new HttpErrors.BadRequestError('failed'); }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError after `await Provider.thing()` (capitalized)', filename: HANDLER_FILE, code: `async function f() { await Provider.thing(); throw new HttpErrors.BadRequestError('failed'); }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError in provider-catch with template literal — provider message style', filename: HANDLER_FILE, code: 'async function f() { try { await provider.x(); } catch (e) { throw new HttpErrors.BadRequestError(`failed: ${e.message}`); } }', errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError after `await provider.x()` with chained call', filename: HANDLER_FILE, code: `async function f() { await provider.api.fetch(); throw new HttpErrors.BadRequestError('boom'); }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError nested in if-block after provider await in outer block', filename: HANDLER_FILE, code: `async function f(y) { await provider.x(); if (y) { throw new HttpErrors.BadRequestError('boom'); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError in catch where async await is nested in if', filename: HANDLER_FILE, code: `async function f(y) { try { if (y) { await provider.x(); } } catch (e) { throw new HttpErrors.BadRequestError('failed'); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError in catch wrapping `await azureService.foo()` (service-helper)', filename: HANDLER_FILE, code: `async function f() { try { await azureService.listSubscriptions(); } catch (e) { throw new HttpErrors.BadRequestError('Failed to list webhooks'); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, { name: 'BadRequestError in catch wrapping `await db.query()` — async I/O, prefer 422', filename: HANDLER_FILE, code: `async function f() { try { await db.query(); } catch (e) { throw new HttpErrors.BadRequestError('db unavailable'); } }`, errors: [{ messageId: 'badRequestNotInputValidation' }], }, ], }); });