allow additional arguments to be passed to handler functions
[squeep-api-dingus] / test / lib / router.js
1 /* eslint-disable capitalized-comments */
2 /* eslint-env mocha */
3 'use strict';
4
5 const assert = require('assert');
6 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
7 const Router = require('../../lib/router');
8 const { DingusError } = require('../../lib/errors');
9
10 const noExpectedException = 'did not get expected exception';
11
12 describe('Router', function () {
13 const router = new Router();
14 let _its; // Save and restore ignoreTrailingSlash
15
16 beforeEach(function () {
17 _its = router.ignoreTrailingSlash;
18 });
19
20 afterEach(function () {
21 sinon.restore();
22 router.ignoreTrailingSlash = _its;
23 });
24
25 describe('_pathDefinitionToPathMatch', function () {
26 it('defines a simple path', function () {
27 const p = '/a/b/c';
28 const expected = ['', 'a', 'b', 'c'];
29 expected[router.METHODS] = {};
30 const r = router._pathDefinitionToPathMatch(p);
31 assert.deepStrictEqual(r, expected);
32 });
33 it('defines a path with parameter', function () {
34 const p = '/a/b/:c';
35 const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }];
36 expected[router.METHODS] = {};
37 const r = router._pathDefinitionToPathMatch(p);
38 assert.deepStrictEqual(r, expected);
39 });
40 it('defines a path with trailing slash', function () {
41 router.ignoreTrailingSlash = true;
42 const p = '/a/b/:c/';
43 const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }];
44 expected[router.METHODS] = {};
45 const r = router._pathDefinitionToPathMatch(p);
46 assert.deepStrictEqual(r, expected);
47 });
48 }); // _pathDefinitionToPathMatch
49
50 describe('_pathCompareExact', function () {
51 let fixedPath, checkPath;
52
53 it('compares static paths which match', function () {
54 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
55 checkPath = router._pathDefinitionToPathMatch('/a/b/c');
56 const r = Router._pathCompareExact(fixedPath, checkPath);
57 assert.strictEqual(r, true);
58 });
59 it('compares static paths which do not match', function () {
60 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
61 checkPath = router._pathDefinitionToPathMatch('/a/b/d');
62 const r = Router._pathCompareExact(fixedPath, checkPath);
63 assert.strictEqual(r, false);
64 });
65 it('compares unequal static paths', function () {
66 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
67 checkPath = router._pathDefinitionToPathMatch('/a/b');
68 const r = Router._pathCompareExact(fixedPath, checkPath);
69 assert.strictEqual(r, false);
70 });
71 it('compares param paths which match', function () {
72 fixedPath = router._pathDefinitionToPathMatch('/a/:b/c');
73 checkPath = router._pathDefinitionToPathMatch('/a/:b/c');
74 const r = Router._pathCompareExact(fixedPath, checkPath);
75 assert.strictEqual(r, true);
76 });
77 it('compares param paths which do not match', function () {
78 fixedPath = router._pathDefinitionToPathMatch('/a/:b/c');
79 checkPath = router._pathDefinitionToPathMatch('/a/:g/c');
80 const r = Router._pathCompareExact(fixedPath, checkPath);
81 assert.strictEqual(r, false);
82 });
83 }); // _pathCompareExact
84
85 describe('_pathCompareParam', function () {
86 let fixedPath, checkPath;
87
88 it('compares static paths which match', function () {
89 const params = {};
90 const expectedParams = {};
91 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
92 checkPath = router._pathDefinitionToPathMatch('/a/b/c');
93 const r = Router._pathCompareParam(fixedPath, checkPath);
94 assert.strictEqual(r, true);
95 assert.deepStrictEqual(params, expectedParams);
96 });
97 it('compares static paths which do not match', function () {
98 const params = {};
99 const expectedParams = {};
100 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
101 checkPath = router._pathDefinitionToPathMatch('/a/b/d');
102 const r = Router._pathCompareParam(fixedPath, checkPath, params);
103 assert.strictEqual(r, false);
104 assert.deepStrictEqual(params, expectedParams);
105 });
106 it('compares unequal static paths', function () {
107 const params = {};
108 const expectedParams = {};
109 fixedPath = router._pathDefinitionToPathMatch('/a/b/c');
110 checkPath = router._pathDefinitionToPathMatch('/a/b');
111 const r = Router._pathCompareParam(fixedPath, checkPath, params);
112 assert.strictEqual(r, false);
113 assert.deepStrictEqual(params, expectedParams);
114 });
115 it('compares param paths which match', function () {
116 const params = {};
117 const expectedParams = {
118 b: 'bar',
119 };
120 fixedPath = router._pathDefinitionToPathMatch('/a/:b/c');
121 checkPath = router._pathDefinitionToPathMatch('/a/bar/c');
122 const r = Router._pathCompareParam(fixedPath, checkPath, params);
123 assert.strictEqual(r, true);
124 assert.deepStrictEqual(params, expectedParams);
125 });
126 it('compares multi param path which match', function () {
127 const params = {};
128 const expectedParams = {
129 b: 'gaz',
130 c: '123',
131 };
132 fixedPath = router._pathDefinitionToPathMatch('/a/:b/:c');
133 checkPath = router._pathDefinitionToPathMatch('/a/gaz/123');
134 const r = Router._pathCompareParam(fixedPath, checkPath, params);
135 assert.strictEqual(r, true);
136 assert.deepStrictEqual(params, expectedParams);
137 });
138 }); // _pathCompareParam
139
140 describe('_pathFind / _pathFindExact', function () {
141 let pathsByLengthOrig;
142
143 beforeEach(function () {
144 pathsByLengthOrig = router.pathsByLength;
145 router.pathsByLength = {
146 2: [ router._pathDefinitionToPathMatch('/:id') ],
147 3: [ router._pathDefinitionToPathMatch('/a/b') ],
148 };
149 });
150 afterEach(function () {
151 router.pathsByLength = pathsByLengthOrig;
152 });
153
154 describe('_pathFind', function () {
155 it('finds a path', function () {
156 const pathParts = ['', '46123f1e-bdca-40ee-9f62-93ad38647aa1'];
157 const { matchedPath, pathParams } = router._pathFind(pathParts);
158 assert.strictEqual(matchedPath, router.pathsByLength[2][0]);
159 assert.deepStrictEqual(pathParams, { id: pathParts[1] });
160 });
161 it('does not find a path', function () {
162 const pathParts = ['', 'flarp', 'baz'];
163 const { matchedPath } = router._pathFind(pathParts);
164 assert.strictEqual(matchedPath, undefined);
165 });
166 }); // _pathFind
167
168 describe('_pathFindExact', function () {
169 it('finds a path', function () {
170 const pathParts = ['', { [router.PARAM]: 'id' }];
171 const r = router._pathFindExact(pathParts);
172 assert.strictEqual(r, router.pathsByLength[2][0]);
173 });
174 it('does not find a path', function () {
175 const pathParts = ['', 'flarp', 'baz'];
176 const r = router._pathFindExact(pathParts);
177 assert.strictEqual(r, undefined);
178 });
179 }); // _pathFindExact
180
181 }); // _pathFind / _pathFindExact
182
183 describe('on', function () {
184 let pathsByLengthOrig;
185 const stubHandler = () => {};
186 const stubEntry = {
187 handler: stubHandler,
188 handlerArgs: [],
189 };
190
191 beforeEach(function () {
192 pathsByLengthOrig = router.pathsByLength;
193 router.pathsByLength = {
194 2: [ router._pathDefinitionToPathMatch('/:id') ],
195 };
196 });
197 afterEach(function () {
198 router.pathsByLength = pathsByLengthOrig;
199 });
200
201 it('adds new path', function () {
202 const urlPath = '/a/:id';
203 const expected = router._pathDefinitionToPathMatch(urlPath);
204 expected[router.METHODS]['GET'] = stubEntry;
205 router.on('GET', urlPath, stubHandler);
206 assert.deepStrictEqual(router.pathsByLength[3][0], expected);
207 });
208
209 it('adds new method to path', function () {
210 const urlPath = '/a/:id';
211 const expected = router._pathDefinitionToPathMatch(urlPath);
212 expected[router.METHODS]['GET'] = stubEntry;
213 expected[router.METHODS]['POST'] = stubEntry;
214 router.on('GET', urlPath, stubHandler);
215
216 router.on('POST', urlPath, stubHandler);
217 assert.deepStrictEqual(router.pathsByLength[3][0], expected);
218 });
219
220 it('add some more paths', function () {
221 let urlPath = '/a/b/c/d';
222 const expected = router._pathDefinitionToPathMatch(urlPath);
223 expected[router.METHODS]['GET'] = stubEntry;
224 router.on('GET', urlPath, stubHandler);
225 urlPath = '/a/b/x/y';
226 router.on('GET', urlPath, stubHandler);
227
228 assert.strictEqual(router.pathsByLength[5].length, 2);
229 });
230
231 it('adds multiple methods', function () {
232 const urlPath = '/:id';
233 const expected = router._pathDefinitionToPathMatch(urlPath);
234 expected[router.METHODS]['GET'] = stubEntry;
235 expected[router.METHODS]['HEAD'] = stubEntry;
236
237 router.on(['GET', 'HEAD'], urlPath, stubHandler);
238 assert.deepStrictEqual(router.pathsByLength[2][0], expected);
239 });
240
241 it('adds new wildcard path', function () {
242 const urlPath = '/a/:id';
243 const expected = router._pathDefinitionToPathMatch(urlPath);
244 expected[router.METHODS]['*'] = stubEntry;
245 router.on('*', urlPath, stubHandler);
246 assert.deepStrictEqual(router.pathsByLength[3][0], expected);
247 });
248
249 it('fails to add unknown method path', function () {
250 const urlPath = '/a/:id';
251 try {
252 router.on('FLARP', urlPath, stubHandler);
253 assert.fail('expected an exception');
254 } catch (e) {
255 assert.strictEqual(e.name, 'DingusError');
256 assert.strictEqual(e.message, 'invalid method \'FLARP\'');
257 }
258 });
259
260 it('requires args to be array', function () {
261 const urlPath = '/a';
262 try {
263 router.on('GET', urlPath, stubHandler, {});
264 assert.fail('expected an exception');
265 } catch (e) {
266 assert(e instanceof TypeError);
267 }
268 });
269 }); // on
270
271 describe('lookup', function () {
272 let pathsByLengthOrig;
273 let ctx;
274 let stubHandler;
275
276 beforeEach(function () {
277 ctx = {};
278 pathsByLengthOrig = router.pathsByLength;
279 stubHandler = sinon.stub();
280 });
281 afterEach(function () {
282 router.pathsByLength = pathsByLengthOrig;
283 });
284
285 it('finds handler', function () {
286 const urlPath = '/:id';
287 const method = 'GET';
288 router.on(method, urlPath, stubHandler);
289 const path = '/abc';
290
291 const { handler } = router.lookup(method, path, ctx);
292 assert.strictEqual(handler, stubHandler);
293 });
294 it('does not find handler with trailing slash', function () {
295 router.ignoreTrailingSlash = false;
296 const urlPath = '/:id';
297 const method = 'GET';
298 router.on(method, urlPath, stubHandler);
299 const path = '/abc/';
300
301 try {
302 router.lookup(method, path, ctx);
303 assert.fail(noExpectedException);
304 } catch (e) {
305 assert(e instanceof DingusError);
306 assert.strictEqual(e.message, 'NoPath');
307 }
308 });
309 it('finds handler', function () {
310 router.ignoreTrailingSlash = true;
311 const urlPath = '/:id';
312 const method = 'GET';
313 router.on(method, urlPath, stubHandler);
314 const path = '/abc/';
315
316 const { handler } = router.lookup(method, path, ctx);
317 assert.strictEqual(handler, stubHandler);
318 });
319 it('finds handler without context', async function () {
320 const urlPath = '/:id';
321 const method = 'GET';
322 router.on(method, urlPath, stubHandler);
323 const path = '/abc';
324
325 const { handler } = router.lookup(method, path);
326 assert.strictEqual(handler, stubHandler);
327 });
328 it('finds fallback handler', async function () {
329 const urlPath = '/abc/:id';
330 const method = 'GET';
331 router.on('*', urlPath, stubHandler);
332 const path = '/abc/def';
333
334 const { handler } = router.lookup(method, path, ctx);
335 assert.strictEqual(handler, stubHandler);
336 });
337 it('calls unsupported method', async function () {
338 const urlPath = '/:id';
339 const method = 'POST';
340 router.on('GET', urlPath, stubHandler);
341 const path = '/abc';
342
343 try {
344 router.lookup(method, path, ctx);
345 assert.fail(noExpectedException);
346 } catch (e) {
347 assert(e instanceof DingusError);
348 assert.strictEqual(e.message, 'NoMethod');
349 }
350 });
351 it('does not lookup non-existent path', async function () {
352 const path = '/foo/bar';
353 const method = 'GET';
354
355 try {
356 router.lookup(method, path, ctx);
357 assert.fail(noExpectedException);
358 } catch (e) {
359 assert(e instanceof DingusError);
360 assert.strictEqual(e.message, 'NoPath');
361 }
362 });
363
364 }); // lookup
365
366 });