@server/next 0.20.5 → 0.20.7

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.5",
3
+ "version": "0.20.7",
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",
@@ -30,12 +30,15 @@
30
30
  "node": ">=20.0.0"
31
31
  },
32
32
  "engineStrict": true,
33
- "dependencies": {},
34
33
  "devDependencies": {
35
- "jest": "^29.7.0"
34
+ "jest": "^29.7.0",
35
+ "polystore": "^0.8.0"
36
36
  },
37
37
  "jest": {
38
38
  "testEnvironment": "jest-environment-node",
39
39
  "transform": {}
40
+ },
41
+ "dependencies": {
42
+ "argon2": "^0.40.3"
40
43
  }
41
44
  }
@@ -0,0 +1,24 @@
1
+ export default class ServerError extends Error {
2
+ constructor(code, status, message, vars = {}) {
3
+ if (typeof message === "function") {
4
+ message = message(vars);
5
+ }
6
+ for (let key in vars) {
7
+ message = message.replaceAll(`{${key}}`, vars[key]);
8
+ }
9
+
10
+ super(message);
11
+ this.code = code;
12
+ this.message = message;
13
+ this.status = status;
14
+ }
15
+ static extend(errors) {
16
+ for (let code in errors) {
17
+ const message = errors[code]?.message || errors[code];
18
+ const status = errors[code]?.status;
19
+ ServerError[code] = (vars) =>
20
+ new ServerError(code, status, message, vars);
21
+ }
22
+ return errors;
23
+ }
24
+ }
@@ -0,0 +1,69 @@
1
+ import kv from "polystore";
2
+
3
+ import server from "./index.js";
4
+
5
+ const url = (token) =>
6
+ new Request("http://localhost:3000/", {
7
+ headers: { authorization: token },
8
+ });
9
+
10
+ describe("auth", () => {
11
+ const store = kv(new Map());
12
+ const app = server({ store }).get("/", (ctx) => {
13
+ return ctx.headers.authorization;
14
+ });
15
+
16
+ it("should be Bearer", async () => {
17
+ const res = await app.fetch(url("Basic REqA2l022l8Q0tuIRtqLOPUy"));
18
+ expect(await res.text()).toBe("Invalid Authorization type, 'Basic'");
19
+ });
20
+
21
+ 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");
24
+ });
25
+
26
+ it("can get the nested get", async () => {
27
+ const res = await app.fetch(url("Bearer REqA2l022l8Q0tuIRtqLOPUy"));
28
+ expect(await res.text()).toBe("Bearer REqA2l022l8Q0tuIRtqLOPUy");
29
+ });
30
+
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);
56
+
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
+ });
68
+ });
69
+ });
@@ -0,0 +1,28 @@
1
+ import { ServerError } from "../";
2
+
3
+ export default async function findAuth(ctx) {
4
+ // NO AUTH AT ALL
5
+ // If there's no even auth option, nothing to do
6
+ const store = ctx.options.auth?.store;
7
+ if (!store) return;
8
+
9
+ // AUTHENTICATION IS AVAILABLE
10
+ ctx.auth = { type: ctx.options.auth.type, store };
11
+
12
+ // If the user is not authenticated, there's no auth to retrieve
13
+ if (!ctx.headers.authorization) return;
14
+
15
+ // AUTHENTICATED REQUEST
16
+ // Check the authentication header
17
+ const [type, id] = ctx.headers.authorization.trim().split(" ");
18
+ if (type.toLowerCase() !== "bearer") {
19
+ throw ServerError.AUTH_INVALID_TYPE({ type });
20
+ }
21
+ if (id.length !== 24) {
22
+ throw ServerError.AUTH_INVALID_TOKEN();
23
+ }
24
+
25
+ // Extend the basics
26
+ ctx.auth.id = id;
27
+ ctx.user = await store.get(id);
28
+ }
@@ -0,0 +1,34 @@
1
+ import { ServerError } from "../";
2
+
3
+ class NoSession {}
4
+
5
+ export default async function findSession(ctx) {
6
+ const store = ctx.options.session?.store;
7
+
8
+ // If there's no store at all, we don't have session available;
9
+ // but that's okay, since it's only a problem if you try to use it
10
+ if (!store) {
11
+ return new Proxy(new NoSession(), {
12
+ get(target, key) {
13
+ if (target[key]) return target[key];
14
+ if (key === "then") return target[key];
15
+ throw ServerError.NO_STORE_READ({ key });
16
+ },
17
+ set(target, key, value) {
18
+ if (target[key] || key === "then") {
19
+ target[key] = value;
20
+ } else {
21
+ throw ServerError.NO_STORE_WRITE({ key });
22
+ }
23
+ },
24
+ });
25
+ }
26
+
27
+ // There's a session cookie; use it as the key to get the data
28
+ // from the store
29
+ if (ctx.cookies.session) {
30
+ return (await store.get(ctx.cookies.session)) || {};
31
+ }
32
+
33
+ return {};
34
+ }
@@ -1,8 +1,10 @@
1
1
  import { define } from "../helpers/index.js";
