@aloma.io/integration-sdk 3.8.67 → 3.8.68

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.
@@ -0,0 +1,172 @@
1
+ import {Config} from '../internal/websocket/config.mjs';
2
+ import {WebsocketConnector} from '../internal/websocket/index.mjs';
3
+ import {onConnect} from '../internal/connector/server/on-connect/index.mjs';
4
+ import {onMessage} from '../internal/connector/server/on-message.mjs';
5
+ import type {Connector} from '../internal/connector/index.mjs';
6
+ import {FakeAloma} from './fake-aloma.mjs';
7
+ import {testKeyPair} from './keys.mjs';
8
+ import {VendorMock} from './vendor-mock.mjs';
9
+
10
+ export type HarnessOptions = {
11
+ /**
12
+ * The connector under test, already configured:
13
+ * `const c = new Connector({id, version, name}); c.configure().types(...).resolvers(...).main(...)`
14
+ */
15
+ connector: Connector;
16
+ /** vendor mock; a fresh one is created when omitted */
17
+ vendor?: VendorMock;
18
+ /**
19
+ * configuration values ALOMA sends after the handshake (encrypted unless the field is `plain`).
20
+ * `null` skips sending any configuration (the connector then never starts its main()).
21
+ */
22
+ config?: Record<string, any> | null;
23
+ /** how long to wait for each step, ms */
24
+ timeoutMs?: number;
25
+ };
26
+
27
+ /**
28
+ * Runs a real connector against a fake ALOMA, in-process.
29
+ *
30
+ * It wires the same SDK parts as Connector.run() and makeServer(): Config, WebsocketConnector (connection,
31
+ * registration, transport), onConnect (config decryption, main()), onMessage (dispatch and reply).
32
+ * Left out on purpose: the Prometheus metrics server (fixed port 4050), process signal handlers and
33
+ * env-var configuration, which are global side effects that cannot run once per test.
34
+ */
35
+ export class ConnectorHarness {
36
+
37
+ private constructor(
38
+ readonly aloma: FakeAloma,
39
+ readonly vendor: VendorMock,
40
+ private readonly server: WebsocketConnector,
41
+ private readonly started: Promise<void> | null
42
+ ) {}
43
+
44
+ static async start(options: HarnessOptions): Promise<ConnectorHarness> {
45
+ const {connector, timeoutMs = 5000} = options;
46
+ const dispatcher: any = connector.dispatcher;
47
+ if (!dispatcher) throw new Error('call connector.configure() before starting the harness');
48
+
49
+ const vendor = options.vendor || new VendorMock();
50
+ const aloma = await FakeAloma.start();
51
+ vendor.allowPassthrough((url) => url.origin === new URL(aloma.url).origin);
52
+ vendor.install();
53
+
54
+ // main() is what a connector runs once ALOMA has sent its configuration; observe it
55
+ let mainDone!: () => void;
56
+ let mainFailed!: (e: unknown) => void;
57
+ const started = new Promise<void>((resolve, reject) => {
58
+ mainDone = resolve;
59
+ mainFailed = reject;
60
+ });
61
+ started.catch(() => {});
62
+
63
+ const originalMain = dispatcher._main;
64
+ dispatcher.main(async (arg) => {
65
+ try {
66
+ await originalMain?.(arg);
67
+ mainDone();
68
+ } catch (e) {
69
+ mainFailed(e);
70
+ throw e;
71
+ }
72
+ });
73
+
74
+ let server: WebsocketConnector | undefined;
75
+
76
+ try {
77
+ const {processPacket, start, introspect, configSchema} = dispatcher.build();
78
+ const keys = await testKeyPair();
79
+
80
+ const config = new Config({
81
+ id: connector.id,
82
+ version: connector.version,
83
+ name: connector.name,
84
+ registrationToken: aloma.registrationToken,
85
+ endpoint: aloma.url,
86
+ wsEndpoint: aloma.wsUrl,
87
+ privateKey: keys.privateKey,
88
+ publicKey: keys.publicKey,
89
+ introspect,
90
+ configSchema,
91
+ icon: connector.icon,
92
+ });
93
+
94
+ server = new WebsocketConnector({
95
+ config,
96
+ onConnect: onConnect({config, configSchema, dispatcher, start}),
97
+ onMessage: onMessage(processPacket),
98
+ });
99
+
100
+ const harness = new ConnectorHarness(aloma, vendor, server, options.config === null ? null : started);
101
+
102
+ await server.start();
103
+ await aloma.waitForConnection(timeoutMs);
104
+
105
+ if (options.config !== null) {
106
+ await aloma.setConfig(options.config || {});
107
+ await withTimeout(started, timeoutMs, 'connector main() did not finish after the configuration was sent');
108
+ }
109
+
110
+ return harness;
111
+ } catch (e) {
112
+ await ConnectorHarness.shutdown(server, vendor, aloma);
113
+ throw e;
114
+ }
115
+ }
116
+
117
+ private static async shutdown(server: WebsocketConnector | undefined, vendor: VendorMock, aloma: FakeAloma) {
118
+ try {
119
+ if (server) {
120
+ const anyServer: any = server;
121
+
122
+ await anyServer.leaving();
123
+ await anyServer.close();
124
+
125
+ // the SDK leaves a 45s ping watchdog timer behind; clear it so a test run can exit
126
+ clearTimeout(anyServer.transport?.ws?.pingTimeout);
127
+ }
128
+ } finally {
129
+ vendor.uninstall();
130
+ await aloma.close();
131
+ }
132
+ }
133
+
134
+ /** call a connector method and return the raw answer (`{error}` when it failed) */
135
+ call(method: string | string[], variables?: any, ctx?: {taskId?: string; namespace?: string; sticky?: boolean}) {
136
+ return this.aloma.call(method, variables, ctx);
137
+ }
138
+
139
+ /** call a connector method; throws when the connector answered with an error */
140
+ async callOk(method: string | string[], variables?: any) {
141
+ const answer = await this.call(method, variables);
142
+ if (answer?.error) throw new Error(`${method} failed: ${answer.error}`);
143
+
144
+ return answer;
145
+ }
146
+
147
+ /**
148
+ * Shut down the connector and the fake endpoint, restore fetch and the network guard.
149
+ * Then fails if the connector made any request that was not mocked or the fake endpoint saw a protocol error.
150
+ */
151
+ async stop({verify = true}: {verify?: boolean} = {}) {
152
+ await ConnectorHarness.shutdown(this.server, this.vendor, this.aloma);
153
+
154
+ if (verify) {
155
+ this.vendor.verify();
156
+ if (this.aloma.protocolErrors.length) {
157
+ throw new Error(`protocol errors seen by the fake ALOMA: ${JSON.stringify(this.aloma.protocolErrors)}`);
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ const withTimeout = <T,>(promise: Promise<T>, ms: number, message: string): Promise<T> => {
164
+ let timer: NodeJS.Timeout;
165
+
166
+ return Promise.race([
167
+ promise,
168
+ new Promise<never>((_, reject) => {
169
+ timer = setTimeout(() => reject(new Error(message)), ms);
170
+ }),
171
+ ]).finally(() => clearTimeout(timer));
172
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Test harness for connectors built on the ALOMA integration SDK.
3
+ *
4
+ * import {ConnectorHarness, VendorMock} from '@aloma.io/integration-sdk/build/testing/index.mjs';
5
+ *
6
+ * Test tooling only: nothing in the SDK runtime imports this folder.
7
+ */
8
+ export {ConnectorHarness} from './harness.mjs';
9
+ export type {HarnessOptions} from './harness.mjs';
10
+ export {FakeAloma} from './fake-aloma.mjs';
11
+ export {VendorMock} from './vendor-mock.mjs';
12
+ export type {VendorRequest, VendorResponse, VendorResponder, UrlMatcher} from './vendor-mock.mjs';
13
+ export {installNetworkGuard, uninstallNetworkGuard} from './network-guard.mjs';
14
+ export {testKeyPair} from './keys.mjs';
15
+ export {connectorFromRuntimeContext} from './runtime-connector.mjs';
@@ -0,0 +1,19 @@
1
+ import JWE from '../internal/util/jwe/index.mjs';
2
+
3
+ let pair: Promise<{privateKey: string; publicKey: string}> | undefined;
4
+
5
+ /**
6
+ * One throwaway RSA key pair per process, generated at test time.
7
+ * Never committed, never a real key. Same encoding the connector expects in
8
+ * env.PRIVATE_KEY / env.PUBLIC_KEY (base64 of the PEM).
9
+ */
10
+ export const testKeyPair = () => {
11
+ pair ||= (async () => {
12
+ const jwe = new JWE({algorithm: 'RSA-OAEP-256'});
13
+ await jwe.newPair();
14
+
15
+ return {privateKey: await jwe.exportPrivateAsBase64(), publicKey: await jwe.exportPublicAsBase64()};
16
+ })();
17
+
18
+ return pair;
19
+ };
@@ -0,0 +1,57 @@
1
+ import net from 'node:net';
2
+
3
+ /**
4
+ * Test-only network guard.
5
+ *
6
+ * While installed, any TCP connection to a host that is not loopback throws
7
+ * immediately. fetch (undici), http, https, ws and axios all end up in
8
+ * net.Socket.prototype.connect, so this is the safety net behind the vendor mock:
9
+ * even code that bypasses global fetch cannot reach the network from a test.
10
+ */
11
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
12
+
13
+ let original: typeof net.Socket.prototype.connect | undefined;
14
+ let installs = 0;
15
+ export const blocked: string[] = [];
16
+
17
+ const targetOf = (args: any[]): {host: string; local: boolean} => {
18
+ // net.connect hands over a normalized [options, callback] array
19
+ const first = Array.isArray(args[0]) ? args[0][0] : args[0];
20
+
21
+ if (first && typeof first === 'object' && !Array.isArray(first)) {
22
+ if (first.path) return {host: String(first.path), local: true};
23
+ return {host: String(first.host ?? 'localhost'), local: false};
24
+ }
25
+ if (typeof first === 'number' || (typeof first === 'string' && !Number.isNaN(Number(first)))) {
26
+ return {host: typeof args[1] === 'string' ? args[1] : 'localhost', local: false};
27
+ }
28
+
29
+ // unix socket path
30
+ return {host: String(first), local: true};
31
+ };
32
+
33
+ export const installNetworkGuard = () => {
34
+ if (installs++ > 0) return;
35
+
36
+ original = net.Socket.prototype.connect;
37
+
38
+ (net.Socket.prototype as any).connect = function (this: net.Socket, ...args: any[]) {
39
+ const {host, local} = targetOf(args);
40
+
41
+ if (!local && !LOOPBACK.has(host)) {
42
+ const message = `NETWORK BLOCKED by connector test harness: connection to ${host} is not allowed in tests`;
43
+ blocked.push(host);
44
+ throw new Error(message);
45
+ }
46
+
47
+ return (original as any).apply(this, args);
48
+ };
49
+ };
50
+
51
+ export const uninstallNetworkGuard = () => {
52
+ if (installs === 0) return;
53
+ if (--installs > 0) return;
54
+
55
+ (net.Socket.prototype as any).connect = original;
56
+ original = undefined;
57
+ };
@@ -0,0 +1,50 @@
1
+ import {AbstractController} from '../controller/index.mjs';
2
+ import {buildResolvers} from '../builder/runtime-context.mjs';
3
+ import type RuntimeContext from '../builder/runtime-context.mjs';
4
+ import {Connector} from '../internal/connector/index.mjs';
5
+
6
+ /**
7
+ * Turn what a connector repo builds (`await new Builder()...build()` gives a RuntimeContext) into a configured
8
+ * `Connector` that the ConnectorHarness can run, without calling `connector.run()` (which starts a metrics
9
+ * server on a fixed port and installs process signal handlers).
10
+ *
11
+ * This repeats the wiring of RuntimeContext.start(): methods become resolvers, config/oauth/endpoint are applied,
12
+ * main() starts the controller and sends a health check. If RuntimeContext.start() changes, change this too.
13
+ */
14
+ export const connectorFromRuntimeContext = (context: RuntimeContext): Connector => {
15
+ const {controller, data} = context as any as {controller: AbstractController; data: any};
16
+
17
+ if (!(controller instanceof AbstractController)) {
18
+ throw new Error('the controller needs to extend AbstractController');
19
+ }
20
+
21
+ const connector = new Connector({
22
+ id: data.id,
23
+ version: data.version,
24
+ name: `${data.id}/${data.version}`,
25
+ icon: undefined,
26
+ });
27
+
28
+ const configuration = connector.configure().config(data.config || {});
29
+ const methods: string[] = [...data.methods, '__autocomplete', '__endpoint', '__default'];
30
+
31
+ configuration.types(data.types).resolvers(buildResolvers(methods, controller));
32
+
33
+ if (data.options?.endpoint?.enabled) {
34
+ configuration.endpoint((arg) => (controller as any).__endpoint(arg), data.options?.endpoint?.required);
35
+ }
36
+
37
+ if (data.auth?.oauth) {
38
+ configuration.oauth(data.auth?.oauth);
39
+ }
40
+
41
+ configuration.main(
42
+ async ({newTask, updateTask, config, oauth, getClient, getBlob, getBlobContent, createBlob, healthCheck}) => {
43
+ await (controller as any)._doStop();
44
+ await (controller as any)._doStart(config, oauth, newTask, updateTask, getClient, getBlob, getBlobContent, createBlob);
45
+ await healthCheck(controller);
46
+ }
47
+ );
48
+
49
+ return connector;
50
+ };
@@ -0,0 +1,166 @@
1
+ import {installNetworkGuard, uninstallNetworkGuard} from './network-guard.mjs';
2
+
3
+ export type VendorRequest = {
4
+ method: string;
5
+ url: string;
6
+ /** the url exactly as the connector passed it to fetch */
7
+ rawUrl: string;
8
+ headers: Record<string, string>;
9
+ body?: string;
10
+ };
11
+
12
+ export type VendorResponse = {
13
+ status?: number;
14
+ headers?: Record<string, string>;
15
+ /** sent as JSON with a json content-type */
16
+ json?: unknown;
17
+ /** sent as is */
18
+ text?: string;
19
+ };
20
+
21
+ export type VendorResponder = VendorResponse | ((request: VendorRequest) => VendorResponse | Promise<VendorResponse>);
22
+ export type UrlMatcher = string | RegExp | ((url: string) => boolean);
23
+
24
+ type Route = {
25
+ method: string;
26
+ matcher: UrlMatcher;
27
+ respond: VendorResponder;
28
+ times: number;
29
+ used: number;
30
+ };
31
+
32
+ // "https:/host/x" (one slash, which the SDK Fetcher produces) and "https://host/x" are the same URL to fetch
33
+ const canonical = (url: string): string => {
34
+ try {
35
+ return new URL(url).href;
36
+ } catch (e) {
37
+ return url;
38
+ }
39
+ };
40
+
41
+ const matches = (matcher: UrlMatcher, url: string): boolean => {
42
+ if (typeof matcher === 'string') return canonical(matcher) === url;
43
+ if (matcher instanceof RegExp) return matcher.test(url);
44
+ return matcher(url);
45
+ };
46
+
47
+ /**
48
+ * Mock for the HTTP calls a connector makes to its vendor API.
49
+ *
50
+ * - intercepts global fetch (what the SDK Fetcher and OAuth code use)
51
+ * - answers from routes you register with on()/get()/post()...
52
+ * - any request without a route is recorded in `unmocked` and rejected,
53
+ * verify() then fails the test even if the connector swallowed the error
54
+ * - also installs the network guard, so nothing can reach a non-loopback host
55
+ */
56
+ export class VendorMock {
57
+ readonly requests: VendorRequest[] = [];
58
+ readonly unmocked: VendorRequest[] = [];
59
+ private routes: Route[] = [];
60
+ private originalFetch?: typeof fetch;
61
+ private passthrough: (url: URL) => boolean = () => false;
62
+
63
+ /** urls for which the real fetch is used (the fake ALOMA endpoint); everything else is mocked */
64
+ allowPassthrough(fn: (url: URL) => boolean) {
65
+ this.passthrough = fn;
66
+ return this;
67
+ }
68
+
69
+ on(method: string, matcher: UrlMatcher, respond: VendorResponder, options: {times?: number} = {}) {
70
+ this.routes.push({method: method.toUpperCase(), matcher, respond, times: options.times ?? Infinity, used: 0});
71
+ return this;
72
+ }
73
+
74
+ get(matcher: UrlMatcher, respond: VendorResponder, options?: {times?: number}) {
75
+ return this.on('GET', matcher, respond, options);
76
+ }
77
+ post(matcher: UrlMatcher, respond: VendorResponder, options?: {times?: number}) {
78
+ return this.on('POST', matcher, respond, options);
79
+ }
80
+ put(matcher: UrlMatcher, respond: VendorResponder, options?: {times?: number}) {
81
+ return this.on('PUT', matcher, respond, options);
82
+ }
83
+ delete(matcher: UrlMatcher, respond: VendorResponder, options?: {times?: number}) {
84
+ return this.on('DELETE', matcher, respond, options);
85
+ }
86
+
87
+ install() {
88
+ if (this.originalFetch) return this;
89
+
90
+ installNetworkGuard();
91
+ this.originalFetch = globalThis.fetch;
92
+ globalThis.fetch = ((input: any, init?: any) => this.handle(input, init)) as typeof fetch;
93
+
94
+ return this;
95
+ }
96
+
97
+ uninstall() {
98
+ if (!this.originalFetch) return;
99
+
100
+ globalThis.fetch = this.originalFetch;
101
+ this.originalFetch = undefined;
102
+ uninstallNetworkGuard();
103
+ }
104
+
105
+ /** throws if the connector made a request that had no mock, even if it caught the error */
106
+ verify() {
107
+ if (this.unmocked.length) {
108
+ const list = this.unmocked.map((r) => `${r.method} ${r.url}`).join('\n ');
109
+ throw new Error(`UNMOCKED vendor requests were made (no network is allowed in tests):\n ${list}`);
110
+ }
111
+ }
112
+
113
+ /** throws if a registered route was never used */
114
+ verifyAllRoutesUsed() {
115
+ const unused = this.routes.filter((r) => r.used === 0);
116
+ if (unused.length) {
117
+ throw new Error(`mocked routes never called: ${unused.map((r) => `${r.method} ${String(r.matcher)}`).join(', ')}`);
118
+ }
119
+ }
120
+
121
+ private async handle(input: any, init: any = {}): Promise<Response> {
122
+ const rawUrl: string = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
123
+ const url = canonical(rawUrl);
124
+ const method = String(init.method || (typeof input === 'object' && input.method) || 'GET').toUpperCase();
125
+
126
+ const headers: Record<string, string> = {};
127
+ new Headers(init.headers || (typeof input === 'object' && (input as any).headers) || {}).forEach((value, key) => {
128
+ headers[key] = value;
129
+ });
130
+
131
+ const request: VendorRequest = {method, url, rawUrl, headers};
132
+ if (init.body != null) request.body = typeof init.body === 'string' ? init.body : String(init.body);
133
+
134
+ const route = this.routes.find((r) => r.method === method && r.used < r.times && matches(r.matcher, url));
135
+
136
+ if (route) {
137
+ route.used++;
138
+ this.requests.push(request);
139
+ const spec = typeof route.respond === 'function' ? await route.respond(request) : route.respond;
140
+
141
+ const responseHeaders = {...spec.headers};
142
+ let body: string | null = null;
143
+ if (spec.json !== undefined) {
144
+ body = JSON.stringify(spec.json);
145
+ responseHeaders['content-type'] ||= 'application/json';
146
+ } else if (spec.text !== undefined) {
147
+ body = spec.text;
148
+ }
149
+
150
+ const status = spec.status ?? 200;
151
+ return new Response([204, 205, 304].includes(status) ? null : body, {status, headers: responseHeaders});
152
+ }
153
+
154
+ if (this.originalFetch && this.passthrough(new URL(url))) {
155
+ return this.originalFetch(input, init);
156
+ }
157
+
158
+ this.unmocked.push(request);
159
+
160
+ // status 400 makes the SDK Fetcher give up at once instead of retrying for seconds
161
+ const error: any = new Error(`UNMOCKED vendor request: ${method} ${url} (add a route with vendor.on(...))`);
162
+ error.status = 400;
163
+ error.alomaHarnessUnmocked = true;
164
+ throw error;
165
+ }
166
+ }
@@ -0,0 +1,21 @@
1
+ # Mutation check for the SDK tests that `yarn test` now runs
2
+
3
+ Result: **8/8 mutants killed** (24 Oct). Run with `yarn build && node tools/mutation-run-entry.mjs`.
4
+
5
+ Each row is one deliberate single-point bug in `src/openapi-to-connector.mts` (the OpenAPI to connector generator). The
6
+ script applies it alone, rebuilds, runs the full `yarn test`, expects a failure, and restores the file.
7
+
8
+ | # | Bug | Result |
9
+ |---|-----|--------|
10
+ | E01 | the HTTP verb is taken in lower case | killed |
11
+ | E02 | path parameter substitution is guarded by the wrong condition | killed |
12
+ | E03 | fallback method name drops the separator | killed |
13
+ | E04 | body properties are documented with the wrong required flag | killed |
14
+ | E05 | headers are dropped from the generated options type | killed |
15
+ | E06 | options is not defaulted in generated methods | killed |
16
+ | E07 | invalid specs are no longer rejected | killed |
17
+ | E08 | a property is required only if both the schema and the body say so | killed |
18
+
19
+ A first run left two survivors: E02 (nothing asserted the path parameter substitution) and a draft E03 aimed at
20
+ `disambiguateMethodName` (no test reaches it, so a different mutant on the fallback name was used). A test for E02 was
21
+ added to `test/openapi-to-connector.test.mts`; `disambiguateMethodName` remains without direct coverage.
@@ -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,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
+ });