laravel-workers 0.4.0 → 0.4.1
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/LICENSE +21 -21
- package/bin/laravel-workers.mjs +141 -141
- package/build.mjs +2599 -2599
- package/compiler/bin/controller.mjs +92 -92
- package/kernel/cache.mjs +405 -405
- package/kernel/db.mjs +703 -703
- package/kernel/encrypter.mjs +1 -1
- package/kernel/hash.mjs +113 -113
- package/kernel/multipart.mjs +161 -161
- package/kernel/router.mjs +276 -276
- package/kernel/scheduler.mjs +236 -236
- package/kernel/str.mjs +445 -445
- package/kernel/tests/cache.test.mjs +290 -290
- package/kernel/tests/db.test.mjs +1068 -1068
- package/kernel/tests/encrypter.test.mjs +1 -1
- package/kernel/tests/hash.test.mjs +149 -149
- package/kernel/tests/http.test.mjs +1182 -1182
- package/kernel/tests/php-json.test.mjs +263 -263
- package/kernel/tests/router.test.mjs +281 -281
- package/kernel/tests/services.test.mjs +474 -474
- package/kernel/tests/str.test.mjs +83 -83
- package/kernel/tests/validation.test.mjs +340 -340
- package/kernel/validation.mjs +123 -123
- package/package.json +69 -69
- package/tools/schema-dump.php +158 -158
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
// edge/kernel/tests/router.test.mjs
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { normalizeTable, routeKey, match, dispatch } from '../router.mjs';
|
|
4
|
-
import { html, json, redirect, notFound, text } from '../response.mjs';
|
|
5
|
-
|
|
6
|
-
const routes = [
|
|
7
|
-
{ uri: 'up', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
8
|
-
{ uri: '/', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
9
|
-
{ uri: 'dbcheck', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
10
|
-
{ uri: 'storage/{path}', name: 'storage.local', methods: ['GET', 'HEAD'], domain: null },
|
|
11
|
-
{ uri: 'storage/{path}', name: 'storage.local.upload', methods: ['PUT'], domain: null },
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
const table = normalizeTable(routes);
|
|
15
|
-
|
|
16
|
-
// basic: 5 routes normalized with leading slashes
|
|
17
|
-
assert.equal(table.length, 5);
|
|
18
|
-
assert.equal(table[0].uri, '/up');
|
|
19
|
-
assert.equal(table[3].uri, '/storage/{path}');
|
|
20
|
-
|
|
21
|
-
// routeKey
|
|
22
|
-
assert.equal(routeKey(table[0]), 'GET /up');
|
|
23
|
-
|
|
24
|
-
// HEAD matched by GET route
|
|
25
|
-
let m = match(table, { method: 'HEAD', path: '/up' });
|
|
26
|
-
assert.ok(m, 'HEAD /up should match');
|
|
27
|
-
assert.equal(m.route.name, null);
|
|
28
|
-
|
|
29
|
-
// trailing slash normalization
|
|
30
|
-
m = match(table, { method: 'GET', path: '/up/' });
|
|
31
|
-
assert.ok(m, 'trailing slash should normalize');
|
|
32
|
-
assert.equal(m.route.uri, '/up');
|
|
33
|
-
|
|
34
|
-
// query string stripped
|
|
35
|
-
m = match(table, { method: 'GET', path: '/dbcheck?foo=bar' });
|
|
36
|
-
assert.ok(m);
|
|
37
|
-
assert.equal(m.route.uri, '/dbcheck');
|
|
38
|
-
|
|
39
|
-
// param capture + decodeURIComponent
|
|
40
|
-
m = match(table, { method: 'GET', path: '/storage/hello%20world.txt' });
|
|
41
|
-
assert.ok(m);
|
|
42
|
-
assert.equal(m.params.path, 'hello world.txt');
|
|
43
|
-
|
|
44
|
-
// method distinction: PUT vs GET on same uri
|
|
45
|
-
m = match(table, { method: 'PUT', path: '/storage/x' });
|
|
46
|
-
assert.ok(m);
|
|
47
|
-
assert.equal(m.route.name, 'storage.local.upload');
|
|
48
|
-
m = match(table, { method: 'GET', path: '/storage/x' });
|
|
49
|
-
assert.ok(m);
|
|
50
|
-
assert.equal(m.route.name, 'storage.local');
|
|
51
|
-
|
|
52
|
-
// unknown path -> null
|
|
53
|
-
assert.equal(match(table, { method: 'GET', path: '/nope' }), null);
|
|
54
|
-
|
|
55
|
-
// unknown method -> null (DELETE not registered)
|
|
56
|
-
assert.equal(match(table, { method: 'DELETE', path: '/up' }), null);
|
|
57
|
-
|
|
58
|
-
// specificity / param: storage/{path} captures one segment only
|
|
59
|
-
m = match(table, { method: 'GET', path: '/storage/a/b' });
|
|
60
|
-
// {path} is one segment, so /storage/a/b should NOT match (b extra)
|
|
61
|
-
assert.equal(m, null, 'multi-segment should not match single {path}');
|
|
62
|
-
|
|
63
|
-
// dispatch: missing handler -> null
|
|
64
|
-
const handlers = {};
|
|
65
|
-
assert.equal(await dispatch({ method: 'GET', path: '/up' }, table, handlers), null);
|
|
66
|
-
|
|
67
|
-
// dispatch: with handler returns response
|
|
68
|
-
handlers['GET /up'] = (ctx) => html('hi ' + (ctx.params.x || ''), 200);
|
|
69
|
-
const r = await dispatch({ method: 'GET', path: '/up' }, table, handlers);
|
|
70
|
-
assert.deepEqual(r, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: 'hi ' });
|
|
71
|
-
|
|
72
|
-
// dispatch: unknown path -> null
|
|
73
|
-
assert.equal(await dispatch({ method: 'GET', path: '/nope' }, table, handlers), null);
|
|
74
|
-
|
|
75
|
-
// duplicate method+uri -> later wins
|
|
76
|
-
const dupTable = normalizeTable([
|
|
77
|
-
{ uri: '/x', name: 'first', methods: ['GET'], domain: null },
|
|
78
|
-
{ uri: '/x', name: 'second', methods: ['GET'], domain: null },
|
|
79
|
-
]);
|
|
80
|
-
assert.equal(dupTable.length, 1);
|
|
81
|
-
assert.equal(dupTable[0].name, 'second');
|
|
82
|
-
|
|
83
|
-
// root path
|
|
84
|
-
m = match(table, { method: 'GET', path: '/' });
|
|
85
|
-
assert.ok(m);
|
|
86
|
-
assert.equal(m.route.uri, '/');
|
|
87
|
-
|
|
88
|
-
// response helpers
|
|
89
|
-
assert.deepEqual(html('a'), { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: 'a' });
|
|
90
|
-
assert.deepEqual(json({ b: 1 }), { status: 200, headers: { 'content-type': 'application/json' }, body: '{"b":1}' });
|
|
91
|
-
assert.deepEqual(redirect('/x'), { status: 302, headers: { location: '/x' }, body: '' });
|
|
92
|
-
assert.deepEqual(notFound(), { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 'Not Found' });
|
|
93
|
-
assert.deepEqual(text('t', 201), { status: 201, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 't' });
|
|
94
|
-
|
|
95
|
-
// header names lowercased by dispatch
|
|
96
|
-
handlers['GET /dbcheck'] = () => ({ status: 200, headers: { 'X-Foo': 'bar' }, body: 'ok' });
|
|
97
|
-
const r2 = await dispatch({ method: 'GET', path: '/dbcheck' }, table, handlers);
|
|
98
|
-
assert.equal(r2.headers['x-foo'], 'bar');
|
|
99
|
-
|
|
100
|
-
// wildcard {path*} captures multiple segments
|
|
101
|
-
const wtable = normalizeTable([{ uri: '/files/{path*}', name: 'files', methods: ['GET'], domain: null }]);
|
|
102
|
-
m = match(wtable, { method: 'GET', path: '/files/a/b/c.txt' });
|
|
103
|
-
assert.ok(m, 'wildcard should match multi-segment');
|
|
104
|
-
assert.equal(m.params.path, 'a/b/c.txt');
|
|
105
|
-
|
|
106
|
-
// wildcard must be the last segment
|
|
107
|
-
assert.throws(
|
|
108
|
-
() => match(normalizeTable([{ uri: '/{a*}/x', name: null, methods: ['GET'], domain: null }]), { method: 'GET', path: '/a/x' }),
|
|
109
|
-
/must be the last segment/,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// domain enforcement + domain param capture
|
|
113
|
-
const dtable = normalizeTable([
|
|
114
|
-
{ uri: '/', name: 'root', methods: ['GET'], domain: null },
|
|
115
|
-
{ uri: '/admin', name: 'admin', methods: ['GET'], domain: 'admin.example.com' },
|
|
116
|
-
{ uri: '/tenant', name: 'tenant', methods: ['GET'], domain: '{sub}.example.com' },
|
|
117
|
-
]);
|
|
118
|
-
m = match(dtable, { method: 'GET', path: '/admin', host: 'admin.example.com' });
|
|
119
|
-
assert.ok(m);
|
|
120
|
-
assert.equal(m.route.name, 'admin');
|
|
121
|
-
assert.equal(match(dtable, { method: 'GET', path: '/admin', host: 'www.example.com' }), null, 'wrong host must not match');
|
|
122
|
-
assert.equal(match(dtable, { method: 'GET', path: '/admin' }), null, 'missing host must not match domain route');
|
|
123
|
-
m = match(dtable, { method: 'GET', path: '/tenant', host: 'acme.example.com' });
|
|
124
|
-
assert.ok(m);
|
|
125
|
-
assert.equal(m.params.sub, 'acme');
|
|
126
|
-
m = match(dtable, { method: 'GET', path: '/admin', host: 'admin.example.com:8787' });
|
|
127
|
-
assert.ok(m, 'port must be stripped from host');
|
|
128
|
-
|
|
129
|
-
// an optional parameter: the separator belongs to the optional group
|
|
130
|
-
// (Symfony's RouteCompiler), so '/users/{id?}' also answers '/users'.
|
|
131
|
-
const otable = normalizeTable([
|
|
132
|
-
{ uri: '/users/{id?}', name: 'users.show', methods: ['GET'], domain: null },
|
|
133
|
-
{ uri: '/files/{path}', name: 'files.show', methods: ['GET'], domain: null },
|
|
134
|
-
]);
|
|
135
|
-
m = match(otable, { method: 'GET', path: '/users' });
|
|
136
|
-
assert.ok(m, 'the optional segment may be absent');
|
|
137
|
-
assert.equal(m.route.name, 'users.show');
|
|
138
|
-
assert.equal(m.params.id, undefined, 'an absent optional parameter is not captured');
|
|
139
|
-
m = match(otable, { method: 'GET', path: '/users/5' });
|
|
140
|
-
assert.ok(m);
|
|
141
|
-
assert.equal(m.params.id, '5');
|
|
142
|
-
// the optional variable as the only token: the separator stays outside
|
|
143
|
-
// (Symfony's RouteCompiler) and the parameter may be absent
|
|
144
|
-
const onlytable = normalizeTable([{ uri: '/{id?}', name: 'root', methods: ['GET'], domain: null }]);
|
|
145
|
-
m = match(onlytable, { method: 'GET', path: '/' });
|
|
146
|
-
assert.ok(m, 'the root route matches');
|
|
147
|
-
assert.equal(m.params.id, undefined);
|
|
148
|
-
m = match(onlytable, { method: 'GET', path: '/7' });
|
|
149
|
-
assert.ok(m);
|
|
150
|
-
assert.equal(m.params.id, '7');
|
|
151
|
-
// an optional parameter that is not the last one is required (Symfony stops
|
|
152
|
-
// its first-optional scan at the text token after it)
|
|
153
|
-
const midtable = normalizeTable([{ uri: '/a/{b?}/c', name: 'mid', methods: ['GET'], domain: null }]);
|
|
154
|
-
assert.equal(match(midtable, { method: 'GET', path: '/a/c' }), null, 'the middle parameter is required');
|
|
155
|
-
assert.ok(match(midtable, { method: 'GET', path: '/a/x/c' }));
|
|
156
|
-
// Laravel decodes the path BEFORE the route matches it (UriValidator), so an
|
|
157
|
-
// encoded slash is a separator there: '/files/a%2Fb' is '/files/a/b' and does
|
|
158
|
-
// not match a one-segment route.
|
|
159
|
-
assert.equal(match(otable, { method: 'GET', path: '/files/a%2Fb' }), null, 'an encoded slash splits the path');
|
|
160
|
-
// Symfony's requirement is `[^/]++` (one or more): an encoded trailing slash
|
|
161
|
-
// ('/users/%2F' decodes to '/users//') leaves the group empty, and Symfony
|
|
162
|
-
// refuses it - Laravel answers 404.
|
|
163
|
-
assert.equal(match(otable, { method: 'GET', path: '/users/%2F' }), null, 'an empty optional parameter does not match');
|
|
164
|
-
|
|
165
|
-
// method override (POST + header -> PUT)
|
|
166
|
-
const mtable = normalizeTable([{ uri: '/things/1', name: 'update', methods: ['PUT'], domain: null }]);
|
|
167
|
-
m = match(mtable, { method: 'POST', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } });
|
|
168
|
-
assert.ok(m, 'POST + override header should match PUT route');
|
|
169
|
-
assert.equal(m.route.name, 'update');
|
|
170
|
-
assert.equal(match(mtable, { method: 'DELETE', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } }), null, 'override ignored for non-POST');
|
|
171
|
-
m = match(mtable, { method: 'GET', path: '/things/1', methodOverride: 'PUT' });
|
|
172
|
-
assert.ok(m, 'explicit methodOverride should work');
|
|
173
|
-
|
|
174
|
-
// dispatch exposes effective method after override
|
|
175
|
-
handlers['PUT /things/1'] = (ctx) => text(ctx.request.method);
|
|
176
|
-
const r3 = await dispatch({ method: 'POST', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } }, mtable, handlers);
|
|
177
|
-
assert.ok(r3);
|
|
178
|
-
assert.equal(r3.body, 'PUT');
|
|
179
|
-
|
|
180
|
-
// ---- route-model binding ----
|
|
181
|
-
class FakeBindModel {
|
|
182
|
-
static query(db) {
|
|
183
|
-
const chain = {
|
|
184
|
-
_col: null,
|
|
185
|
-
_val: null,
|
|
186
|
-
where(col, val) { this._col = col; this._val = val; return this; },
|
|
187
|
-
async first() {
|
|
188
|
-
if (this._val === 'missing') return null;
|
|
189
|
-
return { __col: this._col, __val: this._val };
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
return chain;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const btable = normalizeTable([
|
|
197
|
-
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
198
|
-
]);
|
|
199
|
-
assert.equal(btable[0].bindings.post, 'Post');
|
|
200
|
-
|
|
201
|
-
const bhandlers = {};
|
|
202
|
-
let seenModel = null;
|
|
203
|
-
let seenParams = null;
|
|
204
|
-
bhandlers['GET /posts/{post}'] = (ctx) => {
|
|
205
|
-
seenModel = ctx.models && ctx.models.post;
|
|
206
|
-
seenParams = ctx.params;
|
|
207
|
-
assert.equal(ctx.params.post, '42');
|
|
208
|
-
return text('ok');
|
|
209
|
-
};
|
|
210
|
-
const bres = await dispatch({ method: 'GET', path: '/posts/42' }, btable, bhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
211
|
-
assert.equal(bres.status, 200);
|
|
212
|
-
assert.ok(seenModel, 'handler should see resolved model');
|
|
213
|
-
assert.equal(seenModel.__col, 'id');
|
|
214
|
-
assert.equal(seenModel.__val, '42');
|
|
215
|
-
assert.equal(seenParams.post, '42');
|
|
216
|
-
|
|
217
|
-
// explicit column binding
|
|
218
|
-
const ctable = normalizeTable([
|
|
219
|
-
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: { model: 'Post', column: 'slug' } } },
|
|
220
|
-
]);
|
|
221
|
-
const chandlers = {};
|
|
222
|
-
let cseen = null;
|
|
223
|
-
chandlers['GET /posts/{post}'] = (ctx) => { cseen = ctx.models.post; return text('ok'); };
|
|
224
|
-
const cres = await dispatch({ method: 'GET', path: '/posts/hello-slug' }, ctable, chandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
225
|
-
assert.equal(cres.status, 200);
|
|
226
|
-
assert.equal(cseen.__col, 'slug');
|
|
227
|
-
assert.equal(cseen.__val, 'hello-slug');
|
|
228
|
-
|
|
229
|
-
// missing record -> 404
|
|
230
|
-
const mtable2 = normalizeTable([
|
|
231
|
-
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
232
|
-
]);
|
|
233
|
-
const mhandlers = {};
|
|
234
|
-
mhandlers['GET /posts/{post}'] = () => text('should not run');
|
|
235
|
-
await assert.rejects(
|
|
236
|
-
() => dispatch({ method: 'GET', path: '/posts/missing' }, mtable2, mhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' }),
|
|
237
|
-
(e) => e && e.status === 404,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
// unknown model name -> 500
|
|
241
|
-
const utable = normalizeTable([
|
|
242
|
-
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Nope' } },
|
|
243
|
-
]);
|
|
244
|
-
const uhandlers = {};
|
|
245
|
-
uhandlers['GET /posts/{post}'] = () => text('should not run');
|
|
246
|
-
await assert.rejects(
|
|
247
|
-
() => dispatch({ method: 'GET', path: '/posts/42' }, utable, uhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' }),
|
|
248
|
-
(e) => e && e.status === 500,
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// no options.models -> params unchanged, no throw, backward compatible
|
|
252
|
-
const ntable = normalizeTable([
|
|
253
|
-
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
254
|
-
]);
|
|
255
|
-
const nhandlers = {};
|
|
256
|
-
let nseenModels = 'UNSET';
|
|
257
|
-
let nseenParams = null;
|
|
258
|
-
nhandlers['GET /posts/{post}'] = (ctx) => {
|
|
259
|
-
nseenModels = ctx.models;
|
|
260
|
-
nseenParams = ctx.params;
|
|
261
|
-
return text('ok ' + ctx.params.post);
|
|
262
|
-
};
|
|
263
|
-
const nres = await dispatch({ method: 'GET', path: '/posts/42' }, ntable, nhandlers);
|
|
264
|
-
assert.ok(nres, 'dispatch should not return null');
|
|
265
|
-
assert.equal(nres.status, 200);
|
|
266
|
-
assert.equal(nres.body, 'ok 42');
|
|
267
|
-
assert.equal(nseenParams.post, '42');
|
|
268
|
-
assert.equal(nseenModels, undefined);
|
|
269
|
-
|
|
270
|
-
// route without bindings unaffected (even with models provided)
|
|
271
|
-
const rtable = normalizeTable([
|
|
272
|
-
{ uri: '/health', name: 'health', methods: ['GET'], domain: null },
|
|
273
|
-
]);
|
|
274
|
-
const rhandlers = {};
|
|
275
|
-
rhandlers['GET /health'] = (ctx) => text('healthy ' + (ctx.params.id || ''));
|
|
276
|
-
const rres = await dispatch({ method: 'GET', path: '/health' }, rtable, rhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
277
|
-
assert.ok(rres);
|
|
278
|
-
assert.equal(rres.status, 200);
|
|
279
|
-
assert.equal(rres.body, 'healthy ');
|
|
280
|
-
|
|
281
|
-
console.log('ROUTER TESTS: PASS');
|
|
1
|
+
// edge/kernel/tests/router.test.mjs
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { normalizeTable, routeKey, match, dispatch } from '../router.mjs';
|
|
4
|
+
import { html, json, redirect, notFound, text } from '../response.mjs';
|
|
5
|
+
|
|
6
|
+
const routes = [
|
|
7
|
+
{ uri: 'up', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
8
|
+
{ uri: '/', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
9
|
+
{ uri: 'dbcheck', name: null, methods: ['GET', 'HEAD'], domain: null },
|
|
10
|
+
{ uri: 'storage/{path}', name: 'storage.local', methods: ['GET', 'HEAD'], domain: null },
|
|
11
|
+
{ uri: 'storage/{path}', name: 'storage.local.upload', methods: ['PUT'], domain: null },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const table = normalizeTable(routes);
|
|
15
|
+
|
|
16
|
+
// basic: 5 routes normalized with leading slashes
|
|
17
|
+
assert.equal(table.length, 5);
|
|
18
|
+
assert.equal(table[0].uri, '/up');
|
|
19
|
+
assert.equal(table[3].uri, '/storage/{path}');
|
|
20
|
+
|
|
21
|
+
// routeKey
|
|
22
|
+
assert.equal(routeKey(table[0]), 'GET /up');
|
|
23
|
+
|
|
24
|
+
// HEAD matched by GET route
|
|
25
|
+
let m = match(table, { method: 'HEAD', path: '/up' });
|
|
26
|
+
assert.ok(m, 'HEAD /up should match');
|
|
27
|
+
assert.equal(m.route.name, null);
|
|
28
|
+
|
|
29
|
+
// trailing slash normalization
|
|
30
|
+
m = match(table, { method: 'GET', path: '/up/' });
|
|
31
|
+
assert.ok(m, 'trailing slash should normalize');
|
|
32
|
+
assert.equal(m.route.uri, '/up');
|
|
33
|
+
|
|
34
|
+
// query string stripped
|
|
35
|
+
m = match(table, { method: 'GET', path: '/dbcheck?foo=bar' });
|
|
36
|
+
assert.ok(m);
|
|
37
|
+
assert.equal(m.route.uri, '/dbcheck');
|
|
38
|
+
|
|
39
|
+
// param capture + decodeURIComponent
|
|
40
|
+
m = match(table, { method: 'GET', path: '/storage/hello%20world.txt' });
|
|
41
|
+
assert.ok(m);
|
|
42
|
+
assert.equal(m.params.path, 'hello world.txt');
|
|
43
|
+
|
|
44
|
+
// method distinction: PUT vs GET on same uri
|
|
45
|
+
m = match(table, { method: 'PUT', path: '/storage/x' });
|
|
46
|
+
assert.ok(m);
|
|
47
|
+
assert.equal(m.route.name, 'storage.local.upload');
|
|
48
|
+
m = match(table, { method: 'GET', path: '/storage/x' });
|
|
49
|
+
assert.ok(m);
|
|
50
|
+
assert.equal(m.route.name, 'storage.local');
|
|
51
|
+
|
|
52
|
+
// unknown path -> null
|
|
53
|
+
assert.equal(match(table, { method: 'GET', path: '/nope' }), null);
|
|
54
|
+
|
|
55
|
+
// unknown method -> null (DELETE not registered)
|
|
56
|
+
assert.equal(match(table, { method: 'DELETE', path: '/up' }), null);
|
|
57
|
+
|
|
58
|
+
// specificity / param: storage/{path} captures one segment only
|
|
59
|
+
m = match(table, { method: 'GET', path: '/storage/a/b' });
|
|
60
|
+
// {path} is one segment, so /storage/a/b should NOT match (b extra)
|
|
61
|
+
assert.equal(m, null, 'multi-segment should not match single {path}');
|
|
62
|
+
|
|
63
|
+
// dispatch: missing handler -> null
|
|
64
|
+
const handlers = {};
|
|
65
|
+
assert.equal(await dispatch({ method: 'GET', path: '/up' }, table, handlers), null);
|
|
66
|
+
|
|
67
|
+
// dispatch: with handler returns response
|
|
68
|
+
handlers['GET /up'] = (ctx) => html('hi ' + (ctx.params.x || ''), 200);
|
|
69
|
+
const r = await dispatch({ method: 'GET', path: '/up' }, table, handlers);
|
|
70
|
+
assert.deepEqual(r, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: 'hi ' });
|
|
71
|
+
|
|
72
|
+
// dispatch: unknown path -> null
|
|
73
|
+
assert.equal(await dispatch({ method: 'GET', path: '/nope' }, table, handlers), null);
|
|
74
|
+
|
|
75
|
+
// duplicate method+uri -> later wins
|
|
76
|
+
const dupTable = normalizeTable([
|
|
77
|
+
{ uri: '/x', name: 'first', methods: ['GET'], domain: null },
|
|
78
|
+
{ uri: '/x', name: 'second', methods: ['GET'], domain: null },
|
|
79
|
+
]);
|
|
80
|
+
assert.equal(dupTable.length, 1);
|
|
81
|
+
assert.equal(dupTable[0].name, 'second');
|
|
82
|
+
|
|
83
|
+
// root path
|
|
84
|
+
m = match(table, { method: 'GET', path: '/' });
|
|
85
|
+
assert.ok(m);
|
|
86
|
+
assert.equal(m.route.uri, '/');
|
|
87
|
+
|
|
88
|
+
// response helpers
|
|
89
|
+
assert.deepEqual(html('a'), { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, body: 'a' });
|
|
90
|
+
assert.deepEqual(json({ b: 1 }), { status: 200, headers: { 'content-type': 'application/json' }, body: '{"b":1}' });
|
|
91
|
+
assert.deepEqual(redirect('/x'), { status: 302, headers: { location: '/x' }, body: '' });
|
|
92
|
+
assert.deepEqual(notFound(), { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 'Not Found' });
|
|
93
|
+
assert.deepEqual(text('t', 201), { status: 201, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 't' });
|
|
94
|
+
|
|
95
|
+
// header names lowercased by dispatch
|
|
96
|
+
handlers['GET /dbcheck'] = () => ({ status: 200, headers: { 'X-Foo': 'bar' }, body: 'ok' });
|
|
97
|
+
const r2 = await dispatch({ method: 'GET', path: '/dbcheck' }, table, handlers);
|
|
98
|
+
assert.equal(r2.headers['x-foo'], 'bar');
|
|
99
|
+
|
|
100
|
+
// wildcard {path*} captures multiple segments
|
|
101
|
+
const wtable = normalizeTable([{ uri: '/files/{path*}', name: 'files', methods: ['GET'], domain: null }]);
|
|
102
|
+
m = match(wtable, { method: 'GET', path: '/files/a/b/c.txt' });
|
|
103
|
+
assert.ok(m, 'wildcard should match multi-segment');
|
|
104
|
+
assert.equal(m.params.path, 'a/b/c.txt');
|
|
105
|
+
|
|
106
|
+
// wildcard must be the last segment
|
|
107
|
+
assert.throws(
|
|
108
|
+
() => match(normalizeTable([{ uri: '/{a*}/x', name: null, methods: ['GET'], domain: null }]), { method: 'GET', path: '/a/x' }),
|
|
109
|
+
/must be the last segment/,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// domain enforcement + domain param capture
|
|
113
|
+
const dtable = normalizeTable([
|
|
114
|
+
{ uri: '/', name: 'root', methods: ['GET'], domain: null },
|
|
115
|
+
{ uri: '/admin', name: 'admin', methods: ['GET'], domain: 'admin.example.com' },
|
|
116
|
+
{ uri: '/tenant', name: 'tenant', methods: ['GET'], domain: '{sub}.example.com' },
|
|
117
|
+
]);
|
|
118
|
+
m = match(dtable, { method: 'GET', path: '/admin', host: 'admin.example.com' });
|
|
119
|
+
assert.ok(m);
|
|
120
|
+
assert.equal(m.route.name, 'admin');
|
|
121
|
+
assert.equal(match(dtable, { method: 'GET', path: '/admin', host: 'www.example.com' }), null, 'wrong host must not match');
|
|
122
|
+
assert.equal(match(dtable, { method: 'GET', path: '/admin' }), null, 'missing host must not match domain route');
|
|
123
|
+
m = match(dtable, { method: 'GET', path: '/tenant', host: 'acme.example.com' });
|
|
124
|
+
assert.ok(m);
|
|
125
|
+
assert.equal(m.params.sub, 'acme');
|
|
126
|
+
m = match(dtable, { method: 'GET', path: '/admin', host: 'admin.example.com:8787' });
|
|
127
|
+
assert.ok(m, 'port must be stripped from host');
|
|
128
|
+
|
|
129
|
+
// an optional parameter: the separator belongs to the optional group
|
|
130
|
+
// (Symfony's RouteCompiler), so '/users/{id?}' also answers '/users'.
|
|
131
|
+
const otable = normalizeTable([
|
|
132
|
+
{ uri: '/users/{id?}', name: 'users.show', methods: ['GET'], domain: null },
|
|
133
|
+
{ uri: '/files/{path}', name: 'files.show', methods: ['GET'], domain: null },
|
|
134
|
+
]);
|
|
135
|
+
m = match(otable, { method: 'GET', path: '/users' });
|
|
136
|
+
assert.ok(m, 'the optional segment may be absent');
|
|
137
|
+
assert.equal(m.route.name, 'users.show');
|
|
138
|
+
assert.equal(m.params.id, undefined, 'an absent optional parameter is not captured');
|
|
139
|
+
m = match(otable, { method: 'GET', path: '/users/5' });
|
|
140
|
+
assert.ok(m);
|
|
141
|
+
assert.equal(m.params.id, '5');
|
|
142
|
+
// the optional variable as the only token: the separator stays outside
|
|
143
|
+
// (Symfony's RouteCompiler) and the parameter may be absent
|
|
144
|
+
const onlytable = normalizeTable([{ uri: '/{id?}', name: 'root', methods: ['GET'], domain: null }]);
|
|
145
|
+
m = match(onlytable, { method: 'GET', path: '/' });
|
|
146
|
+
assert.ok(m, 'the root route matches');
|
|
147
|
+
assert.equal(m.params.id, undefined);
|
|
148
|
+
m = match(onlytable, { method: 'GET', path: '/7' });
|
|
149
|
+
assert.ok(m);
|
|
150
|
+
assert.equal(m.params.id, '7');
|
|
151
|
+
// an optional parameter that is not the last one is required (Symfony stops
|
|
152
|
+
// its first-optional scan at the text token after it)
|
|
153
|
+
const midtable = normalizeTable([{ uri: '/a/{b?}/c', name: 'mid', methods: ['GET'], domain: null }]);
|
|
154
|
+
assert.equal(match(midtable, { method: 'GET', path: '/a/c' }), null, 'the middle parameter is required');
|
|
155
|
+
assert.ok(match(midtable, { method: 'GET', path: '/a/x/c' }));
|
|
156
|
+
// Laravel decodes the path BEFORE the route matches it (UriValidator), so an
|
|
157
|
+
// encoded slash is a separator there: '/files/a%2Fb' is '/files/a/b' and does
|
|
158
|
+
// not match a one-segment route.
|
|
159
|
+
assert.equal(match(otable, { method: 'GET', path: '/files/a%2Fb' }), null, 'an encoded slash splits the path');
|
|
160
|
+
// Symfony's requirement is `[^/]++` (one or more): an encoded trailing slash
|
|
161
|
+
// ('/users/%2F' decodes to '/users//') leaves the group empty, and Symfony
|
|
162
|
+
// refuses it - Laravel answers 404.
|
|
163
|
+
assert.equal(match(otable, { method: 'GET', path: '/users/%2F' }), null, 'an empty optional parameter does not match');
|
|
164
|
+
|
|
165
|
+
// method override (POST + header -> PUT)
|
|
166
|
+
const mtable = normalizeTable([{ uri: '/things/1', name: 'update', methods: ['PUT'], domain: null }]);
|
|
167
|
+
m = match(mtable, { method: 'POST', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } });
|
|
168
|
+
assert.ok(m, 'POST + override header should match PUT route');
|
|
169
|
+
assert.equal(m.route.name, 'update');
|
|
170
|
+
assert.equal(match(mtable, { method: 'DELETE', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } }), null, 'override ignored for non-POST');
|
|
171
|
+
m = match(mtable, { method: 'GET', path: '/things/1', methodOverride: 'PUT' });
|
|
172
|
+
assert.ok(m, 'explicit methodOverride should work');
|
|
173
|
+
|
|
174
|
+
// dispatch exposes effective method after override
|
|
175
|
+
handlers['PUT /things/1'] = (ctx) => text(ctx.request.method);
|
|
176
|
+
const r3 = await dispatch({ method: 'POST', path: '/things/1', headers: { 'x-http-method-override': 'PUT' } }, mtable, handlers);
|
|
177
|
+
assert.ok(r3);
|
|
178
|
+
assert.equal(r3.body, 'PUT');
|
|
179
|
+
|
|
180
|
+
// ---- route-model binding ----
|
|
181
|
+
class FakeBindModel {
|
|
182
|
+
static query(db) {
|
|
183
|
+
const chain = {
|
|
184
|
+
_col: null,
|
|
185
|
+
_val: null,
|
|
186
|
+
where(col, val) { this._col = col; this._val = val; return this; },
|
|
187
|
+
async first() {
|
|
188
|
+
if (this._val === 'missing') return null;
|
|
189
|
+
return { __col: this._col, __val: this._val };
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
return chain;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const btable = normalizeTable([
|
|
197
|
+
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
198
|
+
]);
|
|
199
|
+
assert.equal(btable[0].bindings.post, 'Post');
|
|
200
|
+
|
|
201
|
+
const bhandlers = {};
|
|
202
|
+
let seenModel = null;
|
|
203
|
+
let seenParams = null;
|
|
204
|
+
bhandlers['GET /posts/{post}'] = (ctx) => {
|
|
205
|
+
seenModel = ctx.models && ctx.models.post;
|
|
206
|
+
seenParams = ctx.params;
|
|
207
|
+
assert.equal(ctx.params.post, '42');
|
|
208
|
+
return text('ok');
|
|
209
|
+
};
|
|
210
|
+
const bres = await dispatch({ method: 'GET', path: '/posts/42' }, btable, bhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
211
|
+
assert.equal(bres.status, 200);
|
|
212
|
+
assert.ok(seenModel, 'handler should see resolved model');
|
|
213
|
+
assert.equal(seenModel.__col, 'id');
|
|
214
|
+
assert.equal(seenModel.__val, '42');
|
|
215
|
+
assert.equal(seenParams.post, '42');
|
|
216
|
+
|
|
217
|
+
// explicit column binding
|
|
218
|
+
const ctable = normalizeTable([
|
|
219
|
+
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: { model: 'Post', column: 'slug' } } },
|
|
220
|
+
]);
|
|
221
|
+
const chandlers = {};
|
|
222
|
+
let cseen = null;
|
|
223
|
+
chandlers['GET /posts/{post}'] = (ctx) => { cseen = ctx.models.post; return text('ok'); };
|
|
224
|
+
const cres = await dispatch({ method: 'GET', path: '/posts/hello-slug' }, ctable, chandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
225
|
+
assert.equal(cres.status, 200);
|
|
226
|
+
assert.equal(cseen.__col, 'slug');
|
|
227
|
+
assert.equal(cseen.__val, 'hello-slug');
|
|
228
|
+
|
|
229
|
+
// missing record -> 404
|
|
230
|
+
const mtable2 = normalizeTable([
|
|
231
|
+
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
232
|
+
]);
|
|
233
|
+
const mhandlers = {};
|
|
234
|
+
mhandlers['GET /posts/{post}'] = () => text('should not run');
|
|
235
|
+
await assert.rejects(
|
|
236
|
+
() => dispatch({ method: 'GET', path: '/posts/missing' }, mtable2, mhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' }),
|
|
237
|
+
(e) => e && e.status === 404,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// unknown model name -> 500
|
|
241
|
+
const utable = normalizeTable([
|
|
242
|
+
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Nope' } },
|
|
243
|
+
]);
|
|
244
|
+
const uhandlers = {};
|
|
245
|
+
uhandlers['GET /posts/{post}'] = () => text('should not run');
|
|
246
|
+
await assert.rejects(
|
|
247
|
+
() => dispatch({ method: 'GET', path: '/posts/42' }, utable, uhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' }),
|
|
248
|
+
(e) => e && e.status === 500,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// no options.models -> params unchanged, no throw, backward compatible
|
|
252
|
+
const ntable = normalizeTable([
|
|
253
|
+
{ uri: '/posts/{post}', name: 'posts.show', methods: ['GET'], domain: null, bindings: { post: 'Post' } },
|
|
254
|
+
]);
|
|
255
|
+
const nhandlers = {};
|
|
256
|
+
let nseenModels = 'UNSET';
|
|
257
|
+
let nseenParams = null;
|
|
258
|
+
nhandlers['GET /posts/{post}'] = (ctx) => {
|
|
259
|
+
nseenModels = ctx.models;
|
|
260
|
+
nseenParams = ctx.params;
|
|
261
|
+
return text('ok ' + ctx.params.post);
|
|
262
|
+
};
|
|
263
|
+
const nres = await dispatch({ method: 'GET', path: '/posts/42' }, ntable, nhandlers);
|
|
264
|
+
assert.ok(nres, 'dispatch should not return null');
|
|
265
|
+
assert.equal(nres.status, 200);
|
|
266
|
+
assert.equal(nres.body, 'ok 42');
|
|
267
|
+
assert.equal(nseenParams.post, '42');
|
|
268
|
+
assert.equal(nseenModels, undefined);
|
|
269
|
+
|
|
270
|
+
// route without bindings unaffected (even with models provided)
|
|
271
|
+
const rtable = normalizeTable([
|
|
272
|
+
{ uri: '/health', name: 'health', methods: ['GET'], domain: null },
|
|
273
|
+
]);
|
|
274
|
+
const rhandlers = {};
|
|
275
|
+
rhandlers['GET /health'] = (ctx) => text('healthy ' + (ctx.params.id || ''));
|
|
276
|
+
const rres = await dispatch({ method: 'GET', path: '/health' }, rtable, rhandlers, undefined, { models: { Post: FakeBindModel }, db: 'DB' });
|
|
277
|
+
assert.ok(rres);
|
|
278
|
+
assert.equal(rres.status, 200);
|
|
279
|
+
assert.equal(rres.body, 'healthy ');
|
|
280
|
+
|
|
281
|
+
console.log('ROUTER TESTS: PASS');
|