@n8n/eslint-plugin-community-nodes 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE.md +88 -0
  3. package/LICENSE_EE.md +27 -0
  4. package/dist/plugin.d.ts +61 -0
  5. package/dist/plugin.d.ts.map +1 -0
  6. package/dist/plugin.js +51 -0
  7. package/dist/plugin.js.map +1 -0
  8. package/dist/rules/credential-password-field.d.ts +3 -0
  9. package/dist/rules/credential-password-field.d.ts.map +1 -0
  10. package/dist/rules/credential-password-field.js +97 -0
  11. package/dist/rules/credential-password-field.js.map +1 -0
  12. package/dist/rules/credential-test-required.d.ts +3 -0
  13. package/dist/rules/credential-test-required.d.ts.map +1 -0
  14. package/dist/rules/credential-test-required.js +79 -0
  15. package/dist/rules/credential-test-required.js.map +1 -0
  16. package/dist/rules/icon-validation.d.ts +3 -0
  17. package/dist/rules/icon-validation.d.ts.map +1 -0
  18. package/dist/rules/icon-validation.js +131 -0
  19. package/dist/rules/icon-validation.js.map +1 -0
  20. package/dist/rules/index.d.ts +13 -0
  21. package/dist/rules/index.d.ts.map +1 -0
  22. package/dist/rules/index.js +23 -0
  23. package/dist/rules/index.js.map +1 -0
  24. package/dist/rules/no-credential-reuse.d.ts +3 -0
  25. package/dist/rules/no-credential-reuse.d.ts.map +1 -0
  26. package/dist/rules/no-credential-reuse.js +62 -0
  27. package/dist/rules/no-credential-reuse.js.map +1 -0
  28. package/dist/rules/no-deprecated-workflow-functions.d.ts +3 -0
  29. package/dist/rules/no-deprecated-workflow-functions.d.ts.map +1 -0
  30. package/dist/rules/no-deprecated-workflow-functions.js +144 -0
  31. package/dist/rules/no-deprecated-workflow-functions.js.map +1 -0
  32. package/dist/rules/no-restricted-globals.d.ts +3 -0
  33. package/dist/rules/no-restricted-globals.d.ts.map +1 -0
  34. package/dist/rules/no-restricted-globals.js +58 -0
  35. package/dist/rules/no-restricted-globals.js.map +1 -0
  36. package/dist/rules/no-restricted-imports.d.ts +3 -0
  37. package/dist/rules/no-restricted-imports.d.ts.map +1 -0
  38. package/dist/rules/no-restricted-imports.js +80 -0
  39. package/dist/rules/no-restricted-imports.js.map +1 -0
  40. package/dist/rules/node-usable-as-tool.d.ts +3 -0
  41. package/dist/rules/node-usable-as-tool.d.ts.map +1 -0
  42. package/dist/rules/node-usable-as-tool.js +57 -0
  43. package/dist/rules/node-usable-as-tool.js.map +1 -0
  44. package/dist/rules/package-name-convention.d.ts +3 -0
  45. package/dist/rules/package-name-convention.d.ts.map +1 -0
  46. package/dist/rules/package-name-convention.js +52 -0
  47. package/dist/rules/package-name-convention.js.map +1 -0
  48. package/dist/rules/resource-operation-pattern.d.ts +3 -0
  49. package/dist/rules/resource-operation-pattern.d.ts.map +1 -0
  50. package/dist/rules/resource-operation-pattern.js +77 -0
  51. package/dist/rules/resource-operation-pattern.js.map +1 -0
  52. package/dist/utils/ast-utils.d.ts +25 -0
  53. package/dist/utils/ast-utils.d.ts.map +1 -0
  54. package/dist/utils/ast-utils.js +117 -0
  55. package/dist/utils/ast-utils.js.map +1 -0
  56. package/dist/utils/file-utils.d.ts +12 -0
  57. package/dist/utils/file-utils.d.ts.map +1 -0
  58. package/dist/utils/file-utils.js +154 -0
  59. package/dist/utils/file-utils.js.map +1 -0
  60. package/dist/utils/index.d.ts +3 -0
  61. package/dist/utils/index.d.ts.map +1 -0
  62. package/dist/utils/index.js +3 -0
  63. package/dist/utils/index.js.map +1 -0
  64. package/package.json +46 -0
  65. package/src/plugin.ts +55 -0
  66. package/src/rules/credential-password-field.test.ts +231 -0
  67. package/src/rules/credential-password-field.ts +123 -0
  68. package/src/rules/credential-test-required.test.ts +147 -0
  69. package/src/rules/credential-test-required.ts +99 -0
  70. package/src/rules/icon-validation.test.ts +196 -0
  71. package/src/rules/icon-validation.ts +158 -0
  72. package/src/rules/index.ts +24 -0
  73. package/src/rules/no-credential-reuse.test.ts +226 -0
  74. package/src/rules/no-credential-reuse.ts +81 -0
  75. package/src/rules/no-deprecated-workflow-functions.test.ts +117 -0
  76. package/src/rules/no-deprecated-workflow-functions.ts +166 -0
  77. package/src/rules/no-restricted-globals.test.ts +135 -0
  78. package/src/rules/no-restricted-globals.ts +71 -0
  79. package/src/rules/no-restricted-imports.test.ts +181 -0
  80. package/src/rules/no-restricted-imports.ts +86 -0
  81. package/src/rules/node-usable-as-tool.test.ts +80 -0
  82. package/src/rules/node-usable-as-tool.ts +70 -0
  83. package/src/rules/package-name-convention.test.ts +112 -0
  84. package/src/rules/package-name-convention.ts +63 -0
  85. package/src/rules/resource-operation-pattern.test.ts +216 -0
  86. package/src/rules/resource-operation-pattern.ts +97 -0
  87. package/src/utils/ast-utils.ts +179 -0
  88. package/src/utils/file-utils.ts +204 -0
  89. package/src/utils/index.ts +2 -0
  90. package/tsconfig.json +11 -0
  91. package/tsconfig.tsbuildinfo +1 -0
  92. package/vite.config.ts +4 -0
