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