@apifuse/connector-sdk 2.0.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/README.md +44 -0
- package/bin/apifuse-check.ts +408 -0
- package/bin/apifuse-dev.ts +222 -0
- package/bin/apifuse-init.ts +390 -0
- package/bin/apifuse-perf.ts +1101 -0
- package/bin/apifuse-record.ts +446 -0
- package/bin/apifuse-test.ts +688 -0
- package/bin/apifuse.ts +51 -0
- package/package.json +64 -0
- package/src/__tests__/auth.test.ts +396 -0
- package/src/__tests__/browser-auth.test.ts +180 -0
- package/src/__tests__/browser.test.ts +632 -0
- package/src/__tests__/connectors-yaml.test.ts +135 -0
- package/src/__tests__/define.test.ts +225 -0
- package/src/__tests__/errors.test.ts +69 -0
- package/src/__tests__/executor.test.ts +214 -0
- package/src/__tests__/http.test.ts +238 -0
- package/src/__tests__/insights.test.ts +210 -0
- package/src/__tests__/instrumentation.test.ts +290 -0
- package/src/__tests__/otlp.test.ts +141 -0
- package/src/__tests__/perf.test.ts +60 -0
- package/src/__tests__/proxy.test.ts +359 -0
- package/src/__tests__/recipes.test.ts +36 -0
- package/src/__tests__/serve.test.ts +233 -0
- package/src/__tests__/session.test.ts +231 -0
- package/src/__tests__/state.test.ts +100 -0
- package/src/__tests__/stealth.test.ts +57 -0
- package/src/__tests__/testing.test.ts +97 -0
- package/src/__tests__/tls.test.ts +345 -0
- package/src/__tests__/types.test.ts +142 -0
- package/src/__tests__/utils.test.ts +62 -0
- package/src/__tests__/waterfall.test.ts +270 -0
- package/src/config/connectors-yaml.ts +373 -0
- package/src/config/loader.ts +122 -0
- package/src/define.ts +137 -0
- package/src/dev.ts +38 -0
- package/src/errors.ts +68 -0
- package/src/index.test.ts +1 -0
- package/src/index.ts +100 -0
- package/src/protocol.ts +183 -0
- package/src/recipes/gov-api.ts +97 -0
- package/src/recipes/rest-api.ts +152 -0
- package/src/runtime/auth.ts +245 -0
- package/src/runtime/browser.ts +724 -0
- package/src/runtime/connector.ts +20 -0
- package/src/runtime/executor.ts +51 -0
- package/src/runtime/http.ts +248 -0
- package/src/runtime/insights.ts +456 -0
- package/src/runtime/instrumentation.ts +424 -0
- package/src/runtime/otlp.ts +171 -0
- package/src/runtime/perf.ts +73 -0
- package/src/runtime/session.ts +573 -0
- package/src/runtime/state.ts +124 -0
- package/src/runtime/tls.ts +410 -0
- package/src/runtime/trace.ts +261 -0
- package/src/runtime/waterfall.ts +245 -0
- package/src/serve.ts +665 -0
- package/src/stealth/profiles.ts +391 -0
- package/src/testing/helpers.ts +144 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/run.ts +88 -0
- package/src/types/playwright-stealth.d.ts +9 -0
- package/src/types.ts +243 -0
- package/src/utils/date.ts +163 -0
- package/src/utils/parse.ts +66 -0
- package/src/utils/text.ts +20 -0
- package/src/utils/transform.ts +62 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createSessionStore,
|
|
5
|
+
createSqliteSessionStore,
|
|
6
|
+
type SessionFetch,
|
|
7
|
+
SupabaseSessionStore,
|
|
8
|
+
} from "../runtime/session";
|
|
9
|
+
|
|
10
|
+
type StoredSecretRecord = {
|
|
11
|
+
connection_id: string;
|
|
12
|
+
encrypted_blob: string;
|
|
13
|
+
encryption_key_version: number;
|
|
14
|
+
iv: string;
|
|
15
|
+
tag: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function createStore() {
|
|
19
|
+
return createSqliteSessionStore({
|
|
20
|
+
databasePath: ":memory:",
|
|
21
|
+
namespace: "test-suite",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createMockSupabaseFetch(seed?: StoredSecretRecord) {
|
|
26
|
+
let storedRecord = seed ?? null;
|
|
27
|
+
|
|
28
|
+
const fetchImpl: SessionFetch = async (input, init) => {
|
|
29
|
+
const requestUrl = new URL(String(input));
|
|
30
|
+
const method = init?.method ?? "GET";
|
|
31
|
+
|
|
32
|
+
if (requestUrl.pathname !== "/rest/v1/connection_secrets") {
|
|
33
|
+
return new Response("not found", { status: 404 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (method === "GET") {
|
|
37
|
+
const connectionId = requestUrl.searchParams
|
|
38
|
+
.get("connection_id")
|
|
39
|
+
?.replace("eq.", "");
|
|
40
|
+
const result =
|
|
41
|
+
storedRecord && storedRecord.connection_id === connectionId
|
|
42
|
+
? [
|
|
43
|
+
{
|
|
44
|
+
encrypted_blob: storedRecord.encrypted_blob,
|
|
45
|
+
encryption_key_version: storedRecord.encryption_key_version,
|
|
46
|
+
iv: storedRecord.iv,
|
|
47
|
+
tag: storedRecord.tag,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
: [];
|
|
51
|
+
|
|
52
|
+
return Response.json(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (method === "POST") {
|
|
56
|
+
const body = JSON.parse(String(init?.body)) as StoredSecretRecord[];
|
|
57
|
+
storedRecord = body[0] ?? null;
|
|
58
|
+
return new Response(null, { status: 201 });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (method === "DELETE") {
|
|
62
|
+
const connectionId = requestUrl.searchParams
|
|
63
|
+
.get("connection_id")
|
|
64
|
+
?.replace("eq.", "");
|
|
65
|
+
if (storedRecord?.connection_id === connectionId) {
|
|
66
|
+
storedRecord = null;
|
|
67
|
+
}
|
|
68
|
+
return new Response(null, { status: 204 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new Response("method not allowed", { status: 405 });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
fetchImpl,
|
|
76
|
+
getStoredRecord: () => storedRecord,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createSupabaseStore(seed?: StoredSecretRecord) {
|
|
81
|
+
const mock = createMockSupabaseFetch(seed);
|
|
82
|
+
return {
|
|
83
|
+
...mock,
|
|
84
|
+
store: new SupabaseSessionStore({
|
|
85
|
+
connectionId: "conn_123",
|
|
86
|
+
encryptionKey: "12345678901234567890123456789012",
|
|
87
|
+
fetch: mock.fetchImpl,
|
|
88
|
+
supabaseKey: "service-role-key",
|
|
89
|
+
supabaseUrl: "https://example.supabase.co",
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("createSessionStore", () => {
|
|
95
|
+
it("round-trips stored values", async () => {
|
|
96
|
+
const store = createStore();
|
|
97
|
+
const session = "accessToken=token-123;refreshToken=refresh-456";
|
|
98
|
+
|
|
99
|
+
await store.set("auth", session);
|
|
100
|
+
|
|
101
|
+
expect(await store.get("auth")).toEqual(session);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns null for missing keys", async () => {
|
|
105
|
+
const store = createStore();
|
|
106
|
+
|
|
107
|
+
expect(await store.get("missing")).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns null for expired keys", async () => {
|
|
111
|
+
const store = createStore();
|
|
112
|
+
|
|
113
|
+
await store.set("expired", "token=stale", "0ms");
|
|
114
|
+
|
|
115
|
+
expect(await store.get("expired")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("deletes keys", async () => {
|
|
119
|
+
const store = createStore();
|
|
120
|
+
|
|
121
|
+
await store.set("auth", "token=live");
|
|
122
|
+
await store.delete("auth");
|
|
123
|
+
|
|
124
|
+
expect(await store.get("auth")).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("upserts existing keys", async () => {
|
|
128
|
+
const store = createStore();
|
|
129
|
+
|
|
130
|
+
await store.set("auth", "token=first");
|
|
131
|
+
await store.set("auth", "token=second");
|
|
132
|
+
|
|
133
|
+
expect(await store.get("auth")).toBe("token=second");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("stores expires_at based on ttl parsing", async () => {
|
|
137
|
+
const store = createStore();
|
|
138
|
+
const startedAt = Date.now();
|
|
139
|
+
|
|
140
|
+
await store.set("auth", "token=ttl", "15m");
|
|
141
|
+
|
|
142
|
+
const record = store.__unsafeInspect("auth");
|
|
143
|
+
|
|
144
|
+
expect(record).not.toBeNull();
|
|
145
|
+
expect(record?.expiresAt).not.toBeNull();
|
|
146
|
+
expect(record?.expiresAt ?? 0).toBeGreaterThanOrEqual(
|
|
147
|
+
startedAt + 15 * 60 * 1000 - 2_000,
|
|
148
|
+
);
|
|
149
|
+
expect(record?.expiresAt ?? 0).toBeLessThanOrEqual(
|
|
150
|
+
startedAt + 15 * 60 * 1000 + 2_000,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("creates a sqlite store via factory", async () => {
|
|
155
|
+
const store = createSessionStore({
|
|
156
|
+
backend: "sqlite",
|
|
157
|
+
dbPath: ":memory:",
|
|
158
|
+
namespace: "factory-test",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await store.set("auth", "token=factory");
|
|
162
|
+
expect(await store.get("auth")).toBe("token=factory");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("SupabaseSessionStore", () => {
|
|
167
|
+
it("encrypts and round-trips stored values", async () => {
|
|
168
|
+
const { getStoredRecord, store } = createSupabaseStore();
|
|
169
|
+
|
|
170
|
+
await store.set("accessToken", "token-123");
|
|
171
|
+
await store.set("refreshToken", "refresh-456");
|
|
172
|
+
|
|
173
|
+
expect(await store.get("accessToken")).toBe("token-123");
|
|
174
|
+
expect(await store.get("refreshToken")).toBe("refresh-456");
|
|
175
|
+
|
|
176
|
+
const record = getStoredRecord();
|
|
177
|
+
expect(record).not.toBeNull();
|
|
178
|
+
expect(record?.encrypted_blob).not.toContain("token-123");
|
|
179
|
+
expect(record?.iv).toBeTruthy();
|
|
180
|
+
expect(record?.tag).toBeTruthy();
|
|
181
|
+
expect(record?.encryption_key_version).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("returns null for missing keys", async () => {
|
|
185
|
+
const { store } = createSupabaseStore();
|
|
186
|
+
|
|
187
|
+
expect(await store.get("missing")).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("deletes individual keys and removes the row when empty", async () => {
|
|
191
|
+
const { getStoredRecord, store } = createSupabaseStore();
|
|
192
|
+
|
|
193
|
+
await store.set("accessToken", "token-123");
|
|
194
|
+
await store.set("refreshToken", "refresh-456");
|
|
195
|
+
await store.delete("accessToken");
|
|
196
|
+
|
|
197
|
+
expect(await store.get("accessToken")).toBeNull();
|
|
198
|
+
expect(await store.get("refreshToken")).toBe("refresh-456");
|
|
199
|
+
expect(getStoredRecord()).not.toBeNull();
|
|
200
|
+
|
|
201
|
+
await store.delete("refreshToken");
|
|
202
|
+
expect(getStoredRecord()).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("creates a supabase store via factory", async () => {
|
|
206
|
+
const mock = createMockSupabaseFetch();
|
|
207
|
+
const store = createSessionStore({
|
|
208
|
+
backend: "supabase",
|
|
209
|
+
connectionId: "conn_123",
|
|
210
|
+
encryptionKey: "12345678901234567890123456789012",
|
|
211
|
+
fetch: mock.fetchImpl,
|
|
212
|
+
supabaseKey: "service-role-key",
|
|
213
|
+
supabaseUrl: "https://example.supabase.co",
|
|
214
|
+
}) as SupabaseSessionStore;
|
|
215
|
+
|
|
216
|
+
await store.set("accessToken", "factory-token");
|
|
217
|
+
expect(await store.get("accessToken")).toBe("factory-token");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("loads the full session envelope", async () => {
|
|
221
|
+
const { store } = createSupabaseStore();
|
|
222
|
+
|
|
223
|
+
await store.set("accessToken", "token-123");
|
|
224
|
+
await store.set("refreshToken", "refresh-456");
|
|
225
|
+
|
|
226
|
+
expect(await store.loadAll()).toEqual({
|
|
227
|
+
accessToken: "token-123",
|
|
228
|
+
refreshToken: "refresh-456",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { createStateContext } from "../runtime/state";
|
|
4
|
+
|
|
5
|
+
function mutateSegment(value: string): string {
|
|
6
|
+
if (value.length === 0) {
|
|
7
|
+
return "x";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const lastCharacter = value.at(-1);
|
|
11
|
+
const replacement = lastCharacter === "a" ? "b" : "a";
|
|
12
|
+
|
|
13
|
+
return `${value.slice(0, -1)}${replacement}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("createStateContext", () => {
|
|
17
|
+
it("round-trips sealed values", async () => {
|
|
18
|
+
const state = createStateContext();
|
|
19
|
+
const data = {
|
|
20
|
+
bookingId: "booking-123",
|
|
21
|
+
flags: ["priority", "vip"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const token = await state.seal(data);
|
|
25
|
+
|
|
26
|
+
await expect(state.unseal(token)).resolves.toEqual(data);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns null for expired tokens", async () => {
|
|
30
|
+
const state = createStateContext();
|
|
31
|
+
const token = await state.seal({ retry: true }, { ttl: "1ms" });
|
|
32
|
+
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
34
|
+
|
|
35
|
+
await expect(state.unseal(token)).resolves.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns null for tampered hmac values", async () => {
|
|
39
|
+
const state = createStateContext();
|
|
40
|
+
const [payload, signature] = (await state.seal({ secure: true })).split(
|
|
41
|
+
".",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
state.unseal(`${payload}.${mutateSegment(signature ?? "")}`),
|
|
46
|
+
).resolves.toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns null for tampered payload values", async () => {
|
|
50
|
+
const state = createStateContext();
|
|
51
|
+
const [payload, signature] = (await state.seal({ secure: true })).split(
|
|
52
|
+
".",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
state.unseal(`${mutateSegment(payload ?? "")}.${signature}`),
|
|
57
|
+
).resolves.toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns null for malformed tokens without throwing", async () => {
|
|
61
|
+
const state = createStateContext();
|
|
62
|
+
|
|
63
|
+
const resultPromise = state.unseal("garbage");
|
|
64
|
+
expect(resultPromise).toBeInstanceOf(Promise);
|
|
65
|
+
await expect(resultPromise).resolves.toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("supports custom secrets", async () => {
|
|
69
|
+
const writer = createStateContext("custom-secret");
|
|
70
|
+
const reader = createStateContext("custom-secret");
|
|
71
|
+
|
|
72
|
+
const token = await writer.seal({ secretScoped: true });
|
|
73
|
+
|
|
74
|
+
await expect(reader.unseal(token)).resolves.toEqual({ secretScoped: true });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("round-trips null payloads", async () => {
|
|
78
|
+
const state = createStateContext();
|
|
79
|
+
|
|
80
|
+
await expect(state.unseal(await state.seal(null))).resolves.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("round-trips nested objects", async () => {
|
|
84
|
+
const state = createStateContext();
|
|
85
|
+
const data = {
|
|
86
|
+
checkout: {
|
|
87
|
+
items: [
|
|
88
|
+
{ id: "item-1", quantity: 2 },
|
|
89
|
+
{ id: "item-2", quantity: 1 },
|
|
90
|
+
],
|
|
91
|
+
totals: {
|
|
92
|
+
currency: "KRW",
|
|
93
|
+
value: 42_000,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await expect(state.unseal(await state.seal(data))).resolves.toEqual(data);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { SDKError } from "../errors";
|
|
4
|
+
import { getStealthProfile, listStealthProfiles } from "../stealth/profiles";
|
|
5
|
+
|
|
6
|
+
describe("stealth profiles", () => {
|
|
7
|
+
it("returns the chrome-131 profile", () => {
|
|
8
|
+
const profile = getStealthProfile("chrome-131");
|
|
9
|
+
|
|
10
|
+
expect(profile.platform).toBe("macos");
|
|
11
|
+
expect(profile.tlsClientIdentifier).toBe("chrome_131");
|
|
12
|
+
expect(profile.ja4).toBe("t13d1516h2_8daaf6152771_02713d6af862");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns the chrome-146 profile without unverified ja4", () => {
|
|
16
|
+
const profile = getStealthProfile("chrome-146");
|
|
17
|
+
|
|
18
|
+
expect(profile.platform).toBe("macos");
|
|
19
|
+
expect(profile.tlsClientIdentifier).toBe("chrome_146");
|
|
20
|
+
expect(profile.ja4).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns the firefox-147 profile", () => {
|
|
24
|
+
const profile = getStealthProfile("firefox-147");
|
|
25
|
+
|
|
26
|
+
expect(profile.platform).toBe("macos");
|
|
27
|
+
expect(profile.tlsClientIdentifier).toBe("firefox_147");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns the ios-safari-26 profile", () => {
|
|
31
|
+
const profile = getStealthProfile("ios-safari-26");
|
|
32
|
+
|
|
33
|
+
expect(profile.platform).toBe("ios");
|
|
34
|
+
expect(profile.tlsClientIdentifier).toBe("safari_ios_26_0");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws SDKError for unknown profiles", () => {
|
|
38
|
+
expect(() => getStealthProfile("unknown-profile")).toThrow(SDKError);
|
|
39
|
+
expect(() => getStealthProfile("unknown-profile")).toThrow(
|
|
40
|
+
"Unknown stealth profile: unknown-profile",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("lists 20+ bundled profiles", () => {
|
|
45
|
+
const profiles = listStealthProfiles();
|
|
46
|
+
|
|
47
|
+
expect(profiles.length).toBeGreaterThanOrEqual(20);
|
|
48
|
+
expect(profiles).toEqual(
|
|
49
|
+
expect.arrayContaining([
|
|
50
|
+
"chrome-131",
|
|
51
|
+
"chrome-146",
|
|
52
|
+
"firefox-147",
|
|
53
|
+
"ios-safari-26",
|
|
54
|
+
]),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import { defineConnector } from "../define";
|
|
5
|
+
import * as sdk from "../index";
|
|
6
|
+
import {
|
|
7
|
+
describeTransform,
|
|
8
|
+
runStandardTests,
|
|
9
|
+
snapshotTransform,
|
|
10
|
+
toMatchShape,
|
|
11
|
+
} from "../testing";
|
|
12
|
+
|
|
13
|
+
const testConnector = defineConnector({
|
|
14
|
+
id: "test-connector",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
runtime: "standard",
|
|
17
|
+
meta: {
|
|
18
|
+
displayName: "Test Connector",
|
|
19
|
+
category: "test",
|
|
20
|
+
},
|
|
21
|
+
operations: {
|
|
22
|
+
search: {
|
|
23
|
+
input: z.object({ q: z.string() }),
|
|
24
|
+
output: z.object({ result: z.string() }),
|
|
25
|
+
handler: async (_ctx, input: unknown) => {
|
|
26
|
+
const { q } = z.object({ q: z.string() }).parse(input);
|
|
27
|
+
|
|
28
|
+
return { result: `found: ${q}` };
|
|
29
|
+
},
|
|
30
|
+
fixtures: {
|
|
31
|
+
request: { q: "hello" },
|
|
32
|
+
response: { result: "found: hello" },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
runStandardTests(testConnector);
|
|
39
|
+
|
|
40
|
+
describeTransform("double-value", { value: 2 }, { doubled: 4 }, (raw) => ({
|
|
41
|
+
doubled: raw.value * 2,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
describe("toMatchShape", () => {
|
|
45
|
+
it("passes when all shape keys match", () => {
|
|
46
|
+
expect(() => {
|
|
47
|
+
toMatchShape(
|
|
48
|
+
{ name: "Alice", age: 30, extra: "ignored" },
|
|
49
|
+
{ name: "Alice", age: 30 },
|
|
50
|
+
);
|
|
51
|
+
}).not.toThrow();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("supports nested type descriptors", () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
toMatchShape(
|
|
57
|
+
{
|
|
58
|
+
name: "Alice",
|
|
59
|
+
age: 30,
|
|
60
|
+
tags: ["pro"],
|
|
61
|
+
metadata: { source: "fixture" },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "string",
|
|
65
|
+
age: "number",
|
|
66
|
+
tags: "array",
|
|
67
|
+
metadata: { source: "string" },
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
}).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("fails when shape key does not match", () => {
|
|
74
|
+
expect(() => {
|
|
75
|
+
toMatchShape({ name: "Bob" }, { name: "Alice" });
|
|
76
|
+
}).toThrow();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("fails when type descriptor does not match", () => {
|
|
80
|
+
expect(() => {
|
|
81
|
+
toMatchShape({ name: 123 }, { name: "string" });
|
|
82
|
+
}).toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("testing exports", () => {
|
|
87
|
+
it("re-exports testing helpers from package root", () => {
|
|
88
|
+
expect(typeof sdk.runStandardTests).toBe("function");
|
|
89
|
+
expect("describeTransform" in sdk).toBe(false);
|
|
90
|
+
expect(typeof sdk.toMatchShape).toBe("function");
|
|
91
|
+
expect(typeof sdk.snapshotTransform).toBe("function");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("exposes snapshotTransform from testing entrypoint", () => {
|
|
95
|
+
expect(typeof snapshotTransform).toBe("function");
|
|
96
|
+
});
|
|
97
|
+
});
|