Suite of AT Protocol TypeScript libraries built on web standards
1import type { LexiconDoc } from "@atp/lexicon";
2import {
3 XrpcClient,
4 XRPCError,
5 XRPCInvalidResponseError,
6} from "./_xrpc-client.ts";
7import * as xrpcServer from "../mod.ts";
8import { closeServer, createServer } from "./_util.ts";
9import {
10 assert,
11 assertEquals,
12 assertRejects,
13 assertStringIncludes,
14} from "@std/assert";
15
16const UPSTREAM_LEXICONS: LexiconDoc[] = [
17 {
18 lexicon: 1,
19 id: "io.example.upstreamInvalidResponse",
20 defs: {
21 main: {
22 type: "query",
23 output: {
24 encoding: "application/json",
25 schema: {
26 type: "object",
27 required: ["expectedValue"],
28 properties: {
29 expectedValue: { type: "string" },
30 },
31 },
32 },
33 },
34 },
35 },
36];
37
38const LEXICONS: LexiconDoc[] = [
39 {
40 lexicon: 1,
41 id: "io.example.error",
42 defs: {
43 main: {
44 type: "query",
45 parameters: {
46 type: "params",
47 properties: {
48 which: { type: "string", default: "foo" },
49 },
50 },
51 errors: [{ name: "Foo" }, { name: "Bar" }],
52 },
53 },
54 },
55 {
56 lexicon: 1,
57 id: "io.example.throwFalsyValue",
58 defs: {
59 main: {
60 type: "query",
61 },
62 },
63 },
64 {
65 lexicon: 1,
66 id: "io.example.query",
67 defs: {
68 main: {
69 type: "query",
70 },
71 },
72 },
73 {
74 lexicon: 1,
75 id: "io.example.procedure",
76 defs: {
77 main: {
78 type: "procedure",
79 },
80 },
81 },
82 {
83 lexicon: 1,
84 id: "io.example.invalidResponse",
85 defs: {
86 main: {
87 type: "query",
88 output: {
89 encoding: "application/json",
90 schema: {
91 type: "object",
92 required: ["expectedValue"],
93 properties: {
94 expectedValue: { type: "string" },
95 },
96 },
97 },
98 },
99 },
100 },
101 {
102 lexicon: 1,
103 id: "io.example.invalidUpstreamResponse",
104 defs: {
105 main: {
106 type: "query",
107 },
108 },
109 },
110];
111
112const MISMATCHED_LEXICONS: LexiconDoc[] = [
113 {
114 lexicon: 1,
115 id: "io.example.query",
116 defs: {
117 main: {
118 type: "procedure",
119 },
120 },
121 },
122 {
123 lexicon: 1,
124 id: "io.example.procedure",
125 defs: {
126 main: {
127 type: "query",
128 },
129 },
130 },
131 {
132 lexicon: 1,
133 id: "io.example.doesNotExist",
134 defs: {
135 main: {
136 type: "query",
137 },
138 },
139 },
140];
141
142let upstreamServer: ReturnType<typeof xrpcServer.createServer>;
143let upstreamS: Deno.HttpServer;
144let upstreamClient: XrpcClient;
145let server: ReturnType<typeof xrpcServer.createServer>;
146let s: Deno.HttpServer;
147let client: XrpcClient;
148let badClient: XrpcClient;
149
150Deno.test.beforeAll(async () => {
151 // Setup upstream server
152 upstreamServer = xrpcServer.createServer(UPSTREAM_LEXICONS, {
153 validateResponse: false,
154 }); // disable validateResponse to test client validation
155 upstreamServer.method("io.example.upstreamInvalidResponse", () => {
156 return { encoding: "json", body: { something: "else" } };
157 });
158 upstreamS = await createServer(upstreamServer);
159 const upstreamPort = (upstreamS as Deno.HttpServer & { port: number }).port;
160 upstreamClient = new XrpcClient(
161 `http://localhost:${upstreamPort}`,
162 UPSTREAM_LEXICONS,
163 );
164
165 // Setup main server
166 server = xrpcServer.createServer(LEXICONS, {
167 validateResponse: false,
168 }); // disable validateResponse to test client validation
169 s = await createServer(server);
170 const port = (s as Deno.HttpServer & { port: number }).port;
171
172 server.method("io.example.error", (ctx: xrpcServer.HandlerContext) => {
173 if (ctx.params["which"] === "foo") {
174 throw new xrpcServer.InvalidRequestError("It was this one!", "Foo");
175 } else if (ctx.params["which"] === "bar") {
176 return { status: 400, error: "Bar", message: "It was that one!" };
177 } else {
178 return { status: 400 };
179 }
180 });
181 server.method("io.example.throwFalsyValue", () => {
182 throw "";
183 });
184 server.method("io.example.query", () => {
185 return undefined;
186 });
187 // @ts-ignore We're intentionally giving the wrong response! -prf
188 server.method("io.example.invalidResponse", () => {
189 return { encoding: "application/json", body: { something: "else" } };
190 });
191 server.method("io.example.invalidUpstreamResponse", async () => {
192 await upstreamClient.call("io.example.upstreamInvalidResponse");
193 return {
194 encoding: "json",
195 body: {},
196 };
197 });
198 server.method("io.example.procedure", () => {
199 return undefined;
200 });
201
202 client = new XrpcClient(`http://localhost:${port}`, LEXICONS);
203 badClient = new XrpcClient(
204 `http://localhost:${port}`,
205 MISMATCHED_LEXICONS,
206 );
207});
208
209Deno.test.afterAll(async () => {
210 await closeServer(s);
211 await closeServer(upstreamS);
212});
213
214Deno.test("throws XRPCError for foo error", {
215 sanitizeOps: false,
216 sanitizeResources: false,
217}, async () => {
218 await assertRejects(
219 async () => {
220 await client.call("io.example.error", {
221 which: "foo",
222 });
223 },
224 XRPCError,
225 "It was this one!",
226 );
227
228 const fooError = await client.call("io.example.error", { which: "foo" })
229 .catch((e) => e);
230 assert(fooError instanceof XRPCError);
231 assert(!fooError.success);
232 assertEquals(fooError.error, "Foo");
233});
234
235Deno.test("throws XRPCError for bar error", {
236 sanitizeOps: false,
237 sanitizeResources: false,
238}, async () => {
239 await assertRejects(
240 async () => {
241 await client.call("io.example.error", {
242 which: "bar",
243 });
244 },
245 XRPCError,
246 "It was that one!",
247 );
248
249 const barError = await client.call("io.example.error", { which: "bar" })
250 .catch((e) => e);
251 assert(barError instanceof XRPCError);
252 assert(!barError.success);
253 assertEquals(barError.error, "Bar");
254});
255
256Deno.test("throws XRPCError for falsy value", {
257 sanitizeOps: false,
258 sanitizeResources: false,
259}, async () => {
260 await assertRejects(
261 async () => {
262 await client.call("io.example.throwFalsyValue");
263 },
264 XRPCError,
265 "Internal Server Error",
266 );
267
268 const falsyError = await client.call("io.example.throwFalsyValue").catch(
269 (e) => e,
270 );
271 assert(falsyError instanceof XRPCError);
272 assert(!falsyError.success);
273 assertEquals(falsyError.error, "InternalServerError");
274});
275
276Deno.test("throws XRPCError for other error type", {
277 sanitizeOps: false,
278 sanitizeResources: false,
279}, async () => {
280 await assertRejects(
281 async () => {
282 await client.call("io.example.error", {
283 which: "other",
284 });
285 },
286 XRPCError,
287 "Invalid Request",
288 );
289
290 const otherError = await client.call("io.example.error", {
291 which: "other",
292 }).catch((e) => e);
293 assert(otherError instanceof XRPCError);
294 assert(!otherError.success);
295 assertEquals(otherError.error, "InvalidRequest");
296});
297
298Deno.test("throws XRPCInvalidResponseError for invalid response", {
299 sanitizeOps: false,
300 sanitizeResources: false,
301}, async () => {
302 await assertRejects(
303 async () => {
304 await client.call("io.example.invalidResponse");
305 },
306 XRPCInvalidResponseError,
307 "The server gave an invalid response and may be out of date.",
308 );
309
310 const invalidError = await client.call("io.example.invalidResponse")
311 .catch((e) => e);
312 assert(invalidError instanceof XRPCInvalidResponseError);
313 assert(!invalidError.success);
314 assertEquals(invalidError.error, "Invalid Response");
315 assertStringIncludes(invalidError.validationError.message, "expectedValue");
316 assertEquals(invalidError.responseBody, { something: "else" });
317});
318
319Deno.test("throws XRPCError for invalid upstream response", {
320 sanitizeOps: false,
321 sanitizeResources: false,
322}, async () => {
323 await assertRejects(
324 async () => {
325 await client.call("io.example.invalidUpstreamResponse");
326 },
327 XRPCError,
328 "Internal Server Error",
329 );
330
331 const upstreamError = await client.call(
332 "io.example.invalidUpstreamResponse",
333 ).catch((e) => e);
334 assert(upstreamError instanceof XRPCError);
335 assert(!upstreamError.success);
336 assertEquals(upstreamError.status, 500);
337 assertEquals(upstreamError.error, "InternalServerError");
338});
339
340Deno.test("serves successful requests for query and procedure", {
341 sanitizeOps: false,
342 sanitizeResources: false,
343}, async () => {
344 await client.call("io.example.query"); // No error
345 await client.call("io.example.procedure"); // No error
346});
347
348Deno.test("serves error for incorrect HTTP method on query", {
349 sanitizeOps: false,
350 sanitizeResources: false,
351}, async () => {
352 await assertRejects(
353 async () => {
354 await badClient.call("io.example.query");
355 },
356 XRPCError,
357 "Incorrect HTTP method (POST) expected GET",
358 );
359
360 const queryError = await badClient.call("io.example.query").catch((e) => e);
361 assert(queryError instanceof XRPCError);
362 assert(!queryError.success);
363 assertEquals(queryError.error, "InvalidRequest");
364});
365
366Deno.test("serves error for incorrect HTTP method on procedure", {
367 sanitizeOps: false,
368 sanitizeResources: false,
369}, async () => {
370 await assertRejects(
371 async () => {
372 await badClient.call("io.example.procedure");
373 },
374 XRPCError,
375 "Incorrect HTTP method (GET) expected POST",
376 );
377
378 const procError = await badClient.call("io.example.procedure").catch(
379 (e) => e,
380 );
381 assert(procError instanceof XRPCError);
382 assert(!procError.success);
383 assertEquals(procError.error, "InvalidRequest");
384});
385
386Deno.test("serves error for non-existent method", {
387 sanitizeOps: false,
388 sanitizeResources: false,
389}, async () => {
390 await assertRejects(
391 async () => {
392 await badClient.call("io.example.doesNotExist");
393 },
394 XRPCError,
395 "Method Not Implemented",
396 );
397
398 const notFoundError = await badClient.call("io.example.doesNotExist")
399 .catch((e) => e);
400 assert(notFoundError instanceof XRPCError);
401 assert(!notFoundError.success);
402 assertEquals(notFoundError.error, "MethodNotImplemented");
403});