slicetest 0.4.0 → 0.5.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 +110 -2
- package/dist/auth.d.ts +45 -0
- package/dist/auth.js +128 -0
- package/dist/cli.js +8 -3
- package/dist/config.d.ts +14 -0
- package/dist/config.js +16 -0
- package/dist/db.d.ts +26 -0
- package/dist/db.js +50 -0
- package/dist/drivers/driver.d.ts +27 -0
- package/dist/drivers/mysql.d.ts +2 -1
- package/dist/drivers/mysql.js +38 -0
- package/dist/drivers/postgres.d.ts +2 -1
- package/dist/drivers/postgres.js +28 -0
- package/dist/drivers/sqlite.d.ts +2 -1
- package/dist/drivers/sqlite.js +52 -0
- package/dist/factory.d.ts +21 -0
- package/dist/factory.js +129 -0
- package/dist/gen.d.ts +1 -0
- package/dist/gen.js +9 -4
- package/dist/http.d.ts +7 -0
- package/dist/http.js +10 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/init.js +44 -1
- package/dist/openapi.d.ts +8 -0
- package/dist/openapi.js +18 -0
- package/dist/query-log.d.ts +49 -0
- package/dist/query-log.js +261 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.js +40 -2
- package/dist/stub.d.ts +32 -0
- package/dist/stub.js +87 -0
- package/dist/webhook.d.ts +40 -0
- package/dist/webhook.js +52 -0
- package/dist/yaml-runtime.js +41 -3
- package/dist/yaml.d.ts +36 -1
- package/dist/yaml.js +56 -2
- package/package.json +8 -2
- package/schema/scenario.schema.json +180 -0
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
const hmac = (algorithm, key, data) => createHmac(algorithm, key).update(data);
|
|
3
|
+
/**
|
|
4
|
+
* The headers a provider sends with `body`, signed with `secret` the way its
|
|
5
|
+
* SDK verifies them: Stripe's `Stripe-Signature`, GitHub's
|
|
6
|
+
* `X-Hub-Signature-256`, Slack's `v0` signature, Shopify's base64 HMAC and
|
|
7
|
+
* Standard Webhooks (Svix, Resend, Clerk, …).
|
|
8
|
+
*/
|
|
9
|
+
export function signWebhook(body, opts) {
|
|
10
|
+
const now = Math.floor(Date.now() / 1000);
|
|
11
|
+
const ts = opts.timestamp ?? (opts.stale ? now - 600 : now);
|
|
12
|
+
const secret = opts.invalidSignature ? `${opts.secret}-not-the-real-secret` : opts.secret;
|
|
13
|
+
const p = opts.provider;
|
|
14
|
+
if (typeof p === "object") {
|
|
15
|
+
if (!p.header)
|
|
16
|
+
throw new Error("slicetest: a custom webhook provider needs `header`, e.g. { header: \"X-Signature\", prefix: \"sha256=\" }");
|
|
17
|
+
return { [p.header]: `${p.prefix ?? ""}${hmac(p.algorithm ?? "sha256", secret, body).digest(p.encoding ?? "hex")}` };
|
|
18
|
+
}
|
|
19
|
+
switch (p) {
|
|
20
|
+
case "stripe":
|
|
21
|
+
return { "stripe-signature": `t=${ts},v1=${hmac("sha256", secret, `${ts}.${body}`).digest("hex")}` };
|
|
22
|
+
case "github":
|
|
23
|
+
return {
|
|
24
|
+
"x-hub-signature-256": `sha256=${hmac("sha256", secret, body).digest("hex")}`,
|
|
25
|
+
"x-github-event": opts.event ?? "ping",
|
|
26
|
+
"x-github-delivery": randomUUID(),
|
|
27
|
+
};
|
|
28
|
+
case "slack":
|
|
29
|
+
return { "x-slack-request-timestamp": String(ts), "x-slack-signature": `v0=${hmac("sha256", secret, `v0:${ts}:${body}`).digest("hex")}` };
|
|
30
|
+
case "shopify":
|
|
31
|
+
return {
|
|
32
|
+
"x-shopify-hmac-sha256": hmac("sha256", secret, body).digest("base64"),
|
|
33
|
+
...(opts.event ? { "x-shopify-topic": opts.event } : {}),
|
|
34
|
+
"x-shopify-webhook-id": randomUUID(),
|
|
35
|
+
};
|
|
36
|
+
case "standard": {
|
|
37
|
+
const id = opts.id ?? `msg_${randomUUID().replace(/-/g, "")}`;
|
|
38
|
+
const key = secret.startsWith("whsec_") ? Buffer.from(secret.slice(6), "base64") : Buffer.from(secret);
|
|
39
|
+
return { "webhook-id": id, "webhook-timestamp": String(ts), "webhook-signature": `v1,${hmac("sha256", key, `${id}.${ts}.${body}`).digest("base64")}` };
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
throw new Error(`slicetest: unknown webhook provider ${JSON.stringify(p)} (expected stripe, github, slack, shopify, standard or { header, prefix, encoding })`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The bytes to send and their content type: objects as JSON, URLSearchParams as a form (Slack commands), strings as-is. */
|
|
46
|
+
export function webhookBody(payload) {
|
|
47
|
+
if (typeof payload === "string")
|
|
48
|
+
return { body: payload, type: "application/json" };
|
|
49
|
+
if (payload instanceof URLSearchParams)
|
|
50
|
+
return { body: payload.toString(), type: "application/x-www-form-urlencoded" };
|
|
51
|
+
return { body: JSON.stringify(payload ?? {}), type: "application/json" };
|
|
52
|
+
}
|
package/dist/yaml-runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ import "./provided.js";
|
|
|
4
4
|
import { formatChanges } from "./db.js";
|
|
5
5
|
import "./matchers.js";
|
|
6
6
|
import { scenario } from "./scenario.js";
|
|
7
|
+
import { signWebhook, webhookBody } from "./webhook.js";
|
|
7
8
|
/** Registers the scenarios of one parsed YAML file. Called from the module the plugin generates for it. */
|
|
8
9
|
export function defineYamlScenarios(doc) {
|
|
9
10
|
for (const sc of doc.scenarios) {
|
|
@@ -47,6 +48,10 @@ function describeStep(step) {
|
|
|
47
48
|
return `received ${step.received}${step.call ? ` ${step.call}` : ""}`;
|
|
48
49
|
if ("insert" in step)
|
|
49
50
|
return `insert ${step.insert}`;
|
|
51
|
+
if ("make" in step)
|
|
52
|
+
return `make ${step.make}`;
|
|
53
|
+
if ("chaos" in step)
|
|
54
|
+
return `chaos ${step.chaos}`;
|
|
50
55
|
if ("db" in step)
|
|
51
56
|
return `db ${step.db}`;
|
|
52
57
|
if ("changes" in step)
|
|
@@ -159,16 +164,22 @@ async function runStep(step, ctx, vars) {
|
|
|
159
164
|
if ("request" in step) {
|
|
160
165
|
const [method, rawPath] = splitCall(step.request);
|
|
161
166
|
const path = interpolate(rawPath, vars);
|
|
167
|
+
const headers = interpolate(step.headers, vars);
|
|
162
168
|
const opts = {
|
|
163
|
-
headers: interpolate(step.
|
|
169
|
+
headers: step.auth ? { ...ctx.auth.header(step.auth === true ? {} : interpolate(step.auth, vars)), ...headers } : headers,
|
|
164
170
|
query: interpolate(step.query, vars),
|
|
165
171
|
follow: step.follow,
|
|
166
172
|
};
|
|
167
|
-
|
|
173
|
+
let body = step.json !== undefined
|
|
168
174
|
? interpolate(step.json, vars)
|
|
169
175
|
: step.form !== undefined
|
|
170
176
|
? ctx.http.form(interpolate(step.form, vars))
|
|
171
177
|
: interpolate(step.body, vars);
|
|
178
|
+
if (step.webhook) {
|
|
179
|
+
const signed = webhookBody(body);
|
|
180
|
+
body = signed.body;
|
|
181
|
+
opts.headers = { "content-type": signed.type, ...signWebhook(signed.body, interpolate(step.webhook, vars)), ...opts.headers };
|
|
182
|
+
}
|
|
172
183
|
const e = step.expect;
|
|
173
184
|
const verify = (res) => {
|
|
174
185
|
if (e?.status !== undefined)
|
|
@@ -190,7 +201,18 @@ async function runStep(step, ctx, vars) {
|
|
|
190
201
|
all.forEach(verify);
|
|
191
202
|
return;
|
|
192
203
|
}
|
|
193
|
-
const
|
|
204
|
+
const max = e?.queries === undefined ? undefined : interpolate(e.queries, vars);
|
|
205
|
+
let res;
|
|
206
|
+
const queries = await (max === undefined ? undefined : ctx.db.queries(async () => (res = await ctx.http.request(method, path, body, opts))));
|
|
207
|
+
if (!queries)
|
|
208
|
+
res = await ctx.http.request(method, path, body, opts);
|
|
209
|
+
if (queries && max !== undefined) {
|
|
210
|
+
const ran = queries.withoutTransactions();
|
|
211
|
+
if (ran.length > max) {
|
|
212
|
+
const shapes = ran.shapes().map((s) => ` ${s.count > 1 ? `×${s.count} ` : ""}${s.sql}`);
|
|
213
|
+
throw new Error(`expected at most ${max} SQL statements for ${method} ${path}, the app ran ${ran.length}:\n${shapes.join("\n")}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
194
216
|
verify(res);
|
|
195
217
|
capture(step.capture, { status: res.status, json: res.json, text: res.text, headers: Object.fromEntries(res.headers) }, vars);
|
|
196
218
|
return;
|
|
@@ -200,6 +222,22 @@ async function runStep(step, ctx, vars) {
|
|
|
200
222
|
capture(step.capture, { rows, row: rows[0] }, vars);
|
|
201
223
|
return;
|
|
202
224
|
}
|
|
225
|
+
if ("chaos" in step) {
|
|
226
|
+
const { chaos, name: _, ...opts } = step;
|
|
227
|
+
ctx.stub(chaos).chaos(opts);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if ("make" in step) {
|
|
231
|
+
const given = interpolate(step.rows, vars) ?? {};
|
|
232
|
+
const rows = [];
|
|
233
|
+
if (Array.isArray(given))
|
|
234
|
+
for (const r of given)
|
|
235
|
+
rows.push(await ctx.db.make(step.make, r));
|
|
236
|
+
else
|
|
237
|
+
rows.push(...(await ctx.db.makeMany(step.make, step.count ?? 1, given)));
|
|
238
|
+
capture(step.capture, { rows, row: rows[0] }, vars);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
203
241
|
if ("sql" in step) {
|
|
204
242
|
const rows = await ctx.db.query(step.sql, interpolate(step.params, vars) ?? []);
|
|
205
243
|
expectRows(step.expect, rows, vars);
|
package/dist/yaml.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface YamlScenario {
|
|
|
21
21
|
timeout?: number;
|
|
22
22
|
steps: Step[];
|
|
23
23
|
}
|
|
24
|
-
export type Step = (StubStep | RequestStep | InsertStep | SqlStep | DbStep | ReceivedStep | ChangesStep | CheckpointStep | LogStep | SnapshotStep | MailStep) & {
|
|
24
|
+
export type Step = (StubStep | RequestStep | InsertStep | MakeStep | ChaosStep | SqlStep | DbStep | ReceivedStep | ChangesStep | CheckpointStep | LogStep | SnapshotStep | MailStep) & {
|
|
25
25
|
line: number;
|
|
26
26
|
name?: string;
|
|
27
27
|
};
|
|
@@ -57,14 +57,31 @@ export interface RequestStep {
|
|
|
57
57
|
form?: Record<string, unknown>;
|
|
58
58
|
body?: string;
|
|
59
59
|
follow?: boolean;
|
|
60
|
+
/** Send `Authorization: Bearer` with a token from the `auth` issuer: `true`, or the claims, e.g. { sub: u1, roles: [admin] }. */
|
|
61
|
+
auth?: true | Record<string, unknown>;
|
|
62
|
+
/** Sign the body as this provider would deliver a webhook: { provider: stripe, secret: whsec_test, event, stale, invalidSignature }. */
|
|
63
|
+
webhook?: {
|
|
64
|
+
provider: string | {
|
|
65
|
+
header: string;
|
|
66
|
+
algorithm?: string;
|
|
67
|
+
encoding?: "hex" | "base64";
|
|
68
|
+
prefix?: string;
|
|
69
|
+
};
|
|
70
|
+
secret: string;
|
|
71
|
+
event?: string;
|
|
72
|
+
stale?: boolean;
|
|
73
|
+
invalidSignature?: boolean;
|
|
74
|
+
};
|
|
60
75
|
/** Send the request this many times at once. `expect` applies to every response; `statuses` counts them. */
|
|
61
76
|
concurrency?: number;
|
|
77
|
+
/** `queries`: at most this many SQL statements (not counting BEGIN/COMMIT), with `db.queries` on. */
|
|
62
78
|
expect?: {
|
|
63
79
|
status?: number;
|
|
64
80
|
statuses?: Record<string, number>;
|
|
65
81
|
headers?: Record<string, unknown>;
|
|
66
82
|
json?: unknown;
|
|
67
83
|
text?: unknown;
|
|
84
|
+
queries?: number;
|
|
68
85
|
};
|
|
69
86
|
capture?: Record<string, string>;
|
|
70
87
|
}
|
|
@@ -73,6 +90,24 @@ export interface InsertStep {
|
|
|
73
90
|
rows: Record<string, unknown> | Record<string, unknown>[];
|
|
74
91
|
capture?: Record<string, string>;
|
|
75
92
|
}
|
|
93
|
+
/** Rows built by `db.make()`: only the columns given are fixed, the rest is filled from the schema. */
|
|
94
|
+
export interface MakeStep {
|
|
95
|
+
make: string;
|
|
96
|
+
rows?: Record<string, unknown> | Record<string, unknown>[];
|
|
97
|
+
/** Make this many rows, each with `rows` (a mapping). */
|
|
98
|
+
count?: number;
|
|
99
|
+
capture?: Record<string, string>;
|
|
100
|
+
}
|
|
101
|
+
/** Faults injected into a stub's answers for the rest of the scenario, like `stub(name).chaos()`. */
|
|
102
|
+
export interface ChaosStep {
|
|
103
|
+
chaos: string;
|
|
104
|
+
failFirst?: number;
|
|
105
|
+
errorRate?: number;
|
|
106
|
+
statuses?: number[];
|
|
107
|
+
networkErrorRate?: number;
|
|
108
|
+
latency?: number | [number, number];
|
|
109
|
+
seed?: number;
|
|
110
|
+
}
|
|
76
111
|
export interface SqlStep {
|
|
77
112
|
sql: string;
|
|
78
113
|
params?: unknown[];
|
package/dist/yaml.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isMap, isSeq, LineCounter, parseDocument } from "yaml";
|
|
2
2
|
const KINDS = {
|
|
3
3
|
stub: ["on", "when", "reply", "sequence", "networkError", "times", "delay"],
|
|
4
|
-
request: ["headers", "query", "json", "form", "body", "follow", "concurrency", "expect", "capture"],
|
|
4
|
+
request: ["headers", "query", "json", "form", "body", "follow", "auth", "webhook", "concurrency", "expect", "capture"],
|
|
5
5
|
insert: ["rows", "capture"],
|
|
6
6
|
sql: ["params", "expect", "capture", "within"],
|
|
7
7
|
db: ["where", "orderBy", "expect", "capture", "within"],
|
|
@@ -11,13 +11,17 @@ const KINDS = {
|
|
|
11
11
|
checkpoint: [],
|
|
12
12
|
snapshot: ["mask"],
|
|
13
13
|
mail: ["times", "within", "capture"],
|
|
14
|
+
make: ["rows", "count", "capture"],
|
|
15
|
+
chaos: ["failFirst", "errorRate", "statuses", "networkErrorRate", "latency", "seed"],
|
|
14
16
|
};
|
|
15
17
|
const EXPECT_KEYS = {
|
|
16
|
-
request: ["status", "statuses", "headers", "json", "text"],
|
|
18
|
+
request: ["status", "statuses", "headers", "json", "text", "queries"],
|
|
17
19
|
sql: ["rows", "count"],
|
|
18
20
|
db: ["rows", "count"],
|
|
19
21
|
};
|
|
20
22
|
const CONDITION_KEYS = ["query", "headers", "json", "body"];
|
|
23
|
+
const WEBHOOK_KEYS = ["provider", "secret", "event", "stale", "invalidSignature"];
|
|
24
|
+
const WEBHOOK_PROVIDERS = ["stripe", "github", "slack", "shopify", "standard"];
|
|
21
25
|
const MAIL_KEYS = ["to", "from", "subject", "text", "html"];
|
|
22
26
|
const CHANGE_KEYS = ["inserted", "updated", "deleted"];
|
|
23
27
|
const RESPONSE_KEYS = ["status", "headers", "body"];
|
|
@@ -181,8 +185,31 @@ function parseStep(node, fail, lineOf) {
|
|
|
181
185
|
call("request", REQUEST);
|
|
182
186
|
if (["json", "form", "body"].filter((k) => raw[k] !== undefined).length > 1)
|
|
183
187
|
fail(node, "use only one of json / form / body");
|
|
188
|
+
if (raw.webhook !== undefined) {
|
|
189
|
+
const w = raw.webhook;
|
|
190
|
+
if (!w || typeof w !== "object" || Array.isArray(w))
|
|
191
|
+
fail(at("webhook"), "`webhook` must be a mapping such as { provider: stripe, secret: whsec_test }");
|
|
192
|
+
for (const k of Object.keys(w))
|
|
193
|
+
if (!WEBHOOK_KEYS.includes(k))
|
|
194
|
+
fail(at("webhook"), `unknown key "${k}" in webhook (allowed: ${WEBHOOK_KEYS.join(", ")})`);
|
|
195
|
+
if (typeof w.secret !== "string" || !w.secret)
|
|
196
|
+
fail(at("webhook"), "`webhook.secret` is required: the signing secret the app is configured with");
|
|
197
|
+
const p = w.provider;
|
|
198
|
+
if (!(typeof p === "string" ? WEBHOOK_PROVIDERS.includes(p) : p && typeof p === "object" && typeof p.header === "string")) {
|
|
199
|
+
fail(at("webhook"), `\`webhook.provider\` must be one of ${WEBHOOK_PROVIDERS.join(", ")} or { header, prefix, encoding }`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (raw.auth !== undefined && raw.auth !== true && !(raw.auth && typeof raw.auth === "object" && !Array.isArray(raw.auth)))
|
|
203
|
+
fail(at("auth"), "`auth` must be true or the token's claims, e.g. { sub: u1, roles: [admin] }");
|
|
184
204
|
if (raw.concurrency !== undefined && !(Number.isInteger(raw.concurrency) && raw.concurrency >= 1))
|
|
185
205
|
fail(at("concurrency"), "`concurrency` must be a positive whole number");
|
|
206
|
+
{
|
|
207
|
+
const q = raw.expect?.queries;
|
|
208
|
+
if (q !== undefined && !(Number.isInteger(q) && q >= 0))
|
|
209
|
+
fail(at("expect"), "`expect.queries` is the most SQL statements the request may run, e.g. 3");
|
|
210
|
+
if (q !== undefined && raw.concurrency !== undefined)
|
|
211
|
+
fail(at("expect"), "`expect.queries` can't be used with `concurrency`");
|
|
212
|
+
}
|
|
186
213
|
if (raw.concurrency !== undefined && raw.capture !== undefined)
|
|
187
214
|
fail(at("capture"), "`capture` can't be used with `concurrency`: there is more than one response");
|
|
188
215
|
{
|
|
@@ -200,6 +227,33 @@ function parseStep(node, fail, lineOf) {
|
|
|
200
227
|
if (!raw.rows || typeof raw.rows !== "object")
|
|
201
228
|
fail(at("rows"), "an insert step needs `rows` (a mapping or a list of mappings)");
|
|
202
229
|
break;
|
|
230
|
+
case "chaos":
|
|
231
|
+
for (const k of ["errorRate", "networkErrorRate"]) {
|
|
232
|
+
if (raw[k] !== undefined && !(typeof raw[k] === "number" && raw[k] >= 0 && raw[k] <= 1))
|
|
233
|
+
fail(at(k), `\`${k}\` must be a number between 0 and 1`);
|
|
234
|
+
}
|
|
235
|
+
for (const k of ["failFirst", "seed"])
|
|
236
|
+
if (raw[k] !== undefined && !Number.isInteger(raw[k]))
|
|
237
|
+
fail(at(k), `\`${k}\` must be a whole number`);
|
|
238
|
+
if (raw.statuses !== undefined && !(Array.isArray(raw.statuses) && raw.statuses.length > 0 && raw.statuses.every((s) => Number.isInteger(s) && s >= 400 && s <= 599))) {
|
|
239
|
+
fail(at("statuses"), "`statuses` must be a list of 4xx/5xx codes, e.g. [503]");
|
|
240
|
+
}
|
|
241
|
+
{
|
|
242
|
+
const l = raw.latency;
|
|
243
|
+
if (l !== undefined && !(typeof l === "number" || (Array.isArray(l) && l.length === 2 && l.every((x) => typeof x === "number"))))
|
|
244
|
+
fail(at("latency"), "`latency` must be milliseconds or [min, max]");
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case "make":
|
|
248
|
+
if (raw.rows !== undefined && (!raw.rows || typeof raw.rows !== "object"))
|
|
249
|
+
fail(at("rows"), "`rows` must be a mapping or a list of mappings");
|
|
250
|
+
if (raw.count !== undefined) {
|
|
251
|
+
if (!(Number.isInteger(raw.count) && raw.count >= 1))
|
|
252
|
+
fail(at("count"), "`count` must be a positive whole number");
|
|
253
|
+
if (Array.isArray(raw.rows))
|
|
254
|
+
fail(at("count"), "use either `count` or a list of `rows`");
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
203
257
|
case "log":
|
|
204
258
|
try {
|
|
205
259
|
new RegExp(raw.log);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slicetest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Framework-agnostic slice tests: drive a real app process over HTTP with a real Postgres, MySQL or SQLite and stubbed outbound APIs, checked against OpenAPI contracts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"testing",
|
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
"testcontainers",
|
|
19
19
|
"language-agnostic",
|
|
20
20
|
"openapi",
|
|
21
|
-
"contract-testing"
|
|
21
|
+
"contract-testing",
|
|
22
|
+
"jwt",
|
|
23
|
+
"oidc",
|
|
24
|
+
"webhooks",
|
|
25
|
+
"chaos-testing",
|
|
26
|
+
"n-plus-one",
|
|
27
|
+
"test-data-factory"
|
|
22
28
|
],
|
|
23
29
|
"author": "pon-bok",
|
|
24
30
|
"repository": {
|
|
@@ -180,6 +180,81 @@
|
|
|
180
180
|
"follow": {
|
|
181
181
|
"type": "boolean"
|
|
182
182
|
},
|
|
183
|
+
"auth": {
|
|
184
|
+
"description": "Send Authorization: Bearer with a token from the auth issuer: true, or the token's claims",
|
|
185
|
+
"oneOf": [
|
|
186
|
+
{
|
|
187
|
+
"const": true
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"type": "object"
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"webhook": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"description": "Sign the body as this provider delivers webhooks",
|
|
197
|
+
"required": [
|
|
198
|
+
"provider",
|
|
199
|
+
"secret"
|
|
200
|
+
],
|
|
201
|
+
"additionalProperties": false,
|
|
202
|
+
"properties": {
|
|
203
|
+
"provider": {
|
|
204
|
+
"oneOf": [
|
|
205
|
+
{
|
|
206
|
+
"enum": [
|
|
207
|
+
"stripe",
|
|
208
|
+
"github",
|
|
209
|
+
"slack",
|
|
210
|
+
"shopify",
|
|
211
|
+
"standard"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"type": "object",
|
|
216
|
+
"required": [
|
|
217
|
+
"header"
|
|
218
|
+
],
|
|
219
|
+
"additionalProperties": false,
|
|
220
|
+
"properties": {
|
|
221
|
+
"header": {
|
|
222
|
+
"type": "string"
|
|
223
|
+
},
|
|
224
|
+
"algorithm": {
|
|
225
|
+
"type": "string"
|
|
226
|
+
},
|
|
227
|
+
"encoding": {
|
|
228
|
+
"enum": [
|
|
229
|
+
"hex",
|
|
230
|
+
"base64"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
"prefix": {
|
|
234
|
+
"type": "string"
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
},
|
|
240
|
+
"secret": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "The signing secret the app is configured with"
|
|
243
|
+
},
|
|
244
|
+
"event": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"description": "Event type, e.g. X-GitHub-Event"
|
|
247
|
+
},
|
|
248
|
+
"stale": {
|
|
249
|
+
"type": "boolean",
|
|
250
|
+
"description": "Signed ten minutes ago"
|
|
251
|
+
},
|
|
252
|
+
"invalidSignature": {
|
|
253
|
+
"type": "boolean",
|
|
254
|
+
"description": "Signed with a different secret"
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
183
258
|
"concurrency": {
|
|
184
259
|
"type": "integer",
|
|
185
260
|
"minimum": 1,
|
|
@@ -212,6 +287,11 @@
|
|
|
212
287
|
"minimum": 0
|
|
213
288
|
},
|
|
214
289
|
"description": "With `concurrency`: how many responses have each status, e.g. { 201: 1, 409: 9 }"
|
|
290
|
+
},
|
|
291
|
+
"queries": {
|
|
292
|
+
"type": "integer",
|
|
293
|
+
"minimum": 0,
|
|
294
|
+
"description": "At most this many SQL statements for this request, not counting BEGIN/COMMIT (needs db.queries)"
|
|
215
295
|
}
|
|
216
296
|
}
|
|
217
297
|
},
|
|
@@ -255,6 +335,106 @@
|
|
|
255
335
|
}
|
|
256
336
|
}
|
|
257
337
|
},
|
|
338
|
+
{
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": [
|
|
341
|
+
"make"
|
|
342
|
+
],
|
|
343
|
+
"additionalProperties": false,
|
|
344
|
+
"properties": {
|
|
345
|
+
"make": {
|
|
346
|
+
"type": "string",
|
|
347
|
+
"description": "Table to make rows in: required columns and parent rows are filled from the schema"
|
|
348
|
+
},
|
|
349
|
+
"name": {
|
|
350
|
+
"type": "string",
|
|
351
|
+
"description": "Label shown when this step fails"
|
|
352
|
+
},
|
|
353
|
+
"rows": {
|
|
354
|
+
"type": [
|
|
355
|
+
"object",
|
|
356
|
+
"array"
|
|
357
|
+
],
|
|
358
|
+
"description": "Columns to set: a mapping, or a list of mappings for several rows"
|
|
359
|
+
},
|
|
360
|
+
"count": {
|
|
361
|
+
"type": "integer",
|
|
362
|
+
"minimum": 1,
|
|
363
|
+
"description": "Make this many rows, each with `rows`"
|
|
364
|
+
},
|
|
365
|
+
"capture": {
|
|
366
|
+
"type": "object",
|
|
367
|
+
"description": "Save values as variables for later steps, e.g. { orderId: row.id }",
|
|
368
|
+
"additionalProperties": {
|
|
369
|
+
"type": "string"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"type": "object",
|
|
376
|
+
"required": [
|
|
377
|
+
"chaos"
|
|
378
|
+
],
|
|
379
|
+
"additionalProperties": false,
|
|
380
|
+
"properties": {
|
|
381
|
+
"chaos": {
|
|
382
|
+
"type": "string",
|
|
383
|
+
"description": "Stub to inject faults into, for the rest of the scenario"
|
|
384
|
+
},
|
|
385
|
+
"name": {
|
|
386
|
+
"type": "string",
|
|
387
|
+
"description": "Label shown when this step fails"
|
|
388
|
+
},
|
|
389
|
+
"failFirst": {
|
|
390
|
+
"type": "integer",
|
|
391
|
+
"minimum": 0,
|
|
392
|
+
"description": "Fail the first n calls, then answer normally"
|
|
393
|
+
},
|
|
394
|
+
"errorRate": {
|
|
395
|
+
"type": "number",
|
|
396
|
+
"minimum": 0,
|
|
397
|
+
"maximum": 1,
|
|
398
|
+
"description": "Share of calls answered with an error status"
|
|
399
|
+
},
|
|
400
|
+
"statuses": {
|
|
401
|
+
"type": "array",
|
|
402
|
+
"minItems": 1,
|
|
403
|
+
"items": {
|
|
404
|
+
"type": "integer",
|
|
405
|
+
"minimum": 400,
|
|
406
|
+
"maximum": 599
|
|
407
|
+
},
|
|
408
|
+
"description": "Error statuses to pick from (default 500, 502, 503)"
|
|
409
|
+
},
|
|
410
|
+
"networkErrorRate": {
|
|
411
|
+
"type": "number",
|
|
412
|
+
"minimum": 0,
|
|
413
|
+
"maximum": 1,
|
|
414
|
+
"description": "Share of calls whose connection is dropped"
|
|
415
|
+
},
|
|
416
|
+
"latency": {
|
|
417
|
+
"oneOf": [
|
|
418
|
+
{
|
|
419
|
+
"type": "number"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"type": "array",
|
|
423
|
+
"items": {
|
|
424
|
+
"type": "number"
|
|
425
|
+
},
|
|
426
|
+
"minItems": 2,
|
|
427
|
+
"maxItems": 2
|
|
428
|
+
}
|
|
429
|
+
],
|
|
430
|
+
"description": "Extra delay in ms, or [min, max]"
|
|
431
|
+
},
|
|
432
|
+
"seed": {
|
|
433
|
+
"type": "integer",
|
|
434
|
+
"description": "Seed for the random faults"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
},
|
|
258
438
|
{
|
|
259
439
|
"type": "object",
|
|
260
440
|
"required": [
|