2
+ import findAuth from "./findAuth.js";
3
+ import findSession from "./findSession.js";
2
4
  import parseBody from "./parseBody.js";
3
5
  import parseCookies from "./parseCookies.js";
4
6
 
5
- export default async (request, options = {}) => {
7
+ export default async (request, options = {}, app) => {
6
8
  const ctx = {};
7
9
  ctx.options = options;
8
10
  ctx.req = request;
@@ -10,7 +12,9 @@ export default async (request, options = {}) => {
10
12
  ctx.method = request.method.toLowerCase();
11
13
 
12
14
  ctx.headers = request.headers;
13
- define(ctx, "cookies", () => parseCookies(request.headers.cookie));
15
+ ctx.cookies = parseCookies(ctx.headers.cookie);
16
+ ctx.session = await findSession(ctx);
17
+ await findAuth(ctx);
14
18
 
15
19
  const https = request.connection.encrypted ? "https" : "http";
16
20
  const host = ctx.headers.host || "localhost" + options.port;
@@ -38,5 +42,8 @@ export default async (request, options = {}) => {
38
42
  .on("error", reject);
39
43
  });
40
44
 
45
+ ctx.app = app;
46
+ ctx.platform = app.platform;
47
+
41
48
  return ctx;
42
49
  };
@@ -1,3 +1,5 @@
1
+ import { createId } from "../helpers/index.js";
2
+
1
3
  function getBoundary(header) {
2
4
  if (!header) return null;
3
5
  var items = header.split(";");
@@ -21,17 +23,9 @@ function getMatching(string, regex) {
21
23
  return matches[1];
22
24
  }
23
25
 
24
- const nanoid = (size = 12) => {
25
- let str = "";
26
- while (str.length < size + 2) {
27
- str += Math.round(Math.random() * 1000000).toString(16);
28
- }
29
- return str.slice(0, size);
30
- };
31
-
32
26
  const saveFile = async (name, value, bucket) => {
33
27
  const ext = name.split(".").pop();
34
- const id = `file-${nanoid(12)}.${ext}`;
28
+ const id = `${createId()}.${ext}`;
35
29
  await bucket.write(id, value, "binary");
36
30
  return id;
37
31
  };
@@ -48,8 +48,8 @@ describe("parseBody", () => {
48
48
  test: ["test message 123456", "test message number two"],
49
49
  });
50
50
 
51
- const matchMd = expect.stringMatching(/^file-\w{12}.md$/);
52
- const matchTxt = expect.stringMatching(/^file-\w{12}.txt$/);
51
+ const matchMd = expect.stringMatching(/^\w{24}.md$/);
52
+ const matchTxt = expect.stringMatching(/^\w{24}.txt$/);
53
53
  expect(body).toMatchObject({
54
54
  profile: matchMd,
55
55
  gallery: [matchTxt, matchTxt],
@@ -1,16 +1,20 @@
1
1
  import { define } from "../helpers/index.js";
2
+ import findAuth from "./findAuth.js";
3
+ import findSession from "./findSession.js";
2
4
  import parseBody from "./parseBody.js";
3
5
  import parseCookies from "./parseCookies.js";
4
6
 
5
- export default async (request, options = {}) => {
7
+ export default async (request, options = {}, app) => {
6
8
  const ctx = {};
7
9
  ctx.options = options;
8
10
  ctx.req = request;
9
11
  ctx.res = { status: null, headers: {}, cookies: {} };
10
12
  ctx.method = request.method.toLowerCase();
11
13
 
12
- define(ctx, "headers", () => Object.fromEntries(request.headers.entries()));
13
- define(ctx, "cookies", () => parseCookies(request.headers.get("cookie")));
14
+ ctx.headers = Object.fromEntries(request.headers.entries());
15
+ ctx.cookies = parseCookies(ctx.headers.cookie);
16
+ ctx.session = await findSession(ctx);
17
+ await findAuth(ctx);
14
18
 
15
19
  ctx.url = new URL(request.url.replace(/\/$/, ""));
16
20
  define(ctx.url, "query", (url) =>
@@ -22,5 +26,8 @@ export default async (request, options = {}) => {
22
26
  ctx.body = await parseBody(request, type, options.uploads);
23
27
  }
24
28
 
29
+ ctx.app = app;
30
+ ctx.platform = app.platform;
31
+
25
32
  return ctx;
26
33
  };
@@ -0,0 +1,10 @@
1
+ import ServerError from "../ServerError";
2
+
3
+ ServerError.extend({
4
+ NO_STORE_WRITE: `You need a 'store' to write 'ctx.session.{key}'`,
5
+ NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
6
+ AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
7
+ AUTH_INVALID_TOKEN: `Invalid Authorization token`,
8
+ });
9
+
10
+ export default ServerError;
@@ -0,0 +1,13 @@
1
+ // Takes an object and returns a string with the proper cookie values
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(";");
13
+ }
@@ -0,0 +1,17 @@
1
+ const urlAlphabet =
2
+ "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
3
+
4
+ export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
5
+
6
+ export default function createId() {
7
+ let size = 24;
8
+ let id = "";
9
+ let bytes = crypto.getRandomValues(new Uint8Array(size));
10
+ while (size--) {
11
+ // Using the bitwise AND operator to "cap" the value of
12
+ // the random byte from 255 to 63, in that way we can make sure
13
+ // that the value will be a valid index for the "chars" string.
14
+ id += urlAlphabet[bytes[size] & 61];
15
+ }
16
+ return id;
17
+ }
@@ -12,15 +12,11 @@ export default async function handleRequest(handlers, ctx) {
12
12
  define(ctx.url, "params", () => match);
13
13
 
14
14
  for (let cb of cbs) {
15
- try {
16
- validate(ctx, cb);
17
- if (typeof cb === "function") {
18
- const res = await cb(ctx);
19
- const out = await parseResponse(res, ctx);
20
- if (out) return out;
21
- }
22
- } catch (error) {
23
- return new Response(error.message, { status: error.status || 500 });
15
+ validate(ctx, cb);
16
+ if (typeof cb === "function") {
17
+ const res = await cb(ctx);
18
+ const out = await parseResponse(res, ctx);
19
+ if (out) return out;
24
20
  }
25
21
  }
26
22
 
@@ -1,3 +1,5 @@
1
+ export { default as createId } from "./createId.js";
2
+ export { default as createCookies } from "./createCookies.js";
1
3
  export { default as define } from "./define.js";
2
4
  export { default as getMachine } from "./getMachine.js";
3
5
  export { default as handleRequest } from "./handleRequest.js";
@@ -23,7 +23,6 @@ export default function (ctx, schema) {
23
23
  }
24
24
  } catch (error) {
25
25
  if (error.constructor.name === "ZodError") {
26
- console.log(error);
27
26
  const message = error.issues
28
27
  .map(({ path, message }) => `[${base}.${path.join(".")}]: ${message}`)
29
28
  .sort()
package/src/index.js CHANGED
@@ -1,16 +1,85 @@
1
1
  import "./polyfill.js";
2
+ // Define the errors for ServerError
3
+ import "./errors/index.js";
2
4
 
3
5
  import Bucket from "./bucket.js";
4
6
  import createNodeContext from "./context/node.js";
5
7
  import createWinterContext from "./context/winter.js";
6
- import { getMachine, handleRequest, iterate } from "./helpers/index.js";
8
+ import {
9
+ createId,
10
+ getMachine,
11
+ handleRequest,
12
+ iterate,
13
+ } from "./helpers/index.js";
14
+ import middle from "./middle/index.js";
7
15
 
8
16
  // Export the reply helpers
9
17
  export * from "./reply.js";
10
18
 
19
+ export { default as ServerError } from "./ServerError.js";
20
+
11
21
  // Allow to create a sub-router
12
22
  export { default as router } from "./router.js";
13
23
 
24
+ const createNodeServer = async (app, options) => {
25
+ const http = await import("http");
26
+ http
27
+ .createServer(async (request, response) => {
28
+ try {
29
+ const ctx = await createNodeContext(request, options, app);
30
+ extendWithDefaults(ctx);
31
+ const out = await handleRequest(app.handlers, ctx);
32
+
33
+ response.writeHead(out.status || 200, out.headers);
34
+ if (out.body instanceof ReadableStream) {
35
+ await iterate(out.body, (chunk) => response.write(chunk));
36
+ } else {
37
+ response.write(out.body || "");
38
+ }
39
+ response.end();
40
+ } catch (error) {
41
+ response.writeHead(error.status || 500);
42
+ response.write(error.message || "");
43
+ response.end();
44
+ }
45
+ })
46
+ .listen(options.port);
47
+ };
48
+
49
+ const validateOptions = (options, env = {}) => {
50
+ options.port = options.port || env.PORT || 3000;
51
+ options.secret = options.secret || env.SECRET || "unsafe-" + createId();
52
+
53
+ options.views = options.views ? Bucket(options.views) : null;
54
+ options.public = options.public ? Bucket(options.public) : null;
55
+ options.uploads = options.uploads ? Bucket(options.uploads) : null;
56
+
57
+ options.store = options.store ?? null;
58
+ options.cookies = options.cookies ?? {};
59
+ if (options.store && options.cookies) {
60
+ options.session = { store: options.store.prefix("session:") };
61
+ }
62
+ options.auth = options.auth || {};
63
+ if (options.auth) {
64
+ if (typeof options.auth !== "object") {
65
+ options.auth = { type: options.auth };
66
+ }
67
+ if (!options.auth.store && options.store) {
68
+ options.auth.store = options.store.prefix("auth:");
69
+ }
70
+ }
71
+
72
+ return options;
73
+ };
74
+
75
+ const extendWithDefaults = (ctx) => {
76
+ // Only want to execute it once; it needs to happen on a per-request
77
+ // basis since we only have full access to the options there
78
+ if (ctx.app.extended) return;
79
+ middle(ctx);
80
+ ctx.app.extended = true;
81
+ };
82
+
14
83
  // Export the main server()
15
84
  export default function server(options = {}) {
16
85
  if (!(this instanceof server)) {
@@ -29,12 +98,9 @@ export default function server(options = {}) {
29
98
  options: [],
30
99
  };
31
100
 
32
- const platform = getMachine();
33
- options.port = options.port || process.env.PORT || 3000;
101
+ this.extended = false;
34
102
 
35
- options.views = options.views ? Bucket(options.views) : null;
36
- options.public = options.public ? Bucket(options.public) : null;
37
- options.uploads = options.uploads ? Bucket(options.uploads) : null;
103
+ this.platform = getMachine();
38
104
 
39
105
  // WEBSOCKETS stuff
40
106
  const sockets = [];
@@ -48,37 +114,24 @@ export default function server(options = {}) {
48
114
  close: (ws) => sockets.splice(sockets.indexOf(ws), 1),
49
115
  };
50
116
 
51
- if (platform.runtime === "node") {
52
- (async () => {
53
- const http = await import("http");
54
- http
55
- .createServer(async (request, response) => {
56
- const ctx = await createNodeContext(request, options);
57
- ctx.app = this;
58
- ctx.platform = platform;
59
-
60
- const out = await handleRequest(this.handlers, ctx);
61
-
62
- response.writeHead(out.status || 200, { header: out.headers });
63
- if (out.body instanceof ReadableStream) {
64
- await iterate(out.body, (chunk) => response.write(chunk));
65
- } else {
66
- response.write(out.body || "");
67
- }
68
- response.end();
69
- })
70
- .listen(options.port);
71
- })();
117
+ // Starting stuff
118
+ if (this.platform.runtime === "node") {
119
+ options = validateOptions(options, process.env);
120
+
121
+ createNodeServer(this, options);
72
122
  }
73
123
 
74
124
  this.fetch = async (request, env, fetchCtx) => {
75
125
  if (env?.upgrade(request)) return;
76
126
 
77
- const ctx = await createWinterContext(request, options);
78
- ctx.app = this;
79
- ctx.platform = platform;
80
-
81
- return await handleRequest(this.handlers, ctx);
127
+ try {
128
+ options = validateOptions(options, env);
129
+ const ctx = await createWinterContext(request, options, this);
130
+ extendWithDefaults(ctx);
131
+ return await handleRequest(this.handlers, ctx);
132
+ } catch (error) {
133
+ return new Response(error.message, { status: error.status || 500 });
134
+ }
82
135
  };
83
136
  }
84
137
 
@@ -135,7 +188,7 @@ server.prototype.use = function (...middleware) {
135
188
  };
136
189
 
137
190
  server.prototype.router = function (basePath, router) {
138
- basePath = "/" + basePath.replace(/^\//, "").replace(/\/$/, "") + "/";
191
+ basePath = ("/" + basePath + "/").replace(/^\/+/, "/").replace(/\/+$/, "/");
139
192
  for (const method in router.handlers) {
140
193
  router.handlers[method].forEach(([method, path, ...callbacks]) => {
141
194
  this.handle(method, basePath + path.replace(/^\//, ""), ...callbacks);
@@ -0,0 +1,40 @@
1
+ import argon2 from "argon2";
2
+
3
+ import { createId } from "../helpers";
4
+ import { status } from "../reply";
5
+
6
+ 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
+ // }
40
+ }
@@ -1,6 +1,8 @@
1
1
  // import { Readable } from "node:stream";
2
2
 
3
+ import { createCookies, createId } from "./helpers/index.js";
3
4
  import { json } from "./reply.js";
5
+ import ServerError from "./ServerError.js";
4
6
 
5
7
  export default async function parseResponse(out, ctx) {
6
8
  // undefined || null || 0 || false || ~""~ -> empty string is still 200
@@ -36,6 +38,39 @@ export default async function parseResponse(out, ctx) {
36
38
  }
37
39
 
38
40
  // Here it should be a Response
41
+
42
+ // If we have a session, we need to persist it into a cookie
43
+ if (Object.keys(ctx.session || {}).length) {
44
+ if (!ctx.options.session?.store) {
45
+ throw ServerError.NO_STORE_WRITE({});
46
+ }
47
+
48
+ let id;
49
+ // 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;
59
+ }
60
+
61
+ // Saves the session in the session store
62
+ // Note that this is async but we are totally fine deferring it
63
+ ctx.options.session.store.set(id, ctx.session);
64
+ }
65
+
66
+ // Cookies to headers
67
+ if (ctx.options.cookies) {
68
+ if (Object.keys(ctx.res.cookies).length) {
69
+ ctx.res.headers["set-cookie"] = createCookies(ctx.res.cookies);
70
+ }
71
+ }
72
+
73
+ // Add the headers that are neeeded
39
74
  if (ctx?.res?.headers) {
40
75
  for (let key in ctx.res.headers) {
41
76
  out.headers[key] = ctx.res.headers[key];
package/src/reply.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "fs/promises";
2
2
 
3
- import { types } from "./helpers/index.js";
3
+ import { createCookies, types } from "./helpers/index.js";
4
4
 
5
5
  function Reply() {}
6
6
 
@@ -8,9 +8,7 @@ Reply.prototype.res = { headers: {}, cookies: {} };
8
8
 
9
9
  // INTERNAL
10
10
  Reply.prototype.generateHeaders = function () {
11
- const cookies = Object.entries(this.res.cookies)
12
- .map(([k, { value, path = "/" }]) => `${k}=${value};Path=${path}`)
13
- .join(";");
11
+ const cookies = createCookies(this.res.cookies);
14
12
  return { ...this.res.headers, "set-cookie": cookies };
15
13
  };
16
14
 
@@ -0,0 +1,47 @@
1
+ import server, { router } from "./index.js";
2
+
3
+ const url = (path, options = {}) =>
4
+ new Request("http://localhost:3000" + path, options);
5
+
6
+ describe("can route properly", () => {
7
+ const api = router()
8
+ .get("/hello", (ctx) => "Hello " + ctx.url.pathname)
9
+ .put("/hello", (ctx) => "Hello " + ctx.url.pathname)
10
+ .post("/hello", (ctx) => "Hello " + ctx.url.pathname);
11
+
12
+ const app = server()
13
+ .router("/", api)
14
+ .router("/api/", api)
15
+ .get("/", () => "Fallback");
16
+
17
+ // INTERNAL - so this might change in the future
18
+ it("has the correct structure", () => {
19
+ const registeredPaths = app.handlers.get.map((h) => h[1]);
20
+ expect(registeredPaths).toEqual(["/hello", "/api/hello", "/"]);
21
+ });
22
+
23
+ it("can get fallback when nothing matches", async () => {
24
+ const res = await app.fetch(url("/"));
25
+ expect(await res.text()).toBe("Fallback");
26
+ });
27
+
28
+ it("can get the base get", async () => {
29
+ const res = await app.fetch(url("/hello"));
30
+ expect(await res.text()).toBe("Hello /hello");
31
+ });
32
+
33
+ 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");
36
+ });
37
+
38
+ 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");
41
+ });
42
+
43
+ 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");
46
+ });
47
+ });
@@ -0,0 +1,65 @@
1
+ import kv from "polystore";
2
+
3
+ import server from "./index.js";
4
+
5
+ const url = (path, options = {}) =>
6
+ new Request("http://localhost:3000" + path, {
7
+ headers: { cookie: "session=REqA2l022l8Q0tuIRtqLOPUy" },
8
+ ...options,
9
+ });
10
+
11
+ describe("session", () => {
12
+ const store = kv(new Map());
13
+ const app = server({ store })
14
+ .get("/hello", (ctx) => "Hello " + ctx.session.a)
15
+ .post("/hello", (ctx) => {
16
+ if (!ctx.session.a) ctx.session.a = 0;
17
+ ctx.session.a += 1;
18
+ return "Bye " + ctx.session.a;
19
+ })
20
+ .get("/", () => "Fallback");
21
+
22
+ it("can get the nested get", async () => {
23
+ await store.set("session:REqA2l022l8Q0tuIRtqLOPUy", { a: 0 });
24
+
25
+ const res = await app.fetch(url("/hello"));
26
+ expect(await res.text()).toBe("Hello 0");
27
+
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");
32
+ expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
33
+ a: 1,
34
+ });
35
+
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");
40
+ expect(await store.get("session:REqA2l022l8Q0tuIRtqLOPUy")).toEqual({
41
+ a: 2,
42
+ });
43
+ });
44
+
45
+ const missingStore = server({ store: null })
46
+ .get("/read", (ctx) => "Bye " + ctx.session.a)
47
+ .get("/write", (ctx) => {
48
+ ctx.session.a = "hello";
49
+ return "All good";
50
+ });
51
+
52
+ it("cannot read a session without a store", async () => {
53
+ const res = await missingStore.fetch(url("/read"));
54
+ const body = await res.text();
55
+ expect(res.status).toBe(500);
56
+ expect(body).toBe("You need a 'store' to read 'ctx.session.a'");
57
+ });
58
+
59
+ it("cannot write a session without a store", async () => {
60
+ const res = await missingStore.fetch(url("/write"));
61
+ const body = await res.text();
62
+ expect(res.status).toBe(500);
63
+ expect(body).toBe("You need a 'store' to write 'ctx.session.a'");
64
+ });
65
+ });