@aloma.io/integration-sdk 3.8.67 → 3.8.69
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.
- package/README.md +60 -0
- package/build/testing/fake-aloma.d.mts +87 -0
- package/build/testing/fake-aloma.mjs +248 -0
- package/build/testing/harness.d.mts +51 -0
- package/build/testing/harness.mjs +139 -0
- package/build/testing/index.d.mts +15 -0
- package/build/testing/index.mjs +13 -0
- package/build/testing/keys.d.mts +9 -0
- package/build/testing/keys.mjs +15 -0
- package/build/testing/network-guard.d.mts +3 -0
- package/build/testing/network-guard.mjs +49 -0
- package/build/testing/runtime-connector.d.mts +11 -0
- package/build/testing/runtime-connector.mjs +38 -0
- package/build/testing/vendor-mock.d.mts +59 -0
- package/build/testing/vendor-mock.mjs +121 -0
- package/build/transform/index.d.mts +2 -2
- package/build/transform/index.mjs +493 -78
- package/package.json +8 -4
- package/src/testing/fake-aloma.mts +306 -0
- package/src/testing/harness.mts +172 -0
- package/src/testing/index.mts +15 -0
- package/src/testing/keys.mts +19 -0
- package/src/testing/network-guard.mts +57 -0
- package/src/testing/runtime-connector.mts +50 -0
- package/src/testing/vendor-mock.mts +166 -0
- package/src/transform/index.mts +530 -96
- package/test/MUTATION-entry.md +21 -0
- package/test/MUTATION.md +43 -0
- package/test/fixtures/transform/basic/expected.json +9 -0
- package/test/fixtures/transform/basic/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/basic/src/controller/index.mts +30 -0
- package/test/fixtures/transform/basic/tsconfig.json +1 -0
- package/test/fixtures/transform/interface-param/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/interface-param/src/controller/index.mts +12 -0
- package/test/fixtures/transform/interface-param/tsconfig.json +1 -0
- package/test/fixtures/transform/overloads/expected.json +6 -0
- package/test/fixtures/transform/overloads/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/overloads/src/controller/index.mts +11 -0
- package/test/fixtures/transform/overloads/tsconfig.json +1 -0
- package/test/fixtures/transform/tdd-stub/expected.json +8 -0
- package/test/fixtures/transform/tdd-stub/src/controller/abstract.mts +6 -0
- package/test/fixtures/transform/tdd-stub/src/controller/index.mts +204 -0
- package/test/fixtures/transform/tdd-stub/tsconfig.json +1 -0
- package/test/fixtures/transform/two-exports/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/two-exports/src/controller/index.mts +9 -0
- package/test/fixtures/transform/two-exports/tsconfig.json +1 -0
- package/test/fixtures/transform/type-error/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/type-error/src/controller/index.mts +7 -0
- package/test/fixtures/transform/type-error/tsconfig.json +1 -0
- package/test/fixtures/transform/typed/expected.json +8 -0
- package/test/fixtures/transform/typed/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/typed/src/controller/index.mts +13 -0
- package/test/fixtures/transform/typed/tsconfig.json +1 -0
- package/test/fixtures/transform/visibility/expected.json +9 -0
- package/test/fixtures/transform/visibility/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/visibility/src/controller/index.mts +19 -0
- package/test/fixtures/transform/visibility/tsconfig.json +1 -0
- package/test/harness-connector-smoke.test.mjs +72 -0
- package/test/harness.test.mjs +375 -0
- package/test/openapi-to-connector.test.mts +25 -7
- package/test/scenarios.test.mts +15 -7
- package/test/transform.test.mjs +68 -0
- package/tools/mutation-run-entry.mjs +52 -0
- package/tools/mutation-run.mjs +120 -0
- package/tools/run-tests.mjs +64 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Mutation check for the connector test harness.
|
|
2
|
+
//
|
|
3
|
+
// yarn build && yarn test:mutation
|
|
4
|
+
//
|
|
5
|
+
// Applies each deliberate single-point bug below to src/, rebuilds, runs the harness tests and expects them
|
|
6
|
+
// to FAIL. The original file is restored after every mutant (and on Ctrl-C). Writes test/MUTATION.md.
|
|
7
|
+
// Not part of CI: it rebuilds the SDK ~30 times. Run it when the harness or the SDK paths it covers change.
|
|
8
|
+
import {spawnSync} from 'node:child_process';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {fileURLToPath} from 'node:url';
|
|
12
|
+
|
|
13
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const src = (p) => path.join(root, 'src', p);
|
|
15
|
+
|
|
16
|
+
// [id, area, file (under src/), exact text to find (must occur once), replacement, what the bug is]
|
|
17
|
+
const MUTANTS = [
|
|
18
|
+
['M01', 'sdk handshake', 'internal/websocket/connection/index.mjs', 'response.status === 401', 'response.status === 403', 'a 401 from /connect no longer triggers registration'],
|
|
19
|
+
['M02', 'sdk handshake', 'internal/websocket/connection/index.mjs', 'config.setToken(await new Registration(local.config).run());', 'await new Registration(local.config).run();', 'the token from registration is not stored'],
|
|
20
|
+
['M03', 'sdk handshake', 'internal/websocket/connection/registration.mjs', 'return (await response.json()).key;', 'return (await response.json()).token;', 'registration reads the wrong field of the answer'],
|
|
21
|
+
['M04', 'sdk handshake', 'internal/websocket/connection/registration.mjs', 'id: config.id(),', 'id: config.name(),', 'registration sends the name as the connector id'],
|
|
22
|
+
['M05', 'sdk handshake', 'internal/websocket/connection/registration.mjs', 'publicKey: config.publicKey(),', 'publicKey: undefined,', 'registration omits the public key'],
|
|
23
|
+
['M06', 'sdk handshake', 'internal/websocket/connection/constants.mjs', "'User-Agent': config.id() + '/' + config.version(),\n };\n\n what.headers[AUTHORIZATION] = `Connector ${config.token()}`;", "'User-Agent': config.id(),\n };\n\n what.headers[AUTHORIZATION] = `Connector ${config.token()}`;", 'authenticated requests drop the version from the User-Agent'],
|
|
24
|
+
['M07', 'sdk handshake', 'internal/websocket/connection/constants.mjs', 'what.headers[AUTHORIZATION] = `Connector ${config.token()}`;', 'what.headers[AUTHORIZATION] = `Bearer ${config.token()}`;', 'wrong authorization scheme for the session token'],
|
|
25
|
+
['M08', 'sdk handshake', 'internal/websocket/transport/index.mjs', "config.wsUrl(), ['connector']", 'config.wsUrl(), []', 'websocket opened without the "connector" subprotocol'],
|
|
26
|
+
['M09', 'sdk transport', 'internal/websocket/transport/index.mjs', 'msgs.p?.forEach((item) => {', 'msgs.p?.slice(1).forEach((item) => {', 'the first packet of every incoming frame is dropped'],
|
|
27
|
+
['M10', 'sdk transport', 'internal/websocket/transport/index.mjs', 'local.ws.send(JSON.stringify({p: packets}));', 'local.ws.send(JSON.stringify({packets}));', 'outgoing frames use the wrong envelope'],
|
|
28
|
+
['M11', 'sdk transport', 'internal/websocket/transport/index.mjs', 'if (packet.cb() && this.callbacks[packet.cb()]) {', 'if (false && packet.cb() && this.callbacks[packet.cb()]) {', 'answers to connector-initiated requests are never routed to their callback'],
|
|
29
|
+
['M12', 'sdk dispatch', 'internal/dispatcher/index.mts', 'case \'connector.set-config\':\n await local.onConfig({...packet.args().secrets});', 'case \'connector.set-config\':\n await local.onConfig({});', 'configuration secrets are dropped before onConfig'],
|
|
30
|
+
['M13', 'sdk dispatch', 'internal/dispatcher/index.mts', 'const ctx = {taskId, namespace, sticky};', 'const ctx = {taskId, namespace};', 'sticky is not forwarded to the method'],
|
|
31
|
+
['M14', 'sdk dispatch', 'internal/dispatcher/index.mts', 'if (!method && !_resolvers.__default) throw new Error(`${originalQuery} not found`);', 'if (false) throw new Error(`${originalQuery} not found`);', 'unknown methods are no longer reported as "not found"'],
|
|
32
|
+
['M15', 'sdk result return', 'internal/dispatcher/index.mts', 'return typeof ret === \'object\' && !Array.isArray(ret) ? ret : {[packet.args().query]: ret};', 'return typeof ret === \'object\' && !Array.isArray(ret) ? ret : {result: ret};', 'scalar results are wrapped under the wrong key'],
|
|
33
|
+
['M16', 'sdk result return', 'internal/util/index.mts', 'transport.send(transport.newPacket({c: packet.cb(), a: {...arg}}));', 'transport.send(transport.newPacket({c: packet.cb(), a: {}}));', 'successful results are replied empty'],
|
|
34
|
+
['M17', 'sdk result return', 'internal/util/index.mts', "a: {error: '' + e}", "a: {error: 'failed'}", 'errors lose their message'],
|
|
35
|
+
['M18', 'sdk result return', 'internal/connector/server/on-message.mts', 'if (ret) reply(ret, packet, transport);', 'if (false) reply(ret, packet, transport);', 'results are never sent back'],
|
|
36
|
+
['M19', 'sdk config', 'internal/connector/server/on-connect/decrypt-config.mts', 'jwe.decrypt(value, config.id())', "jwe.decrypt(value, 'other-audience')", 'config is decrypted for the wrong audience'],
|
|
37
|
+
['M20', 'sdk config', 'internal/connector/server/on-connect/decrypt-config.mts', 'if (fields[key]?.plain ||', 'if (false ||', 'plain fields are decrypted like secrets'],
|
|
38
|
+
['M21', 'sdk config', 'internal/connector/server/on-connect/index.mts', 'config: decrypted,\n oauth: theOAuth,', 'config: {},\n oauth: theOAuth,', 'main() receives an empty config'],
|
|
39
|
+
['M22', 'harness vendor mock', 'testing/vendor-mock.mts', 'error.alomaHarnessUnmocked = true;\n throw error;', 'error.alomaHarnessUnmocked = true;\n return new Response(\'{}\');', 'an unmocked request is answered instead of rejected'],
|
|
40
|
+
['M23', 'harness vendor mock', 'testing/vendor-mock.mts', 'if (this.unmocked.length) {', 'if (false) {', 'verify() never fails'],
|
|
41
|
+
['M24', 'harness vendor mock', 'testing/vendor-mock.mts', 'const status = spec.status ?? 200;', 'const status = 200;', 'mocked status codes are ignored'],
|
|
42
|
+
['M25', 'harness vendor mock', 'testing/vendor-mock.mts', 'r.used < r.times && ', '', 'the times limit on a route is ignored'],
|
|
43
|
+
['M26', 'harness network guard', 'testing/network-guard.mts', 'if (!local && !LOOPBACK.has(host)) {', 'if (false) {', 'the network guard lets everything through (the mutant may try one TCP connect to the TEST-NET-3 documentation address 203.0.113.9)'],
|
|
44
|
+
['M27', 'harness fake ALOMA', 'testing/fake-aloma.mts', 'const authorized = auth === `Connector ${this.sessionToken}`;', 'const authorized = true;', 'the fake accepts a connect without registration'],
|
|
45
|
+
['M28', 'harness fake ALOMA', 'testing/fake-aloma.mts', '.setAudience(this.id)', ".setAudience('wrong')", 'the fake encrypts config for the wrong audience'],
|
|
46
|
+
['M29', 'harness fake ALOMA', 'testing/fake-aloma.mts', ' done(packet.a);', ' done(undefined);', 'the fake drops the connector\'s answer'],
|
|
47
|
+
['M30', 'harness fake ALOMA', 'testing/fake-aloma.mts', "{query: method, variables, ...ctx}", "{query: method, variables}", 'call() drops the ctx fields'],
|
|
48
|
+
['M31', 'harness', 'testing/harness.mts', 'this.vendor.verify();', '', 'stop() no longer fails on unmocked requests'],
|
|
49
|
+
['M32', 'harness', 'testing/harness.mts', "await aloma.setConfig(options.config || {});", "", 'the harness never sends configuration'],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const run = (cmd, args, timeout) =>
|
|
53
|
+
spawnSync(cmd, args, {cwd: root, encoding: 'utf8', timeout, env: {...process.env, FORCE_COLOR: '0'}});
|
|
54
|
+
|
|
55
|
+
const build = () => {
|
|
56
|
+
const r = run('node', ['./node_modules/typescript/bin/tsc'], 120000);
|
|
57
|
+
if (r.status !== 0) throw new Error('build failed:\n' + r.stdout + r.stderr);
|
|
58
|
+
};
|
|
59
|
+
const mocha = () => run('node', ['./node_modules/mocha/bin/_mocha', '--bail', '--exit', 'test/harness.test.mjs', 'test/harness-connector-smoke.test.mjs'], 150000);
|
|
60
|
+
|
|
61
|
+
let current = null;
|
|
62
|
+
const restore = () => {
|
|
63
|
+
if (current) fs.writeFileSync(current.file, current.original);
|
|
64
|
+
current = null;
|
|
65
|
+
};
|
|
66
|
+
process.on('SIGINT', () => {
|
|
67
|
+
restore();
|
|
68
|
+
process.exit(130);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const results = [];
|
|
72
|
+
try {
|
|
73
|
+
build();
|
|
74
|
+
const baseline = mocha();
|
|
75
|
+
if (baseline.status !== 0) throw new Error('baseline run must pass before mutating:\n' + baseline.stdout.slice(-2000));
|
|
76
|
+
|
|
77
|
+
for (const [id, area, rel, find, replace, bug] of MUTANTS) {
|
|
78
|
+
const file = src(rel);
|
|
79
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
80
|
+
const count = original.split(find).length - 1;
|
|
81
|
+
if (count !== 1) throw new Error(`${id}: expected exactly one match in ${rel}, found ${count}`);
|
|
82
|
+
|
|
83
|
+
current = {file, original};
|
|
84
|
+
let outcome;
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(file, original.replace(find, () => replace));
|
|
87
|
+
build();
|
|
88
|
+
const r = mocha();
|
|
89
|
+
const failing = (r.stdout.match(/^\s+\d+\) .+$/gm) || [])[0]?.trim() || '';
|
|
90
|
+
outcome = r.error?.code === 'ETIMEDOUT' ? 'killed (timeout)' : r.status !== 0 ? 'killed' : 'SURVIVED';
|
|
91
|
+
results.push({id, area, rel, bug, outcome, by: failing});
|
|
92
|
+
} finally {
|
|
93
|
+
restore();
|
|
94
|
+
}
|
|
95
|
+
console.log(id, results[results.length - 1].outcome, '-', bug);
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
restore();
|
|
99
|
+
build();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const killed = results.filter((r) => r.outcome.startsWith('killed')).length;
|
|
103
|
+
const md = [
|
|
104
|
+
'# Mutation check for the connector test harness',
|
|
105
|
+
'',
|
|
106
|
+
`Result: **${killed}/${results.length} mutants killed**${killed === results.length ? '' : ' (survivors listed below)'}.`,
|
|
107
|
+
'',
|
|
108
|
+
'Each row is one deliberate single-point bug in the SDK code paths the harness exercises (handshake, transport,',
|
|
109
|
+
'dispatch, result return, config) or in the harness itself. `tools/mutation-run.mjs` applies it to `src/`, rebuilds,',
|
|
110
|
+
'runs `test/harness.test.mjs` with `--bail`, expects a failure, and restores the file. Re-run with',
|
|
111
|
+
'`yarn build && yarn test:mutation` after changing the harness or those SDK paths.',
|
|
112
|
+
'',
|
|
113
|
+
'| # | Area | File | Bug | Result |',
|
|
114
|
+
'|---|------|------|-----|--------|',
|
|
115
|
+
...results.map((r) => `| ${r.id} | ${r.area} | \`src/${r.rel}\` | ${r.bug} | ${r.outcome} |`),
|
|
116
|
+
'',
|
|
117
|
+
].join('\n');
|
|
118
|
+
fs.writeFileSync(path.join(root, 'test', 'MUTATION.md'), md);
|
|
119
|
+
console.log(`\n${killed}/${results.length} killed`);
|
|
120
|
+
process.exit(killed === results.length ? 0 : 1);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// `yarn test`: runs every test file in test/.
|
|
2
|
+
//
|
|
3
|
+
// Two kinds of test file live in test/:
|
|
4
|
+
// - mocha files (describe/it): files that import from 'mocha'. They run together in ONE mocha process.
|
|
5
|
+
// - standalone scripts (their own tiny test() helper, finish with process.exit): every other test/*.test.mjs, plus
|
|
6
|
+
// test/verify-scenarios.mjs. Each runs in its own node process, because process.exit() inside a mocha process
|
|
7
|
+
// ends the whole run while mocha is still loading files, so none of the describe/it tests would run.
|
|
8
|
+
// A new test file is picked up by name (test/*.test.mjs|mts). Exit code is non-zero if anything fails.
|
|
9
|
+
import {createRequire} from 'node:module';
|
|
10
|
+
import {spawnSync} from 'node:child_process';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import {fileURLToPath} from 'node:url';
|
|
14
|
+
|
|
15
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const testDir = path.join(root, 'test');
|
|
17
|
+
|
|
18
|
+
const files = fs.readdirSync(testDir).filter((f) => /\.test\.m[jt]s$/.test(f)).sort();
|
|
19
|
+
const isMocha = (f) => /from ['"]mocha['"]/.test(fs.readFileSync(path.join(testDir, f), 'utf8'));
|
|
20
|
+
// .mts test files are transpiled (type-stripped) to sibling .mjs files and run by plain mocha. The ts-node/esm loader
|
|
21
|
+
// cannot be used: it fails on Node 18 (ERR_UNKNOWN_FILE_EXTENSION) and Node 20 (ERR_INTERNAL_ASSERTION). The sibling
|
|
22
|
+
// location keeps relative imports and import.meta.url paths identical to the source file.
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const ts = require('typescript');
|
|
25
|
+
const generated = [];
|
|
26
|
+
const compile = (f) => {
|
|
27
|
+
if (!f.endsWith('.mts')) return path.join('test', f);
|
|
28
|
+
const out = path.join('test', `.compiled-${f.replace(/\.mts$/, '.mjs')}`);
|
|
29
|
+
const src = fs.readFileSync(path.join(testDir, f), 'utf8');
|
|
30
|
+
const js = ts.transpileModule(src, {
|
|
31
|
+
compilerOptions: {module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022},
|
|
32
|
+
fileName: f,
|
|
33
|
+
}).outputText;
|
|
34
|
+
fs.writeFileSync(path.join(root, out), js);
|
|
35
|
+
generated.push(path.join(root, out));
|
|
36
|
+
return out;
|
|
37
|
+
};
|
|
38
|
+
const mochaFiles = files.filter(isMocha).map(compile);
|
|
39
|
+
const scripts = files.filter((f) => !isMocha(f)).map((f) => path.join('test', f));
|
|
40
|
+
scripts.push(path.join('test', 'verify-scenarios.mjs'));
|
|
41
|
+
|
|
42
|
+
const failures = [];
|
|
43
|
+
const run = (label, cmd, args) => {
|
|
44
|
+
console.log(`\n=== ${label}`);
|
|
45
|
+
const r = spawnSync(cmd, args, {cwd: root, stdio: 'inherit'});
|
|
46
|
+
if (r.status !== 0) failures.push(label);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (mochaFiles.length) {
|
|
50
|
+
run(`mocha (${mochaFiles.length} files)`, process.execPath, [
|
|
51
|
+
path.join(root, 'node_modules/mocha/bin/_mocha'),
|
|
52
|
+
'--exit',
|
|
53
|
+
...mochaFiles,
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
for (const s of scripts) run(s, process.execPath, [s]);
|
|
57
|
+
|
|
58
|
+
for (const g of generated) fs.rmSync(g, {force: true});
|
|
59
|
+
|
|
60
|
+
console.log(`\n${mochaFiles.length} mocha files, ${scripts.length} standalone scripts, ${failures.length} failed`);
|
|
61
|
+
if (failures.length) {
|
|
62
|
+
console.log(`FAILED: ${failures.join(', ')}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|