@@ -0,0 +1,231 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { CredentialPasswordFieldRule } from './credential-password-field.js';
3
+
4
+ const ruleTester = new RuleTester();
5
+
6
+ function createCredentialCode(properties: string[]): string {
7
+ return `
8
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
9
+
10
+ export class TestCredential implements ICredentialType {
11
+ name = 'testApi';
12
+ displayName = 'Test API';
13
+
14
+ properties: INodeProperties[] = [
15
+ ${properties.map((prop) => `\t\t${prop}`).join(',\n')},
16
+ ];
17
+ }`;
18
+ }
19
+
20
+ function createProperty(
21
+ displayName: string,
22
+ name: string,
23
+ options: { password?: boolean; emptyTypeOptions?: boolean } = {},
24
+ ): string {
25
+ let typeOptionsStr = '';
26
+ if (options.emptyTypeOptions) {
27
+ typeOptionsStr = '\n\t\t\ttypeOptions: {},';
28
+ } else if (options.password !== undefined) {
29
+ typeOptionsStr = `\n\t\t\ttypeOptions: { password: ${options.password} },`;
30
+ }
31
+
32
+ return `{
33
+ displayName: '${displayName}',
34
+ name: '${name}',
35
+ type: 'string',
36
+ default: '',${typeOptionsStr}
37
+ }`;
38
+ }
39
+
40
+ function createOAuth2CredentialCode(hasPasswordProtection: boolean = true): string {
41
+ const passwordOptions = hasPasswordProtection ? '\n\t\t\ttypeOptions: { password: true },' : '';
42
+
43
+ return `
44
+ import type { ICredentialType, INodeProperties } from 'n8n-workflow';
45
+
46
+ export class GithubOAuth2Api implements ICredentialType {
47
+ name = 'githubOAuth2Api';
48
+ extends = ['oAuth2Api'];
49
+ displayName = 'GitHub OAuth2 API';
50
+
51
+ properties: INodeProperties[] = [
52
+ {
53
+ displayName: 'Access Token URL',
54
+ name: 'accessTokenUrl',
55
+ type: 'hidden',
56
+ default: 'https://github.com/login/oauth/access_token',
57
+ },
58
+ {
59
+ displayName: 'Client Secret',
60
+ name: 'clientSecret',
61
+ type: 'string',
62
+ default: '',${passwordOptions}
63
+ },
64
+ ];
65
+ }`;
66
+ }
67
+
68
+ // Helper function to create a regular (non-credential) class
69
+ function createRegularClass(): string {
70
+ return `
71
+ export class RegularClass {
72
+ properties = [
73
+ {
74
+ name: 'password',
75
+ type: 'string',
76
+ },
77
+ ];
78
+ }`;
79
+ }
80
+
81
+ ruleTester.run('credential-password-field', CredentialPasswordFieldRule, {
82
+ valid: [
83
+ {
84
+ name: 'correct usage with password field having typeOptions.password = true',
85
+ code: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]),
86
+ },
87
+ {
88
+ name: 'field name is not sensitive',
89
+ code: createCredentialCode([createProperty('Base URL', 'baseUrl')]),
90
+ },
91
+ {
92
+ name: 'multiple sensitive fields with correct typeOptions',
93
+ code: createCredentialCode([
94
+ createProperty('Password', 'password', { password: true }),
95
+ createProperty('Secret Token', 'secretToken', { password: true }),
96
+ ]),
97
+ },
98
+ {
99
+ name: 'class does not implement ICredentialType',
100
+ code: createRegularClass(),
101
+ },
102
+ {
103
+ name: 'OAuth2 credential with URL fields and proper client secret protection',
104
+ code: createOAuth2CredentialCode(true),
105
+ },
106
+ {
107
+ name: 'public key fields should not be flagged as sensitive',
108
+ code: createCredentialCode([
109
+ createProperty('Public Key', 'publicKey'),
110
+ createProperty('Client ID', 'clientId'),
111
+ ]),
112
+ },
113
+ {
114
+ name: 'certificates should be flagged as sensitive (when properly configured)',
115
+ code: createCredentialCode([
116
+ createProperty('Certificate', 'certificate', { password: true }),
117
+ createProperty('CA Certificate', 'caCert', { password: true }),
118
+ createProperty('Client Certificate', 'clientCert', { password: true }),
119
+ ]),
120
+ },
121
+ {
122
+ name: 'URL fields containing sensitive keywords should not be flagged',
123
+ code: createCredentialCode([
124
+ createProperty('Token URL', 'tokenUrl'),
125
+ createProperty('Authorization URL', 'authorizationUrl'),
126
+ createProperty('Access Token URL', 'accessTokenUrl'),
127
+ ]),
128
+ },
129
+ {
130
+ name: 'ID fields containing sensitive keywords should not be flagged',
131
+ code: createCredentialCode([
132
+ createProperty('Access Key ID', 'accessKeyId'),
133
+ createProperty('Key ID', 'keyId'),
134
+ createProperty('User ID', 'userId'),
135
+ ]),
136
+ },
137
+ {
138
+ name: 'file path and name fields should not be flagged',
139
+ code: createCredentialCode([
140
+ createProperty('Key File Path', 'keyPath'),
141
+ createProperty('Key File Name', 'keyFile'),
142
+ createProperty('Certificate Path', 'keyName'),
143
+ ]),
144
+ },
145
+ {
146
+ name: 'should flag actual sensitive fields while ignoring false positives',
147
+ code: createCredentialCode([
148
+ createProperty('Private Key', 'privateKey', { password: true }),
149
+ createProperty('Public Key', 'publicKey'), // Should not be flagged
150
+ createProperty('Secret Token', 'secretToken', { password: true }),
151
+ createProperty('Client ID', 'clientId'), // Should not be flagged
152
+ ]),
153
+ },
154
+ ],
155
+ invalid: [
156
+ {
157
+ name: 'password field missing typeOptions.password = true',
158
+ code: createCredentialCode([createProperty('Password', 'password')]),
159
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'password' } }],
160
+ output: createCredentialCode([createProperty('Password', 'password', { password: true })]),
161
+ },
162
+ {
163
+ name: 'API key field missing typeOptions.password = true',
164
+ code: createCredentialCode([createProperty('API Key', 'apiKey')]),
165
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'apiKey' } }],
166
+ output: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]),
167
+ },
168
+ {
169
+ name: 'secret field missing typeOptions.password = true',
170
+ code: createCredentialCode([createProperty('Client Secret', 'clientSecret')]),
171
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'clientSecret' } }],
172
+ output: createCredentialCode([
173
+ createProperty('Client Secret', 'clientSecret', { password: true }),
174
+ ]),
175
+ },
176
+ {
177
+ name: 'multiple invalid fields',
178
+ code: createCredentialCode([
179
+ createProperty('Password', 'password'),
180
+ createProperty('Username', 'username'),
181
+ createProperty('Access Token', 'accessToken'),
182
+ ]),
183
+ errors: [
184
+ { messageId: 'missingPasswordOption', data: { fieldName: 'password' } },
185
+ { messageId: 'missingPasswordOption', data: { fieldName: 'accessToken' } },
186
+ ],
187
+ output: createCredentialCode([
188
+ createProperty('Password', 'password', { password: true }),
189
+ createProperty('Username', 'username'),
190
+ createProperty('Access Token', 'accessToken', { password: true }),
191
+ ]),
192
+ },
193
+ {
194
+ name: 'field has typeOptions but password is false',
195
+ code: createCredentialCode([createProperty('API Key', 'apiKey', { password: false })]),
196
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'apiKey' } }],
197
+ output: createCredentialCode([createProperty('API Key', 'apiKey', { password: true })]),
198
+ },
199
+ {
200
+ name: 'OAuth2 credential with missing password protection for clientSecret',
201
+ code: createOAuth2CredentialCode(false),
202
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'clientSecret' } }],
203
+ output: createOAuth2CredentialCode(true),
204
+ },
205
+ {
206
+ name: 'field has empty typeOptions object',
207
+ code: createCredentialCode([
208
+ createProperty('Access Token', 'accessToken', { emptyTypeOptions: true }),
209
+ ]),
210
+ errors: [{ messageId: 'missingPasswordOption', data: { fieldName: 'accessToken' } }],
211
+ output: createCredentialCode([
212
+ createProperty('Access Token', 'accessToken', { password: true }),
213
+ ]),
214
+ },
215
+ {
216
+ name: 'certificate fields should require password protection',
217
+ code: createCredentialCode([
218
+ createProperty('Certificate', 'certificate'),
219
+ createProperty('Client Certificate', 'clientCert'),
220
+ ]),
221
+ errors: [
222
+ { messageId: 'missingPasswordOption', data: { fieldName: 'certificate' } },
223
+ { messageId: 'missingPasswordOption', data: { fieldName: 'clientCert' } },
224
+ ],
225
+ output: createCredentialCode([
226
+ createProperty('Certificate', 'certificate', { password: true }),
227
+ createProperty('Client Certificate', 'clientCert', { password: true }),
228
+ ]),
229
+ },
230
+ ],
231
+ });
@@ -0,0 +1,123 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import {
3
+ isCredentialTypeClass,
4
+ findClassProperty,
5
+ findObjectProperty,
6
+ getStringLiteralValue,
7
+ getBooleanLiteralValue,
8
+ } from '../utils/index.js';
9
+
10
+ const SENSITIVE_PATTERNS = [
11
+ 'password',
12
+ 'secret',
13
+ 'token',
14
+ 'cert',
15
+ 'passphrase',
16
+ 'apikey',
17
+ 'secretkey',
18
+ 'privatekey',
19
+ 'authkey',
20
+ ];
21
+
22
+ const NON_SENSITIVE_PATTERNS = ['url', 'pub', 'id'];
23
+
24
+ function isSensitiveFieldName(name: string): boolean {
25
+ const lowerName = name.toLowerCase();
26
+
27
+ if (NON_SENSITIVE_PATTERNS.some((pattern) => lowerName.includes(pattern))) {
28
+ return false;
29
+ }
30
+
31
+ return SENSITIVE_PATTERNS.some((pattern) => lowerName.includes(pattern));
32
+ }
33
+
34
+ function hasPasswordTypeOption(element: any): boolean {
35
+ const typeOptionsProperty = findObjectProperty(element, 'typeOptions');
36
+
37
+ if (typeOptionsProperty?.value?.type !== 'ObjectExpression') {
38
+ return false;
39
+ }
40
+
41
+ const passwordProperty = findObjectProperty(typeOptionsProperty.value, 'password');
42
+ const passwordValue = passwordProperty ? getBooleanLiteralValue(passwordProperty.value) : null;
43
+
44
+ return passwordValue === true;
45
+ }
46
+
47
+ function createPasswordFix(element: any, typeOptionsProperty: any) {
48
+ return function (fixer: any) {
49
+ if (typeOptionsProperty?.value?.type === 'ObjectExpression') {
50
+ const passwordProperty = findObjectProperty(typeOptionsProperty.value, 'password');
51
+
52
+ if (passwordProperty) {
53
+ return fixer.replaceText(passwordProperty.value, 'true');
54
+ }
55
+
56
+ if (typeOptionsProperty.value.properties.length > 0) {
57
+ const lastProperty =
58
+ typeOptionsProperty.value.properties[typeOptionsProperty.value.properties.length - 1];
59
+ return lastProperty ? fixer.insertTextAfter(lastProperty, ', password: true') : null;
60
+ } else {
61
+ const openBrace = typeOptionsProperty.value.range![0] + 1;
62
+ return fixer.insertTextAfterRange([openBrace, openBrace], ' password: true ');
63
+ }
64
+ }
65
+
66
+ const lastProperty = element.properties[element.properties.length - 1];
67
+ return fixer.insertTextAfter(lastProperty, ',\n\t\t\ttypeOptions: { password: true }');
68
+ };
69
+ }
70
+
71
+ export const CredentialPasswordFieldRule = ESLintUtils.RuleCreator.withoutDocs({
72
+ meta: {
73
+ type: 'problem',
74
+ docs: {
75
+ description: 'Ensure credential fields with sensitive names have typeOptions.password = true',
76
+ },
77
+ messages: {
78
+ missingPasswordOption:
79
+ "Field '{{ fieldName }}' appears to be a sensitive field but is missing 'typeOptions: { password: true }'",
80
+ },
81
+ fixable: 'code',
82
+ schema: [],
83
+ },
84
+ defaultOptions: [],
85
+ create(context) {
86
+ return {
87
+ ClassDeclaration(node) {
88
+ if (!isCredentialTypeClass(node)) {
89
+ return;
90
+ }
91
+
92
+ const propertiesProperty = findClassProperty(node, 'properties');
93
+ if (!propertiesProperty?.value || propertiesProperty.value.type !== 'ArrayExpression') {
94
+ return;
95
+ }
96
+
97
+ for (const element of propertiesProperty.value.elements) {
98
+ if (element?.type !== 'ObjectExpression') {
99
+ continue;
100
+ }
101
+
102
+ const nameProperty = findObjectProperty(element, 'name');
103
+ const fieldName = nameProperty ? getStringLiteralValue(nameProperty.value) : null;
104
+
105
+ if (!fieldName || !isSensitiveFieldName(fieldName)) {
106
+ continue;
107
+ }
108
+
109
+ if (!hasPasswordTypeOption(element)) {
110
+ const typeOptionsProperty = findObjectProperty(element, 'typeOptions');
111
+
112
+ context.report({
113
+ node: element,
114
+ messageId: 'missingPasswordOption',
115
+ data: { fieldName },
116
+ fix: createPasswordFix(element, typeOptionsProperty),
117
+ });
118
+ }
119
+ }
120
+ },
121
+ };
122
+ },
123
+ });
@@ -0,0 +1,147 @@
1
+ import { RuleTester } from '@typescript-eslint/rule-tester';
2
+ import { CredentialTestRequiredRule } from './credential-test-required.js';
3
+
4
+ const ruleTester = new RuleTester();
5
+
6
+ // Helper function to create credential class code
7
+ function createCredentialCode(options: {
8
+ name?: string;
9
+ displayName?: string;
10
+ hasTest?: boolean;
11
+ extends?: string[];
12
+ extraProperties?: string;
13
+ }): string {
14
+ const {
15
+ name = 'myApi',
16
+ displayName = 'My API',
17
+ hasTest = false,
18
+ extends: extendsArray,
19
+ extraProperties = '',
20
+ } = options;
21
+
22
+ const imports = hasTest
23
+ ? `import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';`
24
+ : `import type { ICredentialType, INodeProperties } from 'n8n-workflow';`;
25
+
26
+ const extendsStr = extendsArray ? `\n\textends = ${JSON.stringify(extendsArray)};` : '';
27
+
28
+ const testProperty = hasTest
29
+ ? `\n\n\ttest: ICredentialTestRequest = {\n\t\trequest: {\n\t\t\tbaseURL: 'https://api.example.com',\n\t\t\turl: '/test',\n\t\t},\n\t};`
30
+ : '';
31
+
32
+ return `
33
+ ${imports}
34
+
35
+ export class ${name.charAt(0).toUpperCase() + name.slice(1)} implements ICredentialType {
36
+ name = '${name}';${extendsStr}
37
+ displayName = '${displayName}';
38
+ properties: INodeProperties[] = [];${testProperty}${extraProperties}
39
+ }`;
40
+ }
41
+
42
+ function createNonCredentialClass(className: string = 'SomeOtherClass'): string {
43
+ return `
44
+ export class ${className} {
45
+ name = 'notACredential';
46
+ }`;
47
+ }
48
+
49
+ function createNodeCode(options: {
50
+ name?: string;
51
+ displayName?: string;
52
+ credentials?: Array<string | { name: string; testedBy?: string }>;
53
+ extendsClass?: string;
54
+ }): string {
55
+ const { name = 'myNode', displayName = 'My Node', credentials = [], extendsClass } = options;
56
+
57
+ const credentialsArray = credentials
58
+ .map((cred) => {
59
+ if (typeof cred === 'string') {
60
+ return `'${cred}'`;
61
+ }
62
+ const testedByStr = cred.testedBy ? `, testedBy: '${cred.testedBy}'` : '';
63
+ return `{ name: '${cred.name}'${testedByStr} }`;
64
+ })
65
+ .join(', ');
66
+
67
+ const classDeclaration = extendsClass
68
+ ? `export class ${name.charAt(0).toUpperCase() + name.slice(1)} extends ${extendsClass}`
69
+ : `export class ${name.charAt(0).toUpperCase() + name.slice(1)} implements INodeType`;
70
+
71
+ const descriptionProperty = extendsClass
72
+ ? `description = {` // Extending classes might not need full INodeTypeDescription
73
+ : `description: INodeTypeDescription = {`;
74
+
75
+ return `
76
+ import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
77
+
78
+ ${classDeclaration} {
79
+ ${descriptionProperty}
80
+ displayName: '${displayName}',
81
+ name: '${name}',
82
+ group: ['transform'],
83
+ version: 1,
84
+ description: 'A test node',
85
+ defaults: {
86
+ name: '${displayName}',
87
+ },
88
+ inputs: ['main'],
89
+ outputs: ['main'],
90
+ credentials: [${credentialsArray}],
91
+ properties: [],
92
+ };
93
+
94
+ execute() {
95
+ return Promise.resolve([]);
96
+ }
97
+ }`;
98
+ }
99
+
100
+ ruleTester.run('credential-test-required', CredentialTestRequiredRule, {
101
+ valid: [
102
+ {
103
+ name: 'credential class with test property',
104
+ filename: 'MyApi.credentials.ts',
105
+ code: createCredentialCode({ hasTest: true }),
106
+ },
107
+ {
108
+ name: 'credential class extending oAuth2Api (exempt)',
109
+ filename: 'MyOAuth2Api.credentials.ts',
110
+ code: createCredentialCode({
111
+ name: 'myOAuth2Api',
112
+ displayName: 'My OAuth2 API',
113
+ extends: ['oAuth2Api'],
114
+ }),
115
+ },
116
+ {
117
+ name: 'non-credential class ignored',
118
+ filename: 'MyApi.credentials.ts',
119
+ code: createNonCredentialClass(),
120
+ },
121
+ {
122
+ name: 'non-credential file ignored',
123
+ filename: 'regular-file.ts',
124
+ code: createCredentialCode({}),
125
+ },
126
+ ],
127
+ invalid: [
128
+ {
129
+ name: 'credential class missing test property and no testedBy in package',
130
+ filename: 'MyApi.credentials.ts',
131
+ code: createCredentialCode({}),
132
+ errors: [{ messageId: 'missingCredentialTest', data: { className: 'MyApi' } }],
133
+ },
134
+ {
135
+ name: 'credential class with extends but not oAuth2Api and no testedBy in package',
136
+ filename: 'MyApi.credentials.ts',
137
+ code: createCredentialCode({ extends: ['someOtherApi'] }),
138
+ errors: [{ messageId: 'missingCredentialTest', data: { className: 'MyApi' } }],
139
+ },
140
+ {
141
+ name: 'credential class with empty extends array and no testedBy in package',
142
+ filename: 'MyApi.credentials.ts',
143
+ code: createCredentialCode({ extends: [] }),
144
+ errors: [{ messageId: 'missingCredentialTest', data: { className: 'MyApi' } }],
145
+ },
146
+ ],
147
+ });
@@ -0,0 +1,99 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ import {
3
+ isCredentialTypeClass,
4
+ findClassProperty,
5
+ hasArrayLiteralValue,
6
+ isFileType,
7
+ getStringLiteralValue,
8
+ findPackageJson,
9
+ areAllCredentialUsagesTestedByNodes,
10
+ } from '../utils/index.js';
11
+ import { dirname } from 'node:path';
12
+
13
+ export const CredentialTestRequiredRule = ESLintUtils.RuleCreator.withoutDocs({
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description: 'Ensure credentials have a credential test',
18
+ },
19
+ messages: {
20
+ missingCredentialTest:
21
+ 'Credential class "{{ className }}" must have a test property or be tested by a node via testedBy',
22
+ },
23
+ schema: [],
24
+ },
25
+ defaultOptions: [],
26
+ create(context) {
27
+ if (!isFileType(context.filename, '.credentials.ts')) {
28
+ return {};
29
+ }
30
+
31
+ let packageDir: string | null = null;
32
+
33
+ const getPackageDir = (): string | null => {
34
+ if (packageDir !== null) {
35
+ return packageDir;
36
+ }
37
+
38
+ const packageJsonPath = findPackageJson(context.filename);
39
+ if (!packageJsonPath) {
40
+ packageDir = '';
41
+ return packageDir;
42
+ }
43
+
44
+ packageDir = dirname(packageJsonPath);
45
+ return packageDir;
46
+ };
47
+
48
+ return {
49
+ ClassDeclaration(node) {
50
+ if (!isCredentialTypeClass(node)) {
51
+ return;
52
+ }
53
+
54
+ const extendsProperty = findClassProperty(node, 'extends');
55
+ if (extendsProperty && hasArrayLiteralValue(extendsProperty, 'oAuth2Api')) {
56
+ return;
57
+ }
58
+
59
+ const testProperty = findClassProperty(node, 'test');
60
+ if (testProperty) {
61
+ return;
62
+ }
63
+
64
+ const nameProperty = findClassProperty(node, 'name');
65
+ if (!nameProperty) {
66
+ return;
67
+ }
68
+
69
+ const credentialName = getStringLiteralValue(nameProperty.value);
70
+ if (!credentialName) {
71
+ return;
72
+ }
73
+
74
+ const pkgDir = getPackageDir();
75
+ if (!pkgDir) {
76
+ context.report({
77
+ node,
78
+ messageId: 'missingCredentialTest',
79
+ data: {
80
+ className: node.id?.name || 'Unknown',
81
+ },
82
+ });
83
+ return;
84
+ }
85
+
86
+ const allUsagesTestedByNodes = areAllCredentialUsagesTestedByNodes(credentialName, pkgDir);
87
+ if (!allUsagesTestedByNodes) {
88
+ context.report({
89
+ node,
90
+ messageId: 'missingCredentialTest',
91
+ data: {
92
+ className: node.id?.name || 'Unknown',
93
+ },
94
+ });
95
+ }
96
+ },
97
+ };
98
+ },
99
+ });