@nkmc/agent-fs 0.1.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/dist/chunk-7LIZT7L3.js +966 -0
- package/dist/index.cjs +1278 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +419 -0
- package/dist/rpc-D1IHpjF_.d.cts +330 -0
- package/dist/rpc-D1IHpjF_.d.ts +330 -0
- package/dist/testing.cjs +842 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +10 -0
- package/package.json +25 -0
- package/src/agent-fs.ts +151 -0
- package/src/backends/http.ts +835 -0
- package/src/backends/memory.ts +183 -0
- package/src/backends/rpc.ts +456 -0
- package/src/index.ts +36 -0
- package/src/mount.ts +84 -0
- package/src/parser.ts +162 -0
- package/src/server.ts +158 -0
- package/src/testing.ts +3 -0
- package/src/types.ts +52 -0
- package/test/agent-fs.test.ts +325 -0
- package/test/http-204.test.ts +102 -0
- package/test/http-auth-prefix.test.ts +79 -0
- package/test/http-cloudflare.test.ts +533 -0
- package/test/http-form-encoding.test.ts +119 -0
- package/test/http-github.test.ts +580 -0
- package/test/http-listkey.test.ts +128 -0
- package/test/http-oauth2.test.ts +174 -0
- package/test/http-pagination.test.ts +200 -0
- package/test/http-param-styles.test.ts +98 -0
- package/test/http-passthrough.test.ts +282 -0
- package/test/http-retry.test.ts +132 -0
- package/test/http.test.ts +360 -0
- package/test/memory.test.ts +120 -0
- package/test/mount.test.ts +94 -0
- package/test/parser.test.ts +100 -0
- package/test/rpc-crud.test.ts +627 -0
- package/test/rpc-evm.test.ts +390 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { HttpBackend } from "../src/backends/http.js";
|
|
4
|
+
|
|
5
|
+
describe("HttpBackend OAuth2 client_credentials", () => {
|
|
6
|
+
let server: Server;
|
|
7
|
+
let port: number;
|
|
8
|
+
let tokenRequestCount: number;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
tokenRequestCount = 0;
|
|
12
|
+
|
|
13
|
+
await new Promise<void>((resolve) => {
|
|
14
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
15
|
+
res.setHeader("Content-Type", "application/json");
|
|
16
|
+
|
|
17
|
+
const chunks: Buffer[] = [];
|
|
18
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
19
|
+
req.on("end", () => {
|
|
20
|
+
const body = chunks.length ? Buffer.concat(chunks).toString() : "";
|
|
21
|
+
|
|
22
|
+
// Token endpoint
|
|
23
|
+
if (req.url === "/oauth/token" && req.method === "POST") {
|
|
24
|
+
tokenRequestCount++;
|
|
25
|
+
|
|
26
|
+
// Verify client credentials in Authorization header
|
|
27
|
+
const auth = req.headers.authorization;
|
|
28
|
+
if (!auth?.startsWith("Basic ")) {
|
|
29
|
+
res.writeHead(401);
|
|
30
|
+
res.end(JSON.stringify({ error: "invalid_client" }));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const decoded = Buffer.from(auth.slice(6), "base64").toString();
|
|
35
|
+
const [clientId, clientSecret] = decoded.split(":");
|
|
36
|
+
if (clientId !== "my-client" || clientSecret !== "my-secret") {
|
|
37
|
+
res.writeHead(401);
|
|
38
|
+
res.end(JSON.stringify({ error: "invalid_client" }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Verify grant_type
|
|
43
|
+
const params = new URLSearchParams(body);
|
|
44
|
+
if (params.get("grant_type") !== "client_credentials") {
|
|
45
|
+
res.writeHead(400);
|
|
46
|
+
res.end(JSON.stringify({ error: "unsupported_grant_type" }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
res.writeHead(200);
|
|
51
|
+
res.end(JSON.stringify({
|
|
52
|
+
access_token: `token-${tokenRequestCount}`,
|
|
53
|
+
token_type: "bearer",
|
|
54
|
+
expires_in: 3600,
|
|
55
|
+
}));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Protected API endpoint
|
|
60
|
+
if (req.url === "/api/data" && req.method === "GET") {
|
|
61
|
+
const auth = req.headers.authorization;
|
|
62
|
+
if (!auth?.startsWith("Bearer token-")) {
|
|
63
|
+
res.writeHead(401);
|
|
64
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.writeHead(200);
|
|
69
|
+
res.end(JSON.stringify({ items: [{ id: "1" }], token_used: auth }));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
res.writeHead(404);
|
|
74
|
+
res.end("{}");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
server.listen(0, () => {
|
|
78
|
+
port = (server.address() as any).port;
|
|
79
|
+
resolve();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterAll(() => server?.close());
|
|
85
|
+
|
|
86
|
+
it("should automatically fetch OAuth2 token and use it for requests", async () => {
|
|
87
|
+
tokenRequestCount = 0;
|
|
88
|
+
const backend = new HttpBackend({
|
|
89
|
+
baseUrl: `http://localhost:${port}`,
|
|
90
|
+
auth: {
|
|
91
|
+
type: "oauth2",
|
|
92
|
+
tokenUrl: `http://localhost:${port}/oauth/token`,
|
|
93
|
+
clientId: "my-client",
|
|
94
|
+
clientSecret: "my-secret",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const result = await backend.read("/api/data") as any;
|
|
99
|
+
expect(result.items).toEqual([{ id: "1" }]);
|
|
100
|
+
expect(result.token_used).toBe("Bearer token-1");
|
|
101
|
+
expect(tokenRequestCount).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should cache the token across multiple requests", async () => {
|
|
105
|
+
tokenRequestCount = 0;
|
|
106
|
+
const backend = new HttpBackend({
|
|
107
|
+
baseUrl: `http://localhost:${port}`,
|
|
108
|
+
auth: {
|
|
109
|
+
type: "oauth2",
|
|
110
|
+
tokenUrl: `http://localhost:${port}/oauth/token`,
|
|
111
|
+
clientId: "my-client",
|
|
112
|
+
clientSecret: "my-secret",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await backend.read("/api/data");
|
|
117
|
+
await backend.read("/api/data");
|
|
118
|
+
await backend.read("/api/data");
|
|
119
|
+
|
|
120
|
+
// Token should only be fetched once (cached)
|
|
121
|
+
expect(tokenRequestCount).toBe(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should pass scope if provided", async () => {
|
|
125
|
+
let tokenBody = "";
|
|
126
|
+
const mockServer = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
127
|
+
res.setHeader("Content-Type", "application/json");
|
|
128
|
+
const chunks: Buffer[] = [];
|
|
129
|
+
req.on("data", (c: Buffer) => chunks.push(c));
|
|
130
|
+
req.on("end", () => {
|
|
131
|
+
const body = chunks.length ? Buffer.concat(chunks).toString() : "";
|
|
132
|
+
if (req.url === "/token") {
|
|
133
|
+
tokenBody = body; // capture only the token request body
|
|
134
|
+
res.end(JSON.stringify({ access_token: "t", expires_in: 3600 }));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
res.end(JSON.stringify({ ok: true }));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await new Promise<void>((resolve) => mockServer.listen(0, resolve));
|
|
142
|
+
const p = (mockServer.address() as any).port;
|
|
143
|
+
|
|
144
|
+
const backend = new HttpBackend({
|
|
145
|
+
baseUrl: `http://localhost:${p}`,
|
|
146
|
+
auth: {
|
|
147
|
+
type: "oauth2",
|
|
148
|
+
tokenUrl: `http://localhost:${p}/token`,
|
|
149
|
+
clientId: "c",
|
|
150
|
+
clientSecret: "s",
|
|
151
|
+
scope: "read write",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await backend.read("/test");
|
|
156
|
+
expect(tokenBody).toContain("scope=read+write");
|
|
157
|
+
|
|
158
|
+
mockServer.close();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should fail gracefully on invalid credentials", async () => {
|
|
162
|
+
const backend = new HttpBackend({
|
|
163
|
+
baseUrl: `http://localhost:${port}`,
|
|
164
|
+
auth: {
|
|
165
|
+
type: "oauth2",
|
|
166
|
+
tokenUrl: `http://localhost:${port}/oauth/token`,
|
|
167
|
+
clientId: "wrong",
|
|
168
|
+
clientSecret: "wrong",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await expect(backend.read("/api/data")).rejects.toThrow("OAuth2 token request failed");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { HttpBackend } from "../src/backends/http.js";
|
|
4
|
+
import { getNestedValue } from "../src/backends/http.js";
|
|
5
|
+
|
|
6
|
+
describe("getNestedValue", () => {
|
|
7
|
+
it("should resolve dot-separated paths", () => {
|
|
8
|
+
expect(getNestedValue({ a: { b: { c: 42 } } }, "a.b.c")).toBe(42);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should resolve single-level paths", () => {
|
|
12
|
+
expect(getNestedValue({ cursor: "abc" }, "cursor")).toBe("abc");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return undefined for missing paths", () => {
|
|
16
|
+
expect(getNestedValue({ a: 1 }, "b")).toBeUndefined();
|
|
17
|
+
expect(getNestedValue(null, "a")).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Cursor pagination", () => {
|
|
22
|
+
let server: Server;
|
|
23
|
+
let port: number;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await new Promise<void>((resolve) => {
|
|
27
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
28
|
+
res.setHeader("Content-Type", "application/json");
|
|
29
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
30
|
+
|
|
31
|
+
if (url.pathname === "/items") {
|
|
32
|
+
const cursor = url.searchParams.get("starting_after");
|
|
33
|
+
if (!cursor) {
|
|
34
|
+
// Page 1
|
|
35
|
+
res.end(JSON.stringify({
|
|
36
|
+
data: [{ id: "a" }, { id: "b" }],
|
|
37
|
+
has_more: true,
|
|
38
|
+
next_cursor: "b",
|
|
39
|
+
}));
|
|
40
|
+
} else if (cursor === "b") {
|
|
41
|
+
// Page 2
|
|
42
|
+
res.end(JSON.stringify({
|
|
43
|
+
data: [{ id: "c" }, { id: "d" }],
|
|
44
|
+
has_more: true,
|
|
45
|
+
next_cursor: "d",
|
|
46
|
+
}));
|
|
47
|
+
} else {
|
|
48
|
+
// Page 3 (last)
|
|
49
|
+
res.end(JSON.stringify({
|
|
50
|
+
data: [{ id: "e" }],
|
|
51
|
+
has_more: false,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
res.writeHead(404);
|
|
58
|
+
res.end("{}");
|
|
59
|
+
});
|
|
60
|
+
server.listen(0, () => {
|
|
61
|
+
port = (server.address() as any).port;
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterAll(() => server?.close());
|
|
68
|
+
|
|
69
|
+
it("should follow cursor pagination across multiple pages", async () => {
|
|
70
|
+
const backend = new HttpBackend({
|
|
71
|
+
baseUrl: `http://localhost:${port}`,
|
|
72
|
+
resources: [{ name: "items", apiPath: "/items", listKey: "data" }],
|
|
73
|
+
pagination: {
|
|
74
|
+
type: "cursor",
|
|
75
|
+
cursorParam: "starting_after",
|
|
76
|
+
cursorPath: "next_cursor",
|
|
77
|
+
maxPages: 10,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const items = await backend.list("/items/");
|
|
82
|
+
expect(items).toEqual(["a.json", "b.json", "c.json", "d.json", "e.json"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should respect maxPages limit", async () => {
|
|
86
|
+
const backend = new HttpBackend({
|
|
87
|
+
baseUrl: `http://localhost:${port}`,
|
|
88
|
+
resources: [{ name: "items", apiPath: "/items", listKey: "data" }],
|
|
89
|
+
pagination: {
|
|
90
|
+
type: "cursor",
|
|
91
|
+
cursorParam: "starting_after",
|
|
92
|
+
cursorPath: "next_cursor",
|
|
93
|
+
maxPages: 2, // Only fetch 2 pages total
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const items = await backend.list("/items/");
|
|
98
|
+
// Page 1: a,b + Page 2: c,d (maxPages=2 means 1 additional page)
|
|
99
|
+
expect(items).toEqual(["a.json", "b.json", "c.json", "d.json"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("Offset pagination", () => {
|
|
104
|
+
let server: Server;
|
|
105
|
+
let port: number;
|
|
106
|
+
|
|
107
|
+
beforeAll(async () => {
|
|
108
|
+
await new Promise<void>((resolve) => {
|
|
109
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
110
|
+
res.setHeader("Content-Type", "application/json");
|
|
111
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
112
|
+
|
|
113
|
+
if (url.pathname === "/records") {
|
|
114
|
+
const offset = parseInt(url.searchParams.get("offset") ?? "0");
|
|
115
|
+
const limit = parseInt(url.searchParams.get("limit") ?? "2");
|
|
116
|
+
const allItems = [{ id: "1" }, { id: "2" }, { id: "3" }, { id: "4" }, { id: "5" }];
|
|
117
|
+
const page = allItems.slice(offset, offset + limit);
|
|
118
|
+
res.end(JSON.stringify({ items: page, total: allItems.length }));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.writeHead(404);
|
|
123
|
+
res.end("{}");
|
|
124
|
+
});
|
|
125
|
+
server.listen(0, () => {
|
|
126
|
+
port = (server.address() as any).port;
|
|
127
|
+
resolve();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterAll(() => server?.close());
|
|
133
|
+
|
|
134
|
+
it("should follow offset pagination", async () => {
|
|
135
|
+
const backend = new HttpBackend({
|
|
136
|
+
baseUrl: `http://localhost:${port}`,
|
|
137
|
+
resources: [{ name: "records", apiPath: "/records", listKey: "items" }],
|
|
138
|
+
pagination: {
|
|
139
|
+
type: "offset",
|
|
140
|
+
offsetParam: "offset",
|
|
141
|
+
limitParam: "limit",
|
|
142
|
+
pageSize: 2,
|
|
143
|
+
maxPages: 10,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const items = await backend.list("/records/");
|
|
148
|
+
expect(items).toEqual(["1.json", "2.json", "3.json", "4.json", "5.json"]);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("Page pagination", () => {
|
|
153
|
+
let server: Server;
|
|
154
|
+
let port: number;
|
|
155
|
+
|
|
156
|
+
beforeAll(async () => {
|
|
157
|
+
await new Promise<void>((resolve) => {
|
|
158
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
159
|
+
res.setHeader("Content-Type", "application/json");
|
|
160
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
161
|
+
|
|
162
|
+
if (url.pathname === "/entries") {
|
|
163
|
+
const page = parseInt(url.searchParams.get("page") ?? "1");
|
|
164
|
+
if (page === 1) {
|
|
165
|
+
res.end(JSON.stringify({ results: [{ id: "x" }, { id: "y" }] }));
|
|
166
|
+
} else if (page === 2) {
|
|
167
|
+
res.end(JSON.stringify({ results: [{ id: "z" }] }));
|
|
168
|
+
} else {
|
|
169
|
+
res.end(JSON.stringify({ results: [] }));
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
res.writeHead(404);
|
|
175
|
+
res.end("{}");
|
|
176
|
+
});
|
|
177
|
+
server.listen(0, () => {
|
|
178
|
+
port = (server.address() as any).port;
|
|
179
|
+
resolve();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterAll(() => server?.close());
|
|
185
|
+
|
|
186
|
+
it("should follow page pagination", async () => {
|
|
187
|
+
const backend = new HttpBackend({
|
|
188
|
+
baseUrl: `http://localhost:${port}`,
|
|
189
|
+
resources: [{ name: "entries", apiPath: "/entries", listKey: "results" }],
|
|
190
|
+
pagination: {
|
|
191
|
+
type: "page",
|
|
192
|
+
pageParam: "page",
|
|
193
|
+
maxPages: 10,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const items = await backend.list("/entries/");
|
|
198
|
+
expect(items).toEqual(["x.json", "y.json", "z.json"]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { HttpBackend } from "../src/backends/http.js";
|
|
4
|
+
|
|
5
|
+
describe("HttpBackend {param} path parameter support", () => {
|
|
6
|
+
let server: Server;
|
|
7
|
+
let port: number;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await new Promise<void>((resolve) => {
|
|
11
|
+
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
12
|
+
res.setHeader("Content-Type", "application/json");
|
|
13
|
+
|
|
14
|
+
// /accounts/acc123/records → list
|
|
15
|
+
const listMatch = req.url?.match(/^\/accounts\/(\w+)\/records$/);
|
|
16
|
+
if (listMatch && req.method === "GET") {
|
|
17
|
+
res.end(JSON.stringify([
|
|
18
|
+
{ id: "r1", account: listMatch[1] },
|
|
19
|
+
{ id: "r2", account: listMatch[1] },
|
|
20
|
+
]));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// /accounts/acc123/records/r1 → single item
|
|
25
|
+
const getMatch = req.url?.match(/^\/accounts\/(\w+)\/records\/(\w+)$/);
|
|
26
|
+
if (getMatch && req.method === "GET") {
|
|
27
|
+
res.end(JSON.stringify({ id: getMatch[2], account: getMatch[1], data: "test" }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
res.writeHead(404);
|
|
32
|
+
res.end("{}");
|
|
33
|
+
});
|
|
34
|
+
server.listen(0, () => {
|
|
35
|
+
port = (server.address() as any).port;
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterAll(() => server?.close());
|
|
42
|
+
|
|
43
|
+
it("should resolve {accountId} style params in apiPath", async () => {
|
|
44
|
+
const backend = new HttpBackend({
|
|
45
|
+
baseUrl: `http://localhost:${port}`,
|
|
46
|
+
params: { accountId: "acc123" },
|
|
47
|
+
resources: [{
|
|
48
|
+
name: "records",
|
|
49
|
+
apiPath: "/accounts/{accountId}/records",
|
|
50
|
+
}],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const items = await backend.list("/records/");
|
|
54
|
+
expect(items).toEqual(["r1.json", "r2.json"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should resolve :accountId style params (existing behavior)", async () => {
|
|
58
|
+
const backend = new HttpBackend({
|
|
59
|
+
baseUrl: `http://localhost:${port}`,
|
|
60
|
+
params: { accountId: "acc123" },
|
|
61
|
+
resources: [{
|
|
62
|
+
name: "records",
|
|
63
|
+
apiPath: "/accounts/:accountId/records",
|
|
64
|
+
}],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const items = await backend.list("/records/");
|
|
68
|
+
expect(items).toEqual(["r1.json", "r2.json"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should resolve {param} in cat path", async () => {
|
|
72
|
+
const backend = new HttpBackend({
|
|
73
|
+
baseUrl: `http://localhost:${port}`,
|
|
74
|
+
params: { accountId: "acc123" },
|
|
75
|
+
resources: [{
|
|
76
|
+
name: "records",
|
|
77
|
+
apiPath: "/accounts/{accountId}/records",
|
|
78
|
+
}],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await backend.read("/records/r1.json") as Record<string, unknown>;
|
|
82
|
+
expect(result.id).toBe("r1");
|
|
83
|
+
expect(result.account).toBe("acc123");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should throw for missing {param}", () => {
|
|
87
|
+
const backend = new HttpBackend({
|
|
88
|
+
baseUrl: `http://localhost:${port}`,
|
|
89
|
+
params: {}, // no accountId
|
|
90
|
+
resources: [{
|
|
91
|
+
name: "records",
|
|
92
|
+
apiPath: "/accounts/{accountId}/records",
|
|
93
|
+
}],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(() => backend.list("/records/")).rejects.toThrow("Missing param: accountId");
|
|
97
|
+
});
|
|
98
|
+
});
|