@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
package/test/MUTATION.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Mutation check for the connector test harness
|
|
2
|
+
|
|
3
|
+
Result: **32/32 mutants killed**.
|
|
4
|
+
|
|
5
|
+
Each row is one deliberate single-point bug in the SDK code paths the harness exercises (handshake, transport,
|
|
6
|
+
dispatch, result return, config) or in the harness itself. `tools/mutation-run.mjs` applies it to `src/`, rebuilds,
|
|
7
|
+
runs `test/harness.test.mjs` with `--bail`, expects a failure, and restores the file. Re-run with
|
|
8
|
+
`yarn build && yarn test:mutation` after changing the harness or those SDK paths.
|
|
9
|
+
|
|
10
|
+
| # | Area | File | Bug | Result |
|
|
11
|
+
|---|------|------|-----|--------|
|
|
12
|
+
| M01 | sdk handshake | `src/internal/websocket/connection/index.mjs` | a 401 from /connect no longer triggers registration | killed |
|
|
13
|
+
| M02 | sdk handshake | `src/internal/websocket/connection/index.mjs` | the token from registration is not stored | killed |
|
|
14
|
+
| M03 | sdk handshake | `src/internal/websocket/connection/registration.mjs` | registration reads the wrong field of the answer | killed |
|
|
15
|
+
| M04 | sdk handshake | `src/internal/websocket/connection/registration.mjs` | registration sends the name as the connector id | killed |
|
|
16
|
+
| M05 | sdk handshake | `src/internal/websocket/connection/registration.mjs` | registration omits the public key | killed |
|
|
17
|
+
| M06 | sdk handshake | `src/internal/websocket/connection/constants.mjs` | authenticated requests drop the version from the User-Agent | killed |
|
|
18
|
+
| M07 | sdk handshake | `src/internal/websocket/connection/constants.mjs` | wrong authorization scheme for the session token | killed |
|
|
19
|
+
| M08 | sdk handshake | `src/internal/websocket/transport/index.mjs` | websocket opened without the "connector" subprotocol | killed |
|
|
20
|
+
| M09 | sdk transport | `src/internal/websocket/transport/index.mjs` | the first packet of every incoming frame is dropped | killed |
|
|
21
|
+
| M10 | sdk transport | `src/internal/websocket/transport/index.mjs` | outgoing frames use the wrong envelope | killed |
|
|
22
|
+
| M11 | sdk transport | `src/internal/websocket/transport/index.mjs` | answers to connector-initiated requests are never routed to their callback | killed |
|
|
23
|
+
| M12 | sdk dispatch | `src/internal/dispatcher/index.mts` | configuration secrets are dropped before onConfig | killed |
|
|
24
|
+
| M13 | sdk dispatch | `src/internal/dispatcher/index.mts` | sticky is not forwarded to the method | killed |
|
|
25
|
+
| M14 | sdk dispatch | `src/internal/dispatcher/index.mts` | unknown methods are no longer reported as "not found" | killed |
|
|
26
|
+
| M15 | sdk result return | `src/internal/dispatcher/index.mts` | scalar results are wrapped under the wrong key | killed |
|
|
27
|
+
| M16 | sdk result return | `src/internal/util/index.mts` | successful results are replied empty | killed |
|
|
28
|
+
| M17 | sdk result return | `src/internal/util/index.mts` | errors lose their message | killed |
|
|
29
|
+
| M18 | sdk result return | `src/internal/connector/server/on-message.mts` | results are never sent back | killed |
|
|
30
|
+
| M19 | sdk config | `src/internal/connector/server/on-connect/decrypt-config.mts` | config is decrypted for the wrong audience | killed |
|
|
31
|
+
| M20 | sdk config | `src/internal/connector/server/on-connect/decrypt-config.mts` | plain fields are decrypted like secrets | killed |
|
|
32
|
+
| M21 | sdk config | `src/internal/connector/server/on-connect/index.mts` | main() receives an empty config | killed |
|
|
33
|
+
| M22 | harness vendor mock | `src/testing/vendor-mock.mts` | an unmocked request is answered instead of rejected | killed |
|
|
34
|
+
| M23 | harness vendor mock | `src/testing/vendor-mock.mts` | verify() never fails | killed |
|
|
35
|
+
| M24 | harness vendor mock | `src/testing/vendor-mock.mts` | mocked status codes are ignored | killed |
|
|
36
|
+
| M25 | harness vendor mock | `src/testing/vendor-mock.mts` | the times limit on a route is ignored | killed |
|
|
37
|
+
| M26 | harness network guard | `src/testing/network-guard.mts` | the network guard lets everything through (the mutant may try one TCP connect to the TEST-NET-3 documentation address 203.0.113.9) | killed |
|
|
38
|
+
| M27 | harness fake ALOMA | `src/testing/fake-aloma.mts` | the fake accepts a connect without registration | killed |
|
|
39
|
+
| M28 | harness fake ALOMA | `src/testing/fake-aloma.mts` | the fake encrypts config for the wrong audience | killed |
|
|
40
|
+
| M29 | harness fake ALOMA | `src/testing/fake-aloma.mts` | the fake drops the connector's answer | killed |
|
|
41
|
+
| M30 | harness fake ALOMA | `src/testing/fake-aloma.mts` | call() drops the ctx fields | killed |
|
|
42
|
+
| M31 | harness | `src/testing/harness.mts` | stop() no longer fails on unmocked requests | killed |
|
|
43
|
+
| M32 | harness | `src/testing/harness.mts` | the harness never sends configuration | killed |
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"text": "\n/**\n * greet somebody\n *\n * @namespace demo\n * @example usage\n```javascript\ngreet({name: 'Ann'})\n```\n **/ \ndeclare function greet({name; times = 2; loud = false; label = 'x'; neg = -1; nothing}: {\n/**\nwho to greet, e.g. Ann\n */\n name: any; \n/**\nhow often\n */\n times: any; \n/**\n\n */\n loud: any; \n/**\n\n */\n label: any; \n/**\n\n */\n neg: any; \n/**\n\n */\n nothing: any}): { ok: boolean; greeting: any; };\n \n/**\n * no params\n *\n * \n * \n **/ \ndeclare function ping(): { ok: boolean; };\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function list(): string[];\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function nothing(): void;\n ",
|
|
3
|
+
"methods": [
|
|
4
|
+
"greet",
|
|
5
|
+
"ping",
|
|
6
|
+
"list",
|
|
7
|
+
"nothing"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {AbstractController} from './abstract.mjs';
|
|
2
|
+
|
|
3
|
+
export default class Controller extends AbstractController {
|
|
4
|
+
/**
|
|
5
|
+
* greet somebody
|
|
6
|
+
* @param name - who to greet, e.g. Ann
|
|
7
|
+
* @param times - how often
|
|
8
|
+
* @param opts - extra \@options
|
|
9
|
+
* @namespace demo
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```javascript
|
|
13
|
+
* greet({name: 'Ann'})
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
async greet({name, times = 2, loud = false, label = 'x', neg = -1, nothing = null}, other = 1) {
|
|
17
|
+
return {ok: true, greeting: name + times};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** no params */
|
|
21
|
+
async ping() {
|
|
22
|
+
return {ok: true};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async list(): Promise<string[]> {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async nothing(): Promise<void> {}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {AbstractController} from './abstract.mjs';
|
|
2
|
+
|
|
3
|
+
interface Opts {
|
|
4
|
+
id: string;
|
|
5
|
+
n?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default class Controller extends AbstractController {
|
|
9
|
+
async viaInterface({id, n}: Opts): Promise<{id: string; n: number | undefined}> {
|
|
10
|
+
return {id, n};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {AbstractController} from './abstract.mjs';
|
|
2
|
+
|
|
3
|
+
export default class Controller extends AbstractController {
|
|
4
|
+
/** first form */
|
|
5
|
+
async over({a}: {a: string}): Promise<string>;
|
|
6
|
+
/** second form */
|
|
7
|
+
async over({b}: {b: number}): Promise<number>;
|
|
8
|
+
async over(args: any): Promise<any> {
|
|
9
|
+
return args;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"text": "\n/**\n * Returns recorded calls scoped to the current task.\n *\n * \n * @example usage\n```javascript\nconst log = await connectors.testing.getCallLog({})\n// log.calls = [{method, args, timestamp}, ...] for THIS task only\n```\n **/ \ndeclare function getCallLog(): { calls: CallRecord[]; count: number; };\n \n/**\n * Resets the call log for the current task only.\n *\n * \n * @example usage\n```javascript\nawait connectors.testing.clearCallLog({})\n```\n **/ \ndeclare function clearCallLog(): { cleared: boolean; };\n \n/**\n * Updates the response map for the current task at runtime.\n *\n * \n * @example usage\n```javascript\nawait connectors.testing.setResponses({\n responses: { \"someConnector.create\": { id: \"456\" } }\n})\n```\n **/ \ndeclare function setResponses(): { updated: boolean; responseCount: number; };\n ",
|
|
3
|
+
"methods": [
|
|
4
|
+
"getCallLog",
|
|
5
|
+
"clearCallLog",
|
|
6
|
+
"setResponses"
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { AbstractController } from "./abstract.mjs";
|
|
2
|
+
|
|
3
|
+
interface CallRecord {
|
|
4
|
+
method: string;
|
|
5
|
+
args: any;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Per-task scoped mock connector for ALOMA test workflows.
|
|
11
|
+
*
|
|
12
|
+
* State (responseMap, callLog) is keyed by ctx.taskId so concurrent
|
|
13
|
+
* test tasks against the same Testing pod do not contaminate each
|
|
14
|
+
* other's stubbed responses or recorded calls.
|
|
15
|
+
*
|
|
16
|
+
* Requires @aloma.io/integration-sdk ^3.8.67, which forwards
|
|
17
|
+
* { taskId, namespace, sticky } as the second arg to controller
|
|
18
|
+
* method handlers and to the __default fallback.
|
|
19
|
+
*
|
|
20
|
+
* Backward compatibility: when ctx is missing (legacy invocation or
|
|
21
|
+
* non-task call from admin UI), state falls into the "__no_ctx__"
|
|
22
|
+
* bucket so the connector does not crash. Isolation is NOT guaranteed
|
|
23
|
+
* in that mode — it is degraded compatibility only.
|
|
24
|
+
*/
|
|
25
|
+
export default class Controller extends AbstractController {
|
|
26
|
+
// Per-task scoped state — Maps keyed by taskId (insertion order = LRU order)
|
|
27
|
+
private responseMapByTask: Map<string, Record<string, any>> = new Map();
|
|
28
|
+
private callLogByTask: Map<string, CallRecord[]> = new Map();
|
|
29
|
+
private recording: boolean = true;
|
|
30
|
+
private defaultResponses: Record<string, any> = {};
|
|
31
|
+
// Memory cap (LRU). Configurable via config.maxTasks; default 1000.
|
|
32
|
+
private maxTasks: number = 1000;
|
|
33
|
+
|
|
34
|
+
protected async start() {
|
|
35
|
+
this.loadConfig();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private loadConfig() {
|
|
39
|
+
const config = this.config;
|
|
40
|
+
|
|
41
|
+
// Parse default responses from connector config — these seed each
|
|
42
|
+
// task's bucket when first touched.
|
|
43
|
+
if (config.responses) {
|
|
44
|
+
try {
|
|
45
|
+
this.defaultResponses =
|
|
46
|
+
typeof config.responses === "string"
|
|
47
|
+
? JSON.parse(config.responses)
|
|
48
|
+
: config.responses;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
this.defaultResponses = {};
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
this.defaultResponses = {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.recording = config.recordCalls !== "false";
|
|
57
|
+
|
|
58
|
+
// Allow config-driven override of LRU cap. Default 1000 if not set
|
|
59
|
+
// or not a positive number.
|
|
60
|
+
const configured = Number(config.maxTasks);
|
|
61
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
62
|
+
this.maxTasks = Math.floor(configured);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the bucket key for the current invocation. When ctx is
|
|
68
|
+
* absent or lacks taskId, falls back to a single shared bucket so
|
|
69
|
+
* legacy / hand-rolled callers do not crash. Isolation is not
|
|
70
|
+
* guaranteed in that mode.
|
|
71
|
+
*/
|
|
72
|
+
private bucketKey(ctx?: { taskId?: string }): string {
|
|
73
|
+
return ctx && ctx.taskId ? ctx.taskId : "__no_ctx__";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* LRU touch — ensures the bucket exists for the given key, evicting
|
|
78
|
+
* the least-recently-used entry if the cap would be exceeded. Map
|
|
79
|
+
* iteration order is insertion order; reinserting promotes a key to
|
|
80
|
+
* "most recently used" position.
|
|
81
|
+
*/
|
|
82
|
+
private touchTask(key: string) {
|
|
83
|
+
// If not present and at cap, evict the oldest (insertion order)
|
|
84
|
+
if (!this.responseMapByTask.has(key)) {
|
|
85
|
+
if (this.responseMapByTask.size >= this.maxTasks) {
|
|
86
|
+
const firstKey = this.responseMapByTask.keys().next().value as
|
|
87
|
+
| string
|
|
88
|
+
| undefined;
|
|
89
|
+
if (firstKey !== undefined) {
|
|
90
|
+
this.responseMapByTask.delete(firstKey);
|
|
91
|
+
this.callLogByTask.delete(firstKey);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.responseMapByTask.set(key, { ...this.defaultResponses });
|
|
95
|
+
this.callLogByTask.set(key, []);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Catch-all handler — registered as __default by the SDK via the
|
|
101
|
+
* fallback override below. Looks up the response in the task-scoped
|
|
102
|
+
* map, records the call in the task-scoped log, and returns the
|
|
103
|
+
* configured response or a default stub acknowledgement.
|
|
104
|
+
*/
|
|
105
|
+
private async handleStubCall(
|
|
106
|
+
arg: any,
|
|
107
|
+
ctx?: { taskId?: string },
|
|
108
|
+
): Promise<any> {
|
|
109
|
+
const key = this.bucketKey(ctx);
|
|
110
|
+
this.touchTask(key);
|
|
111
|
+
|
|
112
|
+
const method = Array.isArray(arg?.__method)
|
|
113
|
+
? arg.__method.join(".")
|
|
114
|
+
: arg?.__method || "unknown";
|
|
115
|
+
|
|
116
|
+
if (this.recording) {
|
|
117
|
+
const log = this.callLogByTask.get(key)!;
|
|
118
|
+
log.push({
|
|
119
|
+
method,
|
|
120
|
+
args: { ...arg, __method: undefined },
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const responseMap = this.responseMapByTask.get(key)!;
|
|
126
|
+
if (responseMap[method] !== undefined) {
|
|
127
|
+
return responseMap[method];
|
|
128
|
+
}
|
|
129
|
+
if (responseMap["default"] !== undefined) {
|
|
130
|
+
return responseMap["default"];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { stubbed: true, method };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* SDK override hook — invoked by AbstractController.__default with
|
|
138
|
+
* the per-invocation ctx (taskId, namespace, sticky) as second arg.
|
|
139
|
+
*/
|
|
140
|
+
protected fallback(arg: any, ctx?: { taskId?: string }): Promise<any> {
|
|
141
|
+
return this.handleStubCall(arg, ctx);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns recorded calls scoped to the current task.
|
|
146
|
+
* @example
|
|
147
|
+
* ```javascript
|
|
148
|
+
* const log = await connectors.testing.getCallLog({})
|
|
149
|
+
* // log.calls = [{method, args, timestamp}, ...] for THIS task only
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
async getCallLog(_args: any, ctx?: { taskId?: string }) {
|
|
153
|
+
const key = this.bucketKey(ctx);
|
|
154
|
+
const calls = this.callLogByTask.get(key) || [];
|
|
155
|
+
return { calls: [...calls], count: calls.length };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resets the call log for the current task only.
|
|
160
|
+
* @example
|
|
161
|
+
* ```javascript
|
|
162
|
+
* await connectors.testing.clearCallLog({})
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
async clearCallLog(_args: any, ctx?: { taskId?: string }) {
|
|
166
|
+
const key = this.bucketKey(ctx);
|
|
167
|
+
if (this.callLogByTask.has(key)) {
|
|
168
|
+
this.callLogByTask.set(key, []);
|
|
169
|
+
}
|
|
170
|
+
return { cleared: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Updates the response map for the current task at runtime.
|
|
175
|
+
* @example
|
|
176
|
+
* ```javascript
|
|
177
|
+
* await connectors.testing.setResponses({
|
|
178
|
+
* responses: { "someConnector.create": { id: "456" } }
|
|
179
|
+
* })
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
async setResponses(args: any, ctx?: { taskId?: string }) {
|
|
183
|
+
const key = this.bucketKey(ctx);
|
|
184
|
+
this.touchTask(key);
|
|
185
|
+
|
|
186
|
+
if (args?.responses) {
|
|
187
|
+
try {
|
|
188
|
+
const newResponses =
|
|
189
|
+
typeof args.responses === "string"
|
|
190
|
+
? JSON.parse(args.responses)
|
|
191
|
+
: args.responses;
|
|
192
|
+
const existing = this.responseMapByTask.get(key)!;
|
|
193
|
+
this.responseMapByTask.set(key, { ...existing, ...newResponses });
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// ignore parse errors — leave existing map untouched
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
updated: true,
|
|
201
|
+
responseCount: Object.keys(this.responseMapByTask.get(key)!).length,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"text": "\n/**\n * \n *\n * \n * \n **/ \ndeclare function typed({email; count; tags; mode; either; nested}: {\n/**\n\n */\n email: string; \n/**\n\n */\n count: any; \n/**\n\n */\n tags: string[]; \n/**\n\n */\n mode: any; \n/**\n\n */\n either: any; \n/**\n\n */\n nested: any}): { ok: boolean; tags: number[]; };\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function tuple(): [string, number];\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function maybe(): string | undefined;\n ",
|
|
3
|
+
"methods": [
|
|
4
|
+
"typed",
|
|
5
|
+
"tuple",
|
|
6
|
+
"maybe"
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {AbstractController} from './abstract.mjs';
|
|
2
|
+
|
|
3
|
+
export default class Controller extends AbstractController {
|
|
4
|
+
async typed({email, count, tags, mode, either, nested}: {email: string; count?: number; tags: string[]; mode: 'a' | 'b'; either: string | number; nested: {x: number}}) {
|
|
5
|
+
return {ok: true, tags: [1, 2]};
|
|
6
|
+
}
|
|
7
|
+
async tuple(): Promise<[string, number]> {
|
|
8
|
+
return ['a', 1];
|
|
9
|
+
}
|
|
10
|
+
async maybe(): Promise<string | undefined> {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"text": "\n/**\n * \n *\n * \n * \n **/ \ndeclare function visible({a}: {\n/**\n\n */\n a: string}): string;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function quoted-name({a}: {\n/**\n\n */\n a: string}): void;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function arrow({a}: {\n/**\n\n */\n a: string}): string;\n \n/**\n * \n *\n * \n * \n **/ \ndeclare function inheritedPublic(): void;\n ",
|
|
3
|
+
"methods": [
|
|
4
|
+
"visible",
|
|
5
|
+
"quoted-name",
|
|
6
|
+
"arrow",
|
|
7
|
+
"inheritedPublic"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {AbstractController} from './abstract.mjs';
|
|
2
|
+
|
|
3
|
+
export default class Controller extends AbstractController {
|
|
4
|
+
async visible({a}: {a: string}) {
|
|
5
|
+
return a;
|
|
6
|
+
}
|
|
7
|
+
private async hidden({a}: {a: string}) {}
|
|
8
|
+
protected async guarded({a}: {a: string}) {}
|
|
9
|
+
static async stat({a}: {a: string}) {}
|
|
10
|
+
async _underscore({a}: {a: string}) {}
|
|
11
|
+
async 'quoted-name'({a}: {a: string}) {}
|
|
12
|
+
async '_quoted_private'({a}: {a: string}) {}
|
|
13
|
+
arrow = async ({a}: {a: string}) => a;
|
|
14
|
+
notAMethod = 5;
|
|
15
|
+
get getter() {
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
override async inheritedPublic() {}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"compilerOptions":{"target":"es2022","module":"node16","moduleResolution":"node16","skipLibCheck":true,"strict":true,"noImplicitAny":false,"experimentalDecorators":true,"esModuleInterop":true},"include":["src/**/*"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Handshake smoke test: the one file a connector repo adds (see "Testing a connector" in the SDK README).
|
|
2
|
+
//
|
|
3
|
+
// In a connector repo the RuntimeContext comes from the repo's own builder after `yarn build`:
|
|
4
|
+
// const context = await new Builder().config({...}).build();
|
|
5
|
+
// Here the controller is defined inline so this file also tests the harness path used by connector repos.
|
|
6
|
+
import {describe, it, afterEach} from 'mocha';
|
|
7
|
+
import {expect} from 'chai';
|
|
8
|
+
import {AbstractController} from '../build/index.mjs';
|
|
9
|
+
import RuntimeContext from '../build/builder/runtime-context.mjs';
|
|
10
|
+
import {ConnectorHarness, connectorFromRuntimeContext} from '../build/testing/index.mjs';
|
|
11
|
+
|
|
12
|
+
const VENDOR = 'https://api.crm.example.test';
|
|
13
|
+
|
|
14
|
+
class Controller extends AbstractController {
|
|
15
|
+
async getContact({id}) {
|
|
16
|
+
return this.getClient({baseUrl: VENDOR, retry: 1, customize: (r) => {
|
|
17
|
+
r.headers = {...r.headers, Authorization: `Bearer ${this.config.apiKey}`};
|
|
18
|
+
}}).fetch(`/contacts/${id}`, {method: 'GET'});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const context = () =>
|
|
23
|
+
new RuntimeContext(new Controller(), {
|
|
24
|
+
id: 'crm-connector',
|
|
25
|
+
version: '0.0.1',
|
|
26
|
+
methods: ['getContact'],
|
|
27
|
+
types: 'type Query { getContact(id: String): String }',
|
|
28
|
+
config: {fields: {apiKey: {name: 'API Key', type: 'line'}}},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('crm-connector handshake smoke test', function () {
|
|
32
|
+
this.timeout(15000);
|
|
33
|
+
let harness;
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
// stop() also fails the test when the connector made a request that was not mocked
|
|
37
|
+
if (harness) await harness.stop();
|
|
38
|
+
harness = undefined;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('registers, connects, starts and reports healthy', async () => {
|
|
42
|
+
harness = await ConnectorHarness.start({
|
|
43
|
+
connector: connectorFromRuntimeContext(context()),
|
|
44
|
+
config: {apiKey: 'fake-api-key-not-real'},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(harness.aloma.registration.id).to.equal('crm-connector');
|
|
48
|
+
expect(harness.aloma.registration.schema.configSchema.fields).to.have.property('apiKey');
|
|
49
|
+
|
|
50
|
+
const health = await harness.aloma.waitForPacket('connector.health.check');
|
|
51
|
+
expect(health.a).to.deep.equal({ok: true, error: null});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('answers a method call from a recorded vendor response', async () => {
|
|
55
|
+
harness = await ConnectorHarness.start({
|
|
56
|
+
connector: connectorFromRuntimeContext(context()),
|
|
57
|
+
config: {apiKey: 'fake-api-key-not-real'},
|
|
58
|
+
});
|
|
59
|
+
harness.vendor.get(`${VENDOR}/contacts/42`, {json: {id: 42, email: 'jane@example.test'}});
|
|
60
|
+
|
|
61
|
+
expect(await harness.callOk('getContact', {id: 42})).to.deep.equal({id: 42, email: 'jane@example.test'});
|
|
62
|
+
expect(harness.vendor.requests[0].headers.authorization).to.equal('Bearer fake-api-key-not-real');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('reports a missing configuration value as unhealthy', async () => {
|
|
66
|
+
harness = await ConnectorHarness.start({connector: connectorFromRuntimeContext(context()), config: {}});
|
|
67
|
+
|
|
68
|
+
const health = await harness.aloma.waitForPacket('connector.health.check');
|
|
69
|
+
expect(health.a.ok).to.equal(false);
|
|
70
|
+
expect(health.a.error).to.match(/API Key is required/);
|
|
71
|
+
});
|
|
72
|
+
});
|