|
1 | | -import { describe, expect, mock, test } from "bun:test"; |
| 1 | +import { afterAll, beforeEach, describe, expect, test } from "bun:test"; |
2 | 2 | import { captureOutput, mockConfigModule } from "../testing"; |
3 | 3 |
|
4 | 4 | mockConfigModule(); |
5 | 5 |
|
6 | | -// Mock the createPaymentFetch to return a controlled fetch |
7 | | -mock.module("../client", () => ({ |
8 | | - createPaymentFetch: () => mockFetch, |
9 | | - createA2AClient: async () => ({}), |
10 | | -})); |
11 | | - |
12 | | -let mockFetch: typeof fetch; |
13 | | - |
14 | 6 | const { cli } = await import("../cli"); |
15 | 7 |
|
16 | | -describe("web command", () => { |
17 | | - const out = captureOutput(); |
| 8 | +interface RequestRecord { |
| 9 | + method: string; |
| 10 | + url: string; |
| 11 | + headers: Record<string, string>; |
| 12 | + body: string; |
| 13 | +} |
| 14 | + |
| 15 | +let lastRequest: RequestRecord | null = null; |
| 16 | +let nextResponse: { status: number; body: string; contentType?: string } = { |
| 17 | + status: 200, |
| 18 | + body: JSON.stringify({ ok: true }), |
| 19 | +}; |
| 20 | + |
| 21 | +const server = Bun.serve({ |
| 22 | + port: 0, |
| 23 | + fetch(req) { |
| 24 | + return req.text().then((body) => { |
| 25 | + lastRequest = { |
| 26 | + method: req.method, |
| 27 | + url: req.url, |
| 28 | + headers: Object.fromEntries(req.headers.entries()), |
| 29 | + body, |
| 30 | + }; |
| 31 | + return new Response(nextResponse.body, { |
| 32 | + status: nextResponse.status, |
| 33 | + headers: { "content-type": nextResponse.contentType ?? "application/json" }, |
| 34 | + }); |
| 35 | + }); |
| 36 | + }, |
| 37 | +}); |
18 | 38 |
|
19 | | - test("web GET returns JSON response as YAML text", async () => { |
20 | | - mockFetch = async (_url: string | URL | Request, _init?: RequestInit) => |
21 | | - new Response(JSON.stringify({ hello: "world" }), { status: 200 }); |
| 39 | +const baseUrl = `http://localhost:${server.port}`; |
22 | 40 |
|
23 | | - await cli.parseAsync(["test", "use-agently", "web", "https://example.com/api"]); |
24 | | - expect(out.yaml).toEqual({ hello: "world" }); |
25 | | - }); |
26 | | - |
27 | | - test("web GET returns plain text response", async () => { |
28 | | - mockFetch = async (_url: string | URL | Request, _init?: RequestInit) => |
29 | | - new Response("plain text response", { status: 200 }); |
| 41 | +beforeEach(() => { |
| 42 | + lastRequest = null; |
| 43 | + nextResponse = { status: 200, body: JSON.stringify({ ok: true }) }; |
| 44 | +}); |
30 | 45 |
|
31 | | - await cli.parseAsync(["test", "use-agently", "web", "https://example.com/api"]); |
32 | | - expect(out.stdout).toBe("plain text response"); |
33 | | - }); |
| 46 | +afterAll(() => { |
| 47 | + server.stop(); |
34 | 48 | }); |
35 | 49 |
|
36 | 50 | describe("web:get command", () => { |
37 | 51 | const out = captureOutput(); |
38 | 52 |
|
39 | | - test("GET request sends correct method", async () => { |
40 | | - let capturedMethod: string | undefined; |
41 | | - mockFetch = async (_url: string | URL | Request, init?: RequestInit) => { |
42 | | - capturedMethod = init?.method; |
43 | | - return new Response(JSON.stringify({ ok: true }), { status: 200 }); |
44 | | - }; |
45 | | - |
46 | | - await cli.parseAsync(["test", "use-agently", "web:get", "https://example.com/api"]); |
47 | | - expect(capturedMethod).toBe("GET"); |
48 | | - expect(out.yaml).toEqual({ ok: true }); |
| 53 | + test("sends GET request and outputs JSON response as YAML", async () => { |
| 54 | + nextResponse = { status: 200, body: JSON.stringify({ hello: "world" }) }; |
| 55 | + await cli.parseAsync(["test", "use-agently", "web:get", `${baseUrl}/api`]); |
| 56 | + expect(lastRequest?.method).toBe("GET"); |
| 57 | + expect(out.yaml).toEqual({ hello: "world" }); |
49 | 58 | }); |
50 | 59 |
|
51 | | - test("GET with --header sends custom headers", async () => { |
52 | | - let capturedHeaders: Record<string, string> = {}; |
53 | | - mockFetch = async (_url: string | URL | Request, init?: RequestInit) => { |
54 | | - capturedHeaders = Object.fromEntries(new Headers(init?.headers as HeadersInit).entries()); |
55 | | - return new Response(JSON.stringify({}), { status: 200 }); |
56 | | - }; |
57 | | - |
58 | | - await cli.parseAsync(["test", "use-agently", "web:get", "https://example.com/api", "-H", "X-Custom: value"]); |
59 | | - expect(capturedHeaders["x-custom"]).toBe("value"); |
| 60 | + test("sends custom header", async () => { |
| 61 | + nextResponse = { status: 200, body: JSON.stringify({}) }; |
| 62 | + await cli.parseAsync(["test", "use-agently", "web:get", `${baseUrl}/api`, "-H", "X-Custom: test-value"]); |
| 63 | + expect(lastRequest?.headers["x-custom"]).toBe("test-value"); |
60 | 64 | }); |
61 | 65 |
|
62 | | - test("json output returns response as JSON", async () => { |
63 | | - mockFetch = async (_url: string | URL | Request, _init?: RequestInit) => |
64 | | - new Response(JSON.stringify({ status: "ok" }), { status: 200 }); |
65 | | - |
66 | | - await cli.parseAsync(["test", "use-agently", "-o", "json", "web:get", "https://example.com/api"]); |
| 66 | + test("json output format", async () => { |
| 67 | + nextResponse = { status: 200, body: JSON.stringify({ status: "ok" }) }; |
| 68 | + await cli.parseAsync(["test", "use-agently", "-o", "json", "web:get", `${baseUrl}/api`]); |
67 | 69 | expect(out.json).toEqual({ status: "ok" }); |
68 | 70 | }); |
| 71 | + |
| 72 | + test("outputs plain text for non-JSON response", async () => { |
| 73 | + const textServer = Bun.serve({ |
| 74 | + port: 0, |
| 75 | + fetch() { |
| 76 | + return new Response("plain text", { status: 200, headers: { "content-type": "text/plain" } }); |
| 77 | + }, |
| 78 | + }); |
| 79 | + try { |
| 80 | + await cli.parseAsync(["test", "use-agently", "web:get", `http://localhost:${textServer.port}/text`]); |
| 81 | + expect(out.stdout).toBe("plain text"); |
| 82 | + } finally { |
| 83 | + textServer.stop(); |
| 84 | + } |
| 85 | + }); |
69 | 86 | }); |
70 | 87 |
|
71 | 88 | describe("web:put command", () => { |
72 | 89 | const out = captureOutput(); |
73 | 90 |
|
74 | | - test("PUT request sends correct method and body", async () => { |
75 | | - let capturedMethod: string | undefined; |
76 | | - let capturedBody: string | undefined; |
77 | | - mockFetch = async (_url: string | URL | Request, init?: RequestInit) => { |
78 | | - capturedMethod = init?.method; |
79 | | - capturedBody = init?.body as string; |
80 | | - return new Response(JSON.stringify({ updated: true }), { status: 200 }); |
81 | | - }; |
82 | | - |
83 | | - await cli.parseAsync(["test", "use-agently", "web:put", "https://example.com/api", "-d", '{"key":"value"}']); |
84 | | - expect(capturedMethod).toBe("PUT"); |
85 | | - expect(capturedBody).toBe('{"key":"value"}'); |
| 91 | + test("sends PUT request with body", async () => { |
| 92 | + nextResponse = { status: 200, body: JSON.stringify({ updated: true }) }; |
| 93 | + await cli.parseAsync(["test", "use-agently", "web:put", `${baseUrl}/resource`, "-d", '{"key":"value"}']); |
| 94 | + expect(lastRequest?.method).toBe("PUT"); |
| 95 | + expect(lastRequest?.body).toBe('{"key":"value"}'); |
86 | 96 | expect(out.yaml).toEqual({ updated: true }); |
87 | 97 | }); |
88 | 98 |
|
89 | | - test("PUT with data sets Content-Type header automatically", async () => { |
90 | | - let capturedHeaders: Record<string, string> = {}; |
91 | | - mockFetch = async (_url: string | URL | Request, init?: RequestInit) => { |
92 | | - capturedHeaders = Object.fromEntries(new Headers(init?.headers as HeadersInit).entries()); |
93 | | - return new Response(JSON.stringify({}), { status: 200 }); |
94 | | - }; |
| 99 | + test("auto-sets content-type header when --data is provided", async () => { |
| 100 | + nextResponse = { status: 200, body: JSON.stringify({}) }; |
| 101 | + await cli.parseAsync(["test", "use-agently", "web:put", `${baseUrl}/resource`, "-d", '{"a":1}']); |
| 102 | + expect(lastRequest?.headers["content-type"]).toBe("application/json"); |
| 103 | + }); |
95 | 104 |
|
96 | | - await cli.parseAsync(["test", "use-agently", "web:put", "https://example.com/api", "-d", '{"a":1}']); |
97 | | - expect(capturedHeaders["content-type"]).toBe("application/json"); |
| 105 | + test("does not override explicit content-type header", async () => { |
| 106 | + nextResponse = { status: 200, body: JSON.stringify({}) }; |
| 107 | + await cli.parseAsync([ |
| 108 | + "test", |
| 109 | + "use-agently", |
| 110 | + "web:put", |
| 111 | + `${baseUrl}/resource`, |
| 112 | + "-H", |
| 113 | + "content-type: text/plain", |
| 114 | + "-d", |
| 115 | + "raw body", |
| 116 | + ]); |
| 117 | + expect(lastRequest?.headers["content-type"]).toBe("text/plain"); |
98 | 118 | }); |
99 | 119 | }); |
100 | 120 |
|
101 | 121 | describe("web:delete command", () => { |
102 | 122 | const out = captureOutput(); |
103 | 123 |
|
104 | | - test("DELETE request sends correct method", async () => { |
105 | | - let capturedMethod: string | undefined; |
106 | | - mockFetch = async (_url: string | URL | Request, init?: RequestInit) => { |
107 | | - capturedMethod = init?.method; |
108 | | - return new Response(JSON.stringify({ deleted: true }), { status: 200 }); |
109 | | - }; |
110 | | - |
111 | | - await cli.parseAsync(["test", "use-agently", "web:delete", "https://example.com/resource/123"]); |
112 | | - expect(capturedMethod).toBe("DELETE"); |
| 124 | + test("sends DELETE request", async () => { |
| 125 | + nextResponse = { status: 200, body: JSON.stringify({ deleted: true }) }; |
| 126 | + await cli.parseAsync(["test", "use-agently", "web:delete", `${baseUrl}/resource/123`]); |
| 127 | + expect(lastRequest?.method).toBe("DELETE"); |
113 | 128 | expect(out.yaml).toEqual({ deleted: true }); |
114 | 129 | }); |
115 | 130 | }); |
0 commit comments