import { RuleTester } from '@typescript-eslint/rule-tester'; import { CredentialPasswordFieldRule } from './credential-password-field.js'; const ruleTester = new RuleTester(); function createCredentialCode(properties: string[]): string { return ` import type { ICredentialType, INodeProperties } from 'n8n-workflow'; export class TestCredential implements ICredentialType { name = 'testApi'; displayName = 'Test API'; properties: INodeProperties[] = [ ${properties.map((prop) => `\t\t${prop}`).join(',\n')}, ]; }`; } function createProperty( displayName: string, name: string, options: { password?: boolean; emptyTypeOptions?: boolean } = {}, ): string { let typeOptionsStr = ''; if (options.emptyTypeOptions) { typeOptionsStr = '\n\t\t\ttypeOptions: {},'; } else if (options.password !== undefined) { typeOptionsStr = `\n\t\t\ttypeOptions: { password: ${options.password} },`; } return `{ displayName: '${displayName}', name: '${name}', type: 'string', default: '',${typeOptionsStr} }`; } function createOAuth2CredentialCode(hasPasswordProtection: boolean = true): string { const passwordOptions = hasPasswordProtection ? '\n\t\t\ttypeOptions: { password: true },' : ''; return ` import type { ICredentialType, INodeProperties } from 'n8n-workflow'; export class GithubOAuth2Api implements ICredentialType { name = 'githubOAuth2Api'; extends = ['oAuth2Api']; displayName = 'GitHub OAuth2 API'; properties: INodeProperties[] = [ { displayName: 'Access Token URL', name: 'accessTokenUrl', type: 'hidden', default: 'https://github.com/login/oauth/access_token', }, { displayName: 'Client Secret', name: 'clientSecret', type: 'string', default: '',${passwordOptions} }, ]; }`; } // Helper function to create a regular (non-credential) class function createRegularClass(): string { return ` export class RegularClass { properties = [ { name: 'password', type: 'string', }, ]; }`; } ruleTester.run('credential-password-field', CredentialPasswordFieldRule, { valid: [ { name: 'correct usage with password field having typeOptions.password = true', code: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]), }, { name: 'field name is not sensitive', code: createCredentialCode([createProperty('Base URL', 'baseUrl')]), }, { name: 'multiple sensitive fields with correct typeOptions', code: createCredentialCode([ createProperty('Password', 'password', { password: true }), createProperty('Secret Token', 'secretToken', { password: true }), ]), }, { name: 'class does not implement ICredentialType', code: createRegularClass(), }, { name: 'OAuth2 credential with URL fields and proper client secret protection', code: createOAuth2CredentialCode(true), }, { name: 'public key fields should not be flagged as sensitive', code: createCredentialCode([ createProperty('Public Key', 'publicKey'), createProperty('Client ID', 'clientId'), ]), }, { name: 'certificates should be flagged as sensitive (when properly configured)', code: createCredentialCode([ createProperty('Certificate', 'certificate', { password: true }), createProperty('CA Certificate', 'caCert', { password: true }), createProperty('Client Certificate', 'clientCert', { password: true }), ]), }, { name: 'URL fields containing sensitive keywords should not be flagged', code: createCredentialCode([ createProperty('Token URL', 'tokenUrl'), createProperty('Authorization URL', 'authorizationUrl'), createProperty('Access Token URL', 'accessTokenUrl'), ]), }, { name: 'ID fields containing sensitive keywords should not be flagged', code: createCredentialCode([ createProperty('Access Key ID', 'accessKeyId'), createProperty('Key ID', 'keyId'), createProperty('User ID', 'userId'), ]), }, { name: 'file path and name fields should not be flagged', code: createCredentialCode([ createProperty('Key File Path', 'keyPath'), createProperty('Key File Name', 'keyFile'), createProperty('Certificate Path', 'keyName'), ]), }, { name: 'should flag actual sensitive fields while ignoring false positives', code: createCredentialCode([ createProperty('Private Key', 'privateKey', { password: true }), createProperty('Public Key', 'publicKey'), // Should not be flagged createProperty('Secret Token', 'secretToken', { password: true }), createProperty('Client ID', 'clientId'), // Should not be flagged ]), }, ], invalid: [ { name: 'password field missing typeOptions.password = true', code: createCredentialCode([createProperty('Password', 'password')]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'password' } }], output: createCredentialCode([createProperty('Password', 'password', { password: true })]), }, { name: 'API key field missing typeOptions.password = true', code: createCredentialCode([createProperty('API Key', 'apiKey')]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'apiKey' } }], output: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]), }, { name: 'access key field missing typeOptions.password = true', code: createCredentialCode([createProperty('Access Key', 'accessKey')]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'accessKey' } }], output: createCredentialCode([createProperty('Access Key', 'accessKey', { password: true })]), }, { name: 'secret field missing typeOptions.password = true', code: createCredentialCode([createProperty('Client Secret', 'clientSecret')]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'clientSecret' } }], output: createCredentialCode([ createProperty('Client Secret', 'clientSecret', { password: true }), ]), }, { name: 'multiple invalid fields', code: createCredentialCode([ createProperty('Password', 'password'), createProperty('Username', 'username'), createProperty('Access Token', 'accessToken'), ]), errors: [ { messageId: 'missingPasswordOption', data: { fieldName: 'password' } }, { messageId: 'missingPasswordOption', data: { fieldName: 'accessToken' } }, ], output: createCredentialCode([ createProperty('Password', 'password', { password: true }), createProperty('Username', 'username'), createProperty('Access Token', 'accessToken', { password: true }), ]), }, { name: 'field has typeOptions but password is false', code: createCredentialCode([createProperty('API Key', 'apiKey', { password: false })]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'apiKey' } }], output: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]), }, { name: 'OAuth2 credential with missing password protection for clientSecret', code: createOAuth2CredentialCode(false), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'clientSecret' } }], output: createOAuth2CredentialCode(true), }, { name: 'field has empty typeOptions object', code: createCredentialCode([ createProperty('Access Token', 'accessToken', { emptyTypeOptions: true }), ]), errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'accessToken' } }], output: createCredentialCode([ createProperty('Access Token', 'accessToken', { password: true }), ]), }, { name: 'certificate fields should require password protection', code: createCredentialCode([ createProperty('Certificate', 'certificate'), createProperty('Client Certificate', 'clientCert'), ]), errors: [ { messageId: 'missingPasswordOption', data: { fieldName: 'certificate' } }, { messageId: 'missingPasswordOption', data: { fieldName: 'clientCert' } }, ], output: createCredentialCode([ createProperty('Certificate', 'certificate', { password: true }), createProperty('Client Certificate', 'clientCert', { password: true }), ]), }, ], });