Skip to content

Commit 902027b

Browse files
committed
Extract inline backend connection IDs
1 parent 02cd4c7 commit 902027b

4 files changed

Lines changed: 678 additions & 18 deletions

File tree

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2019-Present Datadog, Inc.
4+
5+
import { parseAst } from 'rollup/parseAst';
6+
import type { AstNode } from 'rollup';
7+
8+
import { extractConnectionIds } from './extract-connection-ids';
9+
10+
const filePath = '/project/src/backend/actions.backend.js';
11+
12+
function parse(code: string): AstNode {
13+
return parseAst(code) as AstNode;
14+
}
15+
16+
describe('Backend Functions - extractConnectionIds', () => {
17+
test('Should extract inline string literal connection IDs from named action-catalog imports', () => {
18+
const ast = parse(`
19+
import { request } from '@datadog/action-catalog/http/http';
20+
21+
export function run() {
22+
return request({ connectionId: 'conn-b', inputs: {} });
23+
}
24+
`);
25+
26+
expect(extractConnectionIds(ast, filePath)).toEqual(['conn-b']);
27+
});
28+
29+
test('Should dedupe and sort connection IDs', () => {
30+
const ast = parse(`
31+
import { request } from '@datadog/action-catalog/http/http';
32+
33+
export function run() {
34+
request({ connectionId: 'conn-b', inputs: {} });
35+
request({ connectionId: 'conn-a', inputs: {} });
36+
request({ connectionId: 'conn-b', inputs: {} });
37+
}
38+
`);
39+
40+
expect(extractConnectionIds(ast, filePath)).toEqual(['conn-a', 'conn-b']);
41+
});
42+
43+
test('Should include same-file helper action calls', () => {
44+
const ast = parse(`
45+
import { request } from '@datadog/action-catalog/http/http';
46+
47+
function helper() {
48+
return request({ connectionId: 'conn-helper', inputs: {} });
49+
}
50+
51+
export function run() {
52+
return helper();
53+
}
54+
`);
55+
56+
expect(extractConnectionIds(ast, filePath)).toEqual(['conn-helper']);
57+
});
58+
59+
test('Should detect default and namespace action-catalog imports', () => {
60+
const ast = parse(`
61+
import request from '@datadog/action-catalog/http/http';
62+
import * as slack from '@datadog/action-catalog/slack/messages';
63+
64+
export function run() {
65+
request({ connectionId: 'conn-default', inputs: {} });
66+
slack.postMessage({ connectionId: 'conn-namespace', inputs: {} });
67+
}
68+
`);
69+
70+
expect(extractConnectionIds(ast, filePath)).toEqual(['conn-default', 'conn-namespace']);
71+
});
72+
73+
test('Should ignore non-action-catalog calls with connectionId properties', () => {
74+
const ast = parse(`
75+
import { request } from './local';
76+
77+
export function run() {
78+
request({ connectionId: 'ignored', inputs: {} });
79+
}
80+
`);
81+
82+
expect(extractConnectionIds(ast, filePath)).toEqual([]);
83+
});
84+
85+
test('Should ignore action-catalog object arguments without connectionId', () => {
86+
const ast = parse(`
87+
import { request } from '@datadog/action-catalog/http/http';
88+
89+
export function run() {
90+
request({ inputs: {} });
91+
}
92+
`);
93+
94+
expect(extractConnectionIds(ast, filePath)).toEqual([]);
95+
});
96+
97+
test('Should ignore type-only action-catalog imports', () => {
98+
const ast = parse(`
99+
import { request } from '@datadog/action-catalog/http/http';
100+
101+
export function run() {
102+
request({ connectionId: 'ignored', inputs: {} });
103+
}
104+
`);
105+
const importDeclaration = (ast as unknown as { body: Array<{ importKind?: string }> })
106+
.body[0];
107+
importDeclaration.importKind = 'type';
108+
109+
expect(extractConnectionIds(ast, filePath)).toEqual([]);
110+
});
111+
112+
test('Should ignore type-only action-catalog import specifiers', () => {
113+
const ast = parse(`
114+
import { request } from '@datadog/action-catalog/http/http';
115+
116+
export function run() {
117+
request({ connectionId: 'ignored', inputs: {} });
118+
}
119+
`);
120+
const importSpecifier = (
121+
ast as unknown as {
122+
body: Array<{ specifiers: Array<{ importKind?: string }> }>;
123+
}
124+
).body[0].specifiers[0];
125+
importSpecifier.importKind = 'type';
126+
127+
expect(extractConnectionIds(ast, filePath)).toEqual([]);
128+
});
129+
130+
test.each([
131+
{
132+
description: 'function parameters that shadow named imports',
133+
code: `
134+
import { request } from '@datadog/action-catalog/http/http';
135+
136+
export function run(request) {
137+
return request({ connectionId: 'ignored', inputs: {} });
138+
}
139+
`,
140+
},
141+
{
142+
description: 'function parameters that shadow namespace imports',
143+
code: `
144+
import * as http from '@datadog/action-catalog/http/http';
145+
146+
export function run(http) {
147+
return http.request({ connectionId: 'ignored', inputs: {} });
148+
}
149+
`,
150+
},
151+
{
152+
description: 'catch parameters that shadow named imports',
153+
code: `
154+
import { request } from '@datadog/action-catalog/http/http';
155+
156+
export function run() {
157+
try {
158+
throw new Error('nope');
159+
} catch (request) {
160+
request({ connectionId: CONNECTIONS.HTTP, inputs: {} });
161+
}
162+
}
163+
`,
164+
},
165+
{
166+
description: 'local aliases of shadowed parameters',
167+
code: `
168+
import { request } from '@datadog/action-catalog/http/http';
169+
170+
export function run(request) {
171+
const action = request;
172+
action({ connectionId: 'ignored', inputs: {} });
173+
}
174+
`,
175+
},
176+
{
177+
description: 'for-of bindings that shadow named imports',
178+
code: `
179+
import { request } from '@datadog/action-catalog/http/http';
180+
181+
export function run(handlers) {
182+
for (const request of handlers) {
183+
request({ connectionId: CONNECTIONS.HTTP, inputs: {} });
184+
}
185+
}
186+
`,
187+
},
188+
{
189+
description: 'for-statement bindings that shadow named imports',
190+
code: `
191+
import { request } from '@datadog/action-catalog/http/http';
192+
193+
export function run(handlers) {
194+
for (const request = handlers.next; request;) {
195+
request({ connectionId: CONNECTIONS.HTTP, inputs: {} });
196+
}
197+
}
198+
`,
199+
},
200+
{
201+
description: 'for-in bindings that shadow namespace imports',
202+
code: `
203+
import * as http from '@datadog/action-catalog/http/http';
204+
205+
export function run(clients) {
206+
for (const http in clients) {
207+
http.request({ connectionId: CONNECTIONS.HTTP, inputs: {} });
208+
}
209+
}
210+
`,
211+
},
212+
])(
213+
'Should not treat shadowed action-catalog import names as action calls: $description',
214+
({ code }) => {
215+
expect(extractConnectionIds(parse(code), filePath)).toEqual([]);
216+
},
217+
);
218+
219+
test.each([
220+
{
221+
description: 'identifier value',
222+
source: 'const ID = "conn"; request({ connectionId: ID, inputs: {} });',
223+
expectedType: 'Identifier',
224+
},
225+
{
226+
description: 'template literal value',
227+
source: 'request({ connectionId: `conn`, inputs: {} });',
228+
expectedType: 'TemplateLiteral',
229+
},
230+
{
231+
description: 'member expression value',
232+
source: 'request({ connectionId: CONNECTIONS.HTTP, inputs: {} });',
233+
expectedType: 'MemberExpression',
234+
},
235+
{
236+
description: 'call expression value',
237+
source: 'request({ connectionId: getConnectionId(), inputs: {} });',
238+
expectedType: 'CallExpression',
239+
},
240+
{
241+
description: 'binary expression value',
242+
source: "request({ connectionId: 'conn-' + suffix, inputs: {} });",
243+
expectedType: 'BinaryExpression',
244+
},
245+
])('Should fail closed for unsupported $description', ({ source, expectedType }) => {
246+
const ast = parse(`
247+
import { request } from '@datadog/action-catalog/http/http';
248+
249+
export function run() {
250+
${source}
251+
}
252+
`);
253+
254+
expect(() => extractConnectionIds(ast, filePath)).toThrow(
255+
`expected an inline string literal, got ${expectedType}`,
256+
);
257+
});
258+
259+
test.each([
260+
{
261+
description: 'non-object first arguments',
262+
source: 'request(opts);',
263+
expectedMessage: 'non-object action-catalog call arguments',
264+
},
265+
{
266+
description: 'spread-composed object arguments',
267+
source: 'request({ ...opts });',
268+
expectedMessage: 'spread object arguments',
269+
},
270+
{
271+
description: 'computed connectionId keys',
272+
source: "request({ ['connectionId']: 'conn' });",
273+
expectedMessage: 'computed object property keys',
274+
},
275+
{
276+
description: 'optional action calls',
277+
source: "request?.({ connectionId: 'conn' });",
278+
expectedMessage: 'optional action-catalog calls',
279+
},
280+
{
281+
description: 'action-catalog import aliases',
282+
source: "const action = request; action({ connectionId: 'conn' });",
283+
expectedMessage: 'action-catalog call aliases',
284+
},
285+
{
286+
description: 'action-catalog namespace member aliases',
287+
source: "const action = http.request; action({ connectionId: 'conn' });",
288+
expectedMessage: 'action-catalog call aliases',
289+
importStatement: "import * as http from '@datadog/action-catalog/http/http';",
290+
},
291+
{
292+
description: 'action-catalog namespace destructuring aliases',
293+
source: "const { request: action } = http; action({ connectionId: 'conn' });",
294+
expectedMessage: 'action-catalog call aliases',
295+
importStatement: "import * as http from '@datadog/action-catalog/http/http';",
296+
},
297+
{
298+
description: 'multiple connectionId properties',
299+
source: "request({ connectionId: 'conn-a', connectionId: 'conn-b' });",
300+
expectedMessage: 'multiple connectionId properties',
301+
},
302+
{
303+
description: 'accessor connectionId properties',
304+
source: 'request({ get connectionId() { return CONNECTIONS.HTTP; } });',
305+
expectedMessage: 'accessor connectionId properties',
306+
},
307+
{
308+
description: 'computed namespace calls',
309+
source: "http['request']({ connectionId: 'conn' });",
310+
expectedMessage: 'optional or computed action-catalog namespace calls',
311+
importStatement: "import * as http from '@datadog/action-catalog/http/http';",
312+
},
313+
])(
314+
'Should fail closed for unsupported $description',
315+
({ source, expectedMessage, importStatement }) => {
316+
const ast = parse(`
317+
${importStatement ?? "import { request } from '@datadog/action-catalog/http/http';"}
318+
319+
export function run() {
320+
${source}
321+
}
322+
`);
323+
324+
expect(() => extractConnectionIds(ast, filePath)).toThrow(expectedMessage);
325+
},
326+
);
327+
328+
test('Should return an empty allowlist when no connection IDs are present', () => {
329+
const ast = parse(`
330+
export function run() {
331+
return 'ok';
332+
}
333+
`);
334+
335+
expect(extractConnectionIds(ast, filePath)).toEqual([]);
336+
});
337+
});

0 commit comments

Comments
 (0)