@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.1
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/AUTHORING.md +93 -0
- package/CHANGELOG.md +21 -0
- package/README.md +133 -28
- package/bin/apifuse-check.ts +78 -71
- package/bin/apifuse-create.ts +12 -0
- package/bin/apifuse-dev.ts +24 -61
- package/bin/apifuse-pack-check.ts +87 -0
- package/bin/apifuse-pack-smoke.ts +122 -0
- package/bin/apifuse-perf.ts +33 -32
- package/bin/apifuse-record.ts +17 -7
- package/bin/apifuse-test.ts +6 -4
- package/bin/apifuse.ts +36 -35
- package/package.json +29 -9
- package/src/ceremonies/index.ts +768 -0
- package/src/cli/commands.ts +87 -0
- package/src/cli/create.ts +845 -0
- package/src/cli/templates/provider/Dockerfile.tpl +7 -0
- package/src/cli/templates/provider/README.md.tpl +41 -0
- package/src/cli/templates/provider/dev.ts.tpl +5 -0
- package/src/cli/templates/provider/index.test.ts.tpl +13 -0
- package/src/cli/templates/provider/index.ts.tpl +58 -0
- package/src/cli/templates/provider/start.ts.tpl +5 -0
- package/src/config/loader.ts +61 -1
- package/src/define.ts +565 -41
- package/src/dev.ts +2 -6
- package/src/errors.ts +42 -0
- package/src/index.ts +44 -38
- package/src/lint.ts +574 -0
- package/src/provider.ts +13 -0
- package/src/runtime/auth-flow.ts +67 -0
- package/src/runtime/credential.ts +95 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +13 -14
- package/src/runtime/http.ts +36 -12
- package/src/runtime/insights.ts +3 -3
- package/src/runtime/key-derivation.ts +122 -0
- package/src/runtime/keyring.ts +148 -0
- package/src/runtime/namespace.ts +33 -0
- package/src/runtime/prevalidate.ts +252 -0
- package/src/runtime/tls.ts +41 -17
- package/src/runtime/waterfall.ts +0 -1
- package/src/schema.ts +77 -0
- package/src/serve.ts +1 -664
- package/src/server/index.ts +22 -0
- package/src/server/serve.ts +624 -0
- package/src/server/types.ts +78 -0
- package/src/stealth/profiles.ts +10 -93
- package/src/testing/run.ts +391 -32
- package/src/types.ts +390 -41
- package/bin/apifuse-init.ts +0 -387
- package/src/__tests__/auth.test.ts +0 -396
- package/src/__tests__/browser-auth.test.ts +0 -180
- package/src/__tests__/browser.test.ts +0 -632
- package/src/__tests__/define.test.ts +0 -225
- package/src/__tests__/errors.test.ts +0 -69
- package/src/__tests__/executor.test.ts +0 -214
- package/src/__tests__/http.test.ts +0 -238
- package/src/__tests__/insights.test.ts +0 -210
- package/src/__tests__/instrumentation.test.ts +0 -290
- package/src/__tests__/otlp.test.ts +0 -141
- package/src/__tests__/perf.test.ts +0 -60
- package/src/__tests__/providers-yaml.test.ts +0 -135
- package/src/__tests__/proxy.test.ts +0 -359
- package/src/__tests__/recipes.test.ts +0 -36
- package/src/__tests__/serve.test.ts +0 -233
- package/src/__tests__/session.test.ts +0 -231
- package/src/__tests__/state.test.ts +0 -100
- package/src/__tests__/stealth.test.ts +0 -57
- package/src/__tests__/testing.test.ts +0 -97
- package/src/__tests__/tls.test.ts +0 -345
- package/src/__tests__/types.test.ts +0 -142
- package/src/__tests__/utils.test.ts +0 -62
- package/src/__tests__/waterfall.test.ts +0 -270
- package/src/config/providers-yaml.ts +0 -370
- package/src/index.test.ts +0 -1
- package/src/protocol.ts +0 -183
- package/src/runtime/auth.ts +0 -245
- package/src/runtime/session.ts +0 -573
- package/src/runtime/state.ts +0 -124
package/src/stealth/profiles.ts
CHANGED
|
@@ -158,6 +158,10 @@ export function generateLayer2Headers(
|
|
|
158
158
|
return headers;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
const STEALTH_PROFILE_ALIASES: Record<string, string> = {
|
|
162
|
+
"chrome-desktop": "chrome-146",
|
|
163
|
+
};
|
|
164
|
+
|
|
161
165
|
const STEALTH_PROFILES: Record<string, StealthProfile> = {
|
|
162
166
|
"chrome-146": createProfile("chrome-146", {
|
|
163
167
|
platform: "macos",
|
|
@@ -169,87 +173,6 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
|
|
|
169
173
|
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
170
174
|
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
171
175
|
}),
|
|
172
|
-
"chrome-144": createProfile("chrome-144", {
|
|
173
|
-
platform: "macos",
|
|
174
|
-
version: "144.0.0.0",
|
|
175
|
-
userAgent:
|
|
176
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
177
|
-
tlsClientIdentifier: "chrome_144",
|
|
178
|
-
ja3: CHROMIUM_JA3,
|
|
179
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
180
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
181
|
-
}),
|
|
182
|
-
"chrome-133": createProfile("chrome-133", {
|
|
183
|
-
platform: "macos",
|
|
184
|
-
version: "133.0.0.0",
|
|
185
|
-
userAgent:
|
|
186
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
|
187
|
-
tlsClientIdentifier: "chrome_133",
|
|
188
|
-
ja3: CHROMIUM_JA3,
|
|
189
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
190
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
191
|
-
}),
|
|
192
|
-
"chrome-131": createProfile("chrome-131", {
|
|
193
|
-
platform: "macos",
|
|
194
|
-
version: "131.0.0.0",
|
|
195
|
-
userAgent:
|
|
196
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
197
|
-
tlsClientIdentifier: "chrome_131",
|
|
198
|
-
ja3: CHROMIUM_JA3,
|
|
199
|
-
ja4: "t13d1516h2_8daaf6152771_02713d6af862",
|
|
200
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
201
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
202
|
-
}),
|
|
203
|
-
"chrome-124": createProfile("chrome-124", {
|
|
204
|
-
platform: "macos",
|
|
205
|
-
version: "124.0.0.0",
|
|
206
|
-
userAgent:
|
|
207
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
208
|
-
tlsClientIdentifier: "chrome_124",
|
|
209
|
-
ja3: CHROMIUM_JA3,
|
|
210
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
211
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
212
|
-
}),
|
|
213
|
-
"chrome-120": createProfile("chrome-120", {
|
|
214
|
-
platform: "macos",
|
|
215
|
-
version: "120.0.0.0",
|
|
216
|
-
userAgent:
|
|
217
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
218
|
-
tlsClientIdentifier: "chrome_120",
|
|
219
|
-
ja3: CHROMIUM_JA3,
|
|
220
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
221
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
222
|
-
}),
|
|
223
|
-
"chrome-146-psk": createProfile("chrome-146-psk", {
|
|
224
|
-
platform: "macos",
|
|
225
|
-
version: "146.0.0.0",
|
|
226
|
-
userAgent:
|
|
227
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
228
|
-
tlsClientIdentifier: "chrome_146_PSK",
|
|
229
|
-
ja3: CHROMIUM_JA3,
|
|
230
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
231
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
232
|
-
}),
|
|
233
|
-
"chrome-131-psk": createProfile("chrome-131-psk", {
|
|
234
|
-
platform: "macos",
|
|
235
|
-
version: "131.0.0.0",
|
|
236
|
-
userAgent:
|
|
237
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
238
|
-
tlsClientIdentifier: "chrome_131_PSK",
|
|
239
|
-
ja3: CHROMIUM_JA3,
|
|
240
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
241
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
242
|
-
}),
|
|
243
|
-
"chrome-130-psk": createProfile("chrome-130-psk", {
|
|
244
|
-
platform: "macos",
|
|
245
|
-
version: "130.0.0.0",
|
|
246
|
-
userAgent:
|
|
247
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
|
|
248
|
-
tlsClientIdentifier: "chrome_130_PSK",
|
|
249
|
-
ja3: CHROMIUM_JA3,
|
|
250
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
251
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
252
|
-
}),
|
|
253
176
|
"firefox-147": createProfile("firefox-147", {
|
|
254
177
|
platform: "macos",
|
|
255
178
|
version: "147.0",
|
|
@@ -310,16 +233,6 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
|
|
|
310
233
|
h2Settings: SAFARI_H2_SETTINGS,
|
|
311
234
|
headerOrder: SAFARI_HEADER_ORDER,
|
|
312
235
|
}),
|
|
313
|
-
"edge-131": createProfile("edge-131", {
|
|
314
|
-
platform: "windows",
|
|
315
|
-
version: "131.0.0.0",
|
|
316
|
-
userAgent:
|
|
317
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
|
318
|
-
tlsClientIdentifier: "chrome_131",
|
|
319
|
-
ja3: CHROMIUM_JA3,
|
|
320
|
-
h2Settings: CHROMIUM_H2_SETTINGS,
|
|
321
|
-
headerOrder: CHROMIUM_HEADER_ORDER,
|
|
322
|
-
}),
|
|
323
236
|
"ios-safari-26": createProfile("ios-safari-26", {
|
|
324
237
|
platform: "ios",
|
|
325
238
|
version: "26.0",
|
|
@@ -373,7 +286,8 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
|
|
|
373
286
|
};
|
|
374
287
|
|
|
375
288
|
export function getStealthProfile(name: string): StealthProfile {
|
|
376
|
-
const
|
|
289
|
+
const canonicalName = STEALTH_PROFILE_ALIASES[name] ?? name;
|
|
290
|
+
const profile = STEALTH_PROFILES[canonicalName];
|
|
377
291
|
|
|
378
292
|
if (!profile) {
|
|
379
293
|
throw new SDKError(`Unknown stealth profile: ${name}`);
|
|
@@ -387,5 +301,8 @@ export function getStealthProfile(name: string): StealthProfile {
|
|
|
387
301
|
}
|
|
388
302
|
|
|
389
303
|
export function listStealthProfiles(): string[] {
|
|
390
|
-
return
|
|
304
|
+
return [
|
|
305
|
+
...Object.keys(STEALTH_PROFILES),
|
|
306
|
+
...Object.keys(STEALTH_PROFILE_ALIASES),
|
|
307
|
+
];
|
|
391
308
|
}
|
package/src/testing/run.ts
CHANGED
|
@@ -1,20 +1,270 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { safeParseSchemaSync } from "../schema";
|
|
4
|
+
import type {
|
|
5
|
+
AuthMode,
|
|
6
|
+
CredentialContext,
|
|
7
|
+
HttpResponse,
|
|
8
|
+
ProviderContext,
|
|
9
|
+
ProviderDefinition,
|
|
10
|
+
} from "../types";
|
|
4
11
|
|
|
5
12
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)+$/;
|
|
13
|
+
const VALID_AUTH_MODES = [
|
|
14
|
+
"none",
|
|
15
|
+
"platform-managed",
|
|
16
|
+
"credentials",
|
|
17
|
+
"oauth2",
|
|
18
|
+
] as const;
|
|
19
|
+
const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
|
|
6
20
|
|
|
7
|
-
|
|
8
|
-
id?:
|
|
9
|
-
displayName?:
|
|
10
|
-
category?:
|
|
11
|
-
version?:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
auth?:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
export interface StandardTestsManifest {
|
|
22
|
+
id?: string;
|
|
23
|
+
displayName?: string;
|
|
24
|
+
category?: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
runtime?: ProviderDefinition["runtime"];
|
|
27
|
+
sdkVersion?: number;
|
|
28
|
+
auth?: AuthMode;
|
|
29
|
+
language?: string;
|
|
30
|
+
signature?: string;
|
|
31
|
+
signatureUri?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StandardTestsOptions {
|
|
35
|
+
/** Validate operation request/response fixtures and JSON raw fixture shape. */
|
|
36
|
+
validateFixture?: boolean;
|
|
37
|
+
/** Write/read __fixtures__/transform.snap.json for handler(raw fixture) output. */
|
|
38
|
+
snapshot?: boolean;
|
|
39
|
+
/** Opt-in integration-only manifest signature assertion. */
|
|
40
|
+
verifyManifest?: boolean;
|
|
41
|
+
/** Opt-in auth mode/operation consistency assertion. */
|
|
42
|
+
validateAuthMode?: boolean;
|
|
43
|
+
/** Override inferred __fixtures__ directory for tests generated outside providers/<id>. */
|
|
44
|
+
fixtureDir?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface FixtureEnvelope {
|
|
48
|
+
request?: unknown;
|
|
49
|
+
response?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isFixtureEnvelope(value: unknown): value is FixtureEnvelope {
|
|
53
|
+
return (
|
|
54
|
+
value !== null &&
|
|
55
|
+
typeof value === "object" &&
|
|
56
|
+
!Array.isArray(value) &&
|
|
57
|
+
("request" in value || "response" in value)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isJsonCompatible(value: unknown): boolean {
|
|
62
|
+
if (
|
|
63
|
+
value === null ||
|
|
64
|
+
typeof value === "string" ||
|
|
65
|
+
typeof value === "number" ||
|
|
66
|
+
typeof value === "boolean"
|
|
67
|
+
) {
|
|
68
|
+
return Number.isFinite(value) || typeof value !== "number";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return value.every(isJsonCompatible);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value === "object" && value !== null) {
|
|
76
|
+
return Object.values(value).every(isJsonCompatible);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sortJson(value: unknown): unknown {
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map(sortJson);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (value !== null && typeof value === "object") {
|
|
88
|
+
return Object.fromEntries(
|
|
89
|
+
Object.entries(value)
|
|
90
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
91
|
+
.map(([key, entry]) => [key, sortJson(entry)]),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stableStringify(value: unknown): string {
|
|
99
|
+
return `${JSON.stringify(sortJson(value), null, 2)}\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shouldUpdateSnapshots(): boolean {
|
|
103
|
+
return process.argv.some((arg) => UPDATE_SNAPSHOT_ARGS.has(arg));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function inferFixtureDir(providerId: string): string {
|
|
107
|
+
const stack = new Error().stack ?? "";
|
|
108
|
+
const testFile = stack
|
|
109
|
+
.split("\n")
|
|
110
|
+
.map((line) => line.match(/\(?((?:file:\/\/)?[^():]+\.test\.ts)/)?.[1])
|
|
111
|
+
.find((file) => file !== undefined);
|
|
112
|
+
|
|
113
|
+
if (testFile) {
|
|
114
|
+
const pathname = testFile.startsWith("file://")
|
|
115
|
+
? new URL(testFile).pathname
|
|
116
|
+
: testFile;
|
|
117
|
+
return `${pathname.replace(/\/[^/]+$/, "")}/../__fixtures__`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return `providers/${providerId}/__fixtures__`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function jsonResponse(data: unknown): HttpResponse {
|
|
124
|
+
return {
|
|
125
|
+
status: 200,
|
|
126
|
+
ok: true,
|
|
127
|
+
headers: {},
|
|
128
|
+
data,
|
|
129
|
+
json: async <_T = unknown>() => JSON.parse(JSON.stringify(data)),
|
|
130
|
+
text: async () => JSON.stringify(data),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function unsupported(name: string): never {
|
|
135
|
+
throw new Error(`Standard test snapshot context does not support ${name}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createSnapshotContext(rawFixture: unknown): ProviderContext {
|
|
139
|
+
const credential: CredentialContext = {
|
|
140
|
+
mode: "none",
|
|
141
|
+
get: () => undefined,
|
|
142
|
+
getAll: () => ({}),
|
|
143
|
+
getAccessToken: () => undefined,
|
|
144
|
+
getScopes: () => [],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
env: { get: () => undefined },
|
|
149
|
+
credential,
|
|
150
|
+
request: { headers: {} },
|
|
151
|
+
http: {
|
|
152
|
+
request: async () => jsonResponse(rawFixture),
|
|
153
|
+
get: async () => jsonResponse(rawFixture),
|
|
154
|
+
post: async () => jsonResponse(rawFixture),
|
|
155
|
+
put: async () => jsonResponse(rawFixture),
|
|
156
|
+
delete: async () => jsonResponse(rawFixture),
|
|
157
|
+
},
|
|
158
|
+
tls: {
|
|
159
|
+
fetch: async () => unsupported("ctx.tls.fetch"),
|
|
160
|
+
createSession: () => unsupported("ctx.tls.createSession"),
|
|
161
|
+
},
|
|
162
|
+
browser: {
|
|
163
|
+
engine: "playwright-stealth",
|
|
164
|
+
newPage: async () => unsupported("ctx.browser.newPage"),
|
|
165
|
+
},
|
|
166
|
+
trace: {
|
|
167
|
+
span: async (_name, fn) => fn(),
|
|
168
|
+
},
|
|
169
|
+
auth: {
|
|
170
|
+
requestField: async (name) =>
|
|
171
|
+
unsupported(`ctx.auth.requestField(${name})`),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function transformSnapshotOutput(
|
|
177
|
+
provider: ProviderDefinition,
|
|
178
|
+
rawFixture: unknown,
|
|
179
|
+
): Promise<unknown> {
|
|
180
|
+
const entries = Object.entries(provider.operations);
|
|
181
|
+
const context = createSnapshotContext(rawFixture);
|
|
182
|
+
const outputs = await Promise.all(
|
|
183
|
+
entries.map(async ([operationName, operation]) => {
|
|
184
|
+
const request = operation.fixtures?.request ?? {};
|
|
185
|
+
const output = await operation.handler(context, request);
|
|
186
|
+
return [operationName, output] as const;
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (outputs.length === 1) {
|
|
191
|
+
return outputs[0]?.[1];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Object.fromEntries(outputs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatJson(value: unknown): string {
|
|
198
|
+
return JSON.stringify(value, null, 2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatJsonDiff(current: unknown, expected: unknown): string {
|
|
202
|
+
const currentLines = formatJson(current).split("\n");
|
|
203
|
+
const expectedLines = formatJson(expected).split("\n");
|
|
204
|
+
const lineCount = Math.max(currentLines.length, expectedLines.length);
|
|
205
|
+
const lines = ["JSON diff (- current, + expected):"];
|
|
206
|
+
|
|
207
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
208
|
+
const currentLine = currentLines[index];
|
|
209
|
+
const expectedLine = expectedLines[index];
|
|
210
|
+
|
|
211
|
+
if (currentLine === expectedLine) {
|
|
212
|
+
if (currentLine !== undefined) {
|
|
213
|
+
lines.push(` ${currentLine}`);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (currentLine !== undefined) {
|
|
219
|
+
lines.push(`- ${currentLine}`);
|
|
220
|
+
}
|
|
221
|
+
if (expectedLine !== undefined) {
|
|
222
|
+
lines.push(`+ ${expectedLine}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function expectSchemaFixture(
|
|
230
|
+
operationName: string,
|
|
231
|
+
fieldName: "request" | "response",
|
|
232
|
+
fixture: unknown,
|
|
233
|
+
result: ReturnType<typeof safeParseSchemaSync>,
|
|
234
|
+
): void {
|
|
235
|
+
if (result.success) {
|
|
236
|
+
expect(result.success).toBe(true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error(
|
|
241
|
+
[
|
|
242
|
+
`Fixture ${operationName}.${fieldName} failed schema validation.`,
|
|
243
|
+
formatJsonDiff(
|
|
244
|
+
{ valid: false, value: fixture, error: result.error },
|
|
245
|
+
{ valid: true, value: fixture },
|
|
246
|
+
),
|
|
247
|
+
].join("\n"),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseSchemaFixture(
|
|
252
|
+
operationName: string,
|
|
253
|
+
fieldName: "request" | "response",
|
|
254
|
+
schema: ProviderDefinition["operations"][string]["input"],
|
|
255
|
+
fixture: unknown,
|
|
256
|
+
): void {
|
|
257
|
+
expectSchemaFixture(
|
|
258
|
+
operationName,
|
|
259
|
+
fieldName,
|
|
260
|
+
fixture,
|
|
261
|
+
safeParseSchemaSync(
|
|
262
|
+
schema,
|
|
263
|
+
fixture,
|
|
264
|
+
`operations.${operationName}.fixtures.${fieldName}`,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
18
268
|
|
|
19
269
|
/**
|
|
20
270
|
* Run standard SDK tests for a provider in one line.
|
|
@@ -22,15 +272,84 @@ type ProviderManifest = {
|
|
|
22
272
|
* Usage:
|
|
23
273
|
* import { myProvider } from "../index";
|
|
24
274
|
* import { runStandardTests } from "@apifuse/provider-sdk/testing";
|
|
25
|
-
* runStandardTests(myProvider);
|
|
275
|
+
* runStandardTests(myProvider, rawFixture, manifest, { snapshot: true });
|
|
26
276
|
*/
|
|
27
277
|
export function runStandardTests(
|
|
28
278
|
provider: ProviderDefinition,
|
|
29
|
-
|
|
30
|
-
manifest?:
|
|
279
|
+
rawFixture?: unknown,
|
|
280
|
+
manifest?: StandardTestsManifest,
|
|
281
|
+
options: StandardTestsOptions = {},
|
|
31
282
|
): void {
|
|
32
283
|
const operations = Object.entries(provider.operations);
|
|
33
284
|
|
|
285
|
+
const assertFixtureValidation = (): void => {
|
|
286
|
+
expect(rawFixture).toBeDefined();
|
|
287
|
+
expect(isJsonCompatible(rawFixture)).toBe(true);
|
|
288
|
+
|
|
289
|
+
for (const [operationName, op] of operations) {
|
|
290
|
+
if (op.fixtures?.request !== undefined) {
|
|
291
|
+
parseSchemaFixture(
|
|
292
|
+
operationName,
|
|
293
|
+
"request",
|
|
294
|
+
op.input,
|
|
295
|
+
op.fixtures.request,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (op.fixtures?.response !== undefined) {
|
|
300
|
+
parseSchemaFixture(
|
|
301
|
+
operationName,
|
|
302
|
+
"response",
|
|
303
|
+
op.output,
|
|
304
|
+
op.fixtures.response,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (isFixtureEnvelope(rawFixture)) {
|
|
309
|
+
if (rawFixture.request !== undefined) {
|
|
310
|
+
parseSchemaFixture(
|
|
311
|
+
operationName,
|
|
312
|
+
"request",
|
|
313
|
+
op.input,
|
|
314
|
+
rawFixture.request,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (rawFixture.response !== undefined) {
|
|
319
|
+
parseSchemaFixture(
|
|
320
|
+
operationName,
|
|
321
|
+
"response",
|
|
322
|
+
op.output,
|
|
323
|
+
rawFixture.response,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const assertManifestSignature = (): void => {
|
|
331
|
+
expect(manifest).toBeDefined();
|
|
332
|
+
expect(Boolean(manifest?.signature ?? manifest?.signatureUri)).toBe(true);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const assertAuthModeContract = (): void => {
|
|
336
|
+
const authMode = provider.auth?.mode ?? "none";
|
|
337
|
+
expect(VALID_AUTH_MODES).toContain(authMode);
|
|
338
|
+
|
|
339
|
+
if (manifest?.auth !== undefined) {
|
|
340
|
+
expect(manifest.auth).toBe(authMode);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (authMode === "credentials" || authMode === "oauth2") {
|
|
344
|
+
expect(provider.auth?.flow).toBeTruthy();
|
|
345
|
+
expect(Object.keys(provider.operations).length).toBeGreaterThan(0);
|
|
346
|
+
expect(provider.credential?.keys.length ?? 0).toBeGreaterThan(0);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
expect(provider.credential?.keys ?? []).toHaveLength(0);
|
|
351
|
+
};
|
|
352
|
+
|
|
34
353
|
describe(`[SDK Standard Tests] ${provider.id}`, () => {
|
|
35
354
|
it("id follows kebab-case format", () => {
|
|
36
355
|
expect(CONNECTOR_ID_REGEX.test(provider.id)).toBe(true);
|
|
@@ -40,7 +359,7 @@ export function runStandardTests(
|
|
|
40
359
|
expect(provider.meta.displayName).toBeTruthy();
|
|
41
360
|
expect(provider.meta.category).toBeTruthy();
|
|
42
361
|
expect(provider.version).toBeTruthy();
|
|
43
|
-
expect(["standard", "browser"]).toContain(provider.runtime);
|
|
362
|
+
expect(["standard", "shared", "browser"]).toContain(provider.runtime);
|
|
44
363
|
});
|
|
45
364
|
|
|
46
365
|
it("has at least one operation", () => {
|
|
@@ -58,31 +377,71 @@ export function runStandardTests(
|
|
|
58
377
|
it("operation schemas can parse fixture data", () => {
|
|
59
378
|
for (const [operationName, op] of operations) {
|
|
60
379
|
if (op.fixtures?.request !== undefined && op.input) {
|
|
61
|
-
|
|
62
|
-
|
|
380
|
+
parseSchemaFixture(
|
|
381
|
+
operationName,
|
|
382
|
+
"request",
|
|
383
|
+
op.input,
|
|
384
|
+
op.fixtures.request,
|
|
385
|
+
);
|
|
63
386
|
}
|
|
64
387
|
|
|
65
388
|
if (op.fixtures?.response !== undefined && op.output) {
|
|
66
|
-
|
|
67
|
-
|
|
389
|
+
parseSchemaFixture(
|
|
390
|
+
operationName,
|
|
391
|
+
"response",
|
|
392
|
+
op.output,
|
|
393
|
+
op.fixtures.response,
|
|
394
|
+
);
|
|
68
395
|
}
|
|
69
396
|
|
|
70
397
|
expect(operationName).toBeTruthy();
|
|
71
398
|
}
|
|
72
399
|
});
|
|
73
400
|
|
|
74
|
-
it("
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
expect(manifest.displayName).toBe(provider.meta.displayName);
|
|
81
|
-
expect(manifest.category).toBe(provider.meta.category);
|
|
82
|
-
expect(manifest.version).toBe(provider.version);
|
|
83
|
-
expect(manifest.runtime).toBe(provider.runtime);
|
|
84
|
-
expect(manifest.sdkVersion).toBe(1);
|
|
85
|
-
expect(manifest.language).toBe("typescript");
|
|
401
|
+
it("provider metadata is declared in defineProvider", () => {
|
|
402
|
+
expect(provider.id).toBeTruthy();
|
|
403
|
+
expect(provider.meta.displayName).toBeTruthy();
|
|
404
|
+
expect(provider.meta.category).toBeTruthy();
|
|
405
|
+
expect(provider.version).toBeTruthy();
|
|
406
|
+
expect(VALID_AUTH_MODES).toContain(provider.auth?.mode ?? "none");
|
|
86
407
|
});
|
|
408
|
+
|
|
409
|
+
if (options.validateFixture) {
|
|
410
|
+
it("validates raw and declared operation fixtures", () => {
|
|
411
|
+
assertFixtureValidation();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (options.verifyManifest) {
|
|
416
|
+
it("verifies manifest signature metadata", () => {
|
|
417
|
+
assertManifestSignature();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (options.validateAuthMode) {
|
|
422
|
+
it("validates auth mode contract", () => {
|
|
423
|
+
assertAuthModeContract();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (options.snapshot) {
|
|
428
|
+
it("matches transform snapshot", async () => {
|
|
429
|
+
expect(rawFixture).toBeDefined();
|
|
430
|
+
const fixtureDir = options.fixtureDir ?? inferFixtureDir(provider.id);
|
|
431
|
+
const snapshotPath = `${fixtureDir}/transform.snap.json`;
|
|
432
|
+
const actual = await transformSnapshotOutput(provider, rawFixture);
|
|
433
|
+
const serialized = stableStringify(actual);
|
|
434
|
+
const snapshotFile = Bun.file(snapshotPath);
|
|
435
|
+
|
|
436
|
+
if (shouldUpdateSnapshots() || !(await snapshotFile.exists())) {
|
|
437
|
+
await Bun.write(snapshotPath, serialized);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const expected: unknown = JSON.parse(
|
|
441
|
+
await Bun.file(snapshotPath).text(),
|
|
442
|
+
);
|
|
443
|
+
expect(actual).toEqual(expected);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
87
446
|
});
|
|
88
447
|
}
|