import { RuleTester } from 'eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/no-error-message-rethrow-to-caller.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const HANDLER_FILE = '/repo/integrations/foo/src/handlers/record.ts'; const ROUTE_FILE = '/repo/integrations/foo/src/routes/comment.ts'; const RATE_LIMITER_FILE = '/repo/integrations/foo/src/handlers/rateLimiter.ts'; const NON_HANDLER_FILE = '/repo/integrations/foo/src/helpers/utils.ts'; const wrap = (body: string): string => `async function f() { ${body} }`; test('no-error-message-rethrow-to-caller', () => { ruleTester.run('no-error-message-rethrow-to-caller', rule, { valid: [ { name: 'static message in HttpErrors throw', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.BadRequestError('Upload failed'); } `), }, { name: 'template literal with non-error context', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.NotFoundError(\`item \${id} not found\`); } `), }, { name: 'commit.message is not error context (different identifier)', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.UnprocessableEntityError(commit.message); } `), }, { name: 'error.status (not message) is fine', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.X(\`status: \${error.status}\`); } `), }, { name: 'logger.warn with error.message is fine — logger has its own redaction', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { context.logger.warn('failed', { error: error.message }); throw new HttpErrors.UnprocessableEntityError('Upload failed'); } `), }, { name: 'helpers/ file is out of scope (not handler/route)', filename: NON_HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.BadRequestError(error.message); } `), }, { name: 'rateLimiter.ts under handlers/ is allowlisted', filename: RATE_LIMITER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.RateLimitExceededError(error.message); } `), }, { name: 'throw outside catch is out of scope', filename: HANDLER_FILE, code: wrap(` if (cond) { throw new HttpErrors.BadRequestError(error.message); } `), }, { name: 'throw inside catch but not HttpErrors', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new Error(error.message); } `), }, { name: 'nested function inside catch — function boundary stops the scope walk', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { const handler = () => { throw new HttpErrors.BadRequestError(error.message); }; handler(); } `), }, ], invalid: [ { name: 'direct error.message rethrow', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.UnprocessableEntityError(error.message); } `), errors: [{ messageId: 'errorMessageRethrow' }], }, { name: 'template literal interpolating err.message', filename: HANDLER_FILE, code: 'async function f() { try { foo(); } catch (err) { throw new HttpErrors.BadRequestError(`upload failed: ${err.message}`); } }', errors: [{ messageId: 'errorMessageRethrow' }], }, { name: 'conditional ternary with error.message branch', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.UnprocessableEntityError(error instanceof Error ? error.message : String(error)); } `), errors: [{ messageId: 'errorMessageRethrow' }], }, { name: 'mixed template w/ both status and message — message wins', filename: HANDLER_FILE, code: 'async function f() { try { foo(); } catch (error) { throw new HttpErrors.X(`status: ${error.status}, msg: ${error.message}`); } }', errors: [{ messageId: 'errorMessageRethrow' }], }, { name: 'route file flagged the same as handler', filename: ROUTE_FILE, code: wrap(` try { foo(); } catch (e) { throw new HttpErrors.BadRequestError(\`failed: \${e.message}\`); } `), errors: [{ messageId: 'errorMessageRethrow' }], }, { name: 'bracket-access error["message"] is also flagged', filename: HANDLER_FILE, code: wrap(` try { foo(); } catch (error) { throw new HttpErrors.BadRequestError(error['message']); } `), errors: [{ messageId: 'errorMessageRethrow' }], }, ], }); });