zuplo 7.4.4 → 7.4.6
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/docs/articles/ci-cd-azure/local-testing.mdx +22 -4
- package/docs/articles/ci-cd-bitbucket/local-testing.mdx +22 -4
- package/docs/articles/ci-cd-circleci/local-testing.mdx +21 -4
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +28 -13
- package/docs/articles/ci-cd-github/local-testing.mdx +36 -16
- package/docs/articles/ci-cd-gitlab/local-testing.mdx +22 -4
- package/docs/articles/github-deployment-testing.mdx +118 -31
- package/docs/articles/testing-getting-started.mdx +220 -0
- package/docs/articles/testing-preview-environments.mdx +148 -0
- package/docs/articles/testing-recipes.mdx +429 -0
- package/docs/articles/testing.mdx +138 -408
- package/docs/mcp-gateway/auth/configuring-auth0.mdx +6 -5
- package/docs/mcp-gateway/auth/configuring-clerk.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-cognito.mdx +5 -4
- package/docs/mcp-gateway/auth/configuring-entra.mdx +5 -4
- package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +8 -8
- package/docs/mcp-gateway/auth/configuring-google.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-keycloak.mdx +4 -3
- package/docs/mcp-gateway/auth/configuring-logto.mdx +4 -4
- package/docs/mcp-gateway/auth/configuring-okta.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-onelogin.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-ping.mdx +3 -3
- package/docs/mcp-gateway/auth/configuring-workos.mdx +5 -4
- package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +8 -8
- package/docs/mcp-gateway/auth/overview.mdx +17 -17
- package/docs/mcp-gateway/auth/upstream-oauth.mdx +5 -5
- package/docs/mcp-gateway/code-config/local-development.mdx +14 -12
- package/docs/mcp-gateway/code-config/overview.mdx +9 -4
- package/docs/mcp-gateway/connect-clients/chatgpt.mdx +114 -56
- package/docs/mcp-gateway/how-it-works.mdx +11 -9
- package/docs/mcp-gateway/introduction.mdx +3 -1
- package/docs/mcp-gateway/quickstart-local.mdx +7 -7
- package/docs/mcp-gateway/reference.mdx +58 -25
- package/docs/mcp-gateway/server-registry.mdx +179 -0
- package/docs/mcp-gateway/test-clients.mdx +2 -2
- package/docs/mcp-server/custom-tools.mdx +32 -0
- package/docs/programmable-api/mcp-gateway-plugin.mdx +137 -0
- package/docs/programmable-api/mcp-sdk.mdx +240 -0
- package/docs/self-hosted/overview.md +2 -0
- package/package.json +5 -5
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Gateway test recipes
|
|
3
|
+
sidebar_label: Test recipes
|
|
4
|
+
description: >-
|
|
5
|
+
Copy-pasteable TypeScript test files for the assertions that matter at a
|
|
6
|
+
gateway—auth rejections, deterministic rate limits, response schema validation
|
|
7
|
+
with Zod, OpenAPI conformance, error contracts, routing, and test data
|
|
8
|
+
hygiene.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
The interesting assertions at a gateway are the rejections. A suite that only
|
|
12
|
+
sends well-formed requests with correct credentials passes while a policy is
|
|
13
|
+
attached to the wrong route — because the request still succeeds, for the wrong
|
|
14
|
+
caller.
|
|
15
|
+
|
|
16
|
+
Each recipe below is a complete file you can drop into `tests/`. They assume the
|
|
17
|
+
setup from [Get started with zuplo test](./testing-getting-started.mdx), which
|
|
18
|
+
installs chai for the assertions. The two schema recipes add one library each
|
|
19
|
+
and say so inline.
|
|
20
|
+
|
|
21
|
+
All of them use the same helper for building URLs:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Auth rejections
|
|
28
|
+
|
|
29
|
+
Test every way a request can be turned away, and finish with the case that
|
|
30
|
+
matters most: a valid token from one tenant asking for another tenant's object.
|
|
31
|
+
Broken object-level authorization is invisible to happy-path testing because the
|
|
32
|
+
request succeeds.
|
|
33
|
+
|
|
34
|
+
```ts title="/tests/auth.test.ts"
|
|
35
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
36
|
+
import { expect } from "chai";
|
|
37
|
+
|
|
38
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
39
|
+
|
|
40
|
+
describe("auth policy", () => {
|
|
41
|
+
it("rejects requests with no token", async () => {
|
|
42
|
+
const response = await fetch(url("/v1/orders/ord_1001"));
|
|
43
|
+
expect(response.status).to.equal(401);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("rejects expired tokens", async () => {
|
|
47
|
+
const response = await fetch(url("/v1/orders/ord_1001"), {
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${TestHelper.environment.EXPIRED_JWT}`,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(response.status).to.equal(401);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("rejects tokens issued for a different audience", async () => {
|
|
56
|
+
const response = await fetch(url("/v1/orders/ord_1001"), {
|
|
57
|
+
headers: {
|
|
58
|
+
Authorization: `Bearer ${TestHelper.environment.WRONG_AUDIENCE_JWT}`,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
expect(response.status).to.equal(401);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("does not serve another tenant's order", async () => {
|
|
65
|
+
// Tenant A's valid token, tenant B's order ID.
|
|
66
|
+
const response = await fetch(url("/v1/orders/ord_2002"), {
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
expect([403, 404]).to.include(response.status);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run the same file against every route that shares the policy. A route added
|
|
77
|
+
later without the policy attached is exactly the defect this catches.
|
|
78
|
+
|
|
79
|
+
## Rate limits, deterministically
|
|
80
|
+
|
|
81
|
+
Most rate limit tests are flaky for two reasons: tests share a bucket, and tests
|
|
82
|
+
sleep through real time windows. Fix both.
|
|
83
|
+
|
|
84
|
+
Give each test its own bucket by generating a fresh value for whatever the
|
|
85
|
+
policy keys on — an API key, a client ID, a header. Then send requests one at a
|
|
86
|
+
time and stop at the first `429`.
|
|
87
|
+
|
|
88
|
+
```ts title="/tests/rate-limit.test.ts"
|
|
89
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
90
|
+
import { expect } from "chai";
|
|
91
|
+
|
|
92
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
93
|
+
|
|
94
|
+
describe("rate limit policy", () => {
|
|
95
|
+
it("returns 429 with Retry-After when the limit is exceeded", async () => {
|
|
96
|
+
// A unique bucket per run: no other test, and no previous run of this
|
|
97
|
+
// test, can have consumed any of this budget.
|
|
98
|
+
const clientId = `rl-test-${crypto.randomUUID()}`;
|
|
99
|
+
|
|
100
|
+
let response!: Response;
|
|
101
|
+
for (let i = 0; i < 50; i++) {
|
|
102
|
+
response = await fetch(url("/v1/search?q=gateways"), {
|
|
103
|
+
headers: { "client-id": clientId },
|
|
104
|
+
});
|
|
105
|
+
if (response.status === 429) break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(response.status).to.equal(429);
|
|
109
|
+
expect(response.headers.get("retry-after")).to.not.be.null;
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Four details are load-bearing:
|
|
115
|
+
|
|
116
|
+
- **Point the test at a low limit.** Test a route configured for a handful of
|
|
117
|
+
requests, not your production 10,000/minute tier. The behavior you are
|
|
118
|
+
verifying — that the policy is attached, keyed correctly, and returns the
|
|
119
|
+
right response — is identical either way. A test that has to send ten thousand
|
|
120
|
+
requests to prove it is slow, expensive, and unkind to shared infrastructure.
|
|
121
|
+
If your real limits are high, add a test-only route with a small limit rather
|
|
122
|
+
than exercising the big one.
|
|
123
|
+
- **A generous upper bound, not the exact limit.** Asserting "request N succeeds
|
|
124
|
+
and request N+1 fails" breaks the moment the configured limit changes or
|
|
125
|
+
anything else touches the window.
|
|
126
|
+
- **Sequential, not concurrent.** Firing a burst with `Promise.all` can
|
|
127
|
+
under-count against a distributed bucket, so the burst sometimes slips through
|
|
128
|
+
and the test fails for no reason you can reproduce.
|
|
129
|
+
- **Assert the whole contract.** The status and the `Retry-After` header, both
|
|
130
|
+
of which are settled standards from RFC 6585.
|
|
131
|
+
|
|
132
|
+
### Distributed counters are eventually consistent
|
|
133
|
+
|
|
134
|
+
Rate limiting runs at the edge, across regions, and the counter behind it
|
|
135
|
+
converges rather than updating everywhere at once. Two consequences for tests:
|
|
136
|
+
|
|
137
|
+
- **Never assert an exact count.** "The 11th request is the one that 429s" is
|
|
138
|
+
not a property the system guarantees. A request that lands in a region whose
|
|
139
|
+
view of the counter is a moment stale can succeed past the nominal limit, and
|
|
140
|
+
a retry can be counted twice. Assert that a 429 arrives _within a bounded
|
|
141
|
+
number of attempts_, which is what the loop above does.
|
|
142
|
+
- **Give propagation a moment.** Immediately after the first request opens a
|
|
143
|
+
window, a request served from a different region may not see it yet. If a test
|
|
144
|
+
needs the counter to be shared, poll for the condition with a deadline instead
|
|
145
|
+
of sleeping a fixed amount and hoping.
|
|
146
|
+
|
|
147
|
+
The same reasoning applies to anything else backed by distributed state —
|
|
148
|
+
caches, quotas, and metering all trade exactness for latency. Test the
|
|
149
|
+
_behavior_ (a limit is enforced, a cached response is returned) rather than the
|
|
150
|
+
arithmetic.
|
|
151
|
+
|
|
152
|
+
:::caution
|
|
153
|
+
|
|
154
|
+
Do not assert on `RateLimit-Limit`, `RateLimit-Remaining`, or `RateLimit-Reset`.
|
|
155
|
+
Zuplo's [Rate Limit policy](../policies/rate-limit-inbound.mdx) emits
|
|
156
|
+
`Retry-After` and nothing else, so those assertions fail. They are also still an
|
|
157
|
+
IETF Internet-Draft rather than a standard, and their names and semantics have
|
|
158
|
+
changed between draft revisions.
|
|
159
|
+
|
|
160
|
+
:::
|
|
161
|
+
|
|
162
|
+
## Response shape
|
|
163
|
+
|
|
164
|
+
Asserting a couple of fields leaves most of the response untested. A schema
|
|
165
|
+
checks the whole body in one line and tells you exactly which field is wrong
|
|
166
|
+
when it fails.
|
|
167
|
+
|
|
168
|
+
[Zod](https://zod.dev) is the recommended way to do this. The schema is ordinary
|
|
169
|
+
TypeScript, so there is no second language to learn and no build step:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
npm install --save-dev zod
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```ts title="/tests/order-shape.test.ts"
|
|
176
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
177
|
+
import { expect } from "chai";
|
|
178
|
+
import { z } from "zod";
|
|
179
|
+
|
|
180
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
181
|
+
|
|
182
|
+
const Order = z.object({
|
|
183
|
+
id: z.string(),
|
|
184
|
+
status: z.enum(["pending", "shipped", "cancelled"]),
|
|
185
|
+
total: z.number(),
|
|
186
|
+
customerId: z.string(),
|
|
187
|
+
note: z.string().optional(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("order shape", () => {
|
|
191
|
+
it("returns a well-formed order", async () => {
|
|
192
|
+
const response = await fetch(url("/v1/orders/ord_1001"), {
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
expect(response.status).to.equal(200);
|
|
198
|
+
|
|
199
|
+
const result = Order.safeParse(await response.json());
|
|
200
|
+
expect(result.success, JSON.stringify(result.error?.issues, null, 2)).to.be
|
|
201
|
+
.true;
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Two things to know. `safeParse` returns a result rather than throwing, so the
|
|
207
|
+
assertion message can carry every problem at once instead of only the first. And
|
|
208
|
+
`z.infer<typeof Order>` gives you a TypeScript type for free, so the rest of the
|
|
209
|
+
test is typed against the same definition it validated.
|
|
210
|
+
|
|
211
|
+
:::tip
|
|
212
|
+
|
|
213
|
+
Zod is strict about extra keys only if you ask. By default, unknown properties
|
|
214
|
+
are stripped and the parse still succeeds, which is usually what you want at a
|
|
215
|
+
gateway — an upstream adding a field should not fail your suite. Opt into a
|
|
216
|
+
strict object schema when you specifically want an unexpected field to fail.
|
|
217
|
+
|
|
218
|
+
:::
|
|
219
|
+
|
|
220
|
+
## OpenAPI conformance
|
|
221
|
+
|
|
222
|
+
The recipe above tests against a shape you wrote by hand, which can drift from
|
|
223
|
+
the spec. When you want `config/routes.oas.json` itself to be the oracle — it
|
|
224
|
+
**is** the gateway configuration, so this closes the loop between the spec and
|
|
225
|
+
the deployed behavior — validate against the document instead.
|
|
226
|
+
|
|
227
|
+
That means JSON Schema, and Zod is the wrong tool for it. Zod schemas are
|
|
228
|
+
authored in TypeScript, not derived from a JSON Schema document. Use a JSON
|
|
229
|
+
Schema validator:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npm install --save-dev ajv
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```ts title="/tests/conformance.test.ts"
|
|
236
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
237
|
+
import { expect } from "chai";
|
|
238
|
+
import Ajv from "ajv";
|
|
239
|
+
import oas from "../config/routes.oas.json";
|
|
240
|
+
|
|
241
|
+
const ajv = new Ajv({ strict: false });
|
|
242
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
243
|
+
|
|
244
|
+
const orderSchema =
|
|
245
|
+
oas.paths["/v1/orders/{orderId}"].get.responses["200"].content[
|
|
246
|
+
"application/json"
|
|
247
|
+
].schema;
|
|
248
|
+
|
|
249
|
+
describe("OpenAPI conformance", () => {
|
|
250
|
+
it("GET /v1/orders/{orderId} matches its declared schema", async () => {
|
|
251
|
+
const response = await fetch(url("/v1/orders/ord_1001"), {
|
|
252
|
+
headers: {
|
|
253
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
expect(response.status).to.equal(200);
|
|
257
|
+
|
|
258
|
+
const body = await response.json();
|
|
259
|
+
const valid = ajv.validate(orderSchema, body);
|
|
260
|
+
expect(valid, ajv.errorsText(ajv.errors)).to.be.true;
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The path, method, and status key in the `orderSchema` lookup must exist in your
|
|
266
|
+
own `routes.oas.json` — TypeScript infers the type of an imported JSON module
|
|
267
|
+
from the file's actual contents, so a typo is a compile error rather than a
|
|
268
|
+
runtime surprise.
|
|
269
|
+
|
|
270
|
+
Pick one: Zod when you want a readable assertion about a response, Ajv when you
|
|
271
|
+
want the spec to be the thing under test.
|
|
272
|
+
|
|
273
|
+
## The error contract
|
|
274
|
+
|
|
275
|
+
Errors are part of your API's contract, and they are the part a gateway is most
|
|
276
|
+
likely to change without anyone noticing. A policy swap can turn a structured
|
|
277
|
+
problem response into a bare 500, and no dashboard flags it.
|
|
278
|
+
|
|
279
|
+
Zuplo returns [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details
|
|
280
|
+
with a `content-type` of `application/problem+json`. Assert the shape, not the
|
|
281
|
+
status alone:
|
|
282
|
+
|
|
283
|
+
```ts title="/tests/errors.test.ts"
|
|
284
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
285
|
+
import { expect } from "chai";
|
|
286
|
+
import { z } from "zod";
|
|
287
|
+
|
|
288
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
289
|
+
|
|
290
|
+
// RFC 9457 problem details.
|
|
291
|
+
const Problem = z.object({
|
|
292
|
+
type: z.string(),
|
|
293
|
+
title: z.string(),
|
|
294
|
+
status: z.number(),
|
|
295
|
+
detail: z.string(),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("error contract", () => {
|
|
299
|
+
it("returns problem+json for a malformed body", async () => {
|
|
300
|
+
const response = await fetch(url("/v1/orders"), {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
304
|
+
"content-type": "application/json",
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify({ quantity: "several" }),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(response.status).to.equal(400);
|
|
310
|
+
expect(response.headers.get("content-type")).to.include(
|
|
311
|
+
"application/problem+json",
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const result = Problem.safeParse(await response.json());
|
|
315
|
+
expect(result.success, JSON.stringify(result.error?.issues, null, 2)).to.be
|
|
316
|
+
.true;
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A hand-rolled `["type", "title"].every((key) => key in problem)` passes on
|
|
322
|
+
`{ type: 123 }` and, when it does fail, tells you only that `true` was expected.
|
|
323
|
+
The schema checks the types too and names the offending field.
|
|
324
|
+
|
|
325
|
+
## Routing and CORS
|
|
326
|
+
|
|
327
|
+
Routing bugs are cheap to catch and expensive to miss. Assert that unknown paths
|
|
328
|
+
return 404, that a deprecated alias still resolves, and that the preflight
|
|
329
|
+
response carries the headers a browser needs.
|
|
330
|
+
|
|
331
|
+
```ts title="/tests/routing.test.ts"
|
|
332
|
+
import { describe, it, TestHelper } from "@zuplo/test";
|
|
333
|
+
import { expect } from "chai";
|
|
334
|
+
|
|
335
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
336
|
+
|
|
337
|
+
describe("routing", () => {
|
|
338
|
+
it("404s an unknown path", async () => {
|
|
339
|
+
const response = await fetch(url("/v1/does-not-exist"));
|
|
340
|
+
expect(response.status).to.equal(404);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("keeps the legacy alias working", async () => {
|
|
344
|
+
const response = await fetch(url("/orders/ord_1001"), {
|
|
345
|
+
headers: {
|
|
346
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
expect(response.status).to.equal(200);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("answers a CORS preflight", async () => {
|
|
353
|
+
const response = await fetch(url("/v1/orders"), {
|
|
354
|
+
method: "OPTIONS",
|
|
355
|
+
headers: {
|
|
356
|
+
Origin: "https://example.com",
|
|
357
|
+
"Access-Control-Request-Method": "GET",
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
expect(response.headers.get("access-control-allow-origin")).to.exist;
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
An alias route is worth its own test for the same reason as the cross-tenant
|
|
366
|
+
case: it is the route most likely to be missing a policy that the canonical
|
|
367
|
+
route has.
|
|
368
|
+
|
|
369
|
+
## Test data hygiene
|
|
370
|
+
|
|
371
|
+
Two tests that touch the same order, the same API key, or the same rate-limit
|
|
372
|
+
bucket eventually run at the same time and corrupt each other. Test files run in
|
|
373
|
+
parallel by default.
|
|
374
|
+
|
|
375
|
+
Each test creates what it needs through the API, owns it exclusively, and cleans
|
|
376
|
+
up after itself:
|
|
377
|
+
|
|
378
|
+
```ts title="/tests/orders.test.ts"
|
|
379
|
+
import { afterEach, describe, it, TestHelper } from "@zuplo/test";
|
|
380
|
+
import { expect } from "chai";
|
|
381
|
+
|
|
382
|
+
const url = (path: string) => new URL(path, TestHelper.TEST_URL).toString();
|
|
383
|
+
const auth = {
|
|
384
|
+
Authorization: `Bearer ${TestHelper.environment.TENANT_A_JWT}`,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
describe("orders", () => {
|
|
388
|
+
const created: string[] = [];
|
|
389
|
+
|
|
390
|
+
afterEach(async () => {
|
|
391
|
+
while (created.length > 0) {
|
|
392
|
+
await fetch(url(`/v1/orders/${created.pop()}`), {
|
|
393
|
+
method: "DELETE",
|
|
394
|
+
headers: auth,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("returns an order it created", async () => {
|
|
400
|
+
const create = await fetch(url("/v1/orders"), {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
403
|
+
body: JSON.stringify({ sku: "widget", quantity: 1 }),
|
|
404
|
+
});
|
|
405
|
+
expect(create.status).to.equal(201);
|
|
406
|
+
|
|
407
|
+
const { id } = await create.json();
|
|
408
|
+
created.push(id);
|
|
409
|
+
|
|
410
|
+
const read = await fetch(url(`/v1/orders/${id}`), { headers: auth });
|
|
411
|
+
expect(read.status).to.equal(200);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Two rules follow from this:
|
|
417
|
+
|
|
418
|
+
- **No order dependency.** If test B passes only because test A ran first, you
|
|
419
|
+
have one long test with a hidden seam, and any filter or reorder exposes it.
|
|
420
|
+
- **A retry is not a fix.** If a test needs a rerun to pass, it has a shared
|
|
421
|
+
resource, a sleep, or the pipeline is reporting readiness before the gateway
|
|
422
|
+
is serving. See [testing GitHub deployments](./github-deployment-testing.mdx)
|
|
423
|
+
for the readiness poll.
|
|
424
|
+
|
|
425
|
+
## Related
|
|
426
|
+
|
|
427
|
+
- [Get started with zuplo test](./testing-getting-started.mdx)
|
|
428
|
+
- [Testing preview environments](./testing-preview-environments.mdx)
|
|
429
|
+
- [Testing overview](./testing.mdx)
|