@deepexi/datasense-connector 3.0.1 → 3.1.0
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 +158 -139
- package/package.json +19 -28
- package/scripts/build.ts +188 -0
- package/src/define.ts +267 -0
- package/src/descriptor.ts +116 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +108 -0
- package/src/internal/location.ts +23 -0
- package/src/runtime/guest/bridge.ts +238 -0
- package/src/runtime/guest/cache.ts +43 -0
- package/src/runtime/guest/check.ts +17 -0
- package/src/runtime/guest/crypto.ts +166 -0
- package/src/runtime/guest/ctx.ts +157 -0
- package/src/runtime/guest/db.ts +59 -0
- package/src/runtime/guest/failure.ts +69 -0
- package/src/runtime/guest/http.ts +194 -0
- package/src/runtime/guest/index.ts +102 -0
- package/{runtime/guest/limits.d.ts → src/runtime/guest/limits.ts} +12 -12
- package/src/runtime/guest/loader.ts +210 -0
- package/src/runtime/guest/log.ts +47 -0
- package/src/runtime/guest/map.ts +45 -0
- package/src/runtime/guest/operation.ts +193 -0
- package/src/runtime/guest/paginate.ts +169 -0
- package/src/runtime/guest/pull.ts +177 -0
- package/src/runtime/guest/push.ts +92 -0
- package/src/runtime/guest/signal.ts +14 -0
- package/src/runtime/guest/suggest.ts +51 -0
- package/src/runtime/guest/time.ts +137 -0
- package/src/runtime/guest-runtime.ts +75 -0
- package/src/runtime/index.ts +53 -0
- package/src/runtime/protocol.ts +557 -0
- package/src/runtime/session.ts +216 -0
- package/src/testing/child.ts +21 -0
- package/src/testing/compliance.ts +343 -0
- package/src/testing/engine.ts +445 -0
- package/src/testing/fixtures.ts +591 -0
- package/src/testing/gateway.ts +482 -0
- package/src/testing/import-guard.ts +221 -0
- package/src/testing/index.ts +43 -0
- package/src/testing/local-db.ts +226 -0
- package/src/testing/mock-external-system.ts +249 -0
- package/src/testing/pinned-http.ts +144 -0
- package/src/testing/recorder.ts +140 -0
- package/src/testing/redaction.ts +94 -0
- package/src/testing/replay.ts +437 -0
- package/src/testing/run-tests.ts +115 -0
- package/src/testing/schema.ts +70 -0
- package/src/testing/source-loader.ts +374 -0
- package/src/testing/types.ts +78 -0
- package/src/types.ts +329 -0
- package/src/validate.ts +267 -0
- package/test/build.test.ts +159 -0
- package/test/define.test-d.ts +182 -0
- package/test/fixtures/acceptance.ts +156 -0
- package/test/fixtures/connectors/http-pull/.ontos/descriptor.json +72 -0
- package/test/fixtures/connectors/http-pull/.ontos/types.ts +61 -0
- package/test/fixtures/connectors/http-pull/package.json +5 -0
- package/test/fixtures/connectors/http-pull/streams/items/index.ts +37 -0
- package/test/fixtures/connectors/http-pull/streams/items/map.ts +12 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/case.json +15 -0
- package/test/fixtures/connectors/http-pull/streams/items/tests/basic/exchanges.ndjson +2 -0
- package/test/fixtures/connectors/pg-pull/.ontos/descriptor.json +151 -0
- package/test/fixtures/connectors/pg-pull/.ontos/types.ts +62 -0
- package/test/fixtures/connectors/pg-pull/lib/sql.ts +14 -0
- package/test/fixtures/connectors/pg-pull/package.json +5 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/index.ts +30 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/case.json +15 -0
- package/test/fixtures/connectors/pg-pull/streams/customers/tests/customers-full/exchanges.ndjson +3 -0
- package/test/fixtures/connectors/pg-pull/streams/orders/index.ts +32 -0
- package/test/fixtures/descriptor.ts +54 -0
- package/test/fixtures/kingdee-example.ts +82 -0
- package/test/fixtures/record-acceptance.ts +32 -0
- package/test/runtime/ctx.test.ts +434 -0
- package/test/runtime/helpers.ts +157 -0
- package/test/runtime/jobs.test.ts +444 -0
- package/test/runtime/operation.test.ts +175 -0
- package/test/runtime/session.test.ts +229 -0
- package/test/testing/acceptance-fixtures.test.ts +69 -0
- package/test/testing/compliance.test.ts +247 -0
- package/test/testing/engine.test.ts +283 -0
- package/test/testing/fixtures.test.ts +221 -0
- package/test/testing/gateway.test.ts +337 -0
- package/test/testing/import-guard.test.ts +45 -0
- package/test/testing/mock-external-system.test.ts +144 -0
- package/test/testing/operation-fixtures.test.ts +160 -0
- package/test/testing/recorder.test.ts +163 -0
- package/test/testing/redaction.test.ts +54 -0
- package/test/testing/replay.test.ts +207 -0
- package/test/testing/run-tests.test.ts +137 -0
- package/test/testing/source-loader.test.ts +220 -0
- package/test/validate-config.test.ts +104 -0
- package/tsconfig.build.json +23 -0
- package/tsconfig.json +19 -0
- package/cjs/index.cjs +0 -1015
- package/cjs/runtime.cjs +0 -752
- package/cjs/testing.cjs +0 -3591
- package/errors.d.ts +0 -17
- package/errors.js +0 -53
- package/guest.cjs +0 -2659
- package/index.d.ts +0 -13
- package/index.js +0 -7
- package/internal/location.d.ts +0 -6
- package/internal/location.js +0 -8
- package/presentation.d.ts +0 -38
- package/presentation.js +0 -268
- package/runtime/guest/bridge.d.ts +0 -53
- package/runtime/guest/bridge.js +0 -187
- package/runtime/guest/cache.d.ts +0 -3
- package/runtime/guest/cache.js +0 -42
- package/runtime/guest/crypto.d.ts +0 -13
- package/runtime/guest/crypto.js +0 -177
- package/runtime/guest/ctx.d.ts +0 -30
- package/runtime/guest/ctx.js +0 -124
- package/runtime/guest/db.d.ts +0 -6
- package/runtime/guest/db.js +0 -59
- package/runtime/guest/discover.d.ts +0 -29
- package/runtime/guest/discover.js +0 -51
- package/runtime/guest/failure.d.ts +0 -24
- package/runtime/guest/failure.js +0 -63
- package/runtime/guest/http.d.ts +0 -4
- package/runtime/guest/http.js +0 -204
- package/runtime/guest/index.d.ts +0 -12
- package/runtime/guest/index.js +0 -89
- package/runtime/guest/limits.js +0 -12
- package/runtime/guest/loader.d.ts +0 -15
- package/runtime/guest/loader.js +0 -42
- package/runtime/guest/log.d.ts +0 -10
- package/runtime/guest/log.js +0 -36
- package/runtime/guest/map.d.ts +0 -4
- package/runtime/guest/map.js +0 -45
- package/runtime/guest/paginate.d.ts +0 -6
- package/runtime/guest/paginate.js +0 -180
- package/runtime/guest/pull.d.ts +0 -12
- package/runtime/guest/pull.js +0 -150
- package/runtime/guest/push.d.ts +0 -18
- package/runtime/guest/push.js +0 -80
- package/runtime/guest/signal.d.ts +0 -3
- package/runtime/guest/signal.js +0 -12
- package/runtime/guest/time.d.ts +0 -5
- package/runtime/guest/time.js +0 -127
- package/runtime/guest-runtime.d.ts +0 -7
- package/runtime/guest-runtime.js +0 -51
- package/runtime/index.d.ts +0 -18
- package/runtime/index.js +0 -3
- package/runtime/protocol.d.ts +0 -276
- package/runtime/protocol.js +0 -409
- package/runtime/session.d.ts +0 -12
- package/runtime/session.js +0 -191
- package/t.d.ts +0 -18
- package/t.js +0 -26
- package/testing/child.cjs +0 -350
- package/testing/child.d.ts +0 -1
- package/testing/child.js +0 -12
- package/testing/compliance.d.ts +0 -41
- package/testing/compliance.js +0 -291
- package/testing/engine.d.ts +0 -66
- package/testing/engine.js +0 -339
- package/testing/fixtures.d.ts +0 -145
- package/testing/fixtures.js +0 -407
- package/testing/gateway.d.ts +0 -65
- package/testing/gateway.js +0 -402
- package/testing/import-guard.d.ts +0 -57
- package/testing/import-guard.js +0 -187
- package/testing/index.d.ts +0 -33
- package/testing/index.js +0 -10
- package/testing/local-db.d.ts +0 -27
- package/testing/local-db.js +0 -181
- package/testing/pinned-http.d.ts +0 -14
- package/testing/pinned-http.js +0 -125
- package/testing/recorder.d.ts +0 -15
- package/testing/recorder.js +0 -116
- package/testing/redaction.d.ts +0 -28
- package/testing/redaction.js +0 -67
- package/testing/replay.d.ts +0 -50
- package/testing/replay.js +0 -313
- package/testing/schema.d.ts +0 -13
- package/testing/schema.js +0 -65
- package/testing/source-loader.d.ts +0 -19
- package/testing/source-loader.js +0 -238
- package/testing/types.d.ts +0 -79
- package/testing/types.js +0 -18
- package/types.d.ts +0 -302
- package/types.js +0 -13
- package/validate.d.ts +0 -78
- package/validate.js +0 -597
package/testing/fixtures.js
DELETED
|
@@ -1,407 +0,0 @@
|
|
|
1
|
-
import { REDACT_MIN_LENGTH, REDACTED } from './redaction.js';
|
|
2
|
-
export class FixtureFormatError extends Error {
|
|
3
|
-
file;
|
|
4
|
-
line;
|
|
5
|
-
constructor(message, file, line = null) {
|
|
6
|
-
super(line === null ? `${file}: ${message}` : `${file}:${line}: ${message}`);
|
|
7
|
-
this.name = 'FixtureFormatError';
|
|
8
|
-
this.file = file;
|
|
9
|
-
this.line = line;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
export const FIXTURE_CASE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
13
|
-
export const FIXTURES_DIR = 'fixtures';
|
|
14
|
-
const CASE_FILE = 'case.json';
|
|
15
|
-
const EXCHANGES_FILE = 'exchanges.ndjson';
|
|
16
|
-
const REQUESTS_FILE = 'requests.ndjson';
|
|
17
|
-
const CASE_FILES = new Set([CASE_FILE, EXCHANGES_FILE, REQUESTS_FILE]);
|
|
18
|
-
class ShapeError extends Error {
|
|
19
|
-
}
|
|
20
|
-
function isRecord(value) {
|
|
21
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
-
}
|
|
23
|
-
function fail(message) {
|
|
24
|
-
throw new ShapeError(message);
|
|
25
|
-
}
|
|
26
|
-
function onlyKeys(value, allowed, where) {
|
|
27
|
-
for (const key of Object.keys(value)) {
|
|
28
|
-
if (!allowed.includes(key))
|
|
29
|
-
fail(`${where} has an unknown field "${key}"`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function requireString(value, where) {
|
|
33
|
-
if (typeof value !== 'string')
|
|
34
|
-
fail(`${where} must be a string`);
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
function requireNonEmptyString(value, where) {
|
|
38
|
-
const text = requireString(value, where);
|
|
39
|
-
if (text === '')
|
|
40
|
-
fail(`${where} must not be empty`);
|
|
41
|
-
return text;
|
|
42
|
-
}
|
|
43
|
-
function requireRecord(value, where) {
|
|
44
|
-
if (!isRecord(value))
|
|
45
|
-
fail(`${where} must be an object`);
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
function requireStringRecord(value, where) {
|
|
49
|
-
const record = requireRecord(value, where);
|
|
50
|
-
for (const [key, item] of Object.entries(record)) {
|
|
51
|
-
if (typeof item !== 'string')
|
|
52
|
-
fail(`${where}.${key} must be a string`);
|
|
53
|
-
}
|
|
54
|
-
return record;
|
|
55
|
-
}
|
|
56
|
-
function requireCount(value, where) {
|
|
57
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
58
|
-
fail(`${where} must be a non-negative integer`);
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
function requireError(value, where) {
|
|
62
|
-
if (value === null)
|
|
63
|
-
return;
|
|
64
|
-
const error = requireRecord(value, where);
|
|
65
|
-
onlyKeys(error, ['code', 'message'], where);
|
|
66
|
-
requireString(error.code, `${where}.code`);
|
|
67
|
-
requireString(error.message, `${where}.message`);
|
|
68
|
-
}
|
|
69
|
-
function requireEncoding(value, where) {
|
|
70
|
-
if (value !== 'utf8' && value !== 'base64')
|
|
71
|
-
fail(`${where} must be "utf8" or "base64"`);
|
|
72
|
-
}
|
|
73
|
-
function validateExpectation(value) {
|
|
74
|
-
const expectation = requireRecord(value, 'expect');
|
|
75
|
-
onlyKeys(expectation, ['records', 'removals', 'rejected', 'checkpoints', 'tables'], 'expect');
|
|
76
|
-
for (const field of ['records', 'removals']) {
|
|
77
|
-
if (expectation[field] === undefined)
|
|
78
|
-
continue;
|
|
79
|
-
for (const [table, count] of Object.entries(requireRecord(expectation[field], `expect.${field}`)))
|
|
80
|
-
requireCount(count, `expect.${field}.${table}`);
|
|
81
|
-
}
|
|
82
|
-
if (expectation.rejected !== undefined)
|
|
83
|
-
requireCount(expectation.rejected, 'expect.rejected');
|
|
84
|
-
if (expectation.checkpoints !== undefined)
|
|
85
|
-
requireCount(expectation.checkpoints, 'expect.checkpoints');
|
|
86
|
-
if (expectation.tables !== undefined) {
|
|
87
|
-
if (!Array.isArray(expectation.tables) || !expectation.tables.every((table) => typeof table === 'string'))
|
|
88
|
-
fail('expect.tables must be an array of strings');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function validateCase(value) {
|
|
92
|
-
const item = requireRecord(value, 'case');
|
|
93
|
-
const common = ['kind', 'config', 'secrets', 'expect'];
|
|
94
|
-
switch (item.kind) {
|
|
95
|
-
case 'pull':
|
|
96
|
-
onlyKeys(item, [...common, 'stream', 'mode', 'state', 'streamConfig', 'limit'], 'case');
|
|
97
|
-
requireNonEmptyString(item.stream, 'stream');
|
|
98
|
-
if (item.mode !== 'full' && item.mode !== 'incremental')
|
|
99
|
-
fail('mode must be "full" or "incremental"');
|
|
100
|
-
if (item.streamConfig !== undefined)
|
|
101
|
-
requireRecord(item.streamConfig, 'streamConfig');
|
|
102
|
-
if (item.limit !== undefined && (typeof item.limit !== 'number' || !Number.isSafeInteger(item.limit) || item.limit <= 0))
|
|
103
|
-
fail('limit must be a positive integer');
|
|
104
|
-
break;
|
|
105
|
-
case 'push':
|
|
106
|
-
onlyKeys(item, [...common, 'push'], 'case');
|
|
107
|
-
requireNonEmptyString(item.push, 'push');
|
|
108
|
-
break;
|
|
109
|
-
case 'discover':
|
|
110
|
-
onlyKeys(item, common, 'case');
|
|
111
|
-
break;
|
|
112
|
-
default:
|
|
113
|
-
fail('kind must be one of "pull", "push", "discover"');
|
|
114
|
-
}
|
|
115
|
-
requireRecord(item.config, 'config');
|
|
116
|
-
if (item.secrets !== undefined)
|
|
117
|
-
requireStringRecord(item.secrets, 'secrets');
|
|
118
|
-
if (item.expect !== undefined)
|
|
119
|
-
validateExpectation(item.expect);
|
|
120
|
-
return item;
|
|
121
|
-
}
|
|
122
|
-
function validateCryptoRequest(value) {
|
|
123
|
-
const request = requireRecord(value, 'request');
|
|
124
|
-
requireString(request.data, 'request.data');
|
|
125
|
-
if (request.encoding !== 'hex' && request.encoding !== 'base64')
|
|
126
|
-
fail('request.encoding must be "hex" or "base64"');
|
|
127
|
-
switch (request.op) {
|
|
128
|
-
case 'hash':
|
|
129
|
-
onlyKeys(request, ['op', 'algorithm', 'data', 'encoding'], 'request');
|
|
130
|
-
if (!['md5', 'sha1', 'sha256', 'sha512'].includes(String(request.algorithm)))
|
|
131
|
-
fail('request.algorithm is not a hash algorithm');
|
|
132
|
-
return;
|
|
133
|
-
case 'hmac':
|
|
134
|
-
onlyKeys(request, ['op', 'algorithm', 'key', 'data', 'encoding'], 'request');
|
|
135
|
-
requireString(request.key, 'request.key');
|
|
136
|
-
if (!['sha1', 'sha256', 'sha512'].includes(String(request.algorithm)))
|
|
137
|
-
fail('request.algorithm is not an hmac algorithm');
|
|
138
|
-
return;
|
|
139
|
-
case 'sign':
|
|
140
|
-
onlyKeys(request, ['op', 'algorithm', 'key', 'data', 'encoding'], 'request');
|
|
141
|
-
requireString(request.key, 'request.key');
|
|
142
|
-
if (request.algorithm !== 'rsa-sha256')
|
|
143
|
-
fail('request.algorithm must be "rsa-sha256"');
|
|
144
|
-
return;
|
|
145
|
-
default:
|
|
146
|
-
fail('request.op must be one of "hmac", "sign", "hash"');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function validateExchange(value) {
|
|
150
|
-
const item = requireRecord(value, 'exchange');
|
|
151
|
-
if (typeof item.seq !== 'number' || !Number.isSafeInteger(item.seq) || item.seq <= 0)
|
|
152
|
-
fail('seq must be a positive integer');
|
|
153
|
-
switch (item.kind) {
|
|
154
|
-
case 'http': {
|
|
155
|
-
onlyKeys(item, ['seq', 'kind', 'request', 'response', 'error'], 'exchange');
|
|
156
|
-
const request = requireRecord(item.request, 'request');
|
|
157
|
-
onlyKeys(request, ['method', 'url', 'headers', 'body', 'bodyEncoding'], 'request');
|
|
158
|
-
requireNonEmptyString(request.method, 'request.method');
|
|
159
|
-
requireNonEmptyString(request.url, 'request.url');
|
|
160
|
-
requireStringRecord(request.headers, 'request.headers');
|
|
161
|
-
if (request.body !== null)
|
|
162
|
-
requireString(request.body, 'request.body');
|
|
163
|
-
requireEncoding(request.bodyEncoding, 'request.bodyEncoding');
|
|
164
|
-
if (item.response !== null) {
|
|
165
|
-
const response = requireRecord(item.response, 'response');
|
|
166
|
-
onlyKeys(response, ['status', 'headers', 'body', 'bodyEncoding'], 'response');
|
|
167
|
-
if (typeof response.status !== 'number' || !Number.isInteger(response.status))
|
|
168
|
-
fail('response.status must be an integer');
|
|
169
|
-
requireStringRecord(response.headers, 'response.headers');
|
|
170
|
-
requireString(response.body, 'response.body');
|
|
171
|
-
requireEncoding(response.bodyEncoding, 'response.bodyEncoding');
|
|
172
|
-
}
|
|
173
|
-
requireError(item.error, 'error');
|
|
174
|
-
if ((item.response === null) === (item.error === null))
|
|
175
|
-
fail('exactly one of response and error must be null');
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
case 'db': {
|
|
179
|
-
onlyKeys(item, ['seq', 'kind', 'sql', 'params', 'fields', 'rows', 'error'], 'exchange');
|
|
180
|
-
requireString(item.sql, 'sql');
|
|
181
|
-
if (!Array.isArray(item.params))
|
|
182
|
-
fail('params must be an array');
|
|
183
|
-
if (!Array.isArray(item.fields))
|
|
184
|
-
fail('fields must be an array');
|
|
185
|
-
for (const field of item.fields) {
|
|
186
|
-
const entry = requireRecord(field, 'fields[]');
|
|
187
|
-
requireString(entry.name, 'fields[].name');
|
|
188
|
-
if (typeof entry.dataTypeId !== 'number' || !Number.isInteger(entry.dataTypeId))
|
|
189
|
-
fail('fields[].dataTypeId must be an integer');
|
|
190
|
-
}
|
|
191
|
-
if (!Array.isArray(item.rows) || !item.rows.every(isRecord))
|
|
192
|
-
fail('rows must be an array of objects');
|
|
193
|
-
requireError(item.error, 'error');
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
case 'crypto':
|
|
197
|
-
onlyKeys(item, ['seq', 'kind', 'request', 'value'], 'exchange');
|
|
198
|
-
validateCryptoRequest(item.request);
|
|
199
|
-
requireString(item.value, 'value');
|
|
200
|
-
break;
|
|
201
|
-
default:
|
|
202
|
-
fail('kind must be one of "http", "db", "crypto"');
|
|
203
|
-
}
|
|
204
|
-
return item;
|
|
205
|
-
}
|
|
206
|
-
function validateRequest(value) {
|
|
207
|
-
const item = requireRecord(value, 'request');
|
|
208
|
-
onlyKeys(item, ['requestId', 'receivedAt', 'headers', 'body'], 'request');
|
|
209
|
-
requireNonEmptyString(item.requestId, 'requestId');
|
|
210
|
-
requireNonEmptyString(item.receivedAt, 'receivedAt');
|
|
211
|
-
requireStringRecord(item.headers, 'headers');
|
|
212
|
-
requireString(item.body, 'body');
|
|
213
|
-
return item;
|
|
214
|
-
}
|
|
215
|
-
function parseJsonFile(file, validate) {
|
|
216
|
-
let value;
|
|
217
|
-
try {
|
|
218
|
-
value = JSON.parse(file.content);
|
|
219
|
-
}
|
|
220
|
-
catch (error) {
|
|
221
|
-
throw new FixtureFormatError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`, file.path);
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
return validate(value);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
if (error instanceof ShapeError)
|
|
228
|
-
throw new FixtureFormatError(error.message, file.path);
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function parseNdjson(file, validate) {
|
|
233
|
-
const items = [];
|
|
234
|
-
const lines = file.content.split('\n');
|
|
235
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
236
|
-
const text = (lines[index] ?? '').replace(/\r$/, '');
|
|
237
|
-
if (text.trim() === '')
|
|
238
|
-
continue;
|
|
239
|
-
const line = index + 1;
|
|
240
|
-
let value;
|
|
241
|
-
try {
|
|
242
|
-
value = JSON.parse(text);
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
throw new FixtureFormatError('not valid JSON', file.path, line);
|
|
246
|
-
}
|
|
247
|
-
try {
|
|
248
|
-
items.push(validate(value, line));
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
if (error instanceof ShapeError)
|
|
252
|
-
throw new FixtureFormatError(error.message, file.path, line);
|
|
253
|
-
throw error;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return items;
|
|
257
|
-
}
|
|
258
|
-
function normalizePath(filePath) {
|
|
259
|
-
return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
260
|
-
}
|
|
261
|
-
export function parseFixtures(files) {
|
|
262
|
-
const groups = new Map();
|
|
263
|
-
for (const raw of files) {
|
|
264
|
-
const file = { path: normalizePath(raw.path), content: raw.content };
|
|
265
|
-
const parts = file.path.split('/');
|
|
266
|
-
if (parts.some((part) => part === '' || part === '.' || part === '..'))
|
|
267
|
-
throw new FixtureFormatError('fixture paths must be plain relative paths', file.path);
|
|
268
|
-
if (parts.some((part) => part.startsWith('.')))
|
|
269
|
-
continue;
|
|
270
|
-
if (parts.length !== 3 || parts[0] !== FIXTURES_DIR) {
|
|
271
|
-
throw new FixtureFormatError(`unexpected file: fixture files live at ${FIXTURES_DIR}/<case>/{${[...CASE_FILES].join(',')}}`, file.path);
|
|
272
|
-
}
|
|
273
|
-
const name = parts[1] ?? '';
|
|
274
|
-
const fileName = parts[2] ?? '';
|
|
275
|
-
if (!FIXTURE_CASE_NAME_PATTERN.test(name)) {
|
|
276
|
-
throw new FixtureFormatError(`case name "${name}" must match ${FIXTURE_CASE_NAME_PATTERN.source}`, file.path);
|
|
277
|
-
}
|
|
278
|
-
if (!CASE_FILES.has(fileName)) {
|
|
279
|
-
throw new FixtureFormatError(`unexpected file in case "${name}": expected ${[...CASE_FILES].join(', ')}`, file.path);
|
|
280
|
-
}
|
|
281
|
-
const group = groups.get(name) ?? new Map();
|
|
282
|
-
if (group.has(fileName))
|
|
283
|
-
throw new FixtureFormatError('duplicate file', file.path);
|
|
284
|
-
group.set(fileName, file);
|
|
285
|
-
groups.set(name, group);
|
|
286
|
-
}
|
|
287
|
-
const fixtures = [];
|
|
288
|
-
for (const name of [...groups.keys()].sort()) {
|
|
289
|
-
const group = groups.get(name) ?? new Map();
|
|
290
|
-
const casePath = `${FIXTURES_DIR}/${name}/${CASE_FILE}`;
|
|
291
|
-
const caseFile = group.get(CASE_FILE);
|
|
292
|
-
if (!caseFile)
|
|
293
|
-
throw new FixtureFormatError(`case "${name}" has no ${CASE_FILE}`, casePath);
|
|
294
|
-
const fixtureCase = parseJsonFile(caseFile, validateCase);
|
|
295
|
-
const exchangesFile = group.get(EXCHANGES_FILE);
|
|
296
|
-
let previousSeq = 0;
|
|
297
|
-
const exchanges = exchangesFile
|
|
298
|
-
? parseNdjson(exchangesFile, (value) => {
|
|
299
|
-
const exchange = validateExchange(value);
|
|
300
|
-
if (exchange.seq <= previousSeq)
|
|
301
|
-
fail(`seq ${exchange.seq} must be greater than the previous seq ${previousSeq}`);
|
|
302
|
-
previousSeq = exchange.seq;
|
|
303
|
-
return exchange;
|
|
304
|
-
})
|
|
305
|
-
: [];
|
|
306
|
-
const requestsFile = group.get(REQUESTS_FILE);
|
|
307
|
-
if (fixtureCase.kind === 'push' && !requestsFile) {
|
|
308
|
-
throw new FixtureFormatError(`push case "${name}" needs ${REQUESTS_FILE} next to ${CASE_FILE}`, casePath);
|
|
309
|
-
}
|
|
310
|
-
if (fixtureCase.kind !== 'push' && requestsFile) {
|
|
311
|
-
throw new FixtureFormatError(`${REQUESTS_FILE} only belongs to push cases`, requestsFile.path);
|
|
312
|
-
}
|
|
313
|
-
const seen = new Set();
|
|
314
|
-
const requests = requestsFile
|
|
315
|
-
? parseNdjson(requestsFile, (value) => {
|
|
316
|
-
const request = validateRequest(value);
|
|
317
|
-
if (seen.has(request.requestId))
|
|
318
|
-
fail(`requestId "${request.requestId}" appears more than once`);
|
|
319
|
-
seen.add(request.requestId);
|
|
320
|
-
return request;
|
|
321
|
-
})
|
|
322
|
-
: [];
|
|
323
|
-
fixtures.push({ name, case: fixtureCase, exchanges, requests });
|
|
324
|
-
}
|
|
325
|
-
return fixtures;
|
|
326
|
-
}
|
|
327
|
-
function redactCaseSecrets(fixtureCase) {
|
|
328
|
-
if (!fixtureCase.secrets)
|
|
329
|
-
return fixtureCase;
|
|
330
|
-
const secrets = Object.fromEntries(Object.entries(fixtureCase.secrets).map(([field, value]) => [field, value.length >= REDACT_MIN_LENGTH ? REDACTED : value]));
|
|
331
|
-
return { ...fixtureCase, secrets };
|
|
332
|
-
}
|
|
333
|
-
function ndjson(items) {
|
|
334
|
-
return items.map((item) => `${JSON.stringify(item)}\n`).join('');
|
|
335
|
-
}
|
|
336
|
-
export function serializeFixture(name, input) {
|
|
337
|
-
const dir = `${FIXTURES_DIR}/${name}`;
|
|
338
|
-
if (!FIXTURE_CASE_NAME_PATTERN.test(name))
|
|
339
|
-
throw new FixtureFormatError(`case name "${name}" must match ${FIXTURE_CASE_NAME_PATTERN.source}`, `${dir}/${CASE_FILE}`);
|
|
340
|
-
const requests = input.requests ?? [];
|
|
341
|
-
if (input.case.kind !== 'push' && requests.length > 0)
|
|
342
|
-
throw new FixtureFormatError(`${REQUESTS_FILE} only belongs to push cases`, `${dir}/${REQUESTS_FILE}`);
|
|
343
|
-
const files = [{ path: `${dir}/${CASE_FILE}`, content: `${JSON.stringify(redactCaseSecrets(input.case), null, 2)}\n` }];
|
|
344
|
-
if (input.exchanges.length > 0)
|
|
345
|
-
files.push({ path: `${dir}/${EXCHANGES_FILE}`, content: ndjson(input.exchanges) });
|
|
346
|
-
if (input.case.kind === 'push')
|
|
347
|
-
files.push({ path: `${dir}/${REQUESTS_FILE}`, content: ndjson(requests) });
|
|
348
|
-
return files;
|
|
349
|
-
}
|
|
350
|
-
export async function readFixtureDir(dir) {
|
|
351
|
-
const fs = await import('node:fs/promises');
|
|
352
|
-
const path = await import('node:path');
|
|
353
|
-
const root = path.join(dir, FIXTURES_DIR);
|
|
354
|
-
const files = [];
|
|
355
|
-
const walk = async (absolute, relative) => {
|
|
356
|
-
const entries = await fs.readdir(absolute, { withFileTypes: true });
|
|
357
|
-
for (const entry of entries) {
|
|
358
|
-
if (entry.name.startsWith('.'))
|
|
359
|
-
continue;
|
|
360
|
-
const childAbsolute = path.join(absolute, entry.name);
|
|
361
|
-
const childRelative = `${relative}/${entry.name}`;
|
|
362
|
-
if (entry.isSymbolicLink())
|
|
363
|
-
throw new FixtureFormatError('symbolic links are not allowed in fixtures', childRelative);
|
|
364
|
-
if (entry.isDirectory())
|
|
365
|
-
await walk(childAbsolute, childRelative);
|
|
366
|
-
else if (entry.isFile())
|
|
367
|
-
files.push({ path: childRelative, content: await fs.readFile(childAbsolute, 'utf8') });
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
let stat;
|
|
371
|
-
try {
|
|
372
|
-
stat = await fs.lstat(root);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
if (isRecord(error) && error.code === 'ENOENT')
|
|
376
|
-
return [];
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
if (stat.isSymbolicLink())
|
|
380
|
-
throw new FixtureFormatError('symbolic links are not allowed in fixtures', FIXTURES_DIR);
|
|
381
|
-
if (!stat.isDirectory())
|
|
382
|
-
throw new FixtureFormatError('must be a directory', FIXTURES_DIR);
|
|
383
|
-
await walk(root, FIXTURES_DIR);
|
|
384
|
-
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
385
|
-
}
|
|
386
|
-
export function canonicalRequestUrl(url, query) {
|
|
387
|
-
let parsed;
|
|
388
|
-
try {
|
|
389
|
-
parsed = new URL(url);
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
return url;
|
|
393
|
-
}
|
|
394
|
-
for (const [name, value] of Object.entries(query ?? {})) {
|
|
395
|
-
for (const item of Array.isArray(value) ? value : [value])
|
|
396
|
-
parsed.searchParams.append(name, item);
|
|
397
|
-
}
|
|
398
|
-
parsed.searchParams.sort();
|
|
399
|
-
return parsed.toString();
|
|
400
|
-
}
|
|
401
|
-
export function canonicalJson(value) {
|
|
402
|
-
return JSON.stringify(value, (_key, item) => {
|
|
403
|
-
if (!isRecord(item))
|
|
404
|
-
return item;
|
|
405
|
-
return Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]]));
|
|
406
|
-
});
|
|
407
|
-
}
|
package/testing/gateway.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type { LookupAddress } from 'node:dns';
|
|
2
|
-
import { LocalDbError } from './local-db.js';
|
|
3
|
-
import { type LocalHost } from './types.js';
|
|
4
|
-
export interface LocalGatewayOptions {
|
|
5
|
-
/** 连接器声明 ∩ 本地配置;空数组 = 全部拒绝。 */
|
|
6
|
-
hosts: string[];
|
|
7
|
-
rateLimit?: {
|
|
8
|
-
perSecond: number;
|
|
9
|
-
concurrency: number;
|
|
10
|
-
};
|
|
11
|
-
/** 字段 → 明文,用于句柄替换与遮蔽。 */
|
|
12
|
-
secrets: Record<string, string>;
|
|
13
|
-
db?: {
|
|
14
|
-
host: string;
|
|
15
|
-
port: number;
|
|
16
|
-
database: string;
|
|
17
|
-
user: string;
|
|
18
|
-
password: string;
|
|
19
|
-
ssl?: boolean;
|
|
20
|
-
} | null;
|
|
21
|
-
/** 测试注入;给了就不走真实网络(不解析主机名、不固定连接地址)。 */
|
|
22
|
-
fetch?: typeof fetch;
|
|
23
|
-
/** 测试注入:真实网络下的主机名解析,缺省 `dns.lookup(host, { all: true })`。 */
|
|
24
|
-
resolve?: (host: string) => Promise<LookupAddress[]>;
|
|
25
|
-
/** 注入 postgres 驱动(CLI 用);缺省动态 `import('postgres')`。 */
|
|
26
|
-
postgres?: unknown;
|
|
27
|
-
/** 录制器挂这里(Phase 3)。 */
|
|
28
|
-
onExchange?: (exchange: LocalExchange) => void;
|
|
29
|
-
}
|
|
30
|
-
/** 未遮蔽的原始往来,录制器负责遮蔽。 */
|
|
31
|
-
export interface LocalExchange {
|
|
32
|
-
kind: 'http' | 'db' | 'crypto';
|
|
33
|
-
request: unknown;
|
|
34
|
-
response: unknown;
|
|
35
|
-
error: {
|
|
36
|
-
code: string;
|
|
37
|
-
message: string;
|
|
38
|
-
} | null;
|
|
39
|
-
durationMs: number;
|
|
40
|
-
}
|
|
41
|
-
/** 本地网关自己的宿主调用失败(码与平台网关一致)。 */
|
|
42
|
-
export declare class LocalGatewayError extends Error {
|
|
43
|
-
readonly code: string;
|
|
44
|
-
readonly status: number | undefined;
|
|
45
|
-
constructor(code: string, message: string, status?: number);
|
|
46
|
-
}
|
|
47
|
-
/** 去掉 IPv6 方括号与末尾的点,转小写;IPv4 映射的 IPv6(`::ffff:a9fe:a9fe`)还原成 IPv4。 */
|
|
48
|
-
export declare function normalizeHost(host: string): string;
|
|
49
|
-
/**
|
|
50
|
-
* 限流:同时在途不超过 `concurrency`,放行间隔 `1 / perSecond` 秒。
|
|
51
|
-
* 释放槽位时若有人排队,槽位直接交给队头(`active` 不减),避免刚进来的请求在交接间隙抢占同一个槽位。
|
|
52
|
-
*/
|
|
53
|
-
export declare class LocalLimiter {
|
|
54
|
-
private readonly perSecond;
|
|
55
|
-
private readonly concurrency;
|
|
56
|
-
private nextAt;
|
|
57
|
-
private active;
|
|
58
|
-
private readonly waiting;
|
|
59
|
-
constructor(perSecond: number, concurrency: number);
|
|
60
|
-
acquire(): Promise<() => void>;
|
|
61
|
-
}
|
|
62
|
-
export declare function createLocalGateway(options: LocalGatewayOptions): LocalHost & {
|
|
63
|
-
redact(text: string): string;
|
|
64
|
-
};
|
|
65
|
-
export { LocalDbError };
|