import { RuleTester } from 'eslint'; import test from 'node:test'; import rule from '../../src/eslint/rules/no-credentials-spread-in-cache-key.js'; const ruleTester = new RuleTester({ languageOptions: { ecmaVersion: 2022, sourceType: 'module', }, }); const CACHE_FILENAME = '/repo/integrations/foo/src/cache.ts'; const NON_CACHE_FILENAME = '/repo/integrations/foo/src/credentials.ts'; const wrap = (body: string): string => `function buildKey(params, credentials, creds) { ${body} }`; test('no-credentials-spread-in-cache-key', () => { ruleTester.run('no-credentials-spread-in-cache-key', rule, { valid: [ { name: 'cache.ts using stable identifier instead of spread', filename: CACHE_FILENAME, code: wrap(` const userIdentifier = credentials.unitoCredentialId ?? credentials.accessToken; return crc32(JSON.stringify({ ...params, userIdentifier })); `), }, { name: 'cache.ts referencing credentials field directly (no spread)', filename: CACHE_FILENAME, code: wrap(` return crc32(JSON.stringify({ ...params, id: credentials.unitoCredentialId })); `), }, { name: 'cache.ts spreading params is fine', filename: CACHE_FILENAME, code: wrap(` return crc32(JSON.stringify({ ...params })); `), }, { name: 'cache.ts spreading non-credential identifier', filename: CACHE_FILENAME, code: wrap(` const opts = { foo: 1 }; return crc32(JSON.stringify({ ...params, ...opts })); `), }, { name: 'credentials.ts spreading credentials (legitimate enrichment)', filename: NON_CACHE_FILENAME, code: wrap(` return { ...credentials, extra: 'data' }; `), }, { name: 'helpers/credentials.ts file (rule does not target it)', filename: '/repo/integrations/foo/src/helpers/credentials.ts', code: wrap(` return { ...credentials, ...params }; `), }, { name: 'cache.ts spreading credentials member (not the whole object)', filename: CACHE_FILENAME, code: wrap(` return crc32(JSON.stringify({ ...params, ...credentials.publicMetadata })); `), }, ], invalid: [ { name: 'cache.ts spreading credentials into JSON.stringify', filename: CACHE_FILENAME, code: wrap(` return crc32(JSON.stringify({ ...params, ...credentials })).toString(16); `), errors: [{ messageId: 'credentialsSpread', data: { name: 'credentials' } }], }, { name: 'cache.ts spreading credentials directly inside hash call', filename: CACHE_FILENAME, code: wrap(` return crc.crc32(JSON.stringify({ ...credentials })).toString(16); `), errors: [{ messageId: 'credentialsSpread', data: { name: 'credentials' } }], }, { name: 'cache.ts spreading creds (alias) in cache key', filename: CACHE_FILENAME, code: wrap(` return crc32(JSON.stringify({ ...params, ...creds })).toString(16); `), errors: [{ messageId: 'credentialsSpread', data: { name: 'creds' } }], }, { name: 'cache.ts spreading credentials in helper-built object literal', filename: CACHE_FILENAME, code: wrap(` const input = { prefix, ...credentials, ...params }; return hash(input); `), errors: [{ messageId: 'credentialsSpread', data: { name: 'credentials' } }], }, { name: 'cache.ts spreading credentials in non-hashing path is still flagged (rule is a hard rule)', filename: CACHE_FILENAME, code: wrap(` const debug = { ...credentials }; return debug; `), errors: [{ messageId: 'credentialsSpread', data: { name: 'credentials' } }], }, { name: 'cache.ts spreading credentials as call-arg is also flagged', filename: CACHE_FILENAME, code: wrap(` return hash(...credentials); `), errors: [{ messageId: 'credentialsSpread', data: { name: 'credentials' } }], }, { name: 'cache.ts spreading singular alias `cred`', filename: CACHE_FILENAME, code: `function buildKey(params, cred) { return crc32(JSON.stringify({ ...params, ...cred })).toString(16); }`, errors: [{ messageId: 'credentialsSpread', data: { name: 'cred' } }], }, ], }); });