primate 0.16.3 → 0.18.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.
@@ -1,61 +1,37 @@
1
- import {Path} from "runtime-compat/fs";
2
- import {serve, Response, URL} from "runtime-compat/http";
3
- import {http404} from "../handlers/http.js";
4
- import {statuses, mime, isResponse, respond} from "./handle/exports.js";
5
- import fromNull from "../fromNull.js";
1
+ import {Response} from "runtime-compat/http";
2
+ import {error as clientError} from "../handlers/exports.js";
3
+ import {mime, isResponse, respond} from "./handle/exports.js";
4
+ import {invalid} from "./route.js";
5
+ import errors from "../errors.js";
6
+ import {OK} from "../http-statuses.js";
6
7
 
7
8
  const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
8
9
 
9
- const contents = {
10
- "application/x-www-form-urlencoded": body =>
11
- fromNull(Object.fromEntries(body.split("&").map(part => part.split("=")
12
- .map(subpart => decodeURIComponent(subpart).replaceAll("+", " "))))),
13
- "application/json": body => JSON.parse(body),
14
- };
15
-
16
- export default async app => {
17
- const {config} = app;
18
- const {http} = config;
10
+ export default app => {
11
+ const {http} = app.config;
19
12
 
20
- const _respond = async request => {
21
- const csp = Object.keys(config.http.csp).reduce((policy_string, key) =>
22
- `${policy_string}${key} ${config.http.csp[key]};`, "");
23
- const scripts = app.resources
24
- .map(resource => `'${resource.integrity}'`).join(" ");
25
- const _csp = scripts === "" ? csp : `${csp}script-src 'self' ${scripts};`;
26
- // remove inline resources
27
- for (let i = app.resources.length - 1; i >= 0; i--) {
28
- const resource = app.resources[i];
29
- if (resource.inline) {
30
- app.resources.splice(i, 1);
31
- }
13
+ const _respond = async (request, headers) => {
14
+ if (invalid(request.url.pathname)) {
15
+ errors.NoFileForPath.throw({pathname: request.url.pathname, config: app.config});
16
+ return;
32
17
  }
18
+ return respond(await app.route(request))(app, headers);
19
+ };
33
20
 
34
- const headers = {
35
- "Content-Security-Policy": _csp,
36
- "Referrer-Policy": "same-origin",
37
- };
21
+ const route = async request => {
22
+ const headers = app.generateHeaders();
38
23
 
39
24
  try {
40
- const {router} = app;
41
- const modules = filter("route", app.modules);
42
- // handle is the last module to be executed
43
- const handlers = [...modules, router.route].reduceRight((acc, handler) =>
44
- input => handler(input, acc));
45
- return await respond(await handlers(request))(app, headers);
25
+ const response = await _respond(request, headers);
26
+ return isResponse(response) ? response : new Response(...response);
46
27
  } catch (error) {
47
28
  app.log.auto(error);
48
- return http404()(app, headers);
29
+ return new Response(...await clientError()(app, {}));
49
30
  }
50
31
  };
51
32
 
52
- const route = async request => {
53
- const response = await _respond(request);
54
- return isResponse(response) ? response : new Response(...response);
55
- };
56
-
57
33
  const staticResource = async file => new Response(file.readable, {
58
- status: statuses.OK,
34
+ status: OK,
59
35
  headers: {
60
36
  "Content-Type": mime(file.name),
61
37
  Etag: await file.modified,
@@ -67,7 +43,7 @@ export default async app => {
67
43
  !inline && src === request.url.pathname);
68
44
  if (published !== undefined) {
69
45
  return new Response(published.code, {
70
- status: statuses.OK,
46
+ status: OK,
71
47
  headers: {
72
48
  "Content-Type": mime(published.src),
73
49
  Etag: published.integrity,
@@ -91,62 +67,9 @@ export default async app => {
91
67
  };
92
68
 
93
69
  const handle = async request => {
94
- try {
95
- return await resource(request);
96
- } catch (error) {
97
- app.log.auto(error);
98
- return new Response(null, {status: statuses.InternalServerError});
99
- }
100
- };
101
-
102
- const parseContentType = (contentType, body) => {
103
- const type = contents[contentType];
104
- return type === undefined ? body : type(body);
70
+ return await resource(request);
105
71
  };
106
72
 
107
- const parseContent = (request, body) => {
108
- try {
109
- return parseContentType(request.headers.get("content-type"), body);
110
- } catch (error) {
111
- app.log.warn(error);
112
- return body;
113
- }
114
- };
115
-
116
- const decoder = new TextDecoder();
117
-
118
- const parseBody = async request => {
119
- const reader = request.body.getReader();
120
- const chunks = [];
121
- let result;
122
- do {
123
- result = await reader.read();
124
- if (result.value !== undefined) {
125
- chunks.push(decoder.decode(result.value));
126
- }
127
- } while (!result.done);
128
-
129
- return chunks.length === 0 ? null : parseContent(request, chunks.join());
130
- };
131
-
132
- const parseRequest = async request => {
133
- const cookies = request.headers.get("cookie");
134
- const {url} = request;
135
-
136
- return {
137
- request,
138
- url: new URL(url.endsWith("/") ? url.slice(0, -1) : url),
139
- body: await parseBody(request),
140
- cookies: fromNull(cookies === null
141
- ? {}
142
- : Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
143
- headers: fromNull(Object.fromEntries(request.headers)),
144
- };
145
- };
146
-
147
- // handle is the last module to be executed
148
- const handlers = [...filter("handle", app.modules), handle]
73
+ return [...filter("handle", app.modules), handle]
149
74
  .reduceRight((acc, handler) => input => handler(input, acc));
150
-
151
- serve(async request => handlers(await parseRequest(request)), config.http);
152
75
  };
@@ -0,0 +1,59 @@
1
+ import {URL} from "runtime-compat/http";
2
+ import fromNull from "../fromNull.js";
3
+ import errors from "../errors.js";
4
+
5
+ const contents = {
6
+ "application/x-www-form-urlencoded": body =>
7
+ fromNull(Object.fromEntries(body.split("&").map(part => part.split("=")
8
+ .map(subpart => decodeURIComponent(subpart).replaceAll("+", " "))))),
9
+ "application/json": body => JSON.parse(body),
10
+ };
11
+ const decoder = new TextDecoder();
12
+
13
+ export default async request => {
14
+ const parseContentType = (contentType, body) => {
15
+ const type = contents[contentType];
16
+ return type === undefined ? body : type(body);
17
+ };
18
+
19
+ const parseContent = async (request, body) => {
20
+ const contentType = request.headers.get("content-type");
21
+ try {
22
+ return parseContentType(contentType, body);
23
+ } catch (error) {
24
+ return errors.CannotParseBody.throw({body, contentType});
25
+ }
26
+ };
27
+
28
+ const parseBody = async request => {
29
+ if (request.body === null) {
30
+ return null;
31
+ }
32
+ const reader = request.body.getReader();
33
+ const chunks = [];
34
+ let result;
35
+ do {
36
+ result = await reader.read();
37
+ if (result.value !== undefined) {
38
+ chunks.push(decoder.decode(result.value));
39
+ }
40
+ } while (!result.done);
41
+
42
+ return parseContent(request, chunks.join());
43
+ };
44
+
45
+ const cookies = request.headers.get("cookie");
46
+ const _url = request.url;
47
+ const url = new URL(_url.endsWith("/") ? _url.slice(0, -1) : _url);
48
+
49
+ return {
50
+ original: request,
51
+ url,
52
+ body: await parseBody(request),
53
+ cookies: fromNull(cookies === null
54
+ ? {}
55
+ : Object.fromEntries(cookies.split(";").map(c => c.trim().split("=")))),
56
+ headers: fromNull(Object.fromEntries(request.headers)),
57
+ query: fromNull(Object.fromEntries(url.searchParams)),
58
+ };
59
+ };
@@ -0,0 +1,72 @@
1
+ import parse from "./parse.js";
2
+ import Logger from "../Logger.js";
3
+
4
+ const {mark} = Logger;
5
+
6
+ const r = await (async () => {
7
+ const p = "https://p.com";
8
+ const request = (method, path = "/", options = {}) =>
9
+ new Request(`${p}${path}`, {method, ...options});
10
+ return Object.fromEntries(["get", "post", "put", "delete"].map(verb =>
11
+ [verb, (...args) => parse(request(verb.toUpperCase(), ...args))]));
12
+ })();
13
+
14
+ export default test => {
15
+ test.case("no body => null", async assert => {
16
+ assert((await r.get("/")).body).null();
17
+ assert((await r.post("/")).body).null();
18
+ });
19
+ test.case("body is application/json", async assert => {
20
+ const body = JSON.stringify({foo: "bar"});
21
+ const contentType = "application/json";
22
+ const headers = {"Content-Type": contentType};
23
+ assert((await r.post("/", {body, headers})).body).equals({foo: "bar"});
24
+
25
+ const faulty = `${body}%`;
26
+ assert(() => r.post("/", {body: faulty, headers}))
27
+ .throws(mark("cannot parse body % as %", faulty, contentType));
28
+ });
29
+ test.case("body is application/x-www-form-urlencoded", async assert => {
30
+ assert((await r.post("/", {
31
+ body: encodeURI("foo=bar &bar=baz"),
32
+ headers: {
33
+ "Content-Type": "application/x-www-form-urlencoded",
34
+ },
35
+ })).body).equals({foo: "bar ", bar: "baz"});
36
+ });
37
+ test.case("no query => {}", async assert => {
38
+ assert((await r.get("/")).query).equals({});
39
+ });
40
+ test.case("query", async assert => {
41
+ assert((await r.get("/?key=value")).query).equals({key: "value"});
42
+ });
43
+ test.case("no cookies => {}", async assert => {
44
+ assert((await r.get("/")).cookies).equals({});
45
+ });
46
+ test.case("cookies", async assert => {
47
+ assert((await r.get("/?key=value", {
48
+ headers: {
49
+ Cookie: "key=value;key2=value2",
50
+ },
51
+ })).cookies).equals({key: "value", key2: "value2"});
52
+ });
53
+ test.case("no headers => {}", async assert => {
54
+ assert((await r.get("/")).headers).equals({});
55
+ });
56
+ test.case("headers", async assert => {
57
+ assert((await r.get("/?key=value", {
58
+ headers: {
59
+ "X-User": "Donald",
60
+ },
61
+ })).headers).equals({"x-user": "Donald"});
62
+ });
63
+ test.case("cookies double as headers", async assert => {
64
+ const response = await r.get("/?key=value", {
65
+ headers: {
66
+ Cookie: "key=value",
67
+ },
68
+ });
69
+ assert(response.headers).equals({cookie: "key=value"});
70
+ assert(response.cookies).equals({key: "value"});
71
+ });
72
+ };
@@ -29,7 +29,7 @@ const post = async app => {
29
29
  };
30
30
 
31
31
  export default async app => {
32
- app.log.info("running publish hooks");
32
+ app.log.info("running publish hooks", {module: "primate"});
33
33
  await [...filter("publish", app.modules), _ => _]
34
34
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
35
35
  await post(app);
@@ -1,7 +1,7 @@
1
1
  const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
2
2
 
3
3
  export default async app => {
4
- app.log.info("running register hooks");
4
+ app.log.info("running register hooks", {module: "primate"});
5
5
  await [...filter("register", app.modules), _ => _]
6
6
  .reduceRight((acc, handler) => input => handler(input, acc))(app);
7
7
  };
@@ -1,6 +1,6 @@
1
- import {Path} from "runtime-compat/fs";
2
- import {Logger} from "primate";
3
- import fromNull from "../fromNull.js";
1
+ import errors from "../errors.js";
2
+
3
+ const filter = (key, array) => array?.flatMap(m => m[key] ?? []) ?? [];
4
4
 
5
5
  // insensitive-case equal
6
6
  const ieq = (left, right) => left.toLowerCase() === right.toLowerCase();
@@ -9,61 +9,99 @@ const verbs = [
9
9
  // CRUD
10
10
  "post", "get", "put", "delete",
11
11
  // extended
12
- "delete", "connect", "options", "trace", "patch",
12
+ "connect", "options", "trace", "patch", "head",
13
13
  ];
14
14
 
15
- const toRoute = file => {
16
- const ending = -3;
17
- const route = file
18
- // remove ending
19
- .slice(0, ending)
15
+ /* routes may not contain dots */
16
+ export const invalid = route => /\./u.test(route);
17
+ const toRoute = path => {
18
+ const double = path.split("/")
19
+ .filter(part => part.startsWith("{") && part.endsWith("}"))
20
+ .map(part => part.slice(1, part.indexOf(":")))
21
+ .find((part, i, array) =>
22
+ array.filter((_, j) => i !== j).includes(part));
23
+ double && errors.DoublePathParameter.throw({path, double});
24
+
25
+ const route = path
20
26
  // transform /index -> ""
21
27
  .replace("/index", "")
22
28
  // transform index -> ""
23
29
  .replace("index", "")
24
30
  // prepare for regex
25
- .replaceAll(/\{(?<named>.*)\}/gu, (_, name) => `(?<${name}>.*?)`)
26
- ;
31
+ .replaceAll(/\{(?<named>.*?)\}/gu, (_, named) => {
32
+ try {
33
+ const {name, type} = /^(?<name>\w*)(?<type>:\w+)?$/u.exec(named).groups;
34
+ const param = type === undefined ? name : `${name}$${type.slice(1)}`;
35
+ return `(?<${param}>[^/]{1,}?)`;
36
+ } catch (error) {
37
+ return errors.InvalidPathParameter.throw({named, path});
38
+ }
39
+ });
40
+
41
+ invalid(route) && errors.InvalidRouteName.throw({path});
42
+
27
43
  return new RegExp(`^/${route}$`, "u");
28
44
  };
29
45
 
30
- export default async app => {
31
- const routes = (await Promise.all(
32
- (await Path.collect(app.paths.routes, /^.*.js$/u))
33
- .map(async route => {
34
- const imported = (await import(route)).default;
35
- const file = `${route}`.replace(app.paths.routes, "").slice(1);
36
- if (imported === undefined) {
37
- app.log.warn(`empty route file at ${file}`);
38
- return [];
39
- }
40
-
41
- const path = toRoute(file);
42
- return Object.entries(imported)
43
- .filter(([verb]) => verbs.includes(verb))
44
- .map(([method, handler]) => ({method, handler, path}));
45
- }))).flat();
46
+ const reentry = (object, mapper) =>
47
+ Object.fromEntries(mapper(Object.entries(object ?? {})));
48
+
49
+ export default app => {
50
+ const double = app.routes
51
+ .map(([route]) => route
52
+ .replaceAll("/index", "")
53
+ .replaceAll(/\{(?<name>\w*)(?<_>:\w+)?\}?/gu, (_, name) => `{${name}}`))
54
+ .find((part, i, array) =>
55
+ array.filter((_, j) => i !== j).includes(part));
56
+
57
+ double && errors.DoubleRoute.throw({double});
58
+
59
+ const routes = app.routes
60
+ .map(([route, imported]) => {
61
+ if (imported === undefined || Object.keys(imported).length === 0) {
62
+ errors.EmptyRouteFile.warn(app.log, {config: app.config, route});
63
+ return [];
64
+ }
65
+
66
+ const path = toRoute(route);
67
+ return Object.entries(imported)
68
+ .filter(([verb]) => verbs.includes(verb))
69
+ .map(([method, handler]) => ({method, handler, path}));
70
+ }).flat();
71
+
72
+ const {types = {}} = app;
73
+ Object.entries(types).every(([name]) => /^(?:\w*)$/u.test(name) ||
74
+ errors.InvalidType.throw({name}));
75
+
76
+ const isType = groups => Object
77
+ .entries(groups ?? {})
78
+ .map(([name, value]) =>
79
+ [types[name] === undefined ? name : `${name}$${name}`, value])
80
+ .filter(([name]) => name.includes("$"))
81
+ .map(([name, value]) => [name.split("$")[1], value])
82
+ .every(([name, value]) => types?.[name](value) === true)
83
+ ;
84
+ const isPath = ({route, path}) => {
85
+ const result = route.path.exec(path);
86
+ return result === null ? false : isType(result.groups);
87
+ };
88
+ const isMethod = ({route, method, path}) => ieq(route.method, method)
89
+ && isPath({route, path});
46
90
  const find = (method, path) => routes.find(route =>
47
- ieq(route.method, method) && route.path.test(path));
48
-
49
- const router = {
50
- async route({request, url, ...rest}) {
51
- const {method} = request;
52
- const {pathname, searchParams} = url;
53
- const verb = find(method, pathname) ?? (() => {
54
- throw new Logger.Warn(`no ${method} route to ${pathname}`);
55
- })();
56
-
57
- const data = {
58
- request,
59
- url,
60
- path: verb.path?.exec(pathname)?.groups ?? Object.create(null),
61
- query: fromNull(Object.fromEntries(searchParams)),
62
- ...rest,
63
- };
64
-
65
- return verb.handler(data);
66
- },
91
+ isMethod({route, method, path}));
92
+ const modules = filter("route", app.modules);
93
+
94
+ return request => {
95
+ const {original: {method}, url: {pathname}} = request;
96
+ const verb = find(method, pathname) ??
97
+ errors.NoRouteToPath.throw({method, pathname, config: app.config});
98
+ const path = reentry(verb.path?.exec(pathname).groups,
99
+ object => object.map(([key, value]) => [key.split("$")[0], value]));
100
+
101
+ // verb.handler is the last module to be executed
102
+ const handlers = [...modules, verb.handler].reduceRight((acc, handler) =>
103
+ input => handler(input, acc));
104
+
105
+ return handlers({...request, path});
67
106
  };
68
- return router;
69
107
  };
@@ -0,0 +1,181 @@
1
+ import Logger from "../Logger.js";
2
+ import route from "./route.js";
3
+
4
+ const {mark} = Logger;
5
+
6
+ const app = {
7
+ config: {
8
+ paths: {
9
+ routes: "/routes",
10
+ },
11
+ },
12
+ routes: [
13
+ "index",
14
+ "user",
15
+ "users/{userId}a",
16
+ "comments/{commentId:comment}",
17
+ "users/{userId}/comments/{commentId}",
18
+ "users/{userId:user}/comments/{commentId}/a",
19
+ "users/{userId:user}/comments/{commentId:comment}/b",
20
+ "users/{_userId}/comments/{commentId}/d",
21
+ "users/{_userId}/comments/{_commentId}/e",
22
+ "comments2/{_commentId}",
23
+ "users2/{_userId}/{commentId}",
24
+ "users3/{_userId}/{_commentId:_commentId}",
25
+ "users4/{_userId}/{_commentId}",
26
+ "users5/{truthy}",
27
+ "{uuid}/{Uuid}/{UUID}",
28
+ ].map(pathname => [pathname, {get: request => request}]),
29
+ types: {
30
+ user: id => /^\d*$/u.test(id),
31
+ comment: id => /^\d*$/u.test(id),
32
+ _userId: id => /^\d*$/u.test(id),
33
+ _commentId: id => /^\d*$/u.test(id),
34
+ truthy: () => 1,
35
+ uuid: _ => _ === "uuid",
36
+ Uuid: _ => _ === "Uuid",
37
+ UUID: _ => _ === "UUID",
38
+ },
39
+ };
40
+
41
+ export default test => {
42
+ const router = route(app);
43
+ const p = "https://p.com";
44
+ const r = pathname => {
45
+ const original = new Request(`${p}${pathname}`, {method: "GET"});
46
+ const {url} = original;
47
+ const end = -1;
48
+ return router({
49
+ original,
50
+ url: new URL(url.endsWith("/") ? url.slice(0, end) : url),
51
+ });
52
+ };
53
+
54
+ test.reassert(assert => ({
55
+ match: (url, result) => {
56
+ assert(r(url).url.pathname).equals(result ?? url);
57
+ },
58
+ fail: (url, result) => {
59
+ const throws = mark("no % route to %", "GET", result ?? url);
60
+ assert(() => r(url)).throws(throws);
61
+ },
62
+ path: (url, result) => assert(r(url).path).equals(result),
63
+ assert,
64
+ }));
65
+
66
+ const get = () => null;
67
+ /* errors {{{ */
68
+ test.case("error DoubleRouted", ({assert}) => {
69
+ const post = ["post", {get}];
70
+ const throws = mark("double route %", "post");
71
+ assert(() => route({routes: [post, post]})).throws(throws);
72
+ });
73
+ test.case("error DoublePathParameter", ({assert}) => {
74
+ const path = "{user}/{user}";
75
+ const throws = mark("double path parameter % in route %", "user", path);
76
+ assert(() => route({routes: [[path, {get}]]})).throws(throws);
77
+ });
78
+ test.case("error EmptyRoutefile", ({assert}) => {
79
+ const path = "user";
80
+ const throws = mark("empty route file at %", `/routes/${path}.js`);
81
+ const base = {
82
+ log: {
83
+ auto(error) {
84
+ throw error;
85
+ },
86
+ },
87
+ config: {
88
+ paths: {
89
+ routes: "/routes",
90
+ },
91
+ },
92
+ };
93
+ assert(() => route({...base, routes: [[path, undefined]]})).throws(throws);
94
+ assert(() => route({...base, routes: [[path, {}]]})).throws(throws);
95
+ });
96
+ test.case("error InvalidRouteName", ({assert}) => {
97
+ const post = ["po.st", {get}];
98
+ const throws = mark("invalid route name %", "po.st");
99
+ assert(() => route({routes: [post], types: {}})).throws(throws);
100
+ });
101
+ test.case("error InvalidParameter", ({assert}) => {
102
+ const path = "{us$er}";
103
+ const throws = mark("invalid path parameter % in route %", "us$er", path);
104
+ assert(() => route({routes: [[path, {get}]]})).throws(throws);
105
+ });
106
+ test.case("error InvalidType", ({assert}) => {
107
+ const throws = mark("invalid type %", "us$er");
108
+ const types = {us$er: () => false};
109
+ assert(() => route({routes: [], types})).throws(throws);
110
+ });
111
+ /* }}} */
112
+
113
+ test.case("index route", ({match}) => {
114
+ match("/");
115
+ });
116
+ test.case("simple route", ({match}) => {
117
+ match("/user");
118
+ });
119
+ test.case("param match/fail", ({match, fail}) => {
120
+ match("/users/1a");
121
+ match("/users/aa");
122
+ match("/users/ba?key=value", "/users/ba");
123
+ fail("/user/1a");
124
+ fail("/users/a");
125
+ fail("/users/aA");
126
+ fail("/users//a");
127
+ fail("/users/?a", "/users/");
128
+ });
129
+ test.case("no params", ({path}) => {
130
+ path("/", {});
131
+ });
132
+ test.case("single param", ({path}) => {
133
+ path("/users/1a", {userId: "1"});
134
+ });
135
+ test.case("params", ({path, fail}) => {
136
+ path("/users/1/comments/2", {userId: "1", commentId: "2"});
137
+ path("/users/1/comments/2/b", {userId: "1", commentId: "2"});
138
+ fail("/users/d/comments/2/b");
139
+ fail("/users/1/comments/d/b");
140
+ fail("/users/d/comments/d/b");
141
+ });
142
+ test.case("single typed param", ({path, fail}) => {
143
+ path("/comments/1", {commentId: "1"});
144
+ fail("/comments/ ", "/comments");
145
+ fail("/comments/1d");
146
+ });
147
+ test.case("mixed untyped and typed params", ({path, fail}) => {
148
+ path("/users/1/comments/2/a", {userId: "1", commentId: "2"});
149
+ fail("/users/d/comments/2/a");
150
+ });
151
+ test.case("single implicit typed param", ({path, fail}) => {
152
+ path("/comments2/1", {_commentId: "1"});
153
+ fail("/comments2/d");
154
+ });
155
+ test.case("mixed implicit and untyped params", ({path, fail}) => {
156
+ path("/users2/1/2", {_userId: "1", commentId: "2"});
157
+ fail("/users2/d/2");
158
+ fail("/users2/d");
159
+ });
160
+ test.case("mixed implicit and explicit params", ({path, fail}) => {
161
+ path("/users3/1/2", {_userId: "1", _commentId: "2"});
162
+ fail("/users3/d/2");
163
+ fail("/users3/1/d");
164
+ fail("/users3");
165
+ });
166
+ test.case("implicit params", ({path, fail}) => {
167
+ path("/users4/1/2", {_userId: "1", _commentId: "2"});
168
+ fail("/users4/d/2");
169
+ fail("/users4/1/d");
170
+ fail("/users4");
171
+ });
172
+ test.case("fail not strictly true implicit params", ({fail}) => {
173
+ fail("/users5/any");
174
+ });
175
+ test.case("different case params", ({path, fail}) => {
176
+ path("/uuid/Uuid/UUID", {uuid: "uuid", Uuid: "Uuid", UUID: "UUID"});
177
+ fail("/uuid/uuid/uuid");
178
+ fail("/Uuid/UUID/uuid");
179
+ fail("/UUID/uuid/Uuid");
180
+ });
181
+ };
@@ -0,0 +1,4 @@
1
+ export const OK = 200;
2
+ export const Found = 302;
3
+ export const NotFound = 404;
4
+ export const InternalServerError = 500;