@server/next 0.20.7 → 0.20.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.20.7",
3
+ "version": "0.20.9",
4
4
  "description": "An experimental reimplementation of server.js focused on the DX",
5
5
  "homepage": "https://node-server.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -9,7 +9,7 @@
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
10
  "license": "UNLICENSED",
11
11
  "scripts": {
12
- "demo": "nodemon ./demo/app.js",
12
+ "demo": "nodemon ./demo/src/index.js",
13
13
  "start": "bun test --watch",
14
14
  "test": "bun test",
15
15
  "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
package/src/auth.test.js CHANGED
@@ -2,68 +2,61 @@ import kv from "polystore";
2
2
 
3
3
  import server from "./index.js";
4
4
 
5
- const url = (token) =>
6
- new Request("http://localhost:3000/", {
7
- headers: { authorization: token },
8
- });
9
-
10
5
  describe("auth", () => {
11
6
  const store = kv(new Map());
12
- const app = server({ store }).get("/", (ctx) => {
13
- return ctx.headers.authorization;
14
- });
7
+ const api = server({ store })
8
+ .get("/", (ctx) => ctx.headers.authorization)
9
+ .test();
15
10
 
16
11
  it("should be Bearer", async () => {
17
- const res = await app.fetch(url("Basic REqA2l022l8Q0tuIRtqLOPUy"));
18
- expect(await res.text()).toBe("Invalid Authorization type, 'Basic'");
12
+ const authorization = "Basic REqA2l022l8Q0tuIRtqLOPUy";
13
+ const { data } = await api.get("/", { headers: { authorization } });
14
+ expect(data).toBe("Invalid Authorization type, 'Basic'");
19
15
  });
20
16
 
21
17
  it("should have the proper token", async () => {
22
- const res = await app.fetch(url("Bearer hola"));
23
- expect(await res.text()).toBe("Invalid Authorization token");
18
+ const authorization = "Bearer hola";
19
+ const { data } = await api.get("/", { headers: { authorization } });
20
+ expect(data).toBe("Invalid Authorization token");
24
21
  });
25
22
 
26
23
  it("can get the nested get", async () => {
27
- const res = await app.fetch(url("Bearer REqA2l022l8Q0tuIRtqLOPUy"));
28
- expect(await res.text()).toBe("Bearer REqA2l022l8Q0tuIRtqLOPUy");
24
+ const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
25
+ const { data } = await api.get("/", { headers: { authorization } });
26
+ expect(data).toBe("Bearer REqA2l022l8Q0tuIRtqLOPUy");
29
27
  });
28
+ });
30
29
 
31
- describe.skip("user creation flow", () => {
32
- const url = (path, options = {}) =>
33
- new Request("http://localhost:3000" + path, {
34
- headers: {
35
- cookie: "session=REqA2l022l8Q0tuIRtqLOPUy",
36
- "content-type": "application/json",
37
- },
38
- ...options,
39
- body: JSON.stringify(options.body),
40
- });
41
-
42
- const store = kv(new Map());
43
- const app = server({ auth: "email", store });
44
-
45
- it("can create a new user", async () => {
46
- const registered = await app.fetch(
47
- url("/register", {
48
- method: "POST",
49
- body: { email: "abc@test.com", password: "11111111" },
50
- })
51
- );
52
- console.log(registered);
53
- console.log(Object.fromEntries(await store.entries()));
54
- console.log(await registered.text());
55
- expect(registered.status).toBe(201);
30
+ describe("user creation flow", () => {
31
+ // These are obviously mock data
32
+ const EMAIL = "abc@test.com";
33
+ const PASS = "11111111";
34
+ const CREDENTIALS = { email: EMAIL, password: PASS };
56
35
 
57
- const login = await app.fetch(
58
- url("/login", {
59
- method: "POST",
60
- body: { email: "abc@test.com", password: "11111111" },
61
- })
62
- );
63
- console.log(login);
64
- console.log(Object.fromEntries(await store.entries()));
65
- console.log(await login.text());
66
- expect(login.status).toBe(200);
67
- });
36
+ const store = kv(new Map());
37
+ const auth = { type: "token", providers: "email" };
38
+ const api = server({ auth, store }).test();
39
+
40
+ it("can create a new user", async () => {
41
+ const register = await api.post("/auth/register/email", CREDENTIALS);
42
+ expect(register.status).toBe(201);
43
+ expect(await store.keys()).toEqual([
44
+ "auth:abc@test.com",
45
+ "session:" + register.data.token,
46
+ ]);
47
+
48
+ const authorization = "Bearer " + register.data.token;
49
+ const headers = { authorization };
50
+ const logout = await api.post("/auth/logout", {}, { headers });
51
+
52
+ expect(logout.status).toBe(200);
53
+ expect(await store.keys()).toEqual(["auth:abc@test.com"]);
54
+
55
+ const login = await api.post("/auth/login/email", CREDENTIALS);
56
+ expect(login.status).toBe(200);
57
+ expect(await store.keys()).toEqual([
58
+ "auth:abc@test.com",
59
+ "session:" + login.data.token,
60
+ ]);
68
61
  });
69
62
  });
@@ -1,4 +1,4 @@
1
- import { ServerError } from "../";
1
+ import { ServerError } from "../index.js";
2
2
 
3
3
  export default async function findAuth(ctx) {
4
4
  // NO AUTH AT ALL
@@ -7,7 +7,11 @@ export default async function findAuth(ctx) {
7
7
  if (!store) return;
8
8
 
9
9
  // AUTHENTICATION IS AVAILABLE
10
- ctx.auth = { type: ctx.options.auth.type, store };
10
+ ctx.auth = {
11
+ type: ctx.options.auth.type,
12
+ providers: ctx.options.auth.providers,
13
+ store,
14
+ };
11
15
 
12
16
  // If the user is not authenticated, there's no auth to retrieve
13
17
  if (!ctx.headers.authorization) return;
@@ -1,4 +1,4 @@
1
- import { ServerError } from "../";
1
+ import { ServerError } from "../index.js";
2
2
 
3
3
  class NoSession {}
4
4
 
@@ -1,10 +1,18 @@
1
- import ServerError from "../ServerError";
1
+ import ServerError from "../ServerError.js";
2
2
 
3
3
  ServerError.extend({
4
+ NO_STORE: `You need a 'store' to write 'ctx.session'`,
4
5
  NO_STORE_WRITE: `You need a 'store' to write 'ctx.session.{key}'`,
5
6
  NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
6
7
  AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
7
8
  AUTH_INVALID_TOKEN: `Invalid Authorization token`,
9
+
10
+ LOGIN_NO_EMAIL: "The email is required to log in",
11
+ LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
12
+ LOGIN_NO_PASSWORD: "The email is required to log in",
13
+ LOGIN_INVALID_PASSWORD: "The email you wrote is not correct",
14
+ LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
15
+ LOGIN_WRONG_PASSWORD: `That is not the valid password`,
8
16
  });
9
17
 
10
18
  export default ServerError;
@@ -0,0 +1,23 @@
1
+ import server, { cookies } from "../index.js";
2
+
3
+ describe("set-cookie", () => {
4
+ const app = server()
5
+ .get("/hello", () => {
6
+ return cookies({ hello: "world" }).send();
7
+ })
8
+ .get("/multiple", () => {
9
+ return cookies({ a: "b", c: "d" }).send();
10
+ });
11
+
12
+ it("sets the right cookie", async () => {
13
+ const api = app.test();
14
+ const res = await api.get("/hello");
15
+ expect(res.headers["set-cookie"]).toBe("hello=world");
16
+ });
17
+
18
+ it("can set multiple cookies", async () => {
19
+ const api = app.test();
20
+ const res = await api.get("/multiple");
21
+ expect(res.headers["set-cookie"]).toEqual(["a=b", "c=d"]);
22
+ });
23
+ });
@@ -1,13 +1,11 @@
1
1
  // Takes an object and returns a string with the proper cookie values
2
2
  export default function createCookies(cookies) {
3
- if (!cookies || !Object.keys(cookies).length) return "";
4
- return Object.entries(cookies)
5
- .map(([key, val]) => {
6
- if (typeof val === "string") {
7
- val = { value: val, path: "/" };
8
- }
9
- const { value, path } = val;
10
- return `${key}=${value};Path=${path}`;
11
- })
12
- .join(";");
3
+ if (!cookies || !Object.keys(cookies).length) return [];
4
+ return Object.entries(cookies).map(([key, val]) => {
5
+ if (typeof val === "string") {
6
+ val = { value: val, path };
7
+ }
8
+ const { value, path } = val;
9
+ return `${key}=${value}${path ? ";Path=" + path : ""}`;
10
+ });
13
11
  }
package/src/index.js CHANGED
@@ -196,3 +196,55 @@ server.prototype.router = function (basePath, router) {
196
196
  }
197
197
  return this;
198
198
  };
199
+
200
+ server.prototype.test = function () {
201
+ let cookie = "";
202
+ const fetch = async (path, options = {}) => {
203
+ if (!options.headers) options.headers = {};
204
+ if (options.body && typeof options.body !== "string") {
205
+ options.headers["content-type"] = "application/json";
206
+ options.body = JSON.stringify(options.body);
207
+ }
208
+ if (cookie && !options.headers.cookie) {
209
+ options.headers.cookie = cookie;
210
+ }
211
+ const res = await this.fetch(
212
+ new Request("http://localhost:3000" + path, options)
213
+ );
214
+
215
+ const headers = {};
216
+ for (const [key, value] of res.headers.entries()) {
217
+ if (headers[key]) {
218
+ if (!Array.isArray(headers[key])) {
219
+ headers[key] = [headers[key]];
220
+ }
221
+ headers[key].push(value);
222
+ } else {
223
+ headers[key] = value;
224
+ }
225
+ }
226
+ let data;
227
+ if (headers["set-cookie"]) {
228
+ // TODO: this should really be a smart merge of the 2
229
+ cookie = headers["set-cookie"];
230
+ }
231
+ if (headers["content-type"]?.includes("application/json")) {
232
+ data = await res.json();
233
+ } else {
234
+ data = await res.text();
235
+ }
236
+ return { status: res.status, headers, data };
237
+ };
238
+ return {
239
+ get: (path, options) => fetch(path, { method: "get", ...options }),
240
+ head: (path, options) => fetch(path, { method: "head", ...options }),
241
+ post: (path, body, options) =>
242
+ fetch(path, { method: "post", body, ...options }),
243
+ put: (path, body, options) =>
244
+ fetch(path, { method: "put", body, ...options }),
245
+ patch: (path, body, options) =>
246
+ fetch(path, { method: "patch", body, ...options }),
247
+ delete: (path, options) => fetch(path, { method: "delete", ...options }),
248
+ options: (path, options) => fetch(path, { method: "options", ...options }),
249
+ };
250
+ };
package/src/index.test.js CHANGED
@@ -1,59 +1,50 @@
1
1
  import server, { status } from "./index.js";
2
2
 
3
3
  describe("return different types", () => {
4
- const app = server()
4
+ const api = server()
5
5
  .get("/", () => "Hello world")
6
6
  .get("/text", () => "Hello world")
7
7
  .get("/array", () => ["Hello world"])
8
8
  .get("/object", () => ({ hello: "world" }))
9
- .get("/status", () => 201);
9
+ .get("/status", () => 201)
10
+ .test();
10
11
 
11
12
  it("can get the plain text", async () => {
12
- const res = await app.fetch(new Request("http://localhost:3000/text"));
13
- expect(await res.text()).toBe("Hello world");
13
+ const { data } = await api.get("/text");
14
+ expect(data).toBe("Hello world");
14
15
  });
15
16
 
16
17
  it("can get the array", async () => {
17
- const res = await app.fetch(new Request("http://localhost:3000/array"));
18
- expect(await res.json()).toEqual(["Hello world"]);
18
+ const { data } = await api.get("/array");
19
+ expect(data).toEqual(["Hello world"]);
19
20
  });
20
21
 
21
22
  it("can get the object", async () => {
22
- const res = await app.fetch(new Request("http://localhost:3000/object"));
23
- expect(await res.json()).toEqual({ hello: "world" });
23
+ const { data } = await api.get("/object");
24
+ expect(data).toEqual({ hello: "world" });
24
25
  });
25
26
 
26
27
  it("can get the status", async () => {
27
- const res = await app.fetch(new Request("http://localhost:3000/status"));
28
- expect(res.status).toBe(201);
28
+ const { status } = await api.get("/status");
29
+ expect(status).toBe(201);
29
30
  });
30
31
  });
31
32
 
32
33
  describe("simple post works", () => {
33
- const app = server().post("/", (ctx) => status(201).send(ctx.body));
34
+ const api = server()
35
+ .post("/", (ctx) => status(201).send(ctx.body))
36
+ .test();
34
37
 
35
38
  it("can post new data", async () => {
36
- const res = await app.fetch(
37
- new Request("http://localhost:3000/", {
38
- method: "POST",
39
- body: "New Data",
40
- })
41
- );
42
-
43
- expect(res.status).toBe(201);
44
- expect(await res.text()).toBe("New Data");
39
+ const { data, status } = await api.post("/", "New Data");
40
+ expect(status).toBe(201);
41
+ expect(data).toBe("New Data");
45
42
  });
46
43
 
47
44
  it("will return JSON", async () => {
48
- const res = await app.fetch(
49
- new Request("http://localhost:3000/", {
50
- method: "POST",
51
- body: JSON.stringify({ hello: "world" }),
52
- headers: { "content-type": "application/json" },
53
- })
54
- );
55
-
56
- expect(res.status).toBe(201);
57
- expect(await res.json()).toEqual({ hello: "world" });
45
+ const { data, status, headers } = await api.post("/", { hello: "world" });
46
+ expect(status).toBe(201);
47
+ expect(data).toEqual({ hello: "world" });
48
+ expect(headers["content-type"]).toBe("application/json");
58
49
  });
59
50
  });
@@ -1,40 +1,71 @@
1
1
  import argon2 from "argon2";
2
2
 
3
- import { createId } from "../helpers";
4
- import { status } from "../reply";
3
+ import { createId } from "../helpers/index.js";
4
+ import { status, type } from "../reply.js";
5
+ import ServerError from "../ServerError.js";
5
6
 
6
7
  export default function middle(ctx) {
7
- // if (ctx.auth?.type?.includes("email")) {
8
- // ctx.app.post("/register", async (ctx) => {
9
- // const { email, password, ...data } = ctx.body;
10
- // if (!email || !/@/.test(email)) throw new Error("Email needed");
11
- // if (!password || password.length < 8) throw new Error("Password needed");
12
- // const id = createId();
13
- // const time = new Date().toISOString();
14
- // const pass = await argon2.hash(password);
15
- // await ctx.auth.store.set(email, { id, email, password: pass, ...data });
16
- // const token = await ctx.options.session.store.add({ id, email, time });
17
- // return status(201).send({ id, token, email, ...data });
18
- // });
19
- //
20
- // ctx.app.post("/login", async (ctx) => {
21
- // const { email, password } = ctx.body;
22
- // if (!email || !/@/.test(email)) throw new Error("Email needed");
23
- // if (!password || password.length < 8) throw new Error("Password needed");
24
- // const time = new Date().toISOString();
25
- // const user = await ctx.auth.store.get(email);
26
- // if (!user) {
27
- // return status(400).send("Invalid email");
28
- // }
29
- // if (!(await argon2.verify(user.password, password))) {
30
- // return status(400).send("Invalid password");
31
- // }
32
- // const token = await ctx.options.session.store.add({
33
- // id: user.id,
34
- // email,
35
- // time,
36
- // });
37
- // return { id: user.id, token, email };
38
- // });
39
- // }
8
+ if (ctx.options.public) {
9
+ ctx.app.handlers.get.unshift([
10
+ "*",
11
+ "*",
12
+ async function publicFolder(ctx) {
13
+ try {
14
+ const asset = await ctx.options.public.read(ctx.url.pathname);
15
+ if (asset) {
16
+ return type(ctx.url.pathname.split(".").pop()).send(asset);
17
+ }
18
+ } catch (error) {}
19
+ },
20
+ ]);
21
+ }
22
+
23
+ if (!ctx.auth) return;
24
+ if (ctx.auth?.providers?.includes("email")) {
25
+ ctx.app.post("/auth/register/email", async (ctx) => {
26
+ const { email, password, ...data } = ctx.body;
27
+ if (!email || !/@/.test(email)) throw new Error("Email needed");
28
+ if (!password || password.length < 8) throw new Error("Password needed");
29
+ if (await ctx.auth.store.has(email)) {
30
+ throw new Error("Email is already registered");
31
+ }
32
+ const id = createId();
33
+ const time = new Date().toISOString();
34
+ const pass = await argon2.hash(password);
35
+ await ctx.auth.store.set(email, { id, email, password: pass, ...data });
36
+
37
+ // TYPE GOES HERE
38
+ const token = await ctx.options.session.store.add({ id, email, time });
39
+ return status(201).json({ id, token, email, ...data });
40
+ });
41
+
42
+ ctx.app.post("/auth/logout", async (ctx) => {
43
+ if (ctx.auth.id) {
44
+ await ctx.options.session.store.del(ctx.auth.id);
45
+ }
46
+ return status(200).send();
47
+ });
48
+
49
+ ctx.app.post("/auth/login/email", async (ctx) => {
50
+ const { email, password } = ctx.body;
51
+ if (!email) throw ServerError.LOGIN_NO_EMAIL();
52
+ if (!/@/.test(email)) throw ServerError.LOGIN_INVALID_EMAIL();
53
+ if (!password) throw ServerError.LOGIN_NO_PASSWORD();
54
+ if (password.length < 8) throw ServerError.LOGIN_INVALID_PASSWORD();
55
+
56
+ const time = new Date().toISOString();
57
+ const user = await ctx.auth.store.get(email);
58
+ if (!user) throw ServerError.LOGIN_WRONG_EMAIL();
59
+ const isValid = await argon2.verify(user.password, password);
60
+ if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
61
+
62
+ // TYPE GOES HERE
63
+ const token = await ctx.options.session.store.add({
64
+ id: user.id,
65
+ email,
66
+ time,
67
+ });
68
+ return { id: user.id, token, email };
69
+ });
70
+ }
40
71
  }
@@ -42,21 +42,16 @@ export default async function parseResponse(out, ctx) {
42
42
  // If we have a session, we need to persist it into a cookie
43
43
  if (Object.keys(ctx.session || {}).length) {
44
44
  if (!ctx.options.session?.store) {
45
- throw ServerError.NO_STORE_WRITE({});
45
+ throw ServerError.NO_STORE({});
46
46
  }
47
47
 
48
- let id;
49
48
  // Persistence is based on the Token
50
- if (ctx.auth) {
51
- id = ctx.auth.id;
52
- } else {
53
- // Persistence is based on the Cookies
54
- // No session cookies, generate a _persistent_ cookie
55
- if (!ctx.cookies.session) {
56
- ctx.res.cookies.session = createId();
57
- }
58
- id = ctx.cookies.session;
49
+ // Persistence is based on the Cookies
50
+ // No session cookies, generate a _persistent_ cookie
51
+ if (!ctx.cookies.session) {
52
+ ctx.res.cookies.session = createId();
59
53
  }
54
+ const id = ctx.cookies.session;
60
55
 
61
56
  // Saves the session in the session store
62
57
  // Note that this is async but we are totally fine deferring it
@@ -66,7 +61,9 @@ export default async function parseResponse(out, ctx) {
66
61
  // Cookies to headers
67
62
  if (ctx.options.cookies) {
68
63
  if (Object.keys(ctx.res.cookies).length) {
69
- ctx.res.headers["set-cookie"] = createCookies(ctx.res.cookies);
64
+ createCookies(ctx.res.cookies).forEach((cookie) => {
65
+ ctx.res.headers.append("set-cookie", cookie);
66
+ });
70
67
  }
71
68
  }
72
69
 
package/src/reply.js CHANGED
@@ -2,14 +2,20 @@ import fs from "fs/promises";
2
2
 
3
3
  import { createCookies, types } from "./helpers/index.js";
4
4
 
5
- function Reply() {}
6
-
7
- Reply.prototype.res = { headers: {}, cookies: {} };
5
+ function Reply() {
6
+ this.res = {
7
+ headers: {},
8
+ cookies: {},
9
+ };
10
+ }
8
11
 
9
12
  // INTERNAL
10
13
  Reply.prototype.generateHeaders = function () {
11
- const cookies = createCookies(this.res.cookies);
12
- return { ...this.res.headers, "set-cookie": cookies };
14
+ const headers = new Headers(this.res.headers);
15
+ createCookies(this.res.cookies).forEach((cookie) => {
16
+ headers.append("set-cookie", cookie);
17
+ });
18
+ return headers;
13
19
  };
14
20
 
15
21
  // PARTIAL
@@ -49,7 +55,7 @@ Reply.prototype.cookies = function (cookies) {
49
55
 
50
56
  // FINAL
51
57
  Reply.prototype.json = function (body) {
52
- return headers({ "content-type": "application/json" }).send(
58
+ return this.headers({ "content-type": "application/json" }).send(
53
59
  JSON.stringify(body)
54
60
  );
55
61
  };
@@ -74,7 +80,7 @@ Reply.prototype.view = async function (path) {
74
80
  }
75
81
  const data = await ctx.options.views.read(path);
76
82
  if (data) return this.type(path.split(".").pop()).send(data);
77
- return status(404).send();
83
+ return this.status(404).send();
78
84
  };
79
85
  };
80
86
 
@@ -1,18 +1,16 @@
1
- import server, { router } from "./index.js";
2
-
3
- const url = (path, options = {}) =>
4
- new Request("http://localhost:3000" + path, options);
1
+ import server, { router, status } from "./index.js";
5
2
 
6
3
  describe("can route properly", () => {
7
- const api = router()
4
+ const apiRouter = router()
8
5
  .get("/hello", (ctx) => "Hello " + ctx.url.pathname)
9
6
  .put("/hello", (ctx) => "Hello " + ctx.url.pathname)
10
7
  .post("/hello", (ctx) => "Hello " + ctx.url.pathname);
11
8
 
12
9
  const app = server()
13
- .router("/", api)
14
- .router("/api/", api)
10
+ .router("/", apiRouter)
11
+ .router("/api/", apiRouter)
15
12
  .get("/", () => "Fallback");
13
+ const api = app.test();
16
14
 
17
15
  // INTERNAL - so this might change in the future
18
16
  it("has the correct structure", () => {
@@ -21,27 +19,39 @@ describe("can route properly", () => {
21
19
  });
22
20
 
23
21
  it("can get fallback when nothing matches", async () => {
24
- const res = await app.fetch(url("/"));
25
- expect(await res.text()).toBe("Fallback");
22
+ const res = await api.get("/");
23
+ expect(res.data).toBe("Fallback");
26
24
  });
27
25
 
28
26
  it("can get the base get", async () => {
29
- const res = await app.fetch(url("/hello"));
30
- expect(await res.text()).toBe("Hello /hello");
27
+ const res = await api.get("/hello");
28
+ expect(res.data).toBe("Hello /hello");
31
29
  });
32
30
 
33
31
  it("can get the nested get", async () => {
34
- const res = await app.fetch(url("/api/hello"));
35
- expect(await res.text()).toBe("Hello /api/hello");
32
+ const res = await api.get("/api/hello");
33
+ expect(res.data).toBe("Hello /api/hello");
36
34
  });
37
35
 
38
36
  it("can post to the base get", async () => {
39
- const res = await app.fetch(url("/hello", { method: "POST" }));
40
- expect(await res.text()).toBe("Hello /hello");
37
+ const res = await api.post("/hello");
38
+ expect(res.data).toBe("Hello /hello");
41
39
  });
42
40
 
43
41
  it("can post to the nested get", async () => {
44
- const res = await app.fetch(url("/api/hello", { method: "POST" }));
45
- expect(await res.text()).toBe("Hello /api/hello");
42
+ const res = await api.post("/api/hello");
43
+ expect(res.data).toBe("Hello /api/hello");
44
+ });
45
+
46
+ it("no status reuse", async () => {
47
+ const api = server()
48
+ .get("/a", () => status(201).send("hello"))
49
+ .get("/b", () => ({ hello: "bye" }))
50
+ .get("/", () => "Fallback")
51
+ .test();
52
+ const { status: statusA } = await api.get("/a");
53
+ expect(statusA).toBe(201);
54
+ const { status: statusB } = await api.get("/b");
55
+ expect(statusB).toBe(200);
46
56
  });
47
57
  });
@@ -2,64 +2,64 @@ import kv from "polystore";
2
2
 
3
3
  import server from "./index.js";
4
4
 
5
- const url = (path, options = {}) =>
6
- new Request("http://localhost:3000" + path, {
7
- headers: { cookie: "session=REqA2l022l8Q0tuIRtqLOPUy" },
8
- ...options,
9
- });
10
-
11
5
  describe("session", () => {
12
6
  const store = kv(new Map());
13
- const app = server({ store })
7
+ const api = server({ store })
14
8
  .get("/hello", (ctx) => "Hello " + ctx.session.a)
15
9
  .post("/hello", (ctx) => {
16
10
  if (!ctx.session.a) ctx.session.a = 0;
17
11
  ctx.session.a += 1;
18
12
  return "Bye " + ctx.session.a;
19
13
  })
20
- .get("/", () => "Fallback");
14
+ .get("/", () => "Fallback")
15
+ .test();
21
16
 
22
17
  it("can get the nested get", async () => {
18
+ const cookie = "session=REqA2l022l8Q0tuIRtqLOPUy";
19
+ const options = { headers: { cookie } };
23
20
  await store.set("session:REqA2l022l8Q0tuIRtqLOPUy", { a: 0 });
24
21
 
25
- const res = await app.fetch(url("/hello"));
26
- expect(await res.text()).toBe("Hello 0");
22
+ const res = await api.get("/hello", options);
23
+ expect(res.data).toBe("Hello 0");
27
24
 
28
- const res2 = await app.fetch(url("/hello", { method: "POST" }));
29
- expect(await res2.text()).toBe("Bye 1");
30
- const res3 = await app.fetch(url("/hello"));
31
- expect(await res3.text()).toBe("Hello 1");
25
+ const res2 = await api.post("/hello", {}, options);
26
+ expect(res2.data).toBe("Bye 1");
27
+
28
+ const res3 = await api.get("/hello", options);
29
+ expect(res3.data).toBe("Hello 1");
32
30
  expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
33
31
  a: 1,
34
32
  });
35
33
 
36
- const res4 = await app.fetch(url("/hello", { method: "POST" }));
37
- expect(await res4.text()).toBe("Bye 2");
38
- const res5 = await app.fetch(url("/hello"));
39
- expect(await res5.text()).toBe("Hello 2");
34
+ const res4 = await api.post("/hello", {}, options);
35
+ expect(res4.data).toBe("Bye 2");
36
+
37
+ const res5 = await api.get("/hello", options);
38
+ expect(res5.data).toBe("Hello 2");
40
39
  expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
41
40
  a: 2,
42
41
  });
43
42
  });
43
+ });
44
44
 
45
- const missingStore = server({ store: null })
45
+ describe("missing store", () => {
46
+ const api = server({ store: null })
46
47
  .get("/read", (ctx) => "Bye " + ctx.session.a)
47
48
  .get("/write", (ctx) => {
48
49
  ctx.session.a = "hello";
49
50
  return "All good";
50
- });
51
+ })
52
+ .test();
51
53
 
52
54
  it("cannot read a session without a store", async () => {
53
- const res = await missingStore.fetch(url("/read"));
54
- const body = await res.text();
55
+ const res = await api.get("/read");
55
56
  expect(res.status).toBe(500);
56
- expect(body).toBe("You need a 'store' to read 'ctx.session.a'");
57
+ expect(res.data).toBe("You need a 'store' to read 'ctx.session.a'");
57
58
  });
58
59
 
59
60
  it("cannot write a session without a store", async () => {
60
- const res = await missingStore.fetch(url("/write"));
61
- const body = await res.text();
61
+ const res = await api.get("/write");
62
62
  expect(res.status).toBe(500);
63
- expect(body).toBe("You need a 'store' to write 'ctx.session.a'");
63
+ expect(res.data).toBe("You need a 'store' to write 'ctx.session.a'");
64
64
  });
65
65
  });
package/src/url.test.js CHANGED
@@ -2,23 +2,25 @@ import server from "./index.js";
2
2
 
3
3
  describe("can match the url", () => {
4
4
  it("stops at the first matching route", async () => {
5
- const app = server()
5
+ const api = server()
6
6
  .get("/:id", (ctx) => ctx.url.params)
7
- .get("/*", (ctx) => ctx.url.params);
7
+ .get("/*", (ctx) => ctx.url.params)
8
+ .test();
8
9
 
9
- const res = await app.fetch(new Request("http://localhost:3000/hello"));
10
- expect(await res.json()).toEqual({ id: "hello" });
10
+ const { data, headers } = await api.get("/hello");
11
+ expect(data).toEqual({ id: "hello" });
12
+ expect(headers["content-type"]).toEqual("application/json");
11
13
  });
12
14
 
13
15
  it("but it doesn't if it's a use", async () => {
14
- const app = server()
15
- .use(() => {
16
- // No-op
17
- })
16
+ const api = server()
17
+ .use(() => {}) // No-op
18
18
  .get("/:id", (ctx) => ctx.url.params)
19
- .get("/*", (ctx) => ctx.url.params);
19
+ .get("/*", (ctx) => ctx.url.params)
20
+ .test();
20
21
 
21
- const res = await app.fetch(new Request("http://localhost:3000/hello"));
22
- expect(await res.json()).toEqual({ id: "hello" });
22
+ const { data, headers, ...rest } = await api.get("/hello");
23
+ expect(data).toEqual({ id: "hello" });
24
+ expect(headers["content-type"]).toEqual("application/json");
23
25
  });
24
26
  });