Skip to content

Commit c7f0a3e

Browse files
authored
Merge pull request #1 from NangoHQ/khaliq/nan-1752-add-nango-custom-scripts-linter-repo-and-rules
feat(tests): [nan-1752] add tests
2 parents 423956e + aa311ac commit c7f0a3e

File tree

5 files changed

+289
-332
lines changed

5 files changed

+289
-332
lines changed

.github/workflows/tests.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Run Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
concurrency:
10+
group: tests-${{ github.event.pull_request.number || github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
tests:
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
matrix:
18+
os: [ubuntu-latest]
19+
node-version: [18.x, 20.x]
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
fetch-depth: '0'
25+
26+
- name: Use Node.js ${{ matrix.node-version }}
27+
uses: actions/setup-node@v4
28+
with:
29+
cache: 'npm'
30+
node-version: ${{ matrix.node-version }}
31+
32+
- run: npm ci
33+
- run: npm run test

src/rules/enforce-proxy-configuration-type.test.ts

Lines changed: 44 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -3,111 +3,51 @@ import { describe, it } from 'vitest';
33
import enforceProxyConfigurationType from './enforce-proxy-configuration-type';
44

55
const ruleTester = new RuleTester({
6-
parser: require.resolve('@typescript-eslint/parser'),
7-
parserOptions: {
8-
ecmaVersion: 2018,
9-
sourceType: 'module',
10-
},
6+
parser: require.resolve('@typescript-eslint/parser'),
7+
parserOptions: { ecmaVersion: 2018, sourceType: 'module' },
118
});
129

13-
describe('enforce-proxy-configuration-type', () => {
14-
it('should pass valid cases and fail invalid cases', () => {
15-
ruleTester.run('enforce-proxy-configuration-type', enforceProxyConfigurationType, {
16-
valid: [
17-
{
18-
code: `
19-
const config: ProxyConfiguration = {
20-
endpoint: 'api.xro/2.0/Contacts',
21-
headers: { 'xero-tenant-id': tenant_id },
22-
params: { summarizeErrors: 'false' },
23-
data: { Contacts: input.map(toXeroContact) }
24-
};
25-
const res = await nango.post(config);
26-
`,
27-
},
28-
{
29-
code: `
30-
let config: ProxyConfiguration;
31-
config = {
32-
endpoint: 'api.xro/2.0/Contacts',
33-
headers: { 'xero-tenant-id': tenant_id },
34-
params: { summarizeErrors: 'false' },
35-
data: { Contacts: input.map(toXeroContact) }
36-
};
37-
const res = await nango.get(config);
38-
`,
39-
},
40-
{
41-
code: `
42-
const res = await nango.put({ endpoint: 'api.example.com', data: {} });
43-
`,
44-
},
45-
],
46-
invalid: [
47-
{
48-
code: `
49-
const config = {
50-
endpoint: 'api.xro/2.0/Contacts',
51-
headers: { 'xero-tenant-id': tenant_id },
52-
params: { summarizeErrors: 'false' },
53-
data: { Contacts: input.map(toXeroContact) }
54-
};
55-
const res = await nango.post(config);
56-
`,
57-
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
58-
output: `
59-
const config: ProxyConfiguration = {
60-
endpoint: 'api.xro/2.0/Contacts',
61-
headers: { 'xero-tenant-id': tenant_id },
62-
params: { summarizeErrors: 'false' },
63-
data: { Contacts: input.map(toXeroContact) }
64-
};
65-
const res = await nango.post(config);
66-
`,
67-
},
68-
{
69-
code: `
70-
let config = {
71-
endpoint: 'api.xro/2.0/Contacts',
72-
headers: { 'xero-tenant-id': tenant_id },
73-
params: { summarizeErrors: 'false' },
74-
data: { Contacts: input.map(toXeroContact) }
75-
};
76-
const res = await nango.get(config);
77-
`,
78-
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
79-
output: `
80-
let config: ProxyConfiguration = {
81-
endpoint: 'api.xro/2.0/Contacts',
82-
headers: { 'xero-tenant-id': tenant_id },
83-
params: { summarizeErrors: 'false' },
84-
data: { Contacts: input.map(toXeroContact) }
85-
};
86-
const res = await nango.get(config);
87-
`,
88-
},
89-
{
90-
code: `
91-
var config = {
92-
endpoint: 'api.xro/2.0/Contacts',
93-
headers: { 'xero-tenant-id': tenant_id },
94-
params: { summarizeErrors: 'false' },
95-
data: { Contacts: input.map(toXeroContact) }
96-
};
97-
const res = await nango.proxy(config);
98-
`,
99-
errors: [{ message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration' }],
100-
output: `
101-
var config: ProxyConfiguration = {
102-
endpoint: 'api.xro/2.0/Contacts',
103-
headers: { 'xero-tenant-id': tenant_id },
104-
params: { summarizeErrors: 'false' },
105-
data: { Contacts: input.map(toXeroContact) }
106-
};
107-
const res = await nango.proxy(config);
108-
`,
109-
},
110-
],
10+
describe('enforce-proxy-configuration-type-tests', () => {
11+
it('should pass valid cases and fail invalid cases', () => {
12+
ruleTester.run('enforce-proxy-configuration-type', enforceProxyConfigurationType, {
13+
valid: [
14+
{
15+
code: `
16+
import type { NangoSync, Account, ProxyConfiguration } from '../../models';
17+
const config: ProxyConfiguration = {
18+
endpoint: 'api.xro/2.0/Accounts',
19+
headers: { 'xero-tenant-id': tenant_id },
20+
params: { order: 'UpdatedDateUTC DESC' },
21+
retries: 10
22+
};
23+
`,
24+
},
25+
],
26+
invalid: [
27+
{
28+
code: `
29+
import type { NangoSync, Account } from '../../models';
30+
const config = {
31+
endpoint: 'api.xro/2.0/Accounts',
32+
headers: { 'xero-tenant-id': tenant_id },
33+
params: { order: 'UpdatedDateUTC DESC' },
34+
retries: 10
35+
};
36+
`,
37+
errors: [
38+
{ message: 'ProxyConfiguration type should be imported and used for Nango API call configurations' },
39+
],
40+
output: `
41+
import type { NangoSync, Account, ProxyConfiguration } from '../../models';
42+
const config: ProxyConfiguration = {
43+
endpoint: 'api.xro/2.0/Accounts',
44+
headers: { 'xero-tenant-id': tenant_id },
45+
params: { order: 'UpdatedDateUTC DESC' },
46+
retries: 10
47+
};
48+
`,
49+
},
50+
],
51+
});
11152
});
112-
});
11353
});
Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Rule } from 'eslint';
2-
import { Node, CallExpression, Identifier, VariableDeclarator } from 'estree';
2+
import { ImportDeclaration, VariableDeclaration, VariableDeclarator, Identifier, ObjectExpression, Property } from 'estree';
33

44
const enforceProxyConfigurationType: Rule.RuleModule = {
55
meta: {
@@ -12,66 +12,71 @@ const enforceProxyConfigurationType: Rule.RuleModule = {
1212
fixable: 'code',
1313
schema: [],
1414
},
15-
create(context: Rule.RuleContext) {
16-
const sourceCode = context.getSourceCode();
15+
create(context) {
16+
let hasProxyConfigurationImport = false;
17+
let configVariableName: string | null = null;
18+
let importNode: ImportDeclaration | null = null;
19+
let configNode: VariableDeclaration | null = null;
1720

1821
return {
19-
CallExpression(node: Node) {
20-
if (isNangoApiCall(node)) {
21-
const options = node.arguments[0];
22-
23-
if (options && options.type === 'Identifier') {
24-
const scope = sourceCode.getScope(node);
25-
const variable = scope.variables.find(v => v.name === options.name);
26-
if (variable && variable.defs[0] && variable.defs[0].node.type === 'VariableDeclarator') {
27-
const declarator = variable.defs[0].node;
28-
if (!hasProxyConfigurationType(declarator, context)) {
29-
context.report({
30-
node: declarator,
31-
message: 'Configuration object for Nango API calls should be typed as ProxyConfiguration',
32-
fix(fixer) {
33-
const declarationToken = sourceCode.getFirstToken(declarator.parent);
34-
if (declarationToken && (declarationToken.value === 'const' || declarationToken.value === 'let' || declarationToken.value === 'var')) {
35-
return fixer.insertTextAfter(declarator.id, ': ProxyConfiguration');
36-
}
37-
return null;
38-
}
39-
});
40-
}
41-
}
22+
ImportDeclaration(node: ImportDeclaration) {
23+
if (node.source.value === '../../models') {
24+
importNode = node;
25+
hasProxyConfigurationImport = node.specifiers.some(
26+
(specifier) => specifier.type === 'ImportSpecifier' &&
27+
'imported' in specifier &&
28+
specifier.imported.type === 'Identifier' &&
29+
specifier.imported.name === 'ProxyConfiguration'
30+
);
31+
}
32+
},
33+
VariableDeclaration(node: VariableDeclaration) {
34+
const declarator = node.declarations[0] as VariableDeclarator;
35+
if (declarator && declarator.type === 'VariableDeclarator' &&
36+
declarator.id.type === 'Identifier' && declarator.init &&
37+
declarator.init.type === 'ObjectExpression') {
38+
const properties = declarator.init.properties;
39+
if (properties.some((prop): prop is Property =>
40+
prop.type === 'Property' &&
41+
prop.key.type === 'Identifier' &&
42+
prop.key.name === 'endpoint')) {
43+
configVariableName = declarator.id.name;
44+
configNode = node;
4245
}
4346
}
4447
},
45-
};
46-
},
47-
};
48-
49-
function isNangoApiCall(node: Node): node is CallExpression {
50-
return (
51-
node.type === 'CallExpression' &&
52-
node.callee.type === 'MemberExpression' &&
53-
node.callee.object.type === 'Identifier' &&
54-
node.callee.object.name === 'nango' &&
55-
node.callee.property.type === 'Identifier' &&
56-
['get', 'post', 'put', 'patch', 'delete', 'proxy'].includes(node.callee.property.name)
57-
);
58-
}
48+
'Program:exit'() {
49+
if (configVariableName && !hasProxyConfigurationImport && importNode && configNode) {
50+
context.report({
51+
node: context.getSourceCode().ast,
52+
message: 'ProxyConfiguration type should be imported and used for Nango API call configurations',
53+
fix(fixer) {
54+
const fixes = [];
5955

60-
function hasProxyConfigurationType(node: VariableDeclarator, context: Rule.RuleContext): boolean {
61-
const sourceCode = context.getSourceCode();
62-
const idToken = sourceCode.getFirstToken(node.id);
63-
if (!idToken) return false;
56+
if (importNode && importNode.specifiers.length > 0) {
57+
fixes.push(fixer.insertTextAfter(
58+
importNode.specifiers[importNode.specifiers.length - 1],
59+
', ProxyConfiguration'
60+
));
61+
}
6462

65-
const nextToken = sourceCode.getTokenAfter(idToken);
66-
if (!nextToken) return false;
63+
if (configNode && configNode.declarations.length > 0) {
64+
const configDeclarator = configNode.declarations[0] as VariableDeclarator;
65+
if (configDeclarator && configDeclarator.id.type === 'Identifier') {
66+
fixes.push(fixer.insertTextAfter(
67+
configDeclarator.id,
68+
': ProxyConfiguration'
69+
));
70+
}
71+
}
6772

68-
const tokenAfterColon = sourceCode.getTokenAfter(nextToken);
69-
70-
return (
71-
nextToken.type === 'Punctuator' &&
72-
nextToken.value === ':' &&
73-
tokenAfterColon?.value === 'ProxyConfiguration'
74-
);
75-
}
73+
return fixes;
74+
},
75+
});
76+
}
77+
},
78+
};
79+
},
80+
};
7681

7782
export default enforceProxyConfigurationType;

0 commit comments

Comments
 (0)