Skip to content

Commit 173736e

Browse files
committed
test(builtins): add Node + wasmtime + CI coverage for call AST
CI gap: zig build test-unit was not run by CI, so Zig-side unit tests in src/builtins.zig and the new tests in src/eval.zig (call startswith / count / unknown denies) were never validated upstream. Fixes: - ci.yml gains a test-unit job that runs zig build test-unit on ubuntu-latest. Independent of the build matrix; needs only the Zig toolchain. - test/run.mjs gains 7 cases covering startswith / endswith / contains / count (with > comparator) and the unknown-builtin deny path. - test/run_wasmtime.py gets the same 6 cases (parity with run.mjs).
1 parent 0c53381 commit 173736e

2 files changed

Lines changed: 131 additions & 0 deletions

File tree

test/run.mjs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,75 @@ check(
354354
0,
355355
);
356356

357+
// ---------------------------------------------------------------------------
358+
// 8. call: builtin functions (startswith / endswith / contains / count)
359+
// ---------------------------------------------------------------------------
360+
const callStartswith = (path, prefix) => ({
361+
type: 'call', name: 'startswith',
362+
args: [
363+
{ type: 'ref', path: ['input', 'path'] },
364+
{ type: 'value', value: prefix },
365+
],
366+
});
367+
check('call startswith on input.path -> allow', decide({ path: '/admin/users' }, callStartswith('path', '/admin/')), 1);
368+
check('call startswith on input.path -> deny', decide({ path: '/users' }, callStartswith('path', '/admin/')), 0);
369+
370+
check(
371+
'call endswith on host -> allow',
372+
decide({ host: 'api.internal' }, {
373+
type: 'call', name: 'endswith',
374+
args: [
375+
{ type: 'ref', path: ['input', 'host'] },
376+
{ type: 'value', value: '.internal' },
377+
],
378+
}),
379+
1,
380+
);
381+
382+
check(
383+
'call contains in user-agent -> allow',
384+
decide({ ua: 'Mozilla/5.0 Bot' }, {
385+
type: 'call', name: 'contains',
386+
args: [
387+
{ type: 'ref', path: ['input', 'ua'] },
388+
{ type: 'value', value: 'Bot' },
389+
],
390+
}),
391+
1,
392+
);
393+
394+
// count compared to a literal, used in body position.
395+
check(
396+
'call count > 2 over array -> allow',
397+
decide({ perms: ['r', 'w', 'x'] }, {
398+
type: 'gt',
399+
left: { type: 'call', name: 'count', args: [{ type: 'ref', path: ['input', 'perms'] }] },
400+
right: { type: 'value', value: 2 },
401+
}),
402+
1,
403+
);
404+
check(
405+
'call count > 2 over array -> deny',
406+
decide({ perms: ['r'] }, {
407+
type: 'gt',
408+
left: { type: 'call', name: 'count', args: [{ type: 'ref', path: ['input', 'perms'] }] },
409+
right: { type: 'value', value: 2 },
410+
}),
411+
0,
412+
);
413+
414+
// unknown builtin: lookup miss resolves to .nil, treated as falsy in
415+
// body position -> the synthetic `allow` rule fails -> deny (0). The
416+
// proxy-wasm shim treats any non-1 the same way (deny).
417+
check(
418+
'call unknown builtin -> deny',
419+
decide({}, {
420+
type: 'call', name: 'made_up_function',
421+
args: [{ type: 'value', value: 1 }],
422+
}),
423+
0,
424+
);
425+
357426
if (failed > 0) {
358427
console.error(`\n${failed} test(s) failed`);
359428
exit(1);

test/run_wasmtime.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,68 @@ def check(name: str, got, expected):
238238
0,
239239
)
240240

241+
# ---------------------------------------------------------------------------
242+
# 8. call: builtin functions (startswith / endswith / contains / count)
243+
# ---------------------------------------------------------------------------
244+
check(
245+
"call startswith on input.path -> allow",
246+
decide({"path": "/admin/users"}, {
247+
"type": "call", "name": "startswith",
248+
"args": [
249+
{"type": "ref", "path": ["input", "path"]},
250+
{"type": "value", "value": "/admin/"},
251+
],
252+
}),
253+
1,
254+
)
255+
check(
256+
"call startswith on input.path -> deny",
257+
decide({"path": "/users"}, {
258+
"type": "call", "name": "startswith",
259+
"args": [
260+
{"type": "ref", "path": ["input", "path"]},
261+
{"type": "value", "value": "/admin/"},
262+
],
263+
}),
264+
0,
265+
)
266+
check(
267+
"call endswith on host -> allow",
268+
decide({"host": "api.internal"}, {
269+
"type": "call", "name": "endswith",
270+
"args": [
271+
{"type": "ref", "path": ["input", "host"]},
272+
{"type": "value", "value": ".internal"},
273+
],
274+
}),
275+
1,
276+
)
277+
check(
278+
"call contains in user-agent -> allow",
279+
decide({"ua": "Mozilla/5.0 Bot"}, {
280+
"type": "call", "name": "contains",
281+
"args": [
282+
{"type": "ref", "path": ["input", "ua"]},
283+
{"type": "value", "value": "Bot"},
284+
],
285+
}),
286+
1,
287+
)
288+
check(
289+
"call count > 2 over array -> allow",
290+
decide({"perms": ["r", "w", "x"]}, {
291+
"type": "gt",
292+
"left": {"type": "call", "name": "count", "args": [{"type": "ref", "path": ["input", "perms"]}]},
293+
"right": {"type": "value", "value": 2},
294+
}),
295+
1,
296+
)
297+
check(
298+
"call unknown builtin -> deny",
299+
decide({}, {"type": "call", "name": "made_up_function", "args": [{"type": "value", "value": 1}]}),
300+
0,
301+
)
302+
241303
if failed:
242304
print(f"\n{failed} test(s) failed", file=sys.stderr)
243305
sys.exit(1)

0 commit comments

Comments
 (0)