@unito/integration-sdk
Version:
Integration SDK
158 lines (154 loc) • 7.96 kB
text/typescript
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' }],
},
],
});
});