1import { assert, describe, it } from "@effect/vitest"
2import type * as McpProtocol from "effect/ai/McpProtocol"
3import * as McpSchema from "effect/ai/McpSchema"
4import * as Cause from "effect/Cause"
5import * as Effect from "effect/Effect"
6import * as Exit from "effect/Exit"
7import * as Fiber from "effect/Fiber"
8import { MCP_ENDPOINT } from "../TestUtils/McpHttpHarness.ts"
9import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts"
10import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts"
11
12const jsonRequest = (method: string, body?: unknown, headers?: HeadersInit) => {
13 const requestHeaders = new Headers({
14 accept: "application/json, text/event-stream",
15 "content-type": "application/json"
16 })
17 new Headers(headers).forEach((value, name) => requestHeaders.set(name, value))
18 return new Request(MCP_ENDPOINT, {
19 method,
20 headers: requestHeaders,
21 ...(body === undefined ? {} : { body: JSON.stringify(body) })
22 })
23}
24
25const httpTransportSuiteName = (protocol: McpProtocol.ProtocolAdapter) =>
26 protocol.protocolVersion === "2024-11-05"
27 ? "Single-endpoint HTTP compatibility extension"
28 : "Streamable HTTP"
29
30export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) =>
31 it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
32 describe("Transports", () => {
33 // https://modelcontextprotocol.io/specification/2024-11-05/basic/transports
34 // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
35 // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports
36 describe("stdio", () => {
37 it.effect("MUST exchange compact UTF-8 newline-delimited JSON-RPC records", () =>
38 Effect.gen(function*() {
39 const fixture = yield* makeMcpStdioHarness(protocol)
40 yield* fixture.sendRaw({
41 jsonrpc: "2.0",
42 id: 1,
43 method: "initialize",
44 params: {
45 protocolVersion: protocol.protocolVersion,
46 capabilities: {},
47 clientInfo: { name: "stdio-client", version: "1.0.0" }
48 }
49 })
50 const frame = yield* fixture.takeRawStdout
51 assert.strictEqual(frame.endsWith("\n"), true)
52 assert.strictEqual(frame.slice(0, -1).includes("\n"), false)
53 const message = JSON.parse(frame)
54 assert.deepInclude(message, { jsonrpc: "2.0", id: 1 })
55 assert.property(message, "result")
56 }))
57
58 it.effect("SCENARIO parses UTF-8 JSON-RPC records split across input chunks", () =>
59 Effect.gen(function*() {
60 const fixture = yield* makeMcpStdioHarness(protocol)
61 const bytes = new TextEncoder().encode(`${
62 JSON.stringify({
63 jsonrpc: "2.0",
64 id: 1,
65 method: "initialize",
66 params: {
67 protocolVersion: protocol.protocolVersion,
68 capabilities: {},
69 clientInfo: { name: "stdio-🧪", version: "1.0.0" }
70 }
71 })
72 }\n`)
73 const splitAt = bytes.indexOf(0xf0) + 2
74
75 yield* fixture.sendChunk(bytes.slice(0, splitAt))
76 yield* fixture.sendChunk(bytes.slice(splitAt))
77
78 assert.deepInclude(yield* fixture.takeFrame, {
79 jsonrpc: "2.0",
80 id: 1
81 })
82 }))
83
84 it.effect("SCENARIO processes consecutive stdio messages independently", () =>
85 Effect.gen(function*() {
86 const fixture = yield* makeMcpStdioHarness(protocol)
87 yield* fixture.sendRaw({
88 jsonrpc: "2.0",
89 id: 1,
90 method: "initialize",
91 params: {
92 protocolVersion: protocol.protocolVersion,
93 capabilities: {},
94 clientInfo: { name: "stdio-client", version: "1.0.0" }
95 }
96 })
97 yield* fixture.takeFrame
98 yield* fixture.sendRaw({ jsonrpc: "2.0", id: 2, method: "ping", params: {} })
99 yield* fixture.sendRaw({ jsonrpc: "2.0", id: 3, method: "ping", params: {} })
100 assert.deepStrictEqual(yield* fixture.takeFrame, {
101 jsonrpc: "2.0",
102 id: 2,
103 result: {}
104 })
105 assert.deepStrictEqual(yield* fixture.takeFrame, {
106 jsonrpc: "2.0",
107 id: 3,
108 result: {}
109 })
110 }))
111
112 it.effect.skipIf(["2025-03-26"].includes(protocol.protocolVersion))(
113 "MUST reject JSON-RPC batches over stdio",
114 () =>
115 Effect.gen(function*() {
116 const fixture = yield* makeMcpStdioHarness(protocol)
117 yield* fixture.sendRaw({
118 jsonrpc: "2.0",
119 id: 1,
120 method: "initialize",
121 params: {
122 protocolVersion: protocol.protocolVersion,
123 capabilities: {},
124 clientInfo: { name: "stdio-client", version: "1.0.0" }
125 }
126 })
127 yield* fixture.takeFrame
128 yield* fixture.sendRaw([
129 { jsonrpc: "2.0", id: 2, method: "ping", params: {} },
130 { jsonrpc: "2.0", id: 3, method: "ping", params: {} }
131 ])
132 assert.deepStrictEqual(yield* fixture.takeFrame, {
133 jsonrpc: "2.0",
134 id: null,
135 error: {
136 code: McpSchema.INVALID_REQUEST_ERROR_CODE,
137 message: "JSON-RPC batches are not supported"
138 }
139 })
140 })
141 )
142
143 it.effect("MUST shut down when the client closes stdin", () =>
144 Effect.gen(function*() {
145 const fixture = yield* makeMcpStdioHarness(protocol)
146 yield* fixture.close
147 const exit = yield* Fiber.await(fixture.serverFiber)
148 assert.isTrue(
149 Exit.isSuccess(exit) ||
150 (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause))
151 )
152 }))
153 })
154
155 describe(httpTransportSuiteName(protocol), () => {
156 describe("Sending Messages to the Server", () => {
157 it.effect.skipIf(["2024-11-05", "2025-03-26"].includes(protocol.protocolVersion))(
158 "MUST reject JSON-RPC batches",
159 () =>
160 Effect.gen(function*() {
161 const test = yield* McpConformance
162 const response = yield* test.post([test.initializeRequest()])
163
164 assert.isAtLeast(response.status, 400)
165 })
166 )
167
168 it.effect("MUST accept JSON-RPC requests through POST on the MCP endpoint", () =>
169 Effect.gen(function*() {
170 const test = yield* McpConformance
171 const response = yield* test.post(test.initializeRequest())
172
173 assert.strictEqual(response.status, 200)
174 }))
175
176 it.effect("MUST accept JSON-RPC notifications through POST on the MCP endpoint", () =>
177 Effect.gen(function*() {
178 const test = yield* McpConformance
179 const initialized = yield* test.initialize()
180 const response = yield* test.notifyInitialized(initialized)
181
182 assert.strictEqual(response.status, 202)
183 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
184 }))
185
186 it.effect("MUST accept JSON-RPC responses through POST on the MCP endpoint", () =>
187 Effect.gen(function*() {
188 const test = yield* McpConformance
189 const initialized = yield* test.initialize()
190 const response = yield* test.send(initialized, {
191 jsonrpc: "2.0",
192 id: 99,
193 result: {}
194 })
195
196 assert.strictEqual(response.status, 202)
197 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
198 }))
199
200 it.effect.skipIf(["2024-11-05", "2025-03-26"].includes(protocol.protocolVersion))(
201 "MUST require the negotiated protocol-version header after initialization",
202 () =>
203 Effect.gen(function*() {
204 const test = yield* McpConformance
205 const initialized = yield* test.initialize()
206 assert.isNotNull(initialized.sessionId)
207
208 const missing = yield* test.ping(initialized, { includeProtocolVersion: false })
209 const mismatched = yield* test.ping(initialized, {
210 id: 3,
211 protocolVersion: protocol.protocolVersion === "2025-11-25" ? "2025-06-18" : "2025-03-26"
212 })
213
214 assert.isAtLeast(missing.status, 400)
215 assert.isAtLeast(mismatched.status, 400)
216 })
217 )
218
219 it.effect("MUST require the application/json content type for POST requests", () =>
220 Effect.gen(function*() {
221 const test = yield* McpConformance
222 const accepted = yield* test.request(jsonRequest("POST", test.initializeRequest(), {
223 "content-type": "Application/JSON; charset=utf-8"
224 }))
225 assert.strictEqual(accepted.status, 200)
226
227 for (const contentType of ["text/plain", "application/json-malicious", ""]) {
228 const response = yield* test.request(jsonRequest("POST", test.initializeRequest(), {
229 "content-type": contentType
230 }))
231 assert.strictEqual(response.status, 415)
232 }
233 }))
234
235 it.effect("MUST require clients to accept application/json and text/event-stream", () =>
236 Effect.gen(function*() {
237 const test = yield* McpConformance
238 const accepted = yield* test.request(jsonRequest("POST", test.initializeRequest(), {
239 accept: " Text/Event-Stream; q=0.9, Application/JSON "
240 }))
241 assert.strictEqual(accepted.status, 200)
242
243 for (
244 const accept of [
245 "application/json",
246 "text/event-stream",
247 "*/*",
248 "application/json, text/event-stream; q=0",
249 "application/json, text/event-stream; q=bogus",
250 "application/json, text/event-stream; q=2",
251 "application/json, text/event-stream; q=-1",
252 "application/json-malicious, text/event-stream",
253 ""
254 ]
255 ) {
256 const response = yield* test.request(jsonRequest("POST", test.initializeRequest(), { accept }))
257 assert.strictEqual(response.status, 406)
258 }
259 }))
260
261 it.effect("MUST return application/json for a single JSON-RPC response", () =>
262 Effect.gen(function*() {
263 const test = yield* McpConformance
264 const response = yield* test.post(test.initializeRequest())
265
266 assert.match(response.headers.get("content-type") ?? "", /^application\/json\b/)
267 }))
268 it.effect("MUST return an empty 202 response for accepted notifications and responses", () =>
269 Effect.gen(function*() {
270 const test = yield* McpConformance
271 const initialized = yield* test.initialize()
272 const notification = yield* test.notifyInitialized(initialized)
273 const response = yield* test.send(initialized, {
274 jsonrpc: "2.0",
275 id: 99,
276 result: {}
277 })
278
279 assert.strictEqual(notification.status, 202)
280 assert.strictEqual(yield* Effect.promise(() => notification.text()), "")
281 assert.strictEqual(response.status, 202)
282 assert.strictEqual(yield* Effect.promise(() => response.text()), "")
283 }))
284
285 it.effect("MUST reject unsupported HTTP methods with method not allowed", () =>
286 Effect.gen(function*() {
287 const test = yield* McpConformance
288 for (const method of ["PUT", "PATCH", "HEAD"] as const) {
289 const response = yield* test.request(jsonRequest(method))
290 assert.strictEqual(response.status, 405)
291 }
292 }))
293 })
294
295 describe("Listening for Messages from the Server", () => {
296 it.effect("MUST return method not allowed when GET SSE is not offered", () =>
297 Effect.gen(function*() {
298 const test = yield* McpConformance
299 const response = yield* test.request(jsonRequest("GET"))
300 assert.strictEqual(response.status, 405)
301 assert.strictEqual(response.headers.get("allow"), "POST")
302 }))
303 })
304
305 describe("Session Management", () => {
306 it.effect("SCENARIO returns an MCP session identifier during initialization", () =>
307 Effect.gen(function*() {
308 const test = yield* McpConformance
309 const initialized = yield* test.initialize()
310
311 assert.isNotNull(initialized.sessionId)
312 }))
313 it.effect("SCENARIO uses distinct UUIDv4 session identifiers", () =>
314 Effect.gen(function*() {
315 const test = yield* McpConformance
316 const first = yield* test.initialize()
317 const second = yield* test.initialize({ id: 2 })
318 assert.match(
319 first.sessionId ?? "",
320 /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
321 )
322 assert.match(
323 second.sessionId ?? "",
324 /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
325 )
326 assert.notStrictEqual(first.sessionId, second.sessionId)
327 }))
328 it.effect("MUST require the returned session identifier on subsequent HTTP requests", () =>
329 Effect.gen(function*() {
330 const test = yield* McpConformance
331 yield* test.initialize()
332 const response = yield* test.request(jsonRequest("POST", test.pingRequest(), {
333 "Mcp-Protocol-Version": protocol.protocolVersion
334 }))
335 assert.strictEqual(response.status, 400)
336 }))
337 it.effect("MUST reject an unknown session identifier with not found", () =>
338 Effect.gen(function*() {
339 const test = yield* McpConformance
340 const response = yield* test.request(jsonRequest("POST", test.pingRequest(), {
341 "Mcp-Protocol-Version": protocol.protocolVersion,
342 "Mcp-Session-Id": "unknown-session"
343 }))
344
345 assert.strictEqual(response.status, 404)
346 }))
347 it.effect("SCENARIO declines client session termination without invalidating the session", () =>
348 Effect.gen(function*() {
349 const test = yield* McpConformance
350 const initialized = yield* test.initialize()
351 assert.isNotNull(initialized.sessionId)
352 const response = yield* test.request(jsonRequest("DELETE", undefined, {
353 "Mcp-Protocol-Version": protocol.protocolVersion,
354 "Mcp-Session-Id": initialized.sessionId
355 }))
356 assert.strictEqual(response.status, 405)
357 assert.strictEqual(response.headers.get("allow"), "POST")
358 assert.strictEqual((yield* test.ping(initialized)).status, 200)
359 }))
360 it.effect("MUST reject initialize requests carrying a session identifier", () =>
361 Effect.gen(function*() {
362 const test = yield* McpConformance
363 const initialized = yield* test.initialize()
364 const repeated = yield* test.send(initialized, test.initializeRequest({ id: 2 }))
365 assert.strictEqual(repeated.status, 400)
366 const unknown = yield* test.request(jsonRequest("POST", test.initializeRequest({ id: 3 }), {
367 "Mcp-Session-Id": "unknown-session"
368 }))
369 assert.strictEqual(unknown.status, 404)
370 }))
371 it.effect("SCENARIO keeps two distinct POST sessions live", () =>
372 Effect.gen(function*() {
373 const test = yield* McpConformance
374 const first = yield* test.initialize()
375 const second = yield* test.initialize({ id: 2 })
376 assert.strictEqual((yield* test.ping(first, { id: 3 })).status, 200)
377 assert.strictEqual((yield* test.ping(second, { id: 4 })).status, 200)
378 }))
379 })
380
381 describe.skipIf(["2024-11-05", "2025-03-26"].includes(protocol.protocolVersion))(
382 "Protocol Version Header",
383 () => {
384 it.effect("MUST apply the revision-specific protocol header requirement", () =>
385 Effect.gen(function*() {
386 const test = yield* McpConformance
387 const initialized = yield* test.initialize()
388 const response = yield* test.ping(initialized, {
389 includeProtocolVersion: false
390 })
391 assert.strictEqual(response.status, 400)
392 }))
393 it.effect("MUST accept the negotiated protocol version", () =>
394 Effect.gen(function*() {
395 const test = yield* McpConformance
396 const initialized = yield* test.initialize()
397 const response = yield* test.ping(initialized, {
398 includeProtocolVersion: true,
399 protocolVersion: protocol.protocolVersion
400 })
401 assert.strictEqual(response.status, 200)
402 }))
403 it.effect("MUST reject an unsupported protocol version with bad request", () =>
404 Effect.gen(function*() {
405 const test = yield* McpConformance
406 const initialized = yield* test.initialize()
407 const response = yield* test.ping(initialized, {
408 includeProtocolVersion: true,
409 protocolVersion: "2099-01-01"
410 })
411 assert.strictEqual(response.status, 400)
412 }))
413 it.effect("SCENARIO replays the selected protocol version on HTTP responses", () =>
414 Effect.gen(function*() {
415 const test = yield* McpConformance
416 const initialized = yield* test.initialize()
417 assert.strictEqual(
418 initialized.response.headers.get("Mcp-Protocol-Version"),
419 protocol.protocolVersion
420 )
421 const response = yield* test.ping(initialized, { includeProtocolVersion: true })
422 assert.strictEqual(response.headers.get("Mcp-Protocol-Version"), protocol.protocolVersion)
423 }))
424 }
425 )
426
427 describe("Security", () => {
428 it.effect("MUST validate the Origin header before every MCP route", () =>
429 Effect.gen(function*() {
430 const test = yield* McpConformance
431 assert.strictEqual((yield* test.post(test.initializeRequest())).status, 200)
432 assert.strictEqual(
433 (yield* test.request(jsonRequest("POST", test.initializeRequest({ id: 2 }), {
434 origin: "https://allowed.example"
435 }))).status,
436 200
437 )
438 for (const method of ["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const) {
439 const response = yield* test.request(jsonRequest(
440 method,
441 method === "POST" ? test.initializeRequest({ id: 3 }) : undefined,
442 { origin: "https://attacker.example" }
443 ))
444 assert.strictEqual(response.status, 403)
445 }
446 }))
447 })
448 })
449 })
450 })
451
452export const statelessModernSuite = (
453 protocol: McpProtocol.ProtocolAdapter,
454 layer: McpConformanceLayer
455) =>
456 it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
457 const metadata = {
458 "io.modelcontextprotocol/protocolVersion": protocol.protocolVersion,
459 "io.modelcontextprotocol/clientCapabilities": {},
460 "io.modelcontextprotocol/clientInfo": { name: "transport-client", version: "1.0.0" }
461 }
462 const request = (id: string | number, method = "server/discover") => ({
463 jsonrpc: "2.0",
464 id,
465 method,
466 params: { _meta: metadata }
467 })
468 const headers = (method = "server/discover"): HeadersInit => ({
469 "MCP-Protocol-Version": protocol.protocolVersion,
470 "Mcp-Method": method
471 })
472
473 describe("Transports > Stateless modern", () => {
474 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio
475 it.effect("should exchange one compact newline-delimited JSON-RPC message per stdio line", () =>
476 Effect.gen(function*() {
477 const fixture = yield* makeMcpStdioHarness(protocol)
478 const response = yield* fixture.sendRequest("server/discover", {}, 1).pipe(Effect.forkChild)
479 const frame = yield* fixture.takeRawStdout
480
481 assert.strictEqual(frame.endsWith("\n"), true)
482 assert.strictEqual(frame.slice(0, -1).includes("\n"), false)
483 assert.deepInclude(JSON.parse(frame), { jsonrpc: "2.0", id: 1 })
484 yield* Fiber.join(response)
485 }))
486
487 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio
488 it.effect("should reconstruct a UTF-8 stdio message when its bytes arrive in separate chunks", () =>
489 Effect.gen(function*() {
490 const fixture = yield* makeMcpStdioHarness(protocol)
491 const bytes = new TextEncoder().encode(`${JSON.stringify(request("stdio-🧪"))}\n`)
492 const splitAt = bytes.indexOf(0xf0) + 2
493
494 yield* fixture.sendChunk(bytes.slice(0, splitAt))
495 yield* fixture.sendChunk(bytes.slice(splitAt))
496
497 assert.deepInclude(yield* fixture.takeFrame, { jsonrpc: "2.0", id: "stdio-🧪" })
498 }))
499
500 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio
501 it.effect("should process consecutive stateless stdio requests independently", () =>
502 Effect.gen(function*() {
503 const fixture = yield* makeMcpStdioHarness(protocol)
504 yield* fixture.sendRaw(request(2))
505 yield* fixture.sendRaw(request(3))
506
507 assert.deepInclude(yield* fixture.takeFrame, { jsonrpc: "2.0", id: 2 })
508 assert.deepInclude(yield* fixture.takeFrame, { jsonrpc: "2.0", id: 3 })
509 }))
510
511 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio
512 it.effect("should shut down the stdio server when the client closes stdin", () =>
513 Effect.gen(function*() {
514 const fixture = yield* makeMcpStdioHarness(protocol)
515 yield* fixture.close
516 const exit = yield* Fiber.await(fixture.serverFiber)
517
518 assert.isTrue(Exit.isSuccess(exit) || (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)))
519 }))
520
521 // MethodNotFound also covers unadvertised capabilities, and modern HTTP maps it to 404.
522 // https://modelcontextprotocol.io/specification/2026-07-28/schema#methodnotfounderror
523 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#protocol-version-header
524 it.effect("should return HTTP 404 when a request needs an unavailable server capability", () =>
525 Effect.gen(function*() {
526 const test = yield* McpConformance
527 const discovered = yield* test.initialize()
528 assert.isUndefined(discovered.message.result.capabilities.prompts)
529
530 const response = yield* test.post(request("unavailable", "prompts/list"), headers("prompts/list"))
531 const error = yield* test.decodeError(response)
532 assert.strictEqual(error.id, "unavailable")
533 assert.strictEqual(error.error.code, McpSchema.METHOD_NOT_FOUND_ERROR_CODE)
534 assert.strictEqual(response.status, 404)
535 }))
536
537 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#sending-messages-to-the-server
538 it.effect("should require application/json content and both supported response media types for HTTP POST", () =>
539 Effect.gen(function*() {
540 const test = yield* McpConformance
541 const body = request(4)
542 const accepted = yield* test.request(jsonRequest("POST", body, {
543 ...headers(),
544 "content-type": "Application/JSON; charset=utf-8",
545 accept: "Text/Event-Stream, Application/JSON"
546 }))
547 assert.strictEqual(accepted.status, 200)
548
549 for (const contentType of ["text/plain", "application/json-malicious", ""]) {
550 const response = yield* test.request(jsonRequest("POST", body, {
551 ...headers(),
552 "content-type": contentType
553 }))
554 assert.strictEqual(response.status, 415)
555 }
556 for (const accept of ["application/json", "text/event-stream", "*/*", ""]) {
557 const response = yield* test.request(jsonRequest("POST", body, { ...headers(), accept }))
558 assert.strictEqual(response.status, 406)
559 }
560 }))
561
562 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#sending-messages-to-the-server
563 it.effect("should return application/json when an HTTP request has one JSON-RPC response", () =>
564 Effect.gen(function*() {
565 const test = yield* McpConformance
566 const response = yield* test.request(jsonRequest("POST", request(5), headers()))
567
568 assert.strictEqual(response.status, 200)
569 assert.match(response.headers.get("content-type") ?? "", /^application\/json\b/)
570 }))
571
572 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#listening-for-messages-from-the-server
573 it.effect("should reject GET and unsupported HTTP methods when only POST is available", () =>
574 Effect.gen(function*() {
575 const test = yield* McpConformance
576 for (const method of ["GET", "PUT", "PATCH", "HEAD"] as const) {
577 const response = yield* test.request(jsonRequest(method))
578 assert.strictEqual(response.status, 405)
579 assert.strictEqual(response.headers.get("allow"), "POST")
580 }
581 }))
582
583 // https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#security-warning
584 it.effect("should reject every MCP HTTP route when its Origin is not explicitly allowed", () =>
585 Effect.gen(function*() {
586 const test = yield* McpConformance
587 for (const method of ["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const) {
588 const response = yield* test.request(jsonRequest(
589 method,
590 method === "POST" ? request(6) : undefined,
591 { ...headers(), origin: "https://attacker.example" }
592 ))
593 assert.strictEqual(response.status, 403)
594 }
595 }))
596 })
597 })