├── .github ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmark ├── bench-thread.js ├── bench.js ├── compare-branches.js └── uri-decoding.js ├── example.js ├── index.d.ts ├── index.js ├── lib ├── constrainer.js ├── handler-storage.js ├── http-methods.js ├── node.js ├── null-object.js ├── pretty-print.js ├── strategies │ ├── accept-host.js │ ├── accept-version.js │ └── http-method.js └── url-sanitizer.js ├── package.json └── test ├── case-insensitive.test.js ├── constraint.custom-versioning.test.js ├── constraint.custom.async.test.js ├── constraint.custom.test.js ├── constraint.default-versioning.test.js ├── constraint.host.test.js ├── constraints.test.js ├── custom-querystring-parser.test.js ├── errors.test.js ├── fastify-issue-3129.test.js ├── fastify-issue-3957.test.js ├── find-route.test.js ├── find.test.js ├── for-in-loop.test.js ├── full-url.test.js ├── has-route.test.js ├── host-storage.test.js ├── http2 └── constraint.host.test.js ├── issue-101.test.js ├── issue-104.test.js ├── issue-110.test.js ├── issue-132.test.js ├── issue-145.test.js ├── issue-149.test.js ├── issue-151.test.js ├── issue-154.test.js ├── issue-161.test.js ├── issue-17.test.js ├── issue-175.test.js ├── issue-182.test.js ├── issue-190.test.js ├── issue-20.test.js ├── issue-206.test.js ├── issue-221.test.js ├── issue-234.test.js ├── issue-238.test.js ├── issue-240.test.js ├── issue-241.test.js ├── issue-247.test.js ├── issue-254.test.js ├── issue-28.test.js ├── issue-280.test.js ├── issue-285.test.js ├── issue-330.test.js ├── issue-44.test.js ├── issue-46.test.js ├── issue-49.test.js ├── issue-59.test.js ├── issue-62.test.js ├── issue-63.test.js ├── issue-67.test.js ├── issue-93.test.js ├── lookup-async.test.js ├── lookup.test.js ├── matching-order.test.js ├── max-param-length.test.js ├── methods.test.js ├── null-object.test.js ├── on-bad-url.test.js ├── optional-params.test.js ├── params-collisions.test.js ├── path-params-match.test.js ├── pretty-print-tree.test.js ├── pretty-print.test.js ├── querystring.test.js ├── regex.test.js ├── routes-registered.test.js ├── server.test.js ├── shorthands.test.js ├── store.test.js └── types └── router.test-d.ts /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | commit-message: 6 | # Prefix all commit messages with "chore: " 7 | prefix: "chore" 8 | schedule: 9 | interval: "monthly" 10 | open-pull-requests-limit: 10 11 | 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | commit-message: 15 | # Prefix all commit messages with "chore: " 16 | prefix: "chore" 17 | schedule: 18 | interval: "monthly" 19 | open-pull-requests-limit: 10 20 | groups: 21 | # Production dependencies without breaking changes 22 | dependencies: 23 | dependency-type: "production" 24 | update-types: 25 | - "minor" 26 | - "patch" 27 | # Production dependencies with breaking changes 28 | dependencies-major: 29 | dependency-type: "production" 30 | update-types: 31 | - "major" 32 | # Development dependencies 33 | dev-dependencies: 34 | dependency-type: "development" 35 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | node-version: 21 | - 20 22 | - 22 23 | - 24 24 | os: 25 | - ubuntu-latest 26 | - windows-latest 27 | - macOS-latest 28 | 29 | steps: 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | with: 32 | persist-credentials: false 33 | 34 | - name: Use Node.js ${{ matrix.node-version }} 35 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 36 | with: 37 | check-latest: true 38 | node-version: ${{ matrix.node-version }} 39 | 40 | - name: Install 41 | run: | 42 | npm install --ignore-scripts 43 | 44 | - name: Lint 45 | run: | 46 | npm run test:lint 47 | 48 | - name: Test 49 | run: | 50 | npm test 51 | 52 | - name: Type Definitions 53 | run: | 54 | npm run test:typescript 55 | 56 | automerge: 57 | if: > 58 | github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' 59 | needs: test 60 | runs-on: ubuntu-latest 61 | permissions: 62 | actions: write 63 | pull-requests: write 64 | contents: write 65 | steps: 66 | - uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 67 | with: 68 | github-token: ${{ secrets.GITHUB_TOKEN }} 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # mac files 40 | .DS_Store 41 | 42 | # vim swap files 43 | *.swp 44 | 45 | package-lock.json 46 | 47 | .tap 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Tomas Della Vedova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/bench-thread.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { workerData: benchmark, parentPort } = require('worker_threads') 4 | 5 | const Benchmark = require('benchmark') 6 | // The default number of samples for Benchmark seems to be low enough that it 7 | // can generate results with significant variance (~2%) for this benchmark 8 | // suite. This makes it sometimes a bit confusing to actually evaluate impact of 9 | // changes on performance. Setting the minimum of samples to 500 results in 10 | // significantly lower variance on my local setup for this tests suite, and 11 | // gives me higher confidence in benchmark results. 12 | Benchmark.options.minSamples = 500 13 | 14 | const suite = Benchmark.Suite() 15 | 16 | const FindMyWay = require('..') 17 | const findMyWay = new FindMyWay() 18 | 19 | for (const { method, url, opts } of benchmark.setupURLs) { 20 | if (opts !== undefined) { 21 | findMyWay.on(method, url, opts, () => true) 22 | } else { 23 | findMyWay.on(method, url, () => true) 24 | } 25 | } 26 | 27 | suite 28 | .add(benchmark.name, () => { 29 | findMyWay.lookup(...benchmark.arguments) 30 | }) 31 | .on('cycle', (event) => { 32 | parentPort.postMessage(String(event.target)) 33 | }) 34 | .on('complete', () => {}) 35 | .run() 36 | -------------------------------------------------------------------------------- /benchmark/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const { Worker } = require('worker_threads') 5 | 6 | const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js') 7 | 8 | const benchmarks = [ 9 | { 10 | name: 'lookup root "/" route', 11 | setupURLs: [{ method: 'GET', url: '/' }], 12 | arguments: [{ method: 'GET', url: '/' }] 13 | }, 14 | { 15 | name: 'lookup short static route', 16 | setupURLs: [{ method: 'GET', url: '/static' }], 17 | arguments: [{ method: 'GET', url: '/static' }] 18 | }, 19 | { 20 | name: 'lookup long static route', 21 | setupURLs: [{ method: 'GET', url: '/static/static/static/static/static' }], 22 | arguments: [{ method: 'GET', url: '/static/static/static/static/static' }] 23 | }, 24 | { 25 | name: 'lookup long static route (common prefix)', 26 | setupURLs: [ 27 | { method: 'GET', url: '/static' }, 28 | { method: 'GET', url: '/static/static' }, 29 | { method: 'GET', url: '/static/static/static' }, 30 | { method: 'GET', url: '/static/static/static/static' }, 31 | { method: 'GET', url: '/static/static/static/static/static' } 32 | ], 33 | arguments: [{ method: 'GET', url: '/static/static/static/static/static' }] 34 | }, 35 | { 36 | name: 'lookup short parametric route', 37 | setupURLs: [{ method: 'GET', url: '/:param' }], 38 | arguments: [{ method: 'GET', url: '/param1' }] 39 | }, 40 | { 41 | name: 'lookup long parametric route', 42 | setupURLs: [{ method: 'GET', url: '/:param' }], 43 | arguments: [{ method: 'GET', url: '/longParamParamParamParamParamParam' }] 44 | }, 45 | { 46 | name: 'lookup short parametric route (encoded unoptimized)', 47 | setupURLs: [{ method: 'GET', url: '/:param' }], 48 | arguments: [{ method: 'GET', url: '/param%2B' }] 49 | }, 50 | { 51 | name: 'lookup short parametric route (encoded optimized)', 52 | setupURLs: [{ method: 'GET', url: '/:param' }], 53 | arguments: [{ method: 'GET', url: '/param%20' }] 54 | }, 55 | { 56 | name: 'lookup parametric route with two short params', 57 | setupURLs: [{ method: 'GET', url: '/:param1/:param2' }], 58 | arguments: [{ method: 'GET', url: '/param1/param2' }] 59 | }, 60 | { 61 | name: 'lookup multi-parametric route with two short params', 62 | setupURLs: [{ method: 'GET', url: '/:param1-:param2' }], 63 | arguments: [{ method: 'GET', url: '/param1-param2' }] 64 | }, 65 | { 66 | name: 'lookup multi-parametric route with two short regex params', 67 | setupURLs: [{ method: 'GET', url: '/:param1([a-z]*)1:param2([a-z]*)2' }], 68 | arguments: [{ method: 'GET', url: '/param1param2' }] 69 | }, 70 | { 71 | name: 'lookup long static + parametric route', 72 | setupURLs: [{ method: 'GET', url: '/static/:param1/static/:param2/static' }], 73 | arguments: [{ method: 'GET', url: '/static/param1/static/param2/static' }] 74 | }, 75 | { 76 | name: 'lookup short wildcard route', 77 | setupURLs: [{ method: 'GET', url: '/*' }], 78 | arguments: [{ method: 'GET', url: '/static' }] 79 | }, 80 | { 81 | name: 'lookup long wildcard route', 82 | setupURLs: [{ method: 'GET', url: '/*' }], 83 | arguments: [{ method: 'GET', url: '/static/static/static/static/static' }] 84 | }, 85 | { 86 | name: 'lookup root route on constrained router', 87 | setupURLs: [ 88 | { method: 'GET', url: '/' }, 89 | { method: 'GET', url: '/static', opts: { constraints: { version: '1.2.0' } } }, 90 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'example.com' } } }, 91 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'fastify.io' } } } 92 | ], 93 | arguments: [{ method: 'GET', url: '/', headers: { host: 'fastify.io' } }] 94 | }, 95 | { 96 | name: 'lookup short static unconstraint route', 97 | setupURLs: [ 98 | { method: 'GET', url: '/static', opts: {} }, 99 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'example.com' } } }, 100 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'fastify.io' } } } 101 | ], 102 | arguments: [{ method: 'GET', url: '/static', headers: {} }] 103 | }, 104 | { 105 | name: 'lookup short static versioned route', 106 | setupURLs: [ 107 | { method: 'GET', url: '/static', opts: { constraints: { version: '1.2.0' } } }, 108 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'example.com' } } }, 109 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'fastify.io' } } } 110 | ], 111 | arguments: [{ method: 'GET', url: '/static', headers: { 'accept-version': '1.x', host: 'fastify.io' } }] 112 | }, 113 | { 114 | name: 'lookup short static constrained (version & host) route', 115 | setupURLs: [ 116 | { method: 'GET', url: '/static', opts: { constraints: { version: '1.2.0' } } }, 117 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'example.com' } } }, 118 | { method: 'GET', url: '/static', opts: { constraints: { version: '2.0.0', host: 'fastify.io' } } } 119 | ], 120 | arguments: [{ method: 'GET', url: '/static', headers: { 'accept-version': '2.x', host: 'fastify.io' } }] 121 | } 122 | ] 123 | 124 | async function runBenchmark (benchmark) { 125 | const worker = new Worker(BENCH_THREAD_PATH, { workerData: benchmark }) 126 | 127 | return new Promise((resolve, reject) => { 128 | let result = null 129 | worker.on('error', reject) 130 | worker.on('message', (benchResult) => { 131 | result = benchResult 132 | }) 133 | worker.on('exit', (code) => { 134 | if (code === 0) { 135 | resolve(result) 136 | } else { 137 | reject(new Error(`Worker stopped with exit code ${code}`)) 138 | } 139 | }) 140 | }) 141 | } 142 | 143 | async function runBenchmarks () { 144 | let maxNameLength = 0 145 | for (const benchmark of benchmarks) { 146 | maxNameLength = Math.max(benchmark.name.length, maxNameLength) 147 | } 148 | 149 | for (const benchmark of benchmarks) { 150 | benchmark.name = benchmark.name.padEnd(maxNameLength, '.') 151 | const resultMessage = await runBenchmark(benchmark) 152 | console.log(resultMessage) 153 | } 154 | } 155 | 156 | runBenchmarks() 157 | -------------------------------------------------------------------------------- /benchmark/compare-branches.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { spawn } = require('child_process') 4 | 5 | const chalk = require('chalk') 6 | const inquirer = require('inquirer') 7 | const simpleGit = require('simple-git') 8 | 9 | const git = simpleGit(process.cwd()) 10 | 11 | const COMMAND = 'npm run bench' 12 | const DEFAULT_BRANCH = 'main' 13 | const PERCENT_THRESHOLD = 5 14 | 15 | async function selectBranchName (message, branches) { 16 | const result = await inquirer.prompt([{ 17 | type: 'list', 18 | name: 'branch', 19 | choices: branches, 20 | loop: false, 21 | pageSize: 20, 22 | message 23 | }]) 24 | return result.branch 25 | } 26 | 27 | async function executeCommandOnBranch (command, branch) { 28 | console.log(chalk.grey(`Checking out "${branch}"`)) 29 | await git.checkout(branch) 30 | 31 | console.log(chalk.grey(`Execute "${command}"`)) 32 | const childProcess = spawn(command, { stdio: 'pipe', shell: true }) 33 | 34 | let result = '' 35 | childProcess.stdout.on('data', (data) => { 36 | process.stdout.write(data.toString()) 37 | result += data.toString() 38 | }) 39 | 40 | await new Promise(resolve => childProcess.on('close', resolve)) 41 | 42 | console.log() 43 | 44 | return parseBenchmarksStdout(result) 45 | } 46 | 47 | function parseBenchmarksStdout (text) { 48 | const results = [] 49 | 50 | for (const line of text.split('\n')) { 51 | const match = /^(.+?)(\.*) x (.+) ops\/sec .*$/.exec(line) 52 | if (match !== null) { 53 | results.push({ 54 | name: match[1], 55 | alignedName: match[1] + match[2], 56 | result: parseInt(match[3].replaceAll(',', '')) 57 | }) 58 | } 59 | } 60 | 61 | return results 62 | } 63 | 64 | function compareResults (featureBranch, mainBranch) { 65 | for (const { name, alignedName, result: mainBranchResult } of mainBranch) { 66 | const featureBranchBenchmark = featureBranch.find(result => result.name === name) 67 | if (featureBranchBenchmark) { 68 | const featureBranchResult = featureBranchBenchmark.result 69 | const percent = (featureBranchResult - mainBranchResult) * 100 / mainBranchResult 70 | const roundedPercent = Math.round(percent * 100) / 100 71 | 72 | const percentString = roundedPercent > 0 ? `+${roundedPercent}%` : `${roundedPercent}%` 73 | const message = alignedName + percentString.padStart(7, '.') 74 | 75 | if (roundedPercent > PERCENT_THRESHOLD) { 76 | console.log(chalk.green(message)) 77 | } else if (roundedPercent < -PERCENT_THRESHOLD) { 78 | console.log(chalk.red(message)) 79 | } else { 80 | console.log(message) 81 | } 82 | } 83 | } 84 | } 85 | 86 | (async function () { 87 | const branches = await git.branch() 88 | const currentBranch = branches.branches[branches.current] 89 | 90 | let featureBranch = null 91 | let mainBranch = null 92 | 93 | if (process.argv[2] === '--ci') { 94 | featureBranch = currentBranch.name 95 | mainBranch = DEFAULT_BRANCH 96 | } else { 97 | featureBranch = await selectBranchName('Select the branch you want to compare (feature branch):', branches.all) 98 | mainBranch = await selectBranchName('Select the branch you want to compare with (main branch):', branches.all) 99 | } 100 | 101 | try { 102 | const featureBranchResult = await executeCommandOnBranch(COMMAND, featureBranch) 103 | const mainBranchResult = await executeCommandOnBranch(COMMAND, mainBranch) 104 | compareResults(featureBranchResult, mainBranchResult) 105 | } catch (error) { 106 | console.error('Switch to origin branch due to an error', error.message) 107 | } 108 | 109 | await git.checkout(currentBranch.commit) 110 | await git.checkout(currentBranch.name) 111 | 112 | console.log(chalk.gray(`Back to ${currentBranch.name} ${currentBranch.commit}`)) 113 | })() 114 | -------------------------------------------------------------------------------- /benchmark/uri-decoding.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastDecode = require('fast-decode-uri-component') 4 | 5 | const Benchmark = require('benchmark') 6 | Benchmark.options.minSamples = 500 7 | 8 | const suite = Benchmark.Suite() 9 | 10 | const uri = [ 11 | encodeURIComponent(' /?!#@=[](),\'"'), 12 | encodeURIComponent('algunas palabras aquí'), 13 | encodeURIComponent('acde=bdfd'), 14 | encodeURIComponent('много русских букв'), 15 | encodeURIComponent('這裡有些話'), 16 | encodeURIComponent('कुछ शब्द यहाँ'), 17 | encodeURIComponent('✌👀🎠🎡🍺') 18 | ] 19 | 20 | function safeFastDecode (uri) { 21 | if (uri.indexOf('%') < 0) return uri 22 | try { 23 | return fastDecode(uri) 24 | } catch (e) { 25 | return null // or it can be null 26 | } 27 | } 28 | 29 | function safeDecodeURIComponent (uri) { 30 | if (uri.indexOf('%') < 0) return uri 31 | try { 32 | return decodeURIComponent(uri) 33 | } catch (e) { 34 | return null // or it can be null 35 | } 36 | } 37 | 38 | uri.forEach(function (u, i) { 39 | suite.add(`safeDecodeURIComponent(${i}) [${u}]`, function () { 40 | safeDecodeURIComponent(u) 41 | }) 42 | suite.add(`fastDecode(${i}) [${u}]`, function () { 43 | fastDecode(u) 44 | }) 45 | suite.add(`safeFastDecode(${i}) [${u}]`, function () { 46 | safeFastDecode(u) 47 | }) 48 | }) 49 | suite 50 | .on('cycle', function (event) { 51 | console.log(String(event.target)) 52 | }) 53 | .on('complete', function () { 54 | }) 55 | .run() 56 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http') 4 | const router = require('./')({ 5 | defaultRoute: (req, res) => { 6 | res.end('not found') 7 | } 8 | }) 9 | 10 | router.on('GET', '/test', (req, res, params) => { 11 | res.end('{"hello":"world"}') 12 | }) 13 | 14 | router.on('GET', '/:test', (req, res, params) => { 15 | res.end(JSON.stringify(params)) 16 | }) 17 | 18 | router.on('GET', '/text/hello', (req, res, params) => { 19 | res.end('{"winter":"is here"}') 20 | }) 21 | 22 | const server = http.createServer((req, res) => { 23 | router.lookup(req, res) 24 | }) 25 | 26 | server.listen(3000, err => { 27 | if (err) throw err 28 | console.log('Server listening on: http://localhost:3000') 29 | }) 30 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http'; 2 | import { Http2ServerRequest, Http2ServerResponse } from 'http2'; 3 | 4 | declare function Router( 5 | config?: Router.Config 6 | ): Router.Instance; 7 | 8 | declare namespace Router { 9 | enum HTTPVersion { 10 | V1 = 'http1', 11 | V2 = 'http2' 12 | } 13 | 14 | type HTTPMethod = 15 | | 'ACL' 16 | | 'BIND' 17 | | 'CHECKOUT' 18 | | 'CONNECT' 19 | | 'COPY' 20 | | 'DELETE' 21 | | 'GET' 22 | | 'HEAD' 23 | | 'LINK' 24 | | 'LOCK' 25 | | 'M-SEARCH' 26 | | 'MERGE' 27 | | 'MKACTIVITY' 28 | | 'MKCALENDAR' 29 | | 'MKCOL' 30 | | 'MOVE' 31 | | 'NOTIFY' 32 | | 'OPTIONS' 33 | | 'PATCH' 34 | | 'POST' 35 | | 'PROPFIND' 36 | | 'PROPPATCH' 37 | | 'PURGE' 38 | | 'PUT' 39 | | 'REBIND' 40 | | 'REPORT' 41 | | 'SEARCH' 42 | | 'SOURCE' 43 | | 'SUBSCRIBE' 44 | | 'TRACE' 45 | | 'UNBIND' 46 | | 'UNLINK' 47 | | 'UNLOCK' 48 | | 'UNSUBSCRIBE'; 49 | 50 | type Req = V extends HTTPVersion.V1 ? IncomingMessage : Http2ServerRequest; 51 | type Res = V extends HTTPVersion.V1 ? ServerResponse : Http2ServerResponse; 52 | 53 | type Handler = ( 54 | req: Req, 55 | res: Res, 56 | params: { [k: string]: string | undefined }, 57 | store: any, 58 | searchParams: { [k: string]: string } 59 | ) => any; 60 | 61 | type Done = (err: Error | null, result: any) => void; 62 | 63 | interface ConstraintStrategy { 64 | name: string, 65 | mustMatchWhenDerived?: boolean, 66 | storage() : { 67 | get(value: T) : Handler | null, 68 | set(value: T, handler: Handler) : void, 69 | del?(value: T) : void, 70 | empty?() : void 71 | }, 72 | validate?(value: unknown): void, 73 | deriveConstraint(req: Req, ctx?: Context) : T, 74 | } 75 | 76 | type QuerystringParser = (s: string) => unknown; 77 | 78 | interface Config { 79 | ignoreTrailingSlash?: boolean; 80 | 81 | ignoreDuplicateSlashes?: boolean; 82 | 83 | allowUnsafeRegex?: boolean; 84 | 85 | caseSensitive?: boolean; 86 | 87 | maxParamLength?: number; 88 | 89 | querystringParser?: QuerystringParser; 90 | 91 | defaultRoute?( 92 | req: Req, 93 | res: Res 94 | ): void; 95 | 96 | onBadUrl?( 97 | path: string, 98 | req: Req, 99 | res: Res 100 | ): void; 101 | 102 | constraints? : { 103 | [key: string]: ConstraintStrategy 104 | } 105 | } 106 | 107 | interface RouteOptions { 108 | constraints?: { [key: string]: any } 109 | } 110 | 111 | interface ShortHandRoute { 112 | (path: string, handler: Handler): void; 113 | (path: string, opts: RouteOptions, handler: Handler): void; 114 | (path: string, handler: Handler, store: any): void; 115 | (path: string, opts: RouteOptions, handler: Handler, store: any): void; 116 | } 117 | 118 | interface FindResult { 119 | handler: Handler; 120 | params: { [k: string]: string | undefined }; 121 | store: any; 122 | searchParams: { [k: string]: string }; 123 | } 124 | 125 | interface FindRouteResult { 126 | handler: Handler; 127 | store: any; 128 | params: string[]; 129 | } 130 | 131 | interface Instance { 132 | on( 133 | method: HTTPMethod | HTTPMethod[], 134 | path: string, 135 | handler: Handler 136 | ): void; 137 | on( 138 | method: HTTPMethod | HTTPMethod[], 139 | path: string, 140 | options: RouteOptions, 141 | handler: Handler 142 | ): void; 143 | on( 144 | method: HTTPMethod | HTTPMethod[], 145 | path: string, 146 | handler: Handler, 147 | store: any 148 | ): void; 149 | on( 150 | method: HTTPMethod | HTTPMethod[], 151 | path: string, 152 | options: RouteOptions, 153 | handler: Handler, 154 | store: any 155 | ): void; 156 | off( 157 | method: HTTPMethod | HTTPMethod[], 158 | path: string, 159 | constraints?: { [key: string]: any } 160 | ): void; 161 | 162 | lookup( 163 | req: Req, 164 | res: Res, 165 | ctx?: Context | Done, 166 | done?: Done 167 | ): any; 168 | 169 | find( 170 | method: HTTPMethod, 171 | path: string, 172 | constraints?: { [key: string]: any } 173 | ): FindResult | null; 174 | 175 | findRoute( 176 | method: HTTPMethod, 177 | path: string, 178 | constraints?: { [key: string]: any } 179 | ): FindRouteResult | null; 180 | 181 | hasRoute( 182 | method: HTTPMethod, 183 | path: string, 184 | constraints?: { [key: string]: any } 185 | ): boolean; 186 | 187 | reset(): void; 188 | prettyPrint(): string; 189 | prettyPrint(opts: { 190 | method?: HTTPMethod, 191 | commonPrefix?: boolean, 192 | includeMeta?: boolean | (string | symbol)[] 193 | }): string; 194 | 195 | hasConstraintStrategy(strategyName: string): boolean; 196 | addConstraintStrategy(constraintStrategy: ConstraintStrategy): void; 197 | 198 | all: ShortHandRoute; 199 | 200 | acl: ShortHandRoute; 201 | bind: ShortHandRoute; 202 | checkout: ShortHandRoute; 203 | connect: ShortHandRoute; 204 | copy: ShortHandRoute; 205 | delete: ShortHandRoute; 206 | get: ShortHandRoute; 207 | head: ShortHandRoute; 208 | link: ShortHandRoute; 209 | lock: ShortHandRoute; 210 | 'm-search': ShortHandRoute; 211 | merge: ShortHandRoute; 212 | mkactivity: ShortHandRoute; 213 | mkcalendar: ShortHandRoute; 214 | mkcol: ShortHandRoute; 215 | move: ShortHandRoute; 216 | notify: ShortHandRoute; 217 | options: ShortHandRoute; 218 | patch: ShortHandRoute; 219 | post: ShortHandRoute; 220 | propfind: ShortHandRoute; 221 | proppatch: ShortHandRoute; 222 | purge: ShortHandRoute; 223 | put: ShortHandRoute; 224 | rebind: ShortHandRoute; 225 | report: ShortHandRoute; 226 | search: ShortHandRoute; 227 | source: ShortHandRoute; 228 | subscribe: ShortHandRoute; 229 | trace: ShortHandRoute; 230 | unbind: ShortHandRoute; 231 | unlink: ShortHandRoute; 232 | unlock: ShortHandRoute; 233 | unsubscribe: ShortHandRoute; 234 | } 235 | } 236 | 237 | export = Router; 238 | -------------------------------------------------------------------------------- /lib/constrainer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const acceptVersionStrategy = require('./strategies/accept-version') 4 | const acceptHostStrategy = require('./strategies/accept-host') 5 | const assert = require('node:assert') 6 | 7 | class Constrainer { 8 | constructor (customStrategies) { 9 | this.strategies = { 10 | version: acceptVersionStrategy, 11 | host: acceptHostStrategy 12 | } 13 | 14 | this.strategiesInUse = new Set() 15 | this.asyncStrategiesInUse = new Set() 16 | 17 | // validate and optimize prototypes of given custom strategies 18 | if (customStrategies) { 19 | for (const strategy of Object.values(customStrategies)) { 20 | this.addConstraintStrategy(strategy) 21 | } 22 | } 23 | } 24 | 25 | isStrategyUsed (strategyName) { 26 | return this.strategiesInUse.has(strategyName) || 27 | this.asyncStrategiesInUse.has(strategyName) 28 | } 29 | 30 | hasConstraintStrategy (strategyName) { 31 | const customConstraintStrategy = this.strategies[strategyName] 32 | if (customConstraintStrategy !== undefined) { 33 | return customConstraintStrategy.isCustom || 34 | this.isStrategyUsed(strategyName) 35 | } 36 | return false 37 | } 38 | 39 | addConstraintStrategy (strategy) { 40 | assert(typeof strategy.name === 'string' && strategy.name !== '', 'strategy.name is required.') 41 | assert(strategy.storage && typeof strategy.storage === 'function', 'strategy.storage function is required.') 42 | assert(strategy.deriveConstraint && typeof strategy.deriveConstraint === 'function', 'strategy.deriveConstraint function is required.') 43 | 44 | if (this.strategies[strategy.name] && this.strategies[strategy.name].isCustom) { 45 | throw new Error(`There already exists a custom constraint with the name ${strategy.name}.`) 46 | } 47 | 48 | if (this.isStrategyUsed(strategy.name)) { 49 | throw new Error(`There already exists a route with ${strategy.name} constraint.`) 50 | } 51 | 52 | strategy.isCustom = true 53 | strategy.isAsync = strategy.deriveConstraint.length === 3 54 | this.strategies[strategy.name] = strategy 55 | 56 | if (strategy.mustMatchWhenDerived) { 57 | this.noteUsage({ [strategy.name]: strategy }) 58 | } 59 | } 60 | 61 | deriveConstraints (req, ctx, done) { 62 | const constraints = this.deriveSyncConstraints(req, ctx) 63 | 64 | if (done === undefined) { 65 | return constraints 66 | } 67 | 68 | this.deriveAsyncConstraints(constraints, req, ctx, done) 69 | } 70 | 71 | deriveSyncConstraints (req, ctx) { 72 | return undefined 73 | } 74 | 75 | // When new constraints start getting used, we need to rebuild the deriver to derive them. Do so if we see novel constraints used. 76 | noteUsage (constraints) { 77 | if (constraints) { 78 | const beforeSize = this.strategiesInUse.size 79 | for (const key in constraints) { 80 | const strategy = this.strategies[key] 81 | if (strategy.isAsync) { 82 | this.asyncStrategiesInUse.add(key) 83 | } else { 84 | this.strategiesInUse.add(key) 85 | } 86 | } 87 | if (beforeSize !== this.strategiesInUse.size) { 88 | this._buildDeriveConstraints() 89 | } 90 | } 91 | } 92 | 93 | newStoreForConstraint (constraint) { 94 | if (!this.strategies[constraint]) { 95 | throw new Error(`No strategy registered for constraint key ${constraint}`) 96 | } 97 | return this.strategies[constraint].storage() 98 | } 99 | 100 | validateConstraints (constraints) { 101 | for (const key in constraints) { 102 | const value = constraints[key] 103 | if (typeof value === 'undefined') { 104 | throw new Error('Can\'t pass an undefined constraint value, must pass null or no key at all') 105 | } 106 | const strategy = this.strategies[key] 107 | if (!strategy) { 108 | throw new Error(`No strategy registered for constraint key ${key}`) 109 | } 110 | if (strategy.validate) { 111 | strategy.validate(value) 112 | } 113 | } 114 | } 115 | 116 | deriveAsyncConstraints (constraints, req, ctx, done) { 117 | let asyncConstraintsCount = this.asyncStrategiesInUse.size 118 | 119 | if (asyncConstraintsCount === 0) { 120 | done(null, constraints) 121 | return 122 | } 123 | 124 | constraints = constraints || {} 125 | for (const key of this.asyncStrategiesInUse) { 126 | const strategy = this.strategies[key] 127 | strategy.deriveConstraint(req, ctx, (err, constraintValue) => { 128 | if (err !== null) { 129 | done(err) 130 | return 131 | } 132 | 133 | constraints[key] = constraintValue 134 | 135 | if (--asyncConstraintsCount === 0) { 136 | done(null, constraints) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | // Optimization: build a fast function for deriving the constraints for all the strategies at once. We inline the definitions of the version constraint and the host constraint for performance. 143 | // If no constraining strategies are in use (no routes constrain on host, or version, or any custom strategies) then we don't need to derive constraints for each route match, so don't do anything special, and just return undefined 144 | // This allows us to not allocate an object to hold constraint values if no constraints are defined. 145 | _buildDeriveConstraints () { 146 | if (this.strategiesInUse.size === 0) return 147 | 148 | const lines = ['return {'] 149 | 150 | for (const key of this.strategiesInUse) { 151 | const strategy = this.strategies[key] 152 | // Optimization: inline the derivation for the common built in constraints 153 | if (!strategy.isCustom) { 154 | if (key === 'version') { 155 | lines.push(' version: req.headers[\'accept-version\'],') 156 | } else { 157 | lines.push(' host: req.headers.host || req.headers[\':authority\'],') 158 | } 159 | } else { 160 | lines.push(` ${strategy.name}: this.strategies.${key}.deriveConstraint(req, ctx),`) 161 | } 162 | } 163 | 164 | lines.push('}') 165 | 166 | this.deriveSyncConstraints = new Function('req', 'ctx', lines.join('\n')).bind(this) // eslint-disable-line 167 | } 168 | } 169 | 170 | module.exports = Constrainer 171 | -------------------------------------------------------------------------------- /lib/http-methods.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // defined by Node.js http module, a snapshot from Node.js 22.9.0 4 | const httpMethods = [ 5 | 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 6 | 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 7 | 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 8 | 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'QUERY', 9 | 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 10 | 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' 11 | ] 12 | 13 | module.exports = httpMethods 14 | -------------------------------------------------------------------------------- /lib/node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const HandlerStorage = require('./handler-storage') 4 | 5 | const NODE_TYPES = { 6 | STATIC: 0, 7 | PARAMETRIC: 1, 8 | WILDCARD: 2 9 | } 10 | 11 | class Node { 12 | constructor () { 13 | this.isLeafNode = false 14 | this.routes = null 15 | this.handlerStorage = null 16 | } 17 | 18 | addRoute (route, constrainer) { 19 | if (this.routes === null) { 20 | this.routes = [] 21 | } 22 | if (this.handlerStorage === null) { 23 | this.handlerStorage = new HandlerStorage() 24 | } 25 | this.isLeafNode = true 26 | this.routes.push(route) 27 | this.handlerStorage.addHandler(constrainer, route) 28 | } 29 | } 30 | 31 | class ParentNode extends Node { 32 | constructor () { 33 | super() 34 | this.staticChildren = {} 35 | } 36 | 37 | findStaticMatchingChild (path, pathIndex) { 38 | const staticChild = this.staticChildren[path.charAt(pathIndex)] 39 | if (staticChild === undefined || !staticChild.matchPrefix(path, pathIndex)) { 40 | return null 41 | } 42 | return staticChild 43 | } 44 | 45 | getStaticChild (path, pathIndex = 0) { 46 | if (path.length === pathIndex) { 47 | return this 48 | } 49 | 50 | const staticChild = this.findStaticMatchingChild(path, pathIndex) 51 | if (staticChild) { 52 | return staticChild.getStaticChild(path, pathIndex + staticChild.prefix.length) 53 | } 54 | 55 | return null 56 | } 57 | 58 | createStaticChild (path) { 59 | if (path.length === 0) { 60 | return this 61 | } 62 | 63 | let staticChild = this.staticChildren[path.charAt(0)] 64 | if (staticChild) { 65 | let i = 1 66 | for (; i < staticChild.prefix.length; i++) { 67 | if (path.charCodeAt(i) !== staticChild.prefix.charCodeAt(i)) { 68 | staticChild = staticChild.split(this, i) 69 | break 70 | } 71 | } 72 | return staticChild.createStaticChild(path.slice(i)) 73 | } 74 | 75 | const label = path.charAt(0) 76 | this.staticChildren[label] = new StaticNode(path) 77 | return this.staticChildren[label] 78 | } 79 | } 80 | 81 | class StaticNode extends ParentNode { 82 | constructor (prefix) { 83 | super() 84 | this.prefix = prefix 85 | this.wildcardChild = null 86 | this.parametricChildren = [] 87 | this.kind = NODE_TYPES.STATIC 88 | this._compilePrefixMatch() 89 | } 90 | 91 | getParametricChild (regex) { 92 | const regexpSource = regex && regex.source 93 | 94 | const parametricChild = this.parametricChildren.find(child => { 95 | const childRegexSource = child.regex && child.regex.source 96 | return childRegexSource === regexpSource 97 | }) 98 | 99 | if (parametricChild) { 100 | return parametricChild 101 | } 102 | 103 | return null 104 | } 105 | 106 | createParametricChild (regex, staticSuffix, nodePath) { 107 | let parametricChild = this.getParametricChild(regex) 108 | if (parametricChild) { 109 | parametricChild.nodePaths.add(nodePath) 110 | return parametricChild 111 | } 112 | 113 | parametricChild = new ParametricNode(regex, staticSuffix, nodePath) 114 | this.parametricChildren.push(parametricChild) 115 | this.parametricChildren.sort((child1, child2) => { 116 | if (!child1.isRegex) return 1 117 | if (!child2.isRegex) return -1 118 | 119 | if (child1.staticSuffix === null) return 1 120 | if (child2.staticSuffix === null) return -1 121 | 122 | if (child2.staticSuffix.endsWith(child1.staticSuffix)) return 1 123 | if (child1.staticSuffix.endsWith(child2.staticSuffix)) return -1 124 | 125 | return 0 126 | }) 127 | 128 | return parametricChild 129 | } 130 | 131 | getWildcardChild () { 132 | return this.wildcardChild 133 | } 134 | 135 | createWildcardChild () { 136 | this.wildcardChild = this.getWildcardChild() || new WildcardNode() 137 | return this.wildcardChild 138 | } 139 | 140 | split (parentNode, length) { 141 | const parentPrefix = this.prefix.slice(0, length) 142 | const childPrefix = this.prefix.slice(length) 143 | 144 | this.prefix = childPrefix 145 | this._compilePrefixMatch() 146 | 147 | const staticNode = new StaticNode(parentPrefix) 148 | staticNode.staticChildren[childPrefix.charAt(0)] = this 149 | parentNode.staticChildren[parentPrefix.charAt(0)] = staticNode 150 | 151 | return staticNode 152 | } 153 | 154 | getNextNode (path, pathIndex, nodeStack, paramsCount) { 155 | let node = this.findStaticMatchingChild(path, pathIndex) 156 | let parametricBrotherNodeIndex = 0 157 | 158 | if (node === null) { 159 | if (this.parametricChildren.length === 0) { 160 | return this.wildcardChild 161 | } 162 | 163 | node = this.parametricChildren[0] 164 | parametricBrotherNodeIndex = 1 165 | } 166 | 167 | if (this.wildcardChild !== null) { 168 | nodeStack.push({ 169 | paramsCount, 170 | brotherPathIndex: pathIndex, 171 | brotherNode: this.wildcardChild 172 | }) 173 | } 174 | 175 | for (let i = this.parametricChildren.length - 1; i >= parametricBrotherNodeIndex; i--) { 176 | nodeStack.push({ 177 | paramsCount, 178 | brotherPathIndex: pathIndex, 179 | brotherNode: this.parametricChildren[i] 180 | }) 181 | } 182 | 183 | return node 184 | } 185 | 186 | _compilePrefixMatch () { 187 | if (this.prefix.length === 1) { 188 | this.matchPrefix = () => true 189 | return 190 | } 191 | 192 | const lines = [] 193 | for (let i = 1; i < this.prefix.length; i++) { 194 | const charCode = this.prefix.charCodeAt(i) 195 | lines.push(`path.charCodeAt(i + ${i}) === ${charCode}`) 196 | } 197 | this.matchPrefix = new Function('path', 'i', `return ${lines.join(' && ')}`) // eslint-disable-line 198 | } 199 | } 200 | 201 | class ParametricNode extends ParentNode { 202 | constructor (regex, staticSuffix, nodePath) { 203 | super() 204 | this.isRegex = !!regex 205 | this.regex = regex || null 206 | this.staticSuffix = staticSuffix || null 207 | this.kind = NODE_TYPES.PARAMETRIC 208 | 209 | this.nodePaths = new Set([nodePath]) 210 | } 211 | 212 | getNextNode (path, pathIndex) { 213 | return this.findStaticMatchingChild(path, pathIndex) 214 | } 215 | } 216 | 217 | class WildcardNode extends Node { 218 | constructor () { 219 | super() 220 | this.kind = NODE_TYPES.WILDCARD 221 | } 222 | 223 | getNextNode () { 224 | return null 225 | } 226 | } 227 | 228 | module.exports = { StaticNode, ParametricNode, WildcardNode, NODE_TYPES } 229 | -------------------------------------------------------------------------------- /lib/null-object.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const NullObject = function () {} 4 | NullObject.prototype = Object.create(null) 5 | 6 | module.exports = { 7 | NullObject 8 | } 9 | -------------------------------------------------------------------------------- /lib/pretty-print.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const deepEqual = require('fast-deep-equal') 4 | 5 | const httpMethodStrategy = require('./strategies/http-method') 6 | const treeDataSymbol = Symbol('treeData') 7 | 8 | function printObjectTree (obj, parentPrefix = '') { 9 | let tree = '' 10 | const keys = Object.keys(obj) 11 | for (let i = 0; i < keys.length; i++) { 12 | const key = keys[i] 13 | const value = obj[key] 14 | const isLast = i === keys.length - 1 15 | 16 | const nodePrefix = isLast ? '└── ' : '├── ' 17 | const childPrefix = isLast ? ' ' : '│ ' 18 | 19 | const nodeData = value[treeDataSymbol] || '' 20 | const prefixedNodeData = nodeData.replaceAll('\n', '\n' + parentPrefix + childPrefix) 21 | 22 | tree += parentPrefix + nodePrefix + key + prefixedNodeData + '\n' 23 | tree += printObjectTree(value, parentPrefix + childPrefix) 24 | } 25 | return tree 26 | } 27 | 28 | function parseFunctionName (fn) { 29 | let fName = fn.name || '' 30 | 31 | fName = fName.replace('bound', '').trim() 32 | fName = (fName || 'anonymous') + '()' 33 | return fName 34 | } 35 | 36 | function parseMeta (meta) { 37 | if (Array.isArray(meta)) return meta.map(m => parseMeta(m)) 38 | if (typeof meta === 'symbol') return meta.toString() 39 | if (typeof meta === 'function') return parseFunctionName(meta) 40 | return meta 41 | } 42 | 43 | function getRouteMetaData (route, options) { 44 | if (!options.includeMeta) return {} 45 | 46 | const metaDataObject = options.buildPrettyMeta(route) 47 | const filteredMetaData = {} 48 | 49 | let includeMetaKeys = options.includeMeta 50 | if (!Array.isArray(includeMetaKeys)) { 51 | includeMetaKeys = Reflect.ownKeys(metaDataObject) 52 | } 53 | 54 | for (const metaKey of includeMetaKeys) { 55 | if (!Object.prototype.hasOwnProperty.call(metaDataObject, metaKey)) continue 56 | 57 | const serializedKey = metaKey.toString() 58 | const metaValue = metaDataObject[metaKey] 59 | 60 | if (metaValue !== undefined && metaValue !== null) { 61 | const serializedValue = JSON.stringify(parseMeta(metaValue)) 62 | filteredMetaData[serializedKey] = serializedValue 63 | } 64 | } 65 | 66 | return filteredMetaData 67 | } 68 | 69 | function serializeMetaData (metaData) { 70 | let serializedMetaData = '' 71 | for (const [key, value] of Object.entries(metaData)) { 72 | serializedMetaData += `\n• (${key}) ${value}` 73 | } 74 | return serializedMetaData 75 | } 76 | 77 | // get original merged tree node route 78 | function normalizeRoute (route) { 79 | const constraints = { ...route.opts.constraints } 80 | const method = constraints[httpMethodStrategy.name] 81 | delete constraints[httpMethodStrategy.name] 82 | return { ...route, method, opts: { constraints } } 83 | } 84 | 85 | function serializeRoute (route) { 86 | let serializedRoute = ` (${route.method})` 87 | 88 | const constraints = route.opts.constraints || {} 89 | if (Object.keys(constraints).length !== 0) { 90 | serializedRoute += ' ' + JSON.stringify(constraints) 91 | } 92 | 93 | serializedRoute += serializeMetaData(route.metaData) 94 | return serializedRoute 95 | } 96 | 97 | function mergeSimilarRoutes (routes) { 98 | return routes.reduce((mergedRoutes, route) => { 99 | for (const nodeRoute of mergedRoutes) { 100 | if ( 101 | deepEqual(route.opts.constraints, nodeRoute.opts.constraints) && 102 | deepEqual(route.metaData, nodeRoute.metaData) 103 | ) { 104 | nodeRoute.method += ', ' + route.method 105 | return mergedRoutes 106 | } 107 | } 108 | mergedRoutes.push(route) 109 | return mergedRoutes 110 | }, []) 111 | } 112 | 113 | function serializeNode (node, prefix, options) { 114 | let routes = node.routes 115 | 116 | if (options.method === undefined) { 117 | routes = routes.map(normalizeRoute) 118 | } 119 | 120 | routes = routes.map(route => { 121 | route.metaData = getRouteMetaData(route, options) 122 | return route 123 | }) 124 | 125 | if (options.method === undefined) { 126 | routes = mergeSimilarRoutes(routes) 127 | } 128 | 129 | return routes.map(serializeRoute).join(`\n${prefix}`) 130 | } 131 | 132 | function buildObjectTree (node, tree, prefix, options) { 133 | if (node.isLeafNode || options.commonPrefix !== false) { 134 | prefix = prefix || '(empty root node)' 135 | tree = tree[prefix] = {} 136 | 137 | if (node.isLeafNode) { 138 | tree[treeDataSymbol] = serializeNode(node, prefix, options) 139 | } 140 | 141 | prefix = '' 142 | } 143 | 144 | if (node.staticChildren) { 145 | for (const child of Object.values(node.staticChildren)) { 146 | buildObjectTree(child, tree, prefix + child.prefix, options) 147 | } 148 | } 149 | 150 | if (node.parametricChildren) { 151 | for (const child of Object.values(node.parametricChildren)) { 152 | const childPrefix = Array.from(child.nodePaths).join('|') 153 | buildObjectTree(child, tree, prefix + childPrefix, options) 154 | } 155 | } 156 | 157 | if (node.wildcardChild) { 158 | buildObjectTree(node.wildcardChild, tree, '*', options) 159 | } 160 | } 161 | 162 | function prettyPrintTree (root, options) { 163 | const objectTree = {} 164 | buildObjectTree(root, objectTree, root.prefix, options) 165 | return printObjectTree(objectTree) 166 | } 167 | 168 | module.exports = { prettyPrintTree } 169 | -------------------------------------------------------------------------------- /lib/strategies/accept-host.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const assert = require('node:assert') 3 | 4 | function HostStorage () { 5 | const hosts = new Map() 6 | const regexHosts = [] 7 | return { 8 | get: (host) => { 9 | const exact = hosts.get(host) 10 | if (exact) { 11 | return exact 12 | } 13 | for (const regex of regexHosts) { 14 | if (regex.host.test(host)) { 15 | return regex.value 16 | } 17 | } 18 | }, 19 | set: (host, value) => { 20 | if (host instanceof RegExp) { 21 | regexHosts.push({ host, value }) 22 | } else { 23 | hosts.set(host, value) 24 | } 25 | } 26 | } 27 | } 28 | 29 | module.exports = { 30 | name: 'host', 31 | mustMatchWhenDerived: false, 32 | storage: HostStorage, 33 | validate (value) { 34 | assert(typeof value === 'string' || Object.prototype.toString.call(value) === '[object RegExp]', 'Host should be a string or a RegExp') 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/strategies/accept-version.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('node:assert') 4 | 5 | function SemVerStore () { 6 | if (!(this instanceof SemVerStore)) { 7 | return new SemVerStore() 8 | } 9 | 10 | this.store = new Map() 11 | this.maxMajor = 0 12 | this.maxMinors = {} 13 | this.maxPatches = {} 14 | } 15 | 16 | SemVerStore.prototype.set = function (version, store) { 17 | if (typeof version !== 'string') { 18 | throw new TypeError('Version should be a string') 19 | } 20 | let [major, minor, patch] = version.split('.', 3) 21 | 22 | if (isNaN(major)) { 23 | throw new TypeError('Major version must be a numeric value') 24 | } 25 | 26 | major = Number(major) 27 | minor = Number(minor) || 0 28 | patch = Number(patch) || 0 29 | 30 | if (major >= this.maxMajor) { 31 | this.maxMajor = major 32 | this.store.set('x', store) 33 | this.store.set('*', store) 34 | this.store.set('x.x', store) 35 | this.store.set('x.x.x', store) 36 | } 37 | 38 | if (minor >= (this.maxMinors[major] || 0)) { 39 | this.maxMinors[major] = minor 40 | this.store.set(`${major}.x`, store) 41 | this.store.set(`${major}.x.x`, store) 42 | } 43 | 44 | if (patch >= (this.maxPatches[`${major}.${minor}`] || 0)) { 45 | this.maxPatches[`${major}.${minor}`] = patch 46 | this.store.set(`${major}.${minor}.x`, store) 47 | } 48 | 49 | this.store.set(`${major}.${minor}.${patch}`, store) 50 | return this 51 | } 52 | 53 | SemVerStore.prototype.get = function (version) { 54 | return this.store.get(version) 55 | } 56 | 57 | module.exports = { 58 | name: 'version', 59 | mustMatchWhenDerived: true, 60 | storage: SemVerStore, 61 | validate (value) { 62 | assert(typeof value === 'string', 'Version should be a string') 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/strategies/http-method.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | name: '__fmw_internal_strategy_merged_tree_http_method__', 5 | storage: function () { 6 | const handlers = new Map() 7 | return { 8 | get: (type) => { return handlers.get(type) || null }, 9 | set: (type, store) => { handlers.set(type, store) } 10 | } 11 | }, 12 | /* c8 ignore next 1 */ 13 | deriveConstraint: (req) => req.method, 14 | mustMatchWhenDerived: true 15 | } 16 | -------------------------------------------------------------------------------- /lib/url-sanitizer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // It must spot all the chars where decodeURIComponent(x) !== decodeURI(x) 4 | // The chars are: # $ & + , / : ; = ? @ 5 | function decodeComponentChar (highCharCode, lowCharCode) { 6 | if (highCharCode === 50) { 7 | if (lowCharCode === 53) return '%' 8 | 9 | if (lowCharCode === 51) return '#' 10 | if (lowCharCode === 52) return '$' 11 | if (lowCharCode === 54) return '&' 12 | if (lowCharCode === 66) return '+' 13 | if (lowCharCode === 98) return '+' 14 | if (lowCharCode === 67) return ',' 15 | if (lowCharCode === 99) return ',' 16 | if (lowCharCode === 70) return '/' 17 | if (lowCharCode === 102) return '/' 18 | return null 19 | } 20 | if (highCharCode === 51) { 21 | if (lowCharCode === 65) return ':' 22 | if (lowCharCode === 97) return ':' 23 | if (lowCharCode === 66) return ';' 24 | if (lowCharCode === 98) return ';' 25 | if (lowCharCode === 68) return '=' 26 | if (lowCharCode === 100) return '=' 27 | if (lowCharCode === 70) return '?' 28 | if (lowCharCode === 102) return '?' 29 | return null 30 | } 31 | if (highCharCode === 52 && lowCharCode === 48) { 32 | return '@' 33 | } 34 | return null 35 | } 36 | 37 | function safeDecodeURI (path, useSemicolonDelimiter) { 38 | let shouldDecode = false 39 | let shouldDecodeParam = false 40 | 41 | let querystring = '' 42 | 43 | for (let i = 1; i < path.length; i++) { 44 | const charCode = path.charCodeAt(i) 45 | 46 | if (charCode === 37) { 47 | const highCharCode = path.charCodeAt(i + 1) 48 | const lowCharCode = path.charCodeAt(i + 2) 49 | 50 | if (decodeComponentChar(highCharCode, lowCharCode) === null) { 51 | shouldDecode = true 52 | } else { 53 | shouldDecodeParam = true 54 | // %25 - encoded % char. We need to encode one more time to prevent double decoding 55 | if (highCharCode === 50 && lowCharCode === 53) { 56 | shouldDecode = true 57 | path = path.slice(0, i + 1) + '25' + path.slice(i + 1) 58 | i += 2 59 | } 60 | i += 2 61 | } 62 | // Some systems do not follow RFC and separate the path and query 63 | // string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`. 64 | // Thus, we need to split on `;` as well as `?` and `#` if the useSemicolonDelimiter option is enabled. 65 | } else if (charCode === 63 || charCode === 35 || (charCode === 59 && useSemicolonDelimiter)) { 66 | querystring = path.slice(i + 1) 67 | path = path.slice(0, i) 68 | break 69 | } 70 | } 71 | const decodedPath = shouldDecode ? decodeURI(path) : path 72 | return { path: decodedPath, querystring, shouldDecodeParam } 73 | } 74 | 75 | function safeDecodeURIComponent (uriComponent) { 76 | const startIndex = uriComponent.indexOf('%') 77 | if (startIndex === -1) return uriComponent 78 | 79 | let decoded = '' 80 | let lastIndex = startIndex 81 | 82 | for (let i = startIndex; i < uriComponent.length; i++) { 83 | if (uriComponent.charCodeAt(i) === 37) { 84 | const highCharCode = uriComponent.charCodeAt(i + 1) 85 | const lowCharCode = uriComponent.charCodeAt(i + 2) 86 | 87 | const decodedChar = decodeComponentChar(highCharCode, lowCharCode) 88 | decoded += uriComponent.slice(lastIndex, i) + decodedChar 89 | 90 | lastIndex = i + 3 91 | } 92 | } 93 | return uriComponent.slice(0, startIndex) + decoded + uriComponent.slice(lastIndex) 94 | } 95 | 96 | module.exports = { safeDecodeURI, safeDecodeURIComponent } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "find-my-way", 3 | "version": "9.3.0", 4 | "description": "Crazy fast http radix based router", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "bench": "node ./benchmark/bench.js", 10 | "bench:cmp": "node ./benchmark/compare-branches.js", 11 | "bench:cmp:ci": "node ./benchmark/compare-branches.js --ci", 12 | "test:lint": "standard", 13 | "test:typescript": "tsd", 14 | "test": "standard && borp && npm run test:typescript" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/delvedor/find-my-way.git" 19 | }, 20 | "keywords": [ 21 | "http", 22 | "router", 23 | "radix", 24 | "fast", 25 | "speed" 26 | ], 27 | "engines": { 28 | "node": ">=20" 29 | }, 30 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/delvedor/find-my-way/issues" 34 | }, 35 | "homepage": "https://github.com/delvedor/find-my-way#readme", 36 | "devDependencies": { 37 | "@types/node": "^22.10.2", 38 | "benchmark": "^2.1.4", 39 | "borp": "^0.20.0", 40 | "chalk": "^5.4.1", 41 | "inquirer": "^12.3.0", 42 | "pre-commit": "^1.2.2", 43 | "proxyquire": "^2.1.3", 44 | "rfdc": "^1.3.0", 45 | "simple-git": "^3.7.1", 46 | "standard": "^17.0.0", 47 | "tsd": "^0.32.0" 48 | }, 49 | "dependencies": { 50 | "fast-deep-equal": "^3.1.3", 51 | "fast-querystring": "^1.0.0", 52 | "safe-regex2": "^5.0.0" 53 | }, 54 | "tsd": { 55 | "directory": "test/types" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/case-insensitive.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('case insensitive static routes of level 1', t => { 7 | t.plan(1) 8 | 9 | const findMyWay = FindMyWay({ 10 | caseSensitive: false, 11 | defaultRoute: (req, res) => { 12 | t.assert.fail('Should not be defaultRoute') 13 | } 14 | }) 15 | 16 | findMyWay.on('GET', '/woo', (req, res, params) => { 17 | t.assert.ok('we should be here') 18 | }) 19 | 20 | findMyWay.lookup({ method: 'GET', url: '/WOO', headers: {} }, null) 21 | }) 22 | 23 | test('case insensitive static routes of level 2', t => { 24 | t.plan(1) 25 | 26 | const findMyWay = FindMyWay({ 27 | caseSensitive: false, 28 | defaultRoute: (req, res) => { 29 | t.assert.fail('Should not be defaultRoute') 30 | } 31 | }) 32 | 33 | findMyWay.on('GET', '/foo/woo', (req, res, params) => { 34 | t.assert.ok('we should be here') 35 | }) 36 | 37 | findMyWay.lookup({ method: 'GET', url: '/FoO/WOO', headers: {} }, null) 38 | }) 39 | 40 | test('case insensitive static routes of level 3', t => { 41 | t.plan(1) 42 | 43 | const findMyWay = FindMyWay({ 44 | caseSensitive: false, 45 | defaultRoute: (req, res) => { 46 | t.assert.fail('Should not be defaultRoute') 47 | } 48 | }) 49 | 50 | findMyWay.on('GET', '/foo/bar/woo', (req, res, params) => { 51 | t.assert.ok('we should be here') 52 | }) 53 | 54 | findMyWay.lookup({ method: 'GET', url: '/Foo/bAR/WoO', headers: {} }, null) 55 | }) 56 | 57 | test('parametric case insensitive', t => { 58 | t.plan(1) 59 | 60 | const findMyWay = FindMyWay({ 61 | caseSensitive: false, 62 | defaultRoute: (req, res) => { 63 | t.assert.fail('Should not be defaultRoute') 64 | } 65 | }) 66 | 67 | findMyWay.on('GET', '/foo/:param', (req, res, params) => { 68 | t.assert.equal(params.param, 'bAR') 69 | }) 70 | 71 | findMyWay.lookup({ method: 'GET', url: '/Foo/bAR', headers: {} }, null) 72 | }) 73 | 74 | test('parametric case insensitive with a static part', t => { 75 | t.plan(1) 76 | 77 | const findMyWay = FindMyWay({ 78 | caseSensitive: false, 79 | defaultRoute: (req, res) => { 80 | t.assert.fail('Should not be defaultRoute') 81 | } 82 | }) 83 | 84 | findMyWay.on('GET', '/foo/my-:param', (req, res, params) => { 85 | t.assert.equal(params.param, 'bAR') 86 | }) 87 | 88 | findMyWay.lookup({ method: 'GET', url: '/Foo/MY-bAR', headers: {} }, null) 89 | }) 90 | 91 | test('parametric case insensitive with capital letter', t => { 92 | t.plan(1) 93 | 94 | const findMyWay = FindMyWay({ 95 | caseSensitive: false, 96 | defaultRoute: (req, res) => { 97 | t.assert.fail('Should not be defaultRoute') 98 | } 99 | }) 100 | 101 | findMyWay.on('GET', '/foo/:Param', (req, res, params) => { 102 | t.assert.equal(params.Param, 'bAR') 103 | }) 104 | 105 | findMyWay.lookup({ method: 'GET', url: '/Foo/bAR', headers: {} }, null) 106 | }) 107 | 108 | test('case insensitive with capital letter in static path with param', t => { 109 | t.plan(1) 110 | 111 | const findMyWay = FindMyWay({ 112 | caseSensitive: false, 113 | defaultRoute: (req, res) => { 114 | t.assert.fail('Should not be defaultRoute') 115 | } 116 | }) 117 | 118 | findMyWay.on('GET', '/Foo/bar/:param', (req, res, params) => { 119 | t.assert.equal(params.param, 'baZ') 120 | }) 121 | 122 | findMyWay.lookup({ method: 'GET', url: '/foo/bar/baZ', headers: {} }, null) 123 | }) 124 | 125 | test('case insensitive with multiple paths containing capital letter in static path with param', t => { 126 | /* 127 | * This is a reproduction of the issue documented at 128 | * https://github.com/delvedor/find-my-way/issues/96. 129 | */ 130 | t.plan(2) 131 | 132 | const findMyWay = FindMyWay({ 133 | caseSensitive: false, 134 | defaultRoute: (req, res) => { 135 | t.assert.fail('Should not be defaultRoute') 136 | } 137 | }) 138 | 139 | findMyWay.on('GET', '/Foo/bar/:param', (req, res, params) => { 140 | t.assert.equal(params.param, 'baZ') 141 | }) 142 | 143 | findMyWay.on('GET', '/Foo/baz/:param', (req, res, params) => { 144 | t.assert.equal(params.param, 'baR') 145 | }) 146 | 147 | findMyWay.lookup({ method: 'GET', url: '/foo/bar/baZ', headers: {} }, null) 148 | findMyWay.lookup({ method: 'GET', url: '/foo/baz/baR', headers: {} }, null) 149 | }) 150 | 151 | test('case insensitive with multiple mixed-case params within same slash couple', t => { 152 | t.plan(2) 153 | 154 | const findMyWay = FindMyWay({ 155 | caseSensitive: false, 156 | defaultRoute: (req, res) => { 157 | t.assert.fail('Should not be defaultRoute') 158 | } 159 | }) 160 | 161 | findMyWay.on('GET', '/foo/:param1-:param2', (req, res, params) => { 162 | t.assert.equal(params.param1, 'My') 163 | t.assert.equal(params.param2, 'bAR') 164 | }) 165 | 166 | findMyWay.lookup({ method: 'GET', url: '/FOO/My-bAR', headers: {} }, null) 167 | }) 168 | 169 | test('case insensitive with multiple mixed-case params', t => { 170 | t.plan(2) 171 | 172 | const findMyWay = FindMyWay({ 173 | caseSensitive: false, 174 | defaultRoute: (req, res) => { 175 | t.assert.fail('Should not be defaultRoute') 176 | } 177 | }) 178 | 179 | findMyWay.on('GET', '/foo/:param1/:param2', (req, res, params) => { 180 | t.assert.equal(params.param1, 'My') 181 | t.assert.equal(params.param2, 'bAR') 182 | }) 183 | 184 | findMyWay.lookup({ method: 'GET', url: '/FOO/My/bAR', headers: {} }, null) 185 | }) 186 | 187 | test('case insensitive with wildcard', t => { 188 | t.plan(1) 189 | 190 | const findMyWay = FindMyWay({ 191 | caseSensitive: false, 192 | defaultRoute: (req, res) => { 193 | t.assert.fail('Should not be defaultRoute') 194 | } 195 | }) 196 | 197 | findMyWay.on('GET', '/foo/*', (req, res, params) => { 198 | t.assert.equal(params['*'], 'baR') 199 | }) 200 | 201 | findMyWay.lookup({ method: 'GET', url: '/FOO/baR', headers: {} }, null) 202 | }) 203 | 204 | test('parametric case insensitive with multiple routes', t => { 205 | t.plan(6) 206 | 207 | const findMyWay = FindMyWay({ 208 | caseSensitive: false, 209 | defaultRoute: (req, res) => { 210 | t.assert.fail('Should not be defaultRoute') 211 | } 212 | }) 213 | 214 | findMyWay.on('POST', '/foo/:param/Static/:userId/Save', (req, res, params) => { 215 | t.assert.equal(params.param, 'bAR') 216 | t.assert.equal(params.userId, 'one') 217 | }) 218 | findMyWay.on('POST', '/foo/:param/Static/:userId/Update', (req, res, params) => { 219 | t.assert.equal(params.param, 'Bar') 220 | t.assert.equal(params.userId, 'two') 221 | }) 222 | findMyWay.on('POST', '/foo/:param/Static/:userId/CANCEL', (req, res, params) => { 223 | t.assert.equal(params.param, 'bAR') 224 | t.assert.equal(params.userId, 'THREE') 225 | }) 226 | 227 | findMyWay.lookup({ method: 'POST', url: '/foo/bAR/static/one/SAVE', headers: {} }, null) 228 | findMyWay.lookup({ method: 'POST', url: '/fOO/Bar/Static/two/update', headers: {} }, null) 229 | findMyWay.lookup({ method: 'POST', url: '/Foo/bAR/STATIC/THREE/cAnCeL', headers: {} }, null) 230 | }) 231 | -------------------------------------------------------------------------------- /test/constraint.custom-versioning.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | const noop = () => { } 6 | 7 | const customVersioning = { 8 | name: 'version', 9 | // storage factory 10 | storage: function () { 11 | let versions = {} 12 | return { 13 | get: (version) => { return versions[version] || null }, 14 | set: (version, store) => { versions[version] = store }, 15 | del: (version) => { delete versions[version] }, 16 | empty: () => { versions = {} } 17 | } 18 | }, 19 | deriveConstraint: (req, ctx) => { 20 | return req.headers.accept 21 | } 22 | } 23 | 24 | test('A route could support multiple versions (find) / 1', t => { 25 | t.plan(5) 26 | 27 | const findMyWay = FindMyWay({ constraints: { version: customVersioning } }) 28 | 29 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, noop) 30 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, noop) 31 | 32 | t.assert.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=2' })) 33 | t.assert.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=3' })) 34 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=4' })) 35 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=5' })) 36 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=6' })) 37 | }) 38 | 39 | test('A route could support multiple versions (find) / 1 (add strategy outside constructor)', t => { 40 | t.plan(5) 41 | 42 | const findMyWay = FindMyWay() 43 | 44 | findMyWay.addConstraintStrategy(customVersioning) 45 | 46 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, noop) 47 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, noop) 48 | 49 | t.assert.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=2' })) 50 | t.assert.ok(findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=3' })) 51 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=4' })) 52 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=5' })) 53 | t.assert.ok(!findMyWay.find('GET', '/', { version: 'application/vnd.example.api+json;version=6' })) 54 | }) 55 | 56 | test('Overriding default strategies uses the custom deriveConstraint function', t => { 57 | t.plan(2) 58 | 59 | const findMyWay = FindMyWay({ constraints: { version: customVersioning } }) 60 | 61 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, (req, res, params) => { 62 | t.assert.equal(req.headers.accept, 'application/vnd.example.api+json;version=2') 63 | }) 64 | 65 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, (req, res, params) => { 66 | t.assert.equal(req.headers.accept, 'application/vnd.example.api+json;version=3') 67 | }) 68 | 69 | findMyWay.lookup({ 70 | method: 'GET', 71 | url: '/', 72 | headers: { accept: 'application/vnd.example.api+json;version=2' } 73 | }) 74 | findMyWay.lookup({ 75 | method: 'GET', 76 | url: '/', 77 | headers: { accept: 'application/vnd.example.api+json;version=3' } 78 | }) 79 | }) 80 | 81 | test('Overriding default strategies uses the custom deriveConstraint function (add strategy outside constructor)', t => { 82 | t.plan(2) 83 | 84 | const findMyWay = FindMyWay() 85 | 86 | findMyWay.addConstraintStrategy(customVersioning) 87 | 88 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=2' } }, (req, res, params) => { 89 | t.assert.equal(req.headers.accept, 'application/vnd.example.api+json;version=2') 90 | }) 91 | 92 | findMyWay.on('GET', '/', { constraints: { version: 'application/vnd.example.api+json;version=3' } }, (req, res, params) => { 93 | t.assert.equal(req.headers.accept, 'application/vnd.example.api+json;version=3') 94 | }) 95 | 96 | findMyWay.lookup({ 97 | method: 'GET', 98 | url: '/', 99 | headers: { accept: 'application/vnd.example.api+json;version=2' } 100 | }) 101 | findMyWay.lookup({ 102 | method: 'GET', 103 | url: '/', 104 | headers: { accept: 'application/vnd.example.api+json;version=3' } 105 | }) 106 | }) 107 | 108 | test('Overriding custom strategies throws as error (add strategy outside constructor)', t => { 109 | t.plan(1) 110 | 111 | const findMyWay = FindMyWay() 112 | 113 | findMyWay.addConstraintStrategy(customVersioning) 114 | 115 | t.assert.throws(() => findMyWay.addConstraintStrategy(customVersioning), 116 | new Error('There already exists a custom constraint with the name version.') 117 | ) 118 | }) 119 | 120 | test('Overriding default strategies after defining a route with constraint', t => { 121 | t.plan(1) 122 | 123 | const findMyWay = FindMyWay() 124 | 125 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, () => {}) 126 | 127 | t.assert.throws(() => findMyWay.addConstraintStrategy(customVersioning), 128 | new Error('There already exists a route with version constraint.') 129 | ) 130 | }) 131 | -------------------------------------------------------------------------------- /test/constraint.custom.async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | const rfdc = require('rfdc')({ proto: true }) 6 | 7 | const customHeaderConstraint = { 8 | name: 'requestedBy', 9 | storage: function () { 10 | const requestedBys = {} 11 | return { 12 | get: (requestedBy) => { return requestedBys[requestedBy] || null }, 13 | set: (requestedBy, store) => { requestedBys[requestedBy] = store } 14 | } 15 | }, 16 | deriveConstraint: (req, ctx, done) => { 17 | if (req.headers['user-agent'] === 'wrong') { 18 | done(new Error('wrong user-agent')) 19 | return 20 | } 21 | 22 | done(null, req.headers['user-agent']) 23 | } 24 | } 25 | 26 | test('should derive multiple async constraints', t => { 27 | t.plan(2) 28 | 29 | const customHeaderConstraint2 = rfdc(customHeaderConstraint) 30 | customHeaderConstraint2.name = 'requestedBy2' 31 | 32 | const router = FindMyWay({ constraints: { requestedBy: customHeaderConstraint, requestedBy2: customHeaderConstraint2 } }) 33 | router.on('GET', '/', { constraints: { requestedBy: 'node', requestedBy2: 'node' } }, () => 'asyncHandler') 34 | 35 | router.lookup( 36 | { 37 | method: 'GET', 38 | url: '/', 39 | headers: { 40 | 'user-agent': 'node' 41 | } 42 | }, 43 | null, 44 | (err, result) => { 45 | t.assert.equal(err, null) 46 | t.assert.equal(result, 'asyncHandler') 47 | } 48 | ) 49 | }) 50 | 51 | test('lookup should return an error from deriveConstraint', t => { 52 | t.plan(2) 53 | 54 | const router = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) 55 | router.on('GET', '/', { constraints: { requestedBy: 'node' } }, () => 'asyncHandler') 56 | 57 | router.lookup( 58 | { 59 | method: 'GET', 60 | url: '/', 61 | headers: { 62 | 'user-agent': 'wrong' 63 | } 64 | }, 65 | null, 66 | (err, result) => { 67 | t.assert.deepStrictEqual(err, new Error('wrong user-agent')) 68 | t.assert.equal(result, undefined) 69 | } 70 | ) 71 | }) 72 | 73 | test('should derive sync and async constraints', t => { 74 | t.plan(4) 75 | 76 | const router = FindMyWay({ constraints: { requestedBy: customHeaderConstraint } }) 77 | router.on('GET', '/', { constraints: { version: '1.0.0', requestedBy: 'node' } }, () => 'asyncHandlerV1') 78 | router.on('GET', '/', { constraints: { version: '2.0.0', requestedBy: 'node' } }, () => 'asyncHandlerV2') 79 | 80 | router.lookup( 81 | { 82 | method: 'GET', 83 | url: '/', 84 | headers: { 85 | 'user-agent': 'node', 86 | 'accept-version': '1.0.0' 87 | } 88 | }, 89 | null, 90 | (err, result) => { 91 | t.assert.equal(err, null) 92 | t.assert.equal(result, 'asyncHandlerV1') 93 | } 94 | ) 95 | 96 | router.lookup( 97 | { 98 | method: 'GET', 99 | url: '/', 100 | headers: { 101 | 'user-agent': 'node', 102 | 'accept-version': '2.0.0' 103 | } 104 | }, 105 | null, 106 | (err, result) => { 107 | t.assert.equal(err, null) 108 | t.assert.equal(result, 'asyncHandlerV2') 109 | } 110 | ) 111 | }) 112 | -------------------------------------------------------------------------------- /test/constraint.host.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | const alpha = () => { } 6 | const beta = () => { } 7 | const gamma = () => { } 8 | 9 | test('A route supports multiple host constraints', t => { 10 | t.plan(4) 11 | 12 | const findMyWay = FindMyWay() 13 | 14 | findMyWay.on('GET', '/', {}, alpha) 15 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) 16 | findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, gamma) 17 | 18 | t.assert.equal(findMyWay.find('GET', '/', {}).handler, alpha) 19 | t.assert.equal(findMyWay.find('GET', '/', { host: 'something-else.io' }).handler, alpha) 20 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) 21 | t.assert.equal(findMyWay.find('GET', '/', { host: 'example.com' }).handler, gamma) 22 | }) 23 | 24 | test('A route supports wildcard host constraints', t => { 25 | t.plan(4) 26 | 27 | const findMyWay = FindMyWay() 28 | 29 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) 30 | findMyWay.on('GET', '/', { constraints: { host: /.*\.fastify\.io/ } }, gamma) 31 | 32 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) 33 | t.assert.equal(findMyWay.find('GET', '/', { host: 'foo.fastify.io' }).handler, gamma) 34 | t.assert.equal(findMyWay.find('GET', '/', { host: 'bar.fastify.io' }).handler, gamma) 35 | t.assert.ok(!findMyWay.find('GET', '/', { host: 'example.com' })) 36 | }) 37 | 38 | test('A route supports multiple host constraints (lookup)', t => { 39 | t.plan(4) 40 | 41 | const findMyWay = FindMyWay() 42 | 43 | findMyWay.on('GET', '/', {}, (req, res) => {}) 44 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, (req, res) => { 45 | t.assert.equal(req.headers.host, 'fastify.io') 46 | }) 47 | findMyWay.on('GET', '/', { constraints: { host: 'example.com' } }, (req, res) => { 48 | t.assert.equal(req.headers.host, 'example.com') 49 | }) 50 | findMyWay.on('GET', '/', { constraints: { host: /.+\.fancy\.ca/ } }, (req, res) => { 51 | t.assert.ok(req.headers.host.endsWith('.fancy.ca')) 52 | }) 53 | 54 | findMyWay.lookup({ 55 | method: 'GET', 56 | url: '/', 57 | headers: { host: 'fastify.io' } 58 | }) 59 | 60 | findMyWay.lookup({ 61 | method: 'GET', 62 | url: '/', 63 | headers: { host: 'example.com' } 64 | }) 65 | findMyWay.lookup({ 66 | method: 'GET', 67 | url: '/', 68 | headers: { host: 'foo.fancy.ca' } 69 | }) 70 | findMyWay.lookup({ 71 | method: 'GET', 72 | url: '/', 73 | headers: { host: 'bar.fancy.ca' } 74 | }) 75 | }) 76 | 77 | test('A route supports up to 31 host constraints', (t) => { 78 | t.plan(1) 79 | 80 | const findMyWay = FindMyWay() 81 | 82 | for (let i = 0; i < 31; i++) { 83 | const host = `h${i.toString().padStart(2, '0')}` 84 | findMyWay.on('GET', '/', { constraints: { host } }, alpha) 85 | } 86 | 87 | t.assert.equal(findMyWay.find('GET', '/', { host: 'h01' }).handler, alpha) 88 | }) 89 | 90 | test('A route throws when constraint limit exceeded', (t) => { 91 | t.plan(1) 92 | 93 | const findMyWay = FindMyWay() 94 | 95 | for (let i = 0; i < 31; i++) { 96 | const host = `h${i.toString().padStart(2, '0')}` 97 | findMyWay.on('GET', '/', { constraints: { host } }, alpha) 98 | } 99 | 100 | t.assert.throws( 101 | () => findMyWay.on('GET', '/', { constraints: { host: 'h31' } }, beta), 102 | new Error('find-my-way supports a maximum of 31 route handlers per node when there are constraints, limit reached') 103 | ) 104 | }) 105 | -------------------------------------------------------------------------------- /test/constraints.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | const alpha = () => { } 6 | const beta = () => { } 7 | const gamma = () => { } 8 | 9 | test('A route could support multiple host constraints while versioned', t => { 10 | t.plan(6) 11 | 12 | const findMyWay = FindMyWay() 13 | 14 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.1.0' } }, beta) 15 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '2.1.0' } }, gamma) 16 | 17 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.x' }).handler, beta) 18 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.1.x' }).handler, beta) 19 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.x' }).handler, gamma) 20 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.1.x' }).handler, gamma) 21 | t.assert.ok(!findMyWay.find('GET', '/', { host: 'fastify.io', version: '3.x' })) 22 | t.assert.ok(!findMyWay.find('GET', '/', { host: 'something-else.io', version: '1.x' })) 23 | }) 24 | 25 | test('Constrained routes are matched before unconstrainted routes when the constrained route is added last', t => { 26 | t.plan(3) 27 | 28 | const findMyWay = FindMyWay() 29 | 30 | findMyWay.on('GET', '/', {}, alpha) 31 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) 32 | 33 | t.assert.equal(findMyWay.find('GET', '/', {}).handler, alpha) 34 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) 35 | t.assert.equal(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha) 36 | }) 37 | 38 | test('Constrained routes are matched before unconstrainted routes when the constrained route is added first', t => { 39 | t.plan(3) 40 | 41 | const findMyWay = FindMyWay() 42 | 43 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, beta) 44 | findMyWay.on('GET', '/', {}, alpha) 45 | 46 | t.assert.equal(findMyWay.find('GET', '/', {}).handler, alpha) 47 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, beta) 48 | t.assert.equal(findMyWay.find('GET', '/', { host: 'example.com' }).handler, alpha) 49 | }) 50 | 51 | test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added last', t => { 52 | t.plan(3) 53 | 54 | const findMyWay = FindMyWay() 55 | 56 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) 57 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) 58 | 59 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha) 60 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) 61 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }), null) 62 | }) 63 | 64 | test('Routes with multiple constraints are matched before routes with one constraint when the doubly-constrained route is added first', t => { 65 | t.plan(3) 66 | 67 | const findMyWay = FindMyWay() 68 | 69 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) 70 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) 71 | 72 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io' }).handler, alpha) 73 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) 74 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }), null) 75 | }) 76 | 77 | test('Routes with multiple constraints are matched before routes with one constraint before unconstrained routes', t => { 78 | t.plan(3) 79 | 80 | const findMyWay = FindMyWay() 81 | 82 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, beta) 83 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, alpha) 84 | findMyWay.on('GET', '/', { constraints: {} }, gamma) 85 | 86 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '1.0.0' }).handler, beta) 87 | t.assert.equal(findMyWay.find('GET', '/', { host: 'fastify.io', version: '2.0.0' }), null) 88 | t.assert.equal(findMyWay.find('GET', '/', { host: 'example.io' }).handler, gamma) 89 | }) 90 | 91 | test('Has constraint strategy method test', t => { 92 | t.plan(6) 93 | 94 | const findMyWay = FindMyWay() 95 | 96 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('version'), false) 97 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('host'), false) 98 | 99 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, () => {}) 100 | 101 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('version'), false) 102 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('host'), true) 103 | 104 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io', version: '1.0.0' } }, () => {}) 105 | 106 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('version'), true) 107 | t.assert.deepEqual(findMyWay.hasConstraintStrategy('host'), true) 108 | }) 109 | -------------------------------------------------------------------------------- /test/custom-querystring-parser.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const querystring = require('fast-querystring') 5 | const FindMyWay = require('../') 6 | 7 | test('Custom querystring parser', t => { 8 | t.plan(2) 9 | 10 | const findMyWay = FindMyWay({ 11 | querystringParser: function (str) { 12 | t.assert.equal(str, 'foo=bar&baz=faz') 13 | return querystring.parse(str) 14 | } 15 | }) 16 | findMyWay.on('GET', '/', () => {}) 17 | 18 | t.assert.deepEqual(findMyWay.find('GET', '/?foo=bar&baz=faz').searchParams, { foo: 'bar', baz: 'faz' }) 19 | }) 20 | 21 | test('Custom querystring parser should be called also if there is nothing to parse', t => { 22 | t.plan(2) 23 | 24 | const findMyWay = FindMyWay({ 25 | querystringParser: function (str) { 26 | t.assert.equal(str, '') 27 | return querystring.parse(str) 28 | } 29 | }) 30 | findMyWay.on('GET', '/', () => {}) 31 | 32 | t.assert.deepEqual(findMyWay.find('GET', '/').searchParams, {}) 33 | }) 34 | 35 | test('Querystring without value', t => { 36 | t.plan(2) 37 | 38 | const findMyWay = FindMyWay({ 39 | querystringParser: function (str) { 40 | t.assert.equal(str, 'foo') 41 | return querystring.parse(str) 42 | } 43 | }) 44 | findMyWay.on('GET', '/', () => {}) 45 | t.assert.deepEqual(findMyWay.find('GET', '/?foo').searchParams, { foo: '' }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/fastify-issue-3129.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('contain param and wildcard together', t => { 7 | t.plan(4) 8 | 9 | const findMyWay = FindMyWay({ 10 | defaultRoute: (req, res) => { 11 | t.assert.fail('we should not be here, the url is: ' + req.url) 12 | } 13 | }) 14 | 15 | findMyWay.on('GET', '/:lang/item/:id', (req, res, params) => { 16 | t.assert.deepEqual(params.lang, 'fr') 17 | t.assert.deepEqual(params.id, '12345') 18 | }) 19 | 20 | findMyWay.on('GET', '/:lang/item/*', (req, res, params) => { 21 | t.assert.deepEqual(params.lang, 'fr') 22 | t.assert.deepEqual(params['*'], '12345/edit') 23 | }) 24 | 25 | findMyWay.lookup( 26 | { method: 'GET', url: '/fr/item/12345', headers: {} }, 27 | null 28 | ) 29 | 30 | findMyWay.lookup( 31 | { method: 'GET', url: '/fr/item/12345/edit', headers: {} }, 32 | null 33 | ) 34 | }) 35 | -------------------------------------------------------------------------------- /test/fastify-issue-3957.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('wildcard should not limit by maxParamLength', t => { 7 | t.plan(1) 8 | 9 | const findMyWay = FindMyWay({ 10 | defaultRoute: (req, res) => { 11 | t.assert.fail('we should not be here, the url is: ' + req.url) 12 | } 13 | }) 14 | 15 | findMyWay.on('GET', '*', (req, res, params) => { 16 | t.assert.deepEqual(params['*'], '/portfolios/b5859fb9-6c76-4db8-b3d1-337c5be3fd8b/instruments/2a694406-b43f-439d-aa11-0c814805c930/positions') 17 | }) 18 | 19 | findMyWay.lookup( 20 | { method: 'GET', url: '/portfolios/b5859fb9-6c76-4db8-b3d1-337c5be3fd8b/instruments/2a694406-b43f-439d-aa11-0c814805c930/positions', headers: {} }, 21 | null 22 | ) 23 | }) 24 | -------------------------------------------------------------------------------- /test/find-route.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const rfdc = require('rfdc')({ proto: true }) 5 | const FindMyWay = require('..') 6 | 7 | function equalRouters (t, router1, router2) { 8 | t.assert.deepStrictEqual(router1._opts, router2._opts) 9 | t.assert.deepEqual(router1.routes, router2.routes) 10 | t.assert.deepEqual(JSON.stringify(router1.trees), JSON.stringify(router2.trees)) 11 | 12 | t.assert.deepStrictEqual(router1.constrainer.strategies, router2.constrainer.strategies) 13 | t.assert.deepStrictEqual( 14 | router1.constrainer.strategiesInUse, 15 | router2.constrainer.strategiesInUse 16 | ) 17 | t.assert.deepStrictEqual( 18 | router1.constrainer.asyncStrategiesInUse, 19 | router2.constrainer.asyncStrategiesInUse 20 | ) 21 | } 22 | 23 | test('findRoute returns null if there is no routes', (t) => { 24 | t.plan(7) 25 | 26 | const findMyWay = FindMyWay() 27 | const fundMyWayClone = rfdc(findMyWay) 28 | 29 | const route = findMyWay.findRoute('GET', '/example') 30 | t.assert.equal(route, null) 31 | 32 | equalRouters(t, findMyWay, fundMyWayClone) 33 | }) 34 | 35 | test('findRoute returns handler and store for a static route', (t) => { 36 | t.plan(9) 37 | 38 | const findMyWay = FindMyWay() 39 | 40 | const handler = () => {} 41 | const store = { hello: 'world' } 42 | findMyWay.on('GET', '/example', handler, store) 43 | 44 | const fundMyWayClone = rfdc(findMyWay) 45 | 46 | const route = findMyWay.findRoute('GET', '/example') 47 | t.assert.equal(route.handler, handler) 48 | t.assert.equal(route.store, store) 49 | t.assert.deepEqual(route.params, []) 50 | 51 | equalRouters(t, findMyWay, fundMyWayClone) 52 | }) 53 | 54 | test('findRoute returns null for a static route', (t) => { 55 | t.plan(7) 56 | 57 | const findMyWay = FindMyWay() 58 | 59 | const handler = () => {} 60 | findMyWay.on('GET', '/example', handler) 61 | 62 | const fundMyWayClone = rfdc(findMyWay) 63 | 64 | const route = findMyWay.findRoute('GET', '/example1') 65 | t.assert.equal(route, null) 66 | 67 | equalRouters(t, findMyWay, fundMyWayClone) 68 | }) 69 | 70 | test('findRoute returns handler and params for a parametric route', (t) => { 71 | t.plan(8) 72 | 73 | const findMyWay = FindMyWay() 74 | 75 | const handler = () => {} 76 | findMyWay.on('GET', '/:param', handler) 77 | 78 | const fundMyWayClone = rfdc(findMyWay) 79 | 80 | const route = findMyWay.findRoute('GET', '/:param') 81 | t.assert.equal(route.handler, handler) 82 | t.assert.deepEqual(route.params, ['param']) 83 | 84 | equalRouters(t, findMyWay, fundMyWayClone) 85 | }) 86 | 87 | test('findRoute returns null for a parametric route', (t) => { 88 | t.plan(7) 89 | 90 | const findMyWay = FindMyWay() 91 | 92 | const handler = () => {} 93 | findMyWay.on('GET', '/foo/:param', handler) 94 | 95 | const fundMyWayClone = rfdc(findMyWay) 96 | 97 | const route = findMyWay.findRoute('GET', '/bar/:param') 98 | t.assert.equal(route, null) 99 | 100 | equalRouters(t, findMyWay, fundMyWayClone) 101 | }) 102 | 103 | test('findRoute returns handler and params for a parametric route with static suffix', (t) => { 104 | t.plan(8) 105 | 106 | const findMyWay = FindMyWay() 107 | 108 | const handler = () => {} 109 | findMyWay.on('GET', '/:param-static', handler) 110 | 111 | const fundMyWayClone = rfdc(findMyWay) 112 | 113 | const route = findMyWay.findRoute('GET', '/:param-static') 114 | t.assert.equal(route.handler, handler) 115 | t.assert.deepEqual(route.params, ['param']) 116 | 117 | equalRouters(t, findMyWay, fundMyWayClone) 118 | }) 119 | 120 | test('findRoute returns null for a parametric route with static suffix', (t) => { 121 | t.plan(7) 122 | 123 | const findMyWay = FindMyWay() 124 | findMyWay.on('GET', '/:param-static1', () => {}) 125 | 126 | const fundMyWayClone = rfdc(findMyWay) 127 | 128 | const route = findMyWay.findRoute('GET', '/:param-static2') 129 | t.assert.equal(route, null) 130 | 131 | equalRouters(t, findMyWay, fundMyWayClone) 132 | }) 133 | 134 | test('findRoute returns handler and original params even if a param name different', (t) => { 135 | t.plan(8) 136 | 137 | const findMyWay = FindMyWay() 138 | 139 | const handler = () => {} 140 | findMyWay.on('GET', '/:param1', handler) 141 | 142 | const fundMyWayClone = rfdc(findMyWay) 143 | 144 | const route = findMyWay.findRoute('GET', '/:param2') 145 | t.assert.equal(route.handler, handler) 146 | t.assert.deepEqual(route.params, ['param1']) 147 | 148 | equalRouters(t, findMyWay, fundMyWayClone) 149 | }) 150 | 151 | test('findRoute returns handler and params for a multi-parametric route', (t) => { 152 | t.plan(8) 153 | 154 | const findMyWay = FindMyWay() 155 | 156 | const handler = () => {} 157 | findMyWay.on('GET', '/:param1-:param2', handler) 158 | 159 | const fundMyWayClone = rfdc(findMyWay) 160 | 161 | const route = findMyWay.findRoute('GET', '/:param1-:param2') 162 | t.assert.equal(route.handler, handler) 163 | t.assert.deepEqual(route.params, ['param1', 'param2']) 164 | 165 | equalRouters(t, findMyWay, fundMyWayClone) 166 | }) 167 | 168 | test('findRoute returns null for a multi-parametric route', (t) => { 169 | t.plan(7) 170 | 171 | const findMyWay = FindMyWay() 172 | findMyWay.on('GET', '/foo/:param1-:param2/bar1', () => {}) 173 | 174 | const fundMyWayClone = rfdc(findMyWay) 175 | 176 | const route = findMyWay.findRoute('GET', '/foo/:param1-:param2/bar2') 177 | t.assert.equal(route, null) 178 | 179 | equalRouters(t, findMyWay, fundMyWayClone) 180 | }) 181 | 182 | test('findRoute returns handler and regexp param for a regexp route', (t) => { 183 | t.plan(8) 184 | 185 | const findMyWay = FindMyWay() 186 | 187 | const handler = () => {} 188 | findMyWay.on('GET', '/:param(^\\d+$)', handler) 189 | 190 | const fundMyWayClone = rfdc(findMyWay) 191 | 192 | const route = findMyWay.findRoute('GET', '/:param(^\\d+$)') 193 | t.assert.equal(route.handler, handler) 194 | t.assert.deepEqual(route.params, ['param']) 195 | 196 | equalRouters(t, findMyWay, fundMyWayClone) 197 | }) 198 | 199 | test('findRoute returns null for a regexp route', (t) => { 200 | t.plan(7) 201 | 202 | const findMyWay = FindMyWay() 203 | findMyWay.on('GET', '/:file(^\\S+).png', () => {}) 204 | 205 | const fundMyWayClone = rfdc(findMyWay) 206 | 207 | const route = findMyWay.findRoute('GET', '/:file(^\\D+).png') 208 | t.assert.equal(route, null) 209 | 210 | equalRouters(t, findMyWay, fundMyWayClone) 211 | }) 212 | 213 | test('findRoute returns handler and wildcard param for a wildcard route', (t) => { 214 | t.plan(8) 215 | 216 | const findMyWay = FindMyWay() 217 | 218 | const handler = () => {} 219 | findMyWay.on('GET', '/example/*', handler) 220 | 221 | const fundMyWayClone = rfdc(findMyWay) 222 | 223 | const route = findMyWay.findRoute('GET', '/example/*') 224 | t.assert.equal(route.handler, handler) 225 | t.assert.deepEqual(route.params, ['*']) 226 | 227 | equalRouters(t, findMyWay, fundMyWayClone) 228 | }) 229 | 230 | test('findRoute returns null for a wildcard route', (t) => { 231 | t.plan(7) 232 | 233 | const findMyWay = FindMyWay() 234 | findMyWay.on('GET', '/foo1/*', () => {}) 235 | 236 | const fundMyWayClone = rfdc(findMyWay) 237 | 238 | const route = findMyWay.findRoute('GET', '/foo2/*') 239 | t.assert.equal(route, null) 240 | 241 | equalRouters(t, findMyWay, fundMyWayClone) 242 | }) 243 | 244 | test('findRoute returns handler for a constrained route', (t) => { 245 | t.plan(9) 246 | 247 | const findMyWay = FindMyWay() 248 | 249 | const handler = () => {} 250 | findMyWay.on( 251 | 'GET', 252 | '/example', 253 | { constraints: { version: '1.0.0' } }, 254 | handler 255 | ) 256 | 257 | const fundMyWayClone = rfdc(findMyWay) 258 | 259 | { 260 | const route = findMyWay.findRoute('GET', '/example') 261 | t.assert.equal(route, null) 262 | } 263 | 264 | { 265 | const route = findMyWay.findRoute('GET', '/example', { version: '1.0.0' }) 266 | t.assert.equal(route.handler, handler) 267 | } 268 | 269 | { 270 | const route = findMyWay.findRoute('GET', '/example', { version: '2.0.0' }) 271 | t.assert.equal(route, null) 272 | } 273 | 274 | equalRouters(t, findMyWay, fundMyWayClone) 275 | }) 276 | -------------------------------------------------------------------------------- /test/find.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('find calls can pass no constraints', t => { 7 | t.plan(3) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/a', () => {}) 11 | findMyWay.on('GET', '/a/b', () => {}) 12 | 13 | t.assert.ok(findMyWay.find('GET', '/a')) 14 | t.assert.ok(findMyWay.find('GET', '/a/b')) 15 | t.assert.ok(!findMyWay.find('GET', '/a/b/c')) 16 | }) 17 | -------------------------------------------------------------------------------- /test/for-in-loop.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint no-extend-native: off */ 4 | 5 | const { test } = require('node:test') 6 | 7 | // Something could extend the Array prototype 8 | Array.prototype.test = null 9 | test('for-in-loop', t => { 10 | t.assert.doesNotThrow(() => { 11 | require('../') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /test/full-url.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('full-url', t => { 7 | const findMyWay = FindMyWay({ 8 | defaultRoute: (req, res) => { 9 | t.assert.fail('Should not be defaultRoute') 10 | } 11 | }) 12 | 13 | findMyWay.on('GET', '/a', (req, res) => { 14 | res.end('{"message":"hello world"}') 15 | }) 16 | 17 | findMyWay.on('GET', '/a/:id', (req, res) => { 18 | res.end('{"message":"hello world"}') 19 | }) 20 | 21 | t.assert.deepEqual(findMyWay.find('GET', 'http://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) 22 | t.assert.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) 23 | t.assert.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a', {}), findMyWay.find('GET', '/a', {})) 24 | t.assert.deepEqual(findMyWay.find('GET', 'https://localhost/a', { host: 'localhost' }), findMyWay.find('GET', '/a', { host: 'localhost' })) 25 | 26 | t.assert.deepEqual(findMyWay.find('GET', 'http://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) 27 | t.assert.deepEqual(findMyWay.find('GET', 'http://localhost:8080/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) 28 | t.assert.deepEqual(findMyWay.find('GET', 'http://123.123.123.123/a/100', {}), findMyWay.find('GET', '/a/100', {})) 29 | t.assert.deepEqual(findMyWay.find('GET', 'https://localhost/a/100', { host: 'localhost' }), findMyWay.find('GET', '/a/100', { host: 'localhost' })) 30 | }) 31 | -------------------------------------------------------------------------------- /test/has-route.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const rfdc = require('rfdc')({ proto: true }) 5 | const FindMyWay = require('..') 6 | 7 | function equalRouters (t, router1, router2) { 8 | t.assert.deepStrictEqual(router1._opts, router2._opts) 9 | t.assert.deepEqual(router1.routes, router2.routes) 10 | t.assert.deepEqual(JSON.stringify(router1.trees), JSON.stringify(router2.trees)) 11 | 12 | t.assert.deepStrictEqual( 13 | router1.constrainer.strategies, 14 | router2.constrainer.strategies 15 | ) 16 | t.assert.deepStrictEqual( 17 | router1.constrainer.strategiesInUse, 18 | router2.constrainer.strategiesInUse 19 | ) 20 | t.assert.deepStrictEqual( 21 | router1.constrainer.asyncStrategiesInUse, 22 | router2.constrainer.asyncStrategiesInUse 23 | ) 24 | } 25 | 26 | test('hasRoute returns false if there is no routes', t => { 27 | t.plan(7) 28 | 29 | const findMyWay = FindMyWay() 30 | const fundMyWayClone = rfdc(findMyWay) 31 | 32 | const hasRoute = findMyWay.hasRoute('GET', '/example') 33 | t.assert.equal(hasRoute, false) 34 | 35 | equalRouters(t, findMyWay, fundMyWayClone) 36 | }) 37 | 38 | test('hasRoute returns true for a static route', t => { 39 | t.plan(7) 40 | 41 | const findMyWay = FindMyWay() 42 | findMyWay.on('GET', '/example', () => {}) 43 | 44 | const fundMyWayClone = rfdc(findMyWay) 45 | 46 | const hasRoute = findMyWay.hasRoute('GET', '/example') 47 | t.assert.equal(hasRoute, true) 48 | 49 | equalRouters(t, findMyWay, fundMyWayClone) 50 | }) 51 | 52 | test('hasRoute returns false for a static route', t => { 53 | t.plan(7) 54 | 55 | const findMyWay = FindMyWay() 56 | findMyWay.on('GET', '/example', () => {}) 57 | 58 | const fundMyWayClone = rfdc(findMyWay) 59 | 60 | const hasRoute = findMyWay.hasRoute('GET', '/example1') 61 | t.assert.equal(hasRoute, false) 62 | 63 | equalRouters(t, findMyWay, fundMyWayClone) 64 | }) 65 | 66 | test('hasRoute returns true for a parametric route', t => { 67 | t.plan(7) 68 | 69 | const findMyWay = FindMyWay() 70 | findMyWay.on('GET', '/:param', () => {}) 71 | 72 | const fundMyWayClone = rfdc(findMyWay) 73 | 74 | const hasRoute = findMyWay.hasRoute('GET', '/:param') 75 | t.assert.equal(hasRoute, true) 76 | 77 | equalRouters(t, findMyWay, fundMyWayClone) 78 | }) 79 | 80 | test('hasRoute returns false for a parametric route', t => { 81 | t.plan(7) 82 | 83 | const findMyWay = FindMyWay() 84 | findMyWay.on('GET', '/foo/:param', () => {}) 85 | 86 | const fundMyWayClone = rfdc(findMyWay) 87 | 88 | const hasRoute = findMyWay.hasRoute('GET', '/bar/:param') 89 | t.assert.equal(hasRoute, false) 90 | 91 | equalRouters(t, findMyWay, fundMyWayClone) 92 | }) 93 | 94 | test('hasRoute returns true for a parametric route with static suffix', t => { 95 | t.plan(7) 96 | 97 | const findMyWay = FindMyWay() 98 | findMyWay.on('GET', '/:param-static', () => {}) 99 | 100 | const fundMyWayClone = rfdc(findMyWay) 101 | 102 | const hasRoute = findMyWay.hasRoute('GET', '/:param-static') 103 | t.assert.equal(hasRoute, true) 104 | 105 | equalRouters(t, findMyWay, fundMyWayClone) 106 | }) 107 | 108 | test('hasRoute returns false for a parametric route with static suffix', t => { 109 | t.plan(7) 110 | 111 | const findMyWay = FindMyWay() 112 | findMyWay.on('GET', '/:param-static1', () => {}) 113 | 114 | const fundMyWayClone = rfdc(findMyWay) 115 | 116 | const hasRoute = findMyWay.hasRoute('GET', '/:param-static2') 117 | t.assert.equal(hasRoute, false) 118 | 119 | equalRouters(t, findMyWay, fundMyWayClone) 120 | }) 121 | 122 | test('hasRoute returns true even if a param name different', t => { 123 | t.plan(7) 124 | 125 | const findMyWay = FindMyWay() 126 | findMyWay.on('GET', '/:param1', () => {}) 127 | 128 | const fundMyWayClone = rfdc(findMyWay) 129 | 130 | const hasRoute = findMyWay.hasRoute('GET', '/:param2') 131 | t.assert.equal(hasRoute, true) 132 | 133 | equalRouters(t, findMyWay, fundMyWayClone) 134 | }) 135 | 136 | test('hasRoute returns true for a multi-parametric route', t => { 137 | t.plan(7) 138 | 139 | const findMyWay = FindMyWay() 140 | findMyWay.on('GET', '/:param1-:param2', () => {}) 141 | 142 | const fundMyWayClone = rfdc(findMyWay) 143 | 144 | const hasRoute = findMyWay.hasRoute('GET', '/:param1-:param2') 145 | t.assert.equal(hasRoute, true) 146 | 147 | equalRouters(t, findMyWay, fundMyWayClone) 148 | }) 149 | 150 | test('hasRoute returns false for a multi-parametric route', t => { 151 | t.plan(7) 152 | 153 | const findMyWay = FindMyWay() 154 | findMyWay.on('GET', '/foo/:param1-:param2/bar1', () => {}) 155 | 156 | const fundMyWayClone = rfdc(findMyWay) 157 | 158 | const hasRoute = findMyWay.hasRoute('GET', '/foo/:param1-:param2/bar2') 159 | t.assert.equal(hasRoute, false) 160 | 161 | equalRouters(t, findMyWay, fundMyWayClone) 162 | }) 163 | 164 | test('hasRoute returns true for a regexp route', t => { 165 | t.plan(7) 166 | 167 | const findMyWay = FindMyWay() 168 | findMyWay.on('GET', '/:param(^\\d+$)', () => {}) 169 | 170 | const fundMyWayClone = rfdc(findMyWay) 171 | 172 | const hasRoute = findMyWay.hasRoute('GET', '/:param(^\\d+$)') 173 | t.assert.equal(hasRoute, true) 174 | 175 | equalRouters(t, findMyWay, fundMyWayClone) 176 | }) 177 | 178 | test('hasRoute returns false for a regexp route', t => { 179 | t.plan(7) 180 | 181 | const findMyWay = FindMyWay() 182 | findMyWay.on('GET', '/:file(^\\S+).png', () => {}) 183 | 184 | const fundMyWayClone = rfdc(findMyWay) 185 | 186 | const hasRoute = findMyWay.hasRoute('GET', '/:file(^\\D+).png') 187 | t.assert.equal(hasRoute, false) 188 | 189 | equalRouters(t, findMyWay, fundMyWayClone) 190 | }) 191 | 192 | test('hasRoute returns true for a wildcard route', t => { 193 | t.plan(7) 194 | 195 | const findMyWay = FindMyWay() 196 | findMyWay.on('GET', '/example/*', () => {}) 197 | 198 | const fundMyWayClone = rfdc(findMyWay) 199 | 200 | const hasRoute = findMyWay.hasRoute('GET', '/example/*') 201 | t.assert.equal(hasRoute, true) 202 | 203 | equalRouters(t, findMyWay, fundMyWayClone) 204 | }) 205 | 206 | test('hasRoute returns false for a wildcard route', t => { 207 | t.plan(7) 208 | 209 | const findMyWay = FindMyWay() 210 | findMyWay.on('GET', '/foo1/*', () => {}) 211 | 212 | const fundMyWayClone = rfdc(findMyWay) 213 | 214 | const hasRoute = findMyWay.hasRoute('GET', '/foo2/*') 215 | t.assert.equal(hasRoute, false) 216 | 217 | equalRouters(t, findMyWay, fundMyWayClone) 218 | }) 219 | -------------------------------------------------------------------------------- /test/host-storage.test.js: -------------------------------------------------------------------------------- 1 | const acceptHostStrategy = require('../lib/strategies/accept-host') 2 | 3 | const { test } = require('node:test') 4 | 5 | test('can get hosts by exact matches', async (t) => { 6 | const storage = acceptHostStrategy.storage() 7 | t.assert.equal(storage.get('fastify.io'), undefined) 8 | storage.set('fastify.io', true) 9 | t.assert.equal(storage.get('fastify.io'), true) 10 | }) 11 | 12 | test('can get hosts by regexp matches', async (t) => { 13 | const storage = acceptHostStrategy.storage() 14 | t.assert.equal(storage.get('fastify.io'), undefined) 15 | storage.set(/.+fastify\.io/, true) 16 | t.assert.equal(storage.get('foo.fastify.io'), true) 17 | t.assert.equal(storage.get('bar.fastify.io'), true) 18 | }) 19 | 20 | test('exact host matches take precendence over regexp matches', async (t) => { 21 | const storage = acceptHostStrategy.storage() 22 | storage.set(/.+fastify\.io/, 'wildcard') 23 | storage.set('auth.fastify.io', 'exact') 24 | t.assert.equal(storage.get('foo.fastify.io'), 'wildcard') 25 | t.assert.equal(storage.get('bar.fastify.io'), 'wildcard') 26 | t.assert.equal(storage.get('auth.fastify.io'), 'exact') 27 | }) 28 | -------------------------------------------------------------------------------- /test/http2/constraint.host.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../..') 5 | 6 | test('A route supports host constraints under http2 protocol', t => { 7 | t.plan(3) 8 | 9 | const findMyWay = FindMyWay() 10 | 11 | findMyWay.on('GET', '/', {}, (req, res) => { 12 | t.assert.assert.fail() 13 | }) 14 | findMyWay.on('GET', '/', { constraints: { host: 'fastify.io' } }, (req, res) => { 15 | t.assert.equal(req.headers[':authority'], 'fastify.io') 16 | }) 17 | findMyWay.on('GET', '/', { constraints: { host: /.+\.de/ } }, (req, res) => { 18 | t.assert.ok(req.headers[':authority'].endsWith('.de')) 19 | }) 20 | 21 | findMyWay.lookup({ 22 | method: 'GET', 23 | url: '/', 24 | headers: { 25 | ':authority': 'fastify.io' 26 | } 27 | }) 28 | 29 | findMyWay.lookup({ 30 | method: 'GET', 31 | url: '/', 32 | headers: { 33 | ':authority': 'fastify.de' 34 | } 35 | }) 36 | 37 | findMyWay.lookup({ 38 | method: 'GET', 39 | url: '/', 40 | headers: { 41 | ':authority': 'find-my-way.de' 42 | } 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /test/issue-101.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Falling back for node\'s parametric brother', t => { 7 | t.plan(3) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/:namespace/:type/:id', () => {}) 15 | findMyWay.on('GET', '/:namespace/jobs/:name/run', () => {}) 16 | 17 | t.assert.deepEqual( 18 | findMyWay.find('GET', '/test_namespace/test_type/test_id').params, 19 | { namespace: 'test_namespace', type: 'test_type', id: 'test_id' } 20 | ) 21 | 22 | t.assert.deepEqual( 23 | findMyWay.find('GET', '/test_namespace/jobss/test_id').params, 24 | { namespace: 'test_namespace', type: 'jobss', id: 'test_id' } 25 | ) 26 | 27 | t.assert.deepEqual( 28 | findMyWay.find('GET', '/test_namespace/jobs/test_id').params, 29 | { namespace: 'test_namespace', type: 'jobs', id: 'test_id' } 30 | ) 31 | }) 32 | -------------------------------------------------------------------------------- /test/issue-104.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Nested static parametric route, url with parameter common prefix > 1', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/a/bbbb', (req, res) => { 15 | res.end('{"message":"hello world"}') 16 | }) 17 | 18 | findMyWay.on('GET', '/a/bbaa', (req, res) => { 19 | res.end('{"message":"hello world"}') 20 | }) 21 | 22 | findMyWay.on('GET', '/a/babb', (req, res) => { 23 | res.end('{"message":"hello world"}') 24 | }) 25 | 26 | findMyWay.on('DELETE', '/a/:id', (req, res) => { 27 | res.end('{"message":"hello world"}') 28 | }) 29 | 30 | t.assert.deepEqual(findMyWay.find('DELETE', '/a/bbar').params, { id: 'bbar' }) 31 | }) 32 | 33 | test('Parametric route, url with parameter common prefix > 1', t => { 34 | t.plan(1) 35 | const findMyWay = FindMyWay({ 36 | defaultRoute: (req, res) => { 37 | t.assert.fail('Should not be defaultRoute') 38 | } 39 | }) 40 | 41 | findMyWay.on('GET', '/aaa', (req, res) => { 42 | res.end('{"message":"hello world"}') 43 | }) 44 | 45 | findMyWay.on('GET', '/aabb', (req, res) => { 46 | res.end('{"message":"hello world"}') 47 | }) 48 | 49 | findMyWay.on('GET', '/abc', (req, res) => { 50 | res.end('{"message":"hello world"}') 51 | }) 52 | 53 | findMyWay.on('GET', '/:id', (req, res) => { 54 | res.end('{"message":"hello world"}') 55 | }) 56 | 57 | t.assert.deepEqual(findMyWay.find('GET', '/aab').params, { id: 'aab' }) 58 | }) 59 | 60 | test('Parametric route, url with multi parameter common prefix > 1', t => { 61 | t.plan(1) 62 | const findMyWay = FindMyWay({ 63 | defaultRoute: (req, res) => { 64 | t.assert.fail('Should not be defaultRoute') 65 | } 66 | }) 67 | 68 | findMyWay.on('GET', '/:id/aaa/:id2', (req, res) => { 69 | res.end('{"message":"hello world"}') 70 | }) 71 | 72 | findMyWay.on('GET', '/:id/aabb/:id2', (req, res) => { 73 | res.end('{"message":"hello world"}') 74 | }) 75 | 76 | findMyWay.on('GET', '/:id/abc/:id2', (req, res) => { 77 | res.end('{"message":"hello world"}') 78 | }) 79 | 80 | findMyWay.on('GET', '/:a/:b', (req, res) => { 81 | res.end('{"message":"hello world"}') 82 | }) 83 | 84 | t.assert.deepEqual(findMyWay.find('GET', '/hello/aab').params, { a: 'hello', b: 'aab' }) 85 | }) 86 | 87 | test('Mixed routes, url with parameter common prefix > 1', t => { 88 | t.plan(11) 89 | const findMyWay = FindMyWay({ 90 | defaultRoute: (req, res) => { 91 | t.assert.fail('Should not be defaultRoute') 92 | } 93 | }) 94 | 95 | findMyWay.on('GET', '/test', (req, res, params) => { 96 | res.end('{"hello":"world"}') 97 | }) 98 | 99 | findMyWay.on('GET', '/testify', (req, res, params) => { 100 | res.end('{"hello":"world"}') 101 | }) 102 | 103 | findMyWay.on('GET', '/test/hello', (req, res, params) => { 104 | res.end('{"hello":"world"}') 105 | }) 106 | 107 | findMyWay.on('GET', '/test/hello/test', (req, res, params) => { 108 | res.end('{"hello":"world"}') 109 | }) 110 | 111 | findMyWay.on('GET', '/te/:a', (req, res, params) => { 112 | res.end('{"hello":"world"}') 113 | }) 114 | 115 | findMyWay.on('GET', '/test/hello/:b', (req, res, params) => { 116 | res.end('{"hello":"world"}') 117 | }) 118 | 119 | findMyWay.on('GET', '/:c', (req, res, params) => { 120 | res.end('{"hello":"world"}') 121 | }) 122 | 123 | findMyWay.on('GET', '/text/hello', (req, res, params) => { 124 | res.end('{"hello":"world"}') 125 | }) 126 | 127 | findMyWay.on('GET', '/text/:d', (req, res, params) => { 128 | res.end('{"winter":"is here"}') 129 | }) 130 | 131 | findMyWay.on('GET', '/text/:e/test', (req, res, params) => { 132 | res.end('{"winter":"is here"}') 133 | }) 134 | 135 | t.assert.deepEqual(findMyWay.find('GET', '/test').params, {}) 136 | t.assert.deepEqual(findMyWay.find('GET', '/testify').params, {}) 137 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello').params, {}) 138 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello/test').params, {}) 139 | t.assert.deepEqual(findMyWay.find('GET', '/te/hello').params, { a: 'hello' }) 140 | t.assert.deepEqual(findMyWay.find('GET', '/te/').params, { a: '' }) 141 | t.assert.deepEqual(findMyWay.find('GET', '/testy').params, { c: 'testy' }) 142 | t.assert.deepEqual(findMyWay.find('GET', '/besty').params, { c: 'besty' }) 143 | t.assert.deepEqual(findMyWay.find('GET', '/text/hellos/test').params, { e: 'hellos' }) 144 | t.assert.deepEqual(findMyWay.find('GET', '/te/hello/'), null) 145 | t.assert.deepEqual(findMyWay.find('GET', '/te/hellos/testy'), null) 146 | }) 147 | 148 | test('Parent parametric brother should not rewrite child node parametric brother', t => { 149 | t.plan(1) 150 | const findMyWay = FindMyWay({ 151 | defaultRoute: (req, res) => { 152 | t.assert.fail('Should not be defaultRoute') 153 | } 154 | }) 155 | 156 | findMyWay.on('GET', '/text/hello', (req, res, params) => { 157 | res.end('{"hello":"world"}') 158 | }) 159 | 160 | findMyWay.on('GET', '/text/:e/test', (req, res, params) => { 161 | res.end('{"winter":"is here"}') 162 | }) 163 | 164 | findMyWay.on('GET', '/:c', (req, res, params) => { 165 | res.end('{"hello":"world"}') 166 | }) 167 | 168 | t.assert.deepEqual(findMyWay.find('GET', '/text/hellos/test').params, { e: 'hellos' }) 169 | }) 170 | 171 | test('Mixed parametric routes, with last defined route being static', t => { 172 | t.plan(4) 173 | const findMyWay = FindMyWay({ 174 | defaultRoute: (req, res) => { 175 | t.assert.fail('Should not be defaultRoute') 176 | } 177 | }) 178 | 179 | findMyWay.on('GET', '/test', (req, res, params) => { 180 | res.end('{"hello":"world"}') 181 | }) 182 | 183 | findMyWay.on('GET', '/test/:a', (req, res, params) => { 184 | res.end('{"hello":"world"}') 185 | }) 186 | 187 | findMyWay.on('GET', '/test/hello/:b', (req, res, params) => { 188 | res.end('{"hello":"world"}') 189 | }) 190 | 191 | findMyWay.on('GET', '/test/hello/:c/test', (req, res, params) => { 192 | res.end('{"hello":"world"}') 193 | }) 194 | findMyWay.on('GET', '/test/hello/:c/:k', (req, res, params) => { 195 | res.end('{"hello":"world"}') 196 | }) 197 | 198 | findMyWay.on('GET', '/test/world', (req, res, params) => { 199 | res.end('{"hello":"world"}') 200 | }) 201 | 202 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello').params, { a: 'hello' }) 203 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello/world/test').params, { c: 'world' }) 204 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello/world/te').params, { c: 'world', k: 'te' }) 205 | t.assert.deepEqual(findMyWay.find('GET', '/test/hello/world/testy').params, { c: 'world', k: 'testy' }) 206 | }) 207 | -------------------------------------------------------------------------------- /test/issue-110.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Nested static parametric route, url with parameter common prefix > 1', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/api/foo/b2', (req, res) => { 15 | res.end('{"message":"hello world"}') 16 | }) 17 | 18 | findMyWay.on('GET', '/api/foo/bar/qux', (req, res) => { 19 | res.end('{"message":"hello world"}') 20 | }) 21 | 22 | findMyWay.on('GET', '/api/foo/:id/bar', (req, res) => { 23 | res.end('{"message":"hello world"}') 24 | }) 25 | 26 | findMyWay.on('GET', '/foo', (req, res) => { 27 | res.end('{"message":"hello world"}') 28 | }) 29 | 30 | t.assert.deepEqual(findMyWay.find('GET', '/api/foo/b-123/bar').params, { id: 'b-123' }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/issue-132.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Wildcard mixed with dynamic and common prefix / 1', t => { 7 | t.plan(5) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('OPTIONS', '/*', (req, res, params) => { 15 | t.assert.equal(req.method, 'OPTIONS') 16 | }) 17 | 18 | findMyWay.on('GET', '/obj/params/*', (req, res, params) => { 19 | t.assert.equal(req.method, 'GET') 20 | }) 21 | 22 | findMyWay.on('GET', '/obj/:id', (req, res, params) => { 23 | t.assert.equal(req.method, 'GET') 24 | }) 25 | 26 | findMyWay.on('GET', '/obj_params/*', (req, res, params) => { 27 | t.assert.equal(req.method, 'GET') 28 | }) 29 | 30 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj/params', headers: {} }, null) 31 | 32 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj/params/12', headers: {} }, null) 33 | 34 | findMyWay.lookup({ method: 'GET', url: '/obj/params/12', headers: {} }, null) 35 | 36 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj_params/12', headers: {} }, null) 37 | 38 | findMyWay.lookup({ method: 'GET', url: '/obj_params/12', headers: {} }, null) 39 | }) 40 | 41 | test('Wildcard mixed with dynamic and common prefix / 2', t => { 42 | t.plan(6) 43 | const findMyWay = FindMyWay({ 44 | defaultRoute: (req, res) => { 45 | t.assert.fail('Should not be defaultRoute') 46 | } 47 | }) 48 | 49 | findMyWay.on('OPTIONS', '/*', (req, res, params) => { 50 | t.assert.equal(req.method, 'OPTIONS') 51 | }) 52 | 53 | findMyWay.on('OPTIONS', '/obj/*', (req, res, params) => { 54 | t.assert.equal(req.method, 'OPTIONS') 55 | }) 56 | 57 | findMyWay.on('GET', '/obj/params/*', (req, res, params) => { 58 | t.assert.equal(req.method, 'GET') 59 | }) 60 | 61 | findMyWay.on('GET', '/obj/:id', (req, res, params) => { 62 | t.assert.equal(req.method, 'GET') 63 | }) 64 | 65 | findMyWay.on('GET', '/obj_params/*', (req, res, params) => { 66 | t.assert.equal(req.method, 'GET') 67 | }) 68 | 69 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj_params/params', headers: {} }, null) 70 | 71 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj/params', headers: {} }, null) 72 | 73 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj/params/12', headers: {} }, null) 74 | 75 | findMyWay.lookup({ method: 'GET', url: '/obj/params/12', headers: {} }, null) 76 | 77 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj_params/12', headers: {} }, null) 78 | 79 | findMyWay.lookup({ method: 'GET', url: '/obj_params/12', headers: {} }, null) 80 | }) 81 | -------------------------------------------------------------------------------- /test/issue-145.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('issue-145', (t) => { 7 | t.plan(8) 8 | 9 | const findMyWay = FindMyWay({ ignoreTrailingSlash: true }) 10 | 11 | const fixedPath = function staticPath () {} 12 | const varPath = function parameterPath () {} 13 | findMyWay.on('GET', '/a/b', fixedPath) 14 | findMyWay.on('GET', '/a/:pam/c', varPath) 15 | 16 | t.assert.equal(findMyWay.find('GET', '/a/b').handler, fixedPath) 17 | t.assert.equal(findMyWay.find('GET', '/a/b/').handler, fixedPath) 18 | t.assert.equal(findMyWay.find('GET', '/a/b/c').handler, varPath) 19 | t.assert.equal(findMyWay.find('GET', '/a/b/c/').handler, varPath) 20 | t.assert.equal(findMyWay.find('GET', '/a/foo/c').handler, varPath) 21 | t.assert.equal(findMyWay.find('GET', '/a/foo/c/').handler, varPath) 22 | t.assert.ok(!findMyWay.find('GET', '/a/c')) 23 | t.assert.ok(!findMyWay.find('GET', '/a/c/')) 24 | }) 25 | -------------------------------------------------------------------------------- /test/issue-149.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Falling back for node\'s parametric brother', t => { 7 | t.plan(3) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/foo/:id', () => {}) 15 | findMyWay.on('GET', '/foo/:color/:id', () => {}) 16 | findMyWay.on('GET', '/foo/red', () => {}) 17 | 18 | t.assert.deepEqual(findMyWay.find('GET', '/foo/red/123').params, { color: 'red', id: '123' }) 19 | t.assert.deepEqual(findMyWay.find('GET', '/foo/blue/123').params, { color: 'blue', id: '123' }) 20 | t.assert.deepEqual(findMyWay.find('GET', '/foo/red').params, {}) 21 | }) 22 | -------------------------------------------------------------------------------- /test/issue-151.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Wildcard route should not be blocked by Parametric with different method / 1', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('OPTIONS', '/*', (req, res, params) => { 15 | t.assert.fail('Should not be here') 16 | }) 17 | 18 | findMyWay.on('OPTIONS', '/obj/*', (req, res, params) => { 19 | t.assert.equal(req.method, 'OPTIONS') 20 | }) 21 | 22 | findMyWay.on('GET', '/obj/:id', (req, res, params) => { 23 | t.assert.fail('Should not be GET') 24 | }) 25 | 26 | findMyWay.lookup({ method: 'OPTIONS', url: '/obj/params', headers: {} }, null) 27 | }) 28 | 29 | test('Wildcard route should not be blocked by Parametric with different method / 2', t => { 30 | t.plan(1) 31 | const findMyWay = FindMyWay({ 32 | defaultRoute: (req, res) => { 33 | t.assert.fail('Should not be defaultRoute') 34 | } 35 | }) 36 | 37 | findMyWay.on('OPTIONS', '/*', { version: '1.2.3' }, (req, res, params) => { 38 | t.assert.fail('Should not be here') 39 | }) 40 | 41 | findMyWay.on('OPTIONS', '/obj/*', { version: '1.2.3' }, (req, res, params) => { 42 | t.assert.equal(req.method, 'OPTIONS') 43 | }) 44 | 45 | findMyWay.on('GET', '/obj/:id', { version: '1.2.3' }, (req, res, params) => { 46 | t.assert.fail('Should not be GET') 47 | }) 48 | 49 | findMyWay.lookup({ 50 | method: 'OPTIONS', 51 | url: '/obj/params', 52 | headers: { 'accept-version': '1.2.3' } 53 | }, null) 54 | }) 55 | -------------------------------------------------------------------------------- /test/issue-154.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | const noop = () => {} 6 | 7 | test('Should throw when not sending a string', t => { 8 | t.plan(3) 9 | 10 | const findMyWay = FindMyWay() 11 | 12 | t.assert.throws(() => { 13 | findMyWay.on('GET', '/t1', { constraints: { version: 42 } }, noop) 14 | }) 15 | t.assert.throws(() => { 16 | findMyWay.on('GET', '/t2', { constraints: { version: null } }, noop) 17 | }) 18 | t.assert.throws(() => { 19 | findMyWay.on('GET', '/t2', { constraints: { version: true } }, noop) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/issue-161.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Falling back for node\'s parametric brother without ignoreTrailingSlash', t => { 7 | t.plan(4) 8 | const findMyWay = FindMyWay({ 9 | ignoreTrailingSlash: false, 10 | defaultRoute: (req, res) => { 11 | t.assert.fail('Should not be defaultRoute') 12 | } 13 | }) 14 | 15 | findMyWay.on('GET', '/static/param1', () => {}) 16 | findMyWay.on('GET', '/static/param2', () => {}) 17 | findMyWay.on('GET', '/static/:paramA/next', () => {}) 18 | 19 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1').params, {}) 20 | t.assert.deepEqual(findMyWay.find('GET', '/static/param2').params, {}) 21 | t.assert.deepEqual(findMyWay.find('GET', '/static/paramOther/next').params, { paramA: 'paramOther' }) 22 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next').params, { paramA: 'param1' }) 23 | }) 24 | 25 | test('Falling back for node\'s parametric brother with ignoreTrailingSlash', t => { 26 | t.plan(4) 27 | const findMyWay = FindMyWay({ 28 | ignoreTrailingSlash: true, 29 | defaultRoute: (req, res) => { 30 | t.assert.fail('Should not be defaultRoute') 31 | } 32 | }) 33 | 34 | findMyWay.on('GET', '/static/param1', () => {}) 35 | findMyWay.on('GET', '/static/param2', () => {}) 36 | findMyWay.on('GET', '/static/:paramA/next', () => {}) 37 | 38 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1').params, {}) 39 | t.assert.deepEqual(findMyWay.find('GET', '/static/param2').params, {}) 40 | t.assert.deepEqual(findMyWay.find('GET', '/static/paramOther/next').params, { paramA: 'paramOther' }) 41 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next').params, { paramA: 'param1' }) 42 | }) 43 | 44 | test('Falling back for node\'s parametric brother without ignoreTrailingSlash', t => { 45 | t.plan(4) 46 | const findMyWay = FindMyWay({ 47 | ignoreTrailingSlash: false, 48 | defaultRoute: (req, res) => { 49 | t.assert.fail('Should not be defaultRoute') 50 | } 51 | }) 52 | 53 | findMyWay.on('GET', '/static/param1', () => {}) 54 | findMyWay.on('GET', '/static/param2', () => {}) 55 | findMyWay.on('GET', '/static/:paramA/next', () => {}) 56 | 57 | findMyWay.on('GET', '/static/param1/next/param3', () => {}) 58 | findMyWay.on('GET', '/static/param1/next/param4', () => {}) 59 | findMyWay.on('GET', '/static/:paramA/next/:paramB/other', () => {}) 60 | 61 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param3').params, {}) 62 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param4').params, {}) 63 | t.assert.deepEqual(findMyWay.find('GET', '/static/paramOther/next/paramOther2/other').params, { paramA: 'paramOther', paramB: 'paramOther2' }) 64 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param3/other').params, { paramA: 'param1', paramB: 'param3' }) 65 | }) 66 | 67 | test('Falling back for node\'s parametric brother with ignoreTrailingSlash', t => { 68 | t.plan(4) 69 | const findMyWay = FindMyWay({ 70 | ignoreTrailingSlash: true, 71 | defaultRoute: (req, res) => { 72 | t.assert.fail('Should not be defaultRoute') 73 | } 74 | }) 75 | 76 | findMyWay.on('GET', '/static/param1', () => {}) 77 | findMyWay.on('GET', '/static/param2', () => {}) 78 | findMyWay.on('GET', '/static/:paramA/next', () => {}) 79 | 80 | findMyWay.on('GET', '/static/param1/next/param3', () => {}) 81 | findMyWay.on('GET', '/static/param1/next/param4', () => {}) 82 | findMyWay.on('GET', '/static/:paramA/next/:paramB/other', () => {}) 83 | 84 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param3').params, {}) 85 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param4').params, {}) 86 | t.assert.deepEqual(findMyWay.find('GET', '/static/paramOther/next/paramOther2/other').params, { paramA: 'paramOther', paramB: 'paramOther2' }) 87 | t.assert.deepEqual(findMyWay.find('GET', '/static/param1/next/param3/other').params, { paramA: 'param1', paramB: 'param3' }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/issue-175.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('double colon is replaced with single colon, no parameters', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: () => t.assert.fail('should not be default route') 10 | }) 11 | 12 | function handler (req, res, params) { 13 | t.assert.deepEqual(params, {}) 14 | } 15 | 16 | findMyWay.on('GET', '/name::customVerb', handler) 17 | 18 | findMyWay.lookup({ method: 'GET', url: '/name:customVerb' }, null) 19 | }) 20 | 21 | test('exactly one match for static route with colon', t => { 22 | t.plan(2) 23 | const findMyWay = FindMyWay() 24 | 25 | function handler () {} 26 | findMyWay.on('GET', '/name::customVerb', handler) 27 | 28 | t.assert.equal(findMyWay.find('GET', '/name:customVerb').handler, handler) 29 | t.assert.equal(findMyWay.find('GET', '/name:test'), null) 30 | }) 31 | 32 | test('double colon is replaced with single colon, no parameters, same parent node name', t => { 33 | t.plan(1) 34 | const findMyWay = FindMyWay({ 35 | defaultRoute: () => t.assert.fail('should not be default route') 36 | }) 37 | 38 | findMyWay.on('GET', '/name', () => { 39 | t.assert.fail('should not be parent route') 40 | }) 41 | 42 | findMyWay.on('GET', '/name::customVerb', (req, res, params) => { 43 | t.assert.deepEqual(params, {}) 44 | }) 45 | 46 | findMyWay.lookup({ method: 'GET', url: '/name:customVerb', headers: {} }, null) 47 | }) 48 | 49 | test('double colon is replaced with single colon, default route, same parent node name', t => { 50 | t.plan(1) 51 | const findMyWay = FindMyWay({ 52 | defaultRoute: () => t.assert.ok('should be default route') 53 | }) 54 | 55 | findMyWay.on('GET', '/name', () => { 56 | t.assert.fail('should not be parent route') 57 | }) 58 | 59 | findMyWay.on('GET', '/name::customVerb', () => { 60 | t.assert.fail('should not be child route') 61 | }) 62 | 63 | findMyWay.lookup({ method: 'GET', url: '/name:wrongCustomVerb', headers: {} }, null) 64 | }) 65 | 66 | test('double colon is replaced with single colon, with parameters', t => { 67 | t.plan(1) 68 | const findMyWay = FindMyWay({ 69 | defaultRoute: () => t.assert.fail('should not be default route') 70 | }) 71 | 72 | findMyWay.on('GET', '/name1::customVerb1/:param1/name2::customVerb2:param2', (req, res, params) => { 73 | t.assert.deepEqual(params, { 74 | param1: 'value1', 75 | param2: 'value2' 76 | }) 77 | }) 78 | 79 | findMyWay.lookup({ method: 'GET', url: '/name1:customVerb1/value1/name2:customVerb2value2', headers: {} }, null) 80 | }) 81 | -------------------------------------------------------------------------------- /test/issue-182.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Set method property when splitting node', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay() 9 | 10 | function handler (req, res, params) { 11 | t.assert.ok() 12 | } 13 | 14 | findMyWay.on('GET', '/health-a/health', handler) 15 | findMyWay.on('GET', '/health-b/health', handler) 16 | 17 | t.assert.ok(!findMyWay.prettyPrint().includes('undefined')) 18 | }) 19 | -------------------------------------------------------------------------------- /test/issue-190.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('issue-190', (t) => { 7 | t.plan(6) 8 | 9 | const findMyWay = FindMyWay() 10 | 11 | let staticCounter = 0 12 | let paramCounter = 0 13 | const staticPath = function staticPath () { staticCounter++ } 14 | const paramPath = function paramPath () { paramCounter++ } 15 | const extraPath = function extraPath () { } 16 | findMyWay.on('GET', '/api/users/award_winners', staticPath) 17 | findMyWay.on('GET', '/api/users/admins', staticPath) 18 | findMyWay.on('GET', '/api/users/:id', paramPath) 19 | findMyWay.on('GET', '/api/:resourceType/foo', extraPath) 20 | 21 | t.assert.equal(findMyWay.find('GET', '/api/users/admins').handler, staticPath) 22 | t.assert.equal(findMyWay.find('GET', '/api/users/award_winners').handler, staticPath) 23 | t.assert.equal(findMyWay.find('GET', '/api/users/a766c023-34ec-40d2-923c-e8259a28d2c5').handler, paramPath) 24 | t.assert.equal(findMyWay.find('GET', '/api/users/b766c023-34ec-40d2-923c-e8259a28d2c5').handler, paramPath) 25 | 26 | findMyWay.lookup({ 27 | method: 'GET', 28 | url: '/api/users/admins', 29 | headers: { } 30 | }) 31 | findMyWay.lookup({ 32 | method: 'GET', 33 | url: '/api/users/award_winners', 34 | headers: { } 35 | }) 36 | findMyWay.lookup({ 37 | method: 'GET', 38 | url: '/api/users/a766c023-34ec-40d2-923c-e8259a28d2c5', 39 | headers: { } 40 | }) 41 | 42 | t.assert.equal(staticCounter, 2) 43 | t.assert.equal(paramCounter, 1) 44 | }) 45 | -------------------------------------------------------------------------------- /test/issue-20.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Standard case', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be here') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/a/:param', (req, res, params) => { 15 | t.assert.equal(params.param, 'perfectly-fine-route') 16 | }) 17 | 18 | findMyWay.lookup({ method: 'GET', url: '/a/perfectly-fine-route', headers: {} }, null) 19 | }) 20 | 21 | test('Should be 404 / 1', t => { 22 | t.plan(1) 23 | const findMyWay = FindMyWay({ 24 | defaultRoute: (req, res) => { 25 | t.assert.ok('Everything good') 26 | } 27 | }) 28 | 29 | findMyWay.on('GET', '/a/:param', (req, res, params) => { 30 | t.assert.fail('We should not be here') 31 | }) 32 | 33 | findMyWay.lookup({ method: 'GET', url: '/a', headers: {} }, null) 34 | }) 35 | 36 | test('Should be 404 / 2', t => { 37 | t.plan(1) 38 | const findMyWay = FindMyWay({ 39 | defaultRoute: (req, res) => { 40 | t.assert.ok('Everything good') 41 | } 42 | }) 43 | 44 | findMyWay.on('GET', '/a/:param', (req, res, params) => { 45 | t.assert.fail('We should not be here') 46 | }) 47 | 48 | findMyWay.lookup({ method: 'GET', url: '/a-non-existing-route', headers: {} }, null) 49 | }) 50 | 51 | test('Should be 404 / 3', t => { 52 | t.plan(1) 53 | const findMyWay = FindMyWay({ 54 | defaultRoute: (req, res) => { 55 | t.assert.ok('Everything good') 56 | } 57 | }) 58 | 59 | findMyWay.on('GET', '/a/:param', (req, res, params) => { 60 | t.assert.fail('We should not be here') 61 | }) 62 | 63 | findMyWay.lookup({ method: 'GET', url: '/a//', headers: {} }, null) 64 | }) 65 | 66 | test('Should get an empty parameter', t => { 67 | t.plan(1) 68 | const findMyWay = FindMyWay({ 69 | defaultRoute: (req, res) => { 70 | t.assert.fail('We should not be here') 71 | } 72 | }) 73 | 74 | findMyWay.on('GET', '/a/:param', (req, res, params) => { 75 | t.assert.equal(params.param, '') 76 | }) 77 | 78 | findMyWay.lookup({ method: 'GET', url: '/a/', headers: {} }, null) 79 | }) 80 | -------------------------------------------------------------------------------- /test/issue-206.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Decode the URL before the routing', t => { 7 | t.plan(8) 8 | const findMyWay = FindMyWay() 9 | 10 | function space (req, res, params) {} 11 | function percentTwenty (req, res, params) {} 12 | function percentTwentyfive (req, res, params) {} 13 | 14 | findMyWay.on('GET', '/static/:pathParam', () => {}) 15 | findMyWay.on('GET', '/[...]/a .html', space) 16 | findMyWay.on('GET', '/[...]/a%20.html', percentTwenty) 17 | findMyWay.on('GET', '/[...]/a%2520.html', percentTwentyfive) 18 | 19 | t.assert.equal(findMyWay.find('GET', '/[...]/a .html').handler, space) 20 | t.assert.equal(findMyWay.find('GET', '/%5B...%5D/a .html').handler, space) 21 | t.assert.equal(findMyWay.find('GET', '/[...]/a%20.html').handler, space, 'a%20 decode is a ') 22 | t.assert.equal(findMyWay.find('GET', '/%5B...%5D/a%20.html').handler, space, 'a%20 decode is a ') 23 | t.assert.equal(findMyWay.find('GET', '/[...]/a%2520.html').handler, percentTwenty, 'a%2520 decode is a%20') 24 | t.assert.equal(findMyWay.find('GET', '/%5B...%5D/a%252520.html').handler, percentTwentyfive, 'a%252520.html is a%2520') 25 | t.assert.equal(findMyWay.find('GET', '/[...]/a .html'), null, 'double space') 26 | t.assert.equal(findMyWay.find('GET', '/static/%25E0%A4%A'), null, 'invalid encoded path param') 27 | }) 28 | 29 | test('double encoding', t => { 30 | t.plan(8) 31 | const findMyWay = FindMyWay() 32 | 33 | function pathParam (req, res, params) { 34 | t.assert.deepEqual(params, this.expect, 'path param') 35 | t.assert.deepEqual(pathParam, this.handler, 'match handler') 36 | } 37 | function regexPathParam (req, res, params) { 38 | t.assert.deepEqual(params, this.expect, 'regex param') 39 | t.assert.deepEqual(regexPathParam, this.handler, 'match handler') 40 | } 41 | function wildcard (req, res, params) { 42 | t.assert.deepEqual(params, this.expect, 'wildcard param') 43 | t.assert.deepEqual(wildcard, this.handler, 'match handler') 44 | } 45 | 46 | findMyWay.on('GET', '/:pathParam', pathParam) 47 | findMyWay.on('GET', '/reg/:regExeParam(^.*$)', regexPathParam) 48 | findMyWay.on('GET', '/wild/*', wildcard) 49 | 50 | findMyWay.lookup(get('/' + doubleEncode('reg/hash# .png')), null, 51 | { expect: { pathParam: singleEncode('reg/hash# .png') }, handler: pathParam } 52 | ) 53 | findMyWay.lookup(get('/' + doubleEncode('special # $ & + , / : ; = ? @')), null, 54 | { expect: { pathParam: singleEncode('special # $ & + , / : ; = ? @') }, handler: pathParam } 55 | ) 56 | findMyWay.lookup(get('/reg/' + doubleEncode('hash# .png')), null, 57 | { expect: { regExeParam: singleEncode('hash# .png') }, handler: regexPathParam } 58 | ) 59 | findMyWay.lookup(get('/wild/' + doubleEncode('mail@mail.it')), null, 60 | { expect: { '*': singleEncode('mail@mail.it') }, handler: wildcard } 61 | ) 62 | 63 | function doubleEncode (str) { 64 | return encodeURIComponent(encodeURIComponent(str)) 65 | } 66 | function singleEncode (str) { 67 | return encodeURIComponent(str) 68 | } 69 | }) 70 | 71 | test('Special chars on path parameter', t => { 72 | t.plan(10) 73 | const findMyWay = FindMyWay() 74 | 75 | function pathParam (req, res, params) { 76 | t.assert.deepEqual(params, this.expect, 'path param') 77 | t.assert.deepEqual(pathParam, this.handler, 'match handler') 78 | } 79 | function regexPathParam (req, res, params) { 80 | t.assert.deepEqual(params, this.expect, 'regex param') 81 | t.assert.deepEqual(regexPathParam, this.handler, 'match handler') 82 | } 83 | function staticEncoded (req, res, params) { 84 | t.assert.deepEqual(params, this.expect, 'static match') 85 | t.assert.deepEqual(staticEncoded, this.handler, 'match handler') 86 | } 87 | 88 | findMyWay.on('GET', '/:pathParam', pathParam) 89 | findMyWay.on('GET', '/reg/:regExeParam(^\\d+) .png', regexPathParam) 90 | findMyWay.on('GET', '/[...]/a%2520.html', staticEncoded) 91 | 92 | findMyWay.lookup(get('/%5B...%5D/a%252520.html'), null, { expect: {}, handler: staticEncoded }) 93 | findMyWay.lookup(get('/[...].html'), null, { expect: { pathParam: '[...].html' }, handler: pathParam }) 94 | findMyWay.lookup(get('/reg/123 .png'), null, { expect: { regExeParam: '123' }, handler: regexPathParam }) 95 | findMyWay.lookup(get('/reg%2F123 .png'), null, { expect: { pathParam: 'reg/123 .png' }, handler: pathParam }) // en encoded / is considered a parameter 96 | findMyWay.lookup(get('/reg/123%20.png'), null, { expect: { regExeParam: '123' }, handler: regexPathParam }) 97 | }) 98 | 99 | test('Multi parametric route with encoded colon separator', t => { 100 | t.plan(1) 101 | const findMyWay = FindMyWay({ 102 | defaultRoute: (req, res) => { 103 | t.assert.fail('Should not be defaultRoute') 104 | } 105 | }) 106 | 107 | findMyWay.on('GET', '/:param(.*)::suffix', (req, res, params) => { 108 | t.assert.equal(params.param, 'foo-bar') 109 | }) 110 | 111 | findMyWay.lookup({ method: 'GET', url: '/foo-bar%3Asuffix', headers: {} }, null) 112 | }) 113 | 114 | function get (url) { 115 | return { method: 'GET', url, headers: {} } 116 | } 117 | 118 | // http://localhost:3000/parameter with / in it 119 | // http://localhost:3000/parameter%20with%20%2F%20in%20it 120 | 121 | // http://localhost:3000/parameter with %252F in it 122 | -------------------------------------------------------------------------------- /test/issue-221.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Should return correct param after switching from static route', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/prefix-:id', () => {}) 11 | findMyWay.on('GET', '/prefix-111', () => {}) 12 | 13 | t.assert.deepEqual(findMyWay.find('GET', '/prefix-1111').params, { id: '1111' }) 14 | }) 15 | 16 | test('Should return correct param after switching from static route', t => { 17 | t.plan(1) 18 | const findMyWay = FindMyWay() 19 | 20 | findMyWay.on('GET', '/prefix-111', () => {}) 21 | findMyWay.on('GET', '/prefix-:id/hello', () => {}) 22 | 23 | t.assert.deepEqual(findMyWay.find('GET', '/prefix-1111/hello').params, { id: '1111' }) 24 | }) 25 | 26 | test('Should return correct param after switching from parametric route', t => { 27 | t.plan(1) 28 | const findMyWay = FindMyWay() 29 | 30 | findMyWay.on('GET', '/prefix-111', () => {}) 31 | findMyWay.on('GET', '/prefix-:id/hello', () => {}) 32 | findMyWay.on('GET', '/:id', () => {}) 33 | 34 | t.assert.deepEqual(findMyWay.find('GET', '/prefix-1111-hello').params, { id: 'prefix-1111-hello' }) 35 | }) 36 | 37 | test('Should return correct params after switching from parametric route', t => { 38 | t.plan(1) 39 | const findMyWay = FindMyWay() 40 | 41 | findMyWay.on('GET', '/test/:param1/test/:param2/prefix-111', () => {}) 42 | findMyWay.on('GET', '/test/:param1/test/:param2/prefix-:id/hello', () => {}) 43 | findMyWay.on('GET', '/test/:param1/test/:param2/:id', () => {}) 44 | 45 | t.assert.deepEqual(findMyWay.find('GET', '/test/value1/test/value2/prefix-1111-hello').params, { 46 | param1: 'value1', 47 | param2: 'value2', 48 | id: 'prefix-1111-hello' 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/issue-234.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Match static url without encoding option', t => { 7 | t.plan(2) 8 | 9 | const findMyWay = FindMyWay() 10 | 11 | const handler = () => {} 12 | 13 | findMyWay.on('GET', '/🍌', handler) 14 | 15 | t.assert.deepEqual(findMyWay.find('GET', '/🍌').handler, handler) 16 | t.assert.deepEqual(findMyWay.find('GET', '/%F0%9F%8D%8C').handler, handler) 17 | }) 18 | 19 | test('Match parametric url with encoding option', t => { 20 | t.plan(2) 21 | 22 | const findMyWay = FindMyWay() 23 | 24 | findMyWay.on('GET', '/🍌/:param', () => {}) 25 | 26 | t.assert.deepEqual(findMyWay.find('GET', '/🍌/@').params, { param: '@' }) 27 | t.assert.deepEqual(findMyWay.find('GET', '/%F0%9F%8D%8C/@').params, { param: '@' }) 28 | }) 29 | 30 | test('Match encoded parametric url with encoding option', t => { 31 | t.plan(2) 32 | 33 | const findMyWay = FindMyWay() 34 | 35 | findMyWay.on('GET', '/🍌/:param', () => {}) 36 | 37 | t.assert.deepEqual(findMyWay.find('GET', '/🍌/%23').params, { param: '#' }) 38 | t.assert.deepEqual(findMyWay.find('GET', '/%F0%9F%8D%8C/%23').params, { param: '#' }) 39 | }) 40 | 41 | test('Decode url components', t => { 42 | t.plan(3) 43 | 44 | const findMyWay = FindMyWay() 45 | 46 | findMyWay.on('GET', '/:param1/:param2', () => {}) 47 | 48 | t.assert.deepEqual(findMyWay.find('GET', '/foo%23bar/foo%23bar').params, { param1: 'foo#bar', param2: 'foo#bar' }) 49 | t.assert.deepEqual(findMyWay.find('GET', '/%F0%9F%8D%8C/%F0%9F%8D%8C').params, { param1: '🍌', param2: '🍌' }) 50 | t.assert.deepEqual(findMyWay.find('GET', '/%F0%9F%8D%8C/foo%23bar').params, { param1: '🍌', param2: 'foo#bar' }) 51 | }) 52 | 53 | test('Decode url components', t => { 54 | t.plan(5) 55 | 56 | const findMyWay = FindMyWay() 57 | 58 | findMyWay.on('GET', '/foo🍌bar/:param1/:param2', () => {}) 59 | findMyWay.on('GET', '/user/:id', () => {}) 60 | 61 | t.assert.deepEqual(findMyWay.find('GET', '/foo%F0%9F%8D%8Cbar/foo%23bar/foo%23bar').params, { param1: 'foo#bar', param2: 'foo#bar' }) 62 | t.assert.deepEqual(findMyWay.find('GET', '/user/maintainer+tomas').params, { id: 'maintainer+tomas' }) 63 | t.assert.deepEqual(findMyWay.find('GET', '/user/maintainer%2Btomas').params, { id: 'maintainer+tomas' }) 64 | t.assert.deepEqual(findMyWay.find('GET', '/user/maintainer%20tomas').params, { id: 'maintainer tomas' }) 65 | t.assert.deepEqual(findMyWay.find('GET', '/user/maintainer%252Btomas').params, { id: 'maintainer%2Btomas' }) 66 | }) 67 | 68 | test('Decode url components', t => { 69 | t.plan(18) 70 | 71 | const findMyWay = FindMyWay() 72 | 73 | findMyWay.on('GET', '/:param1', () => {}) 74 | t.assert.deepEqual(findMyWay.find('GET', '/foo%23bar').params, { param1: 'foo#bar' }) 75 | t.assert.deepEqual(findMyWay.find('GET', '/foo%24bar').params, { param1: 'foo$bar' }) 76 | t.assert.deepEqual(findMyWay.find('GET', '/foo%26bar').params, { param1: 'foo&bar' }) 77 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2bbar').params, { param1: 'foo+bar' }) 78 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2Bbar').params, { param1: 'foo+bar' }) 79 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2cbar').params, { param1: 'foo,bar' }) 80 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2Cbar').params, { param1: 'foo,bar' }) 81 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2fbar').params, { param1: 'foo/bar' }) 82 | t.assert.deepEqual(findMyWay.find('GET', '/foo%2Fbar').params, { param1: 'foo/bar' }) 83 | 84 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3abar').params, { param1: 'foo:bar' }) 85 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3Abar').params, { param1: 'foo:bar' }) 86 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3bbar').params, { param1: 'foo;bar' }) 87 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3Bbar').params, { param1: 'foo;bar' }) 88 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3dbar').params, { param1: 'foo=bar' }) 89 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3Dbar').params, { param1: 'foo=bar' }) 90 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3fbar').params, { param1: 'foo?bar' }) 91 | t.assert.deepEqual(findMyWay.find('GET', '/foo%3Fbar').params, { param1: 'foo?bar' }) 92 | 93 | t.assert.deepEqual(findMyWay.find('GET', '/foo%40bar').params, { param1: 'foo@bar' }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/issue-238.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Multi-parametric tricky path', t => { 7 | t.plan(6) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: () => t.assert.fail('Should not be defaultRoute') 10 | }) 11 | 12 | findMyWay.on('GET', '/:param1-static-:param2', () => {}) 13 | 14 | t.assert.deepEqual( 15 | findMyWay.find('GET', '/param1-static-param2', {}).params, 16 | { param1: 'param1', param2: 'param2' } 17 | ) 18 | t.assert.deepEqual( 19 | findMyWay.find('GET', '/param1.1-param1.2-static-param2.1-param2.2', {}).params, 20 | { param1: 'param1.1-param1.2', param2: 'param2.1-param2.2' } 21 | ) 22 | t.assert.deepEqual( 23 | findMyWay.find('GET', '/param1-1-param1-2-static-param2-1-param2-2', {}).params, 24 | { param1: 'param1-1-param1-2', param2: 'param2-1-param2-2' } 25 | ) 26 | t.assert.deepEqual( 27 | findMyWay.find('GET', '/static-static-static', {}).params, 28 | { param1: 'static', param2: 'static' } 29 | ) 30 | t.assert.deepEqual( 31 | findMyWay.find('GET', '/static-static-static-static', {}).params, 32 | { param1: 'static', param2: 'static-static' } 33 | ) 34 | t.assert.deepEqual( 35 | findMyWay.find('GET', '/static-static1-static-static', {}).params, 36 | { param1: 'static-static1', param2: 'static' } 37 | ) 38 | }) 39 | 40 | test('Multi-parametric nodes with different static ending 1', t => { 41 | t.plan(4) 42 | const findMyWay = FindMyWay({ 43 | defaultRoute: () => t.assert.fail('Should not be defaultRoute') 44 | }) 45 | 46 | const paramHandler = () => {} 47 | const multiParamHandler = () => {} 48 | 49 | findMyWay.on('GET', '/v1/foo/:code', paramHandler) 50 | findMyWay.on('GET', '/v1/foo/:code.png', multiParamHandler) 51 | 52 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello', {}).handler, paramHandler) 53 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello', {}).params, { code: 'hello' }) 54 | 55 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png', {}).handler, multiParamHandler) 56 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png', {}).params, { code: 'hello' }) 57 | }) 58 | 59 | test('Multi-parametric nodes with different static ending 2', t => { 60 | t.plan(4) 61 | const findMyWay = FindMyWay({ 62 | defaultRoute: () => t.assert.fail('Should not be defaultRoute') 63 | }) 64 | 65 | const jpgHandler = () => {} 66 | const pngHandler = () => {} 67 | 68 | findMyWay.on('GET', '/v1/foo/:code.jpg', jpgHandler) 69 | findMyWay.on('GET', '/v1/foo/:code.png', pngHandler) 70 | 71 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg', {}).handler, jpgHandler) 72 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg', {}).params, { code: 'hello' }) 73 | 74 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png', {}).handler, pngHandler) 75 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png', {}).params, { code: 'hello' }) 76 | }) 77 | 78 | test('Multi-parametric nodes with different static ending 3', t => { 79 | t.plan(4) 80 | const findMyWay = FindMyWay({ 81 | defaultRoute: () => t.assert.fail('Should not be defaultRoute') 82 | }) 83 | 84 | const jpgHandler = () => {} 85 | const pngHandler = () => {} 86 | 87 | findMyWay.on('GET', '/v1/foo/:code.jpg/bar', jpgHandler) 88 | findMyWay.on('GET', '/v1/foo/:code.png/bar', pngHandler) 89 | 90 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg/bar', {}).handler, jpgHandler) 91 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg/bar', {}).params, { code: 'hello' }) 92 | 93 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png/bar', {}).handler, pngHandler) 94 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png/bar', {}).params, { code: 'hello' }) 95 | }) 96 | 97 | test('Multi-parametric nodes with different static ending 4', t => { 98 | t.plan(6) 99 | const findMyWay = FindMyWay({ 100 | defaultRoute: () => t.assert.fail('Should not be defaultRoute') 101 | }) 102 | 103 | const handler = () => {} 104 | const jpgHandler = () => {} 105 | const pngHandler = () => {} 106 | 107 | findMyWay.on('GET', '/v1/foo/:code/bar', handler) 108 | findMyWay.on('GET', '/v1/foo/:code.jpg/bar', jpgHandler) 109 | findMyWay.on('GET', '/v1/foo/:code.png/bar', pngHandler) 110 | 111 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello/bar', {}).handler, handler) 112 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello/bar', {}).params, { code: 'hello' }) 113 | 114 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg/bar', {}).handler, jpgHandler) 115 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.jpg/bar', {}).params, { code: 'hello' }) 116 | 117 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png/bar', {}).handler, pngHandler) 118 | t.assert.deepEqual(findMyWay.find('GET', '/v1/foo/hello.png/bar', {}).params, { code: 'hello' }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/issue-240.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('issue-240: .find matching', (t) => { 7 | t.plan(14) 8 | 9 | const findMyWay = FindMyWay({ ignoreDuplicateSlashes: true }) 10 | 11 | const fixedPath = function staticPath () {} 12 | const varPath = function parameterPath () {} 13 | findMyWay.on('GET', '/a/b', fixedPath) 14 | findMyWay.on('GET', '/a/:pam/c', varPath) 15 | 16 | t.assert.equal(findMyWay.find('GET', '/a/b').handler, fixedPath) 17 | t.assert.equal(findMyWay.find('GET', '/a//b').handler, fixedPath) 18 | t.assert.equal(findMyWay.find('GET', '/a/b/c').handler, varPath) 19 | t.assert.equal(findMyWay.find('GET', '/a//b/c').handler, varPath) 20 | t.assert.equal(findMyWay.find('GET', '/a///b/c').handler, varPath) 21 | t.assert.equal(findMyWay.find('GET', '/a//b//c').handler, varPath) 22 | t.assert.equal(findMyWay.find('GET', '/a///b///c').handler, varPath) 23 | t.assert.equal(findMyWay.find('GET', '/a/foo/c').handler, varPath) 24 | t.assert.equal(findMyWay.find('GET', '/a//foo/c').handler, varPath) 25 | t.assert.equal(findMyWay.find('GET', '/a///foo/c').handler, varPath) 26 | t.assert.equal(findMyWay.find('GET', '/a//foo//c').handler, varPath) 27 | t.assert.equal(findMyWay.find('GET', '/a///foo///c').handler, varPath) 28 | t.assert.ok(!findMyWay.find('GET', '/a/c')) 29 | t.assert.ok(!findMyWay.find('GET', '/a//c')) 30 | }) 31 | -------------------------------------------------------------------------------- /test/issue-241.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Double colon and parametric children', t => { 7 | t.plan(2) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/::articles', () => {}) 11 | findMyWay.on('GET', '/:article_name', () => {}) 12 | 13 | t.assert.deepEqual(findMyWay.find('GET', '/:articles').params, {}) 14 | t.assert.deepEqual(findMyWay.find('GET', '/articles_param').params, { article_name: 'articles_param' }) 15 | }) 16 | 17 | test('Double colon and parametric children', t => { 18 | t.plan(2) 19 | const findMyWay = FindMyWay() 20 | 21 | findMyWay.on('GET', '/::test::foo/:param/::articles', () => {}) 22 | findMyWay.on('GET', '/::test::foo/:param/:article_name', () => {}) 23 | 24 | t.assert.deepEqual( 25 | findMyWay.find('GET', '/:test:foo/param_value1/:articles').params, 26 | { param: 'param_value1' } 27 | ) 28 | t.assert.deepEqual( 29 | findMyWay.find('GET', '/:test:foo/param_value2/articles_param').params, 30 | { param: 'param_value2', article_name: 'articles_param' } 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /test/issue-247.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('If there are constraints param, router.off method support filter', t => { 7 | t.plan(12) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/a', { constraints: { host: '1' } }, () => {}, { name: 1 }) 11 | findMyWay.on('GET', '/a', { constraints: { host: '2', version: '1.0.0' } }, () => {}, { name: 2 }) 12 | findMyWay.on('GET', '/a', { constraints: { host: '2', version: '2.0.0' } }, () => {}, { name: 3 }) 13 | 14 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }).store, { name: 1 }) 15 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '1.0.0' }).store, { name: 2 }) 16 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '2.0.0' }).store, { name: 3 }) 17 | 18 | findMyWay.off('GET', '/a', { host: '1' }) 19 | 20 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }), null) 21 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '1.0.0' }).store, { name: 2 }) 22 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '2.0.0' }).store, { name: 3 }) 23 | 24 | findMyWay.off('GET', '/a', { host: '2', version: '1.0.0' }) 25 | 26 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }), null) 27 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '1.0.0' }), null) 28 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '2.0.0' }).store, { name: 3 }) 29 | 30 | findMyWay.off('GET', '/a', { host: '2', version: '2.0.0' }) 31 | 32 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }), null) 33 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '1.0.0' }), null) 34 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2', version: '2.0.0' }), null) 35 | }) 36 | 37 | test('If there are no constraints param, router.off method remove all matched router', t => { 38 | t.plan(4) 39 | const findMyWay = FindMyWay() 40 | 41 | findMyWay.on('GET', '/a', { constraints: { host: '1' } }, () => {}, { name: 1 }) 42 | findMyWay.on('GET', '/a', { constraints: { host: '2' } }, () => {}, { name: 2 }) 43 | 44 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }).store, { name: 1 }) 45 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2' }).store, { name: 2 }) 46 | 47 | findMyWay.off('GET', '/a') 48 | 49 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '1' }), null) 50 | t.assert.deepEqual(findMyWay.find('GET', '/a', { host: '2' }), null) 51 | }) 52 | -------------------------------------------------------------------------------- /test/issue-254.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Constraints should not be overrided when multiple router is created', t => { 7 | t.plan(1) 8 | 9 | const constraint = { 10 | name: 'secret', 11 | storage: function () { 12 | const secrets = {} 13 | return { 14 | get: (secret) => { return secrets[secret] || null }, 15 | set: (secret, store) => { secrets[secret] = store } 16 | } 17 | }, 18 | deriveConstraint: (req, ctx) => { 19 | return req.headers['x-secret'] 20 | }, 21 | validate () { return true } 22 | } 23 | 24 | const router1 = FindMyWay({ constraints: { secret: constraint } }) 25 | FindMyWay() 26 | 27 | router1.on('GET', '/', { constraints: { secret: 'alpha' } }, () => {}) 28 | router1.find('GET', '/', { secret: 'alpha' }) 29 | 30 | t.assert.ok('constraints is not overrided') 31 | }) 32 | -------------------------------------------------------------------------------- /test/issue-280.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Wildcard route match when regexp route fails', (t) => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/:a(a)', () => {}) 11 | findMyWay.on('GET', '/*', () => {}) 12 | 13 | t.assert.deepEqual(findMyWay.find('GET', '/b', {}).params, { '*': 'b' }) 14 | }) 15 | -------------------------------------------------------------------------------- /test/issue-285.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Parametric regex match with similar routes', (t) => { 7 | t.plan(2) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/:a(a)', () => {}) 11 | findMyWay.on('GET', '/:param/static', () => {}) 12 | 13 | t.assert.deepEqual(findMyWay.find('GET', '/a', {}).params, { a: 'a' }) 14 | t.assert.deepEqual(findMyWay.find('GET', '/param/static', {}).params, { param: 'param' }) 15 | }) 16 | 17 | test('Parametric regex match with similar routes', (t) => { 18 | t.plan(2) 19 | const findMyWay = FindMyWay() 20 | 21 | findMyWay.on('GET', '/:a(a)', () => {}) 22 | findMyWay.on('GET', '/:b(b)/static', () => {}) 23 | 24 | t.assert.deepEqual(findMyWay.find('GET', '/a', {}).params, { a: 'a' }) 25 | t.assert.deepEqual(findMyWay.find('GET', '/b/static', {}).params, { b: 'b' }) 26 | }) 27 | 28 | test('Parametric regex match with similar routes', (t) => { 29 | t.plan(2) 30 | const findMyWay = FindMyWay() 31 | 32 | findMyWay.on('GET', '/:a(a)/static', { constraints: { version: '1.0.0' } }, () => {}) 33 | findMyWay.on('GET', '/:b(b)/static', { constraints: { version: '2.0.0' } }, () => {}) 34 | 35 | t.assert.deepEqual(findMyWay.find('GET', '/a/static', { version: '1.0.0' }).params, { a: 'a' }) 36 | t.assert.deepEqual(findMyWay.find('GET', '/b/static', { version: '2.0.0' }).params, { b: 'b' }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/issue-330.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('node:test') 2 | const FindMyWay = require('..') 3 | const proxyquire = require('proxyquire') 4 | const HandlerStorage = require('../lib/handler-storage') 5 | const Constrainer = require('../lib/constrainer') 6 | const { safeDecodeURIComponent } = require('../lib/url-sanitizer') 7 | const acceptVersionStrategy = require('../lib/strategies/accept-version') 8 | const httpMethodStrategy = require('../lib/strategies/http-method') 9 | 10 | test('FULL_PATH_REGEXP and OPTIONAL_PARAM_REGEXP should be considered safe', (t) => { 11 | t.plan(1) 12 | 13 | t.assert.doesNotThrow(() => require('..')) 14 | }) 15 | 16 | test('should throw an error for unsafe FULL_PATH_REGEXP', (t) => { 17 | t.plan(1) 18 | 19 | t.assert.throws(() => proxyquire('..', { 20 | 'safe-regex2': () => false 21 | }), new Error('the FULL_PATH_REGEXP is not safe, update this module')) 22 | }) 23 | 24 | test('Should throw an error for unsafe OPTIONAL_PARAM_REGEXP', (t) => { 25 | t.plan(1) 26 | 27 | let callCount = 0 28 | t.assert.throws(() => proxyquire('..', { 29 | 'safe-regex2': () => { 30 | return ++callCount < 2 31 | } 32 | }), new Error('the OPTIONAL_PARAM_REGEXP is not safe, update this module')) 33 | }) 34 | 35 | test('double colon does not define parametric node', (t) => { 36 | t.plan(2) 37 | 38 | const findMyWay = FindMyWay() 39 | 40 | findMyWay.on('GET', '/::id', () => {}) 41 | const route1 = findMyWay.findRoute('GET', '/::id') 42 | t.assert.deepStrictEqual(route1.params, []) 43 | 44 | findMyWay.on('GET', '/:foo(\\d+)::bar', () => {}) 45 | const route2 = findMyWay.findRoute('GET', '/:foo(\\d+)::bar') 46 | t.assert.deepStrictEqual(route2.params, ['foo']) 47 | }) 48 | 49 | test('case insensitive static routes', (t) => { 50 | t.plan(3) 51 | 52 | const findMyWay = FindMyWay({ 53 | caseSensitive: false 54 | }) 55 | 56 | findMyWay.on('GET', '/foo', () => {}) 57 | findMyWay.on('GET', '/foo/bar', () => {}) 58 | findMyWay.on('GET', '/foo/bar/baz', () => {}) 59 | 60 | t.assert.ok(findMyWay.findRoute('GET', '/FoO')) 61 | t.assert.ok(findMyWay.findRoute('GET', '/FOo/Bar')) 62 | t.assert.ok(findMyWay.findRoute('GET', '/fOo/Bar/bAZ')) 63 | }) 64 | 65 | test('wildcard must be the last character in the route', (t) => { 66 | t.plan(3) 67 | 68 | const expectedError = new Error('Wildcard must be the last character in the route') 69 | 70 | const findMyWay = FindMyWay() 71 | 72 | findMyWay.on('GET', '*', () => {}) 73 | t.assert.throws(() => findMyWay.findRoute('GET', '*1'), expectedError) 74 | t.assert.throws(() => findMyWay.findRoute('GET', '*/'), expectedError) 75 | t.assert.throws(() => findMyWay.findRoute('GET', '*?'), expectedError) 76 | }) 77 | 78 | test('does not find the route if maxParamLength is exceeded', t => { 79 | t.plan(2) 80 | const findMyWay = FindMyWay({ 81 | maxParamLength: 2 82 | }) 83 | 84 | findMyWay.on('GET', '/:id(\\d+)', () => {}) 85 | 86 | t.assert.equal(findMyWay.find('GET', '/123'), null) 87 | t.assert.ok(findMyWay.find('GET', '/12')) 88 | }) 89 | 90 | test('Should check if a regex is safe to use', (t) => { 91 | t.plan(1) 92 | 93 | const findMyWay = FindMyWay() 94 | 95 | // we must pass a safe regex to register the route 96 | // findRoute will still throws the expected assertion error if we try to access it with unsafe reggex 97 | findMyWay.on('GET', '/test/:id(\\d+)', () => {}) 98 | 99 | const unSafeRegex = /(x+x+)+y/ 100 | t.assert.throws(() => findMyWay.findRoute('GET', `/test/:id(${unSafeRegex.toString()})`), { 101 | message: "The regex '(/(x+x+)+y/)' is not safe!" 102 | }) 103 | }) 104 | 105 | test('Disable safe regex check', (t) => { 106 | t.plan(1) 107 | 108 | const findMyWay = FindMyWay({ allowUnsafeRegex: true }) 109 | 110 | const unSafeRegex = /(x+x+)+y/ 111 | findMyWay.on('GET', `/test2/:id(${unSafeRegex.toString()})`, () => {}) 112 | t.assert.doesNotThrow(() => findMyWay.findRoute('GET', `/test2/:id(${unSafeRegex.toString()})`)) 113 | }) 114 | 115 | test('throws error if no strategy registered for constraint key', (t) => { 116 | t.plan(2) 117 | 118 | const constrainer = new Constrainer() 119 | const error = new Error('No strategy registered for constraint key invalid-constraint') 120 | t.assert.throws(() => constrainer.newStoreForConstraint('invalid-constraint'), error) 121 | t.assert.throws(() => constrainer.validateConstraints({ 'invalid-constraint': 'foo' }), error) 122 | }) 123 | 124 | test('throws error if pass an undefined constraint value', (t) => { 125 | t.plan(1) 126 | 127 | const constrainer = new Constrainer() 128 | const error = new Error('Can\'t pass an undefined constraint value, must pass null or no key at all') 129 | t.assert.throws(() => constrainer.validateConstraints({ key: undefined }), error) 130 | }) 131 | 132 | test('Constrainer.noteUsage', (t) => { 133 | t.plan(3) 134 | 135 | const constrainer = new Constrainer() 136 | t.assert.equal(constrainer.strategiesInUse.size, 0) 137 | 138 | constrainer.noteUsage() 139 | t.assert.equal(constrainer.strategiesInUse.size, 0) 140 | 141 | constrainer.noteUsage({ host: 'fastify.io' }) 142 | t.assert.equal(constrainer.strategiesInUse.size, 1) 143 | }) 144 | 145 | test('Cannot derive constraints without active strategies.', (t) => { 146 | t.plan(1) 147 | 148 | const constrainer = new Constrainer() 149 | const before = constrainer.deriveSyncConstraints 150 | constrainer._buildDeriveConstraints() 151 | t.assert.deepEqual(constrainer.deriveSyncConstraints, before) 152 | }) 153 | 154 | test('getMatchingHandler should return null if not compiled', (t) => { 155 | t.plan(1) 156 | 157 | const handlerStorage = new HandlerStorage() 158 | t.assert.equal(handlerStorage.getMatchingHandler({ foo: 'bar' }), null) 159 | }) 160 | 161 | test('safeDecodeURIComponent should replace %3x to null for every x that is not a valid lowchar', (t) => { 162 | t.plan(1) 163 | 164 | t.assert.equal(safeDecodeURIComponent('Hello%3xWorld'), 'HellonullWorld') 165 | }) 166 | 167 | test('SemVerStore version should be a string', (t) => { 168 | t.plan(1) 169 | 170 | const Storage = acceptVersionStrategy.storage 171 | 172 | t.assert.throws(() => new Storage().set(1), new TypeError('Version should be a string')) 173 | }) 174 | 175 | test('SemVerStore.maxMajor should increase automatically', (t) => { 176 | t.plan(3) 177 | 178 | const Storage = acceptVersionStrategy.storage 179 | const storage = new Storage() 180 | 181 | t.assert.equal(storage.maxMajor, 0) 182 | 183 | storage.set('2') 184 | t.assert.equal(storage.maxMajor, 2) 185 | 186 | storage.set('1') 187 | t.assert.equal(storage.maxMajor, 2) 188 | }) 189 | 190 | test('SemVerStore.maxPatches should increase automatically', (t) => { 191 | t.plan(3) 192 | 193 | const Storage = acceptVersionStrategy.storage 194 | const storage = new Storage() 195 | 196 | storage.set('2.0.0') 197 | t.assert.deepEqual(storage.maxPatches, { '2.0': 0 }) 198 | 199 | storage.set('2.0.2') 200 | t.assert.deepEqual(storage.maxPatches, { '2.0': 2 }) 201 | 202 | storage.set('2.0.1') 203 | t.assert.deepEqual(storage.maxPatches, { '2.0': 2 }) 204 | }) 205 | 206 | test('Major version must be a numeric value', t => { 207 | t.plan(1) 208 | 209 | const findMyWay = FindMyWay() 210 | 211 | t.assert.throws(() => findMyWay.on('GET', '/test', { constraints: { version: 'x' } }, () => {}), 212 | new TypeError('Major version must be a numeric value')) 213 | }) 214 | 215 | test('httpMethodStrategy storage handles set and get operations correctly', (t) => { 216 | t.plan(2) 217 | 218 | const storage = httpMethodStrategy.storage() 219 | 220 | t.assert.equal(storage.get('foo'), null) 221 | 222 | storage.set('foo', { bar: 'baz' }) 223 | t.assert.deepStrictEqual(storage.get('foo'), { bar: 'baz' }) 224 | }) 225 | 226 | test('if buildPrettyMeta argument is undefined, will return an object', (t) => { 227 | t.plan(1) 228 | 229 | const findMyWay = FindMyWay() 230 | t.assert.deepEqual(findMyWay.buildPrettyMeta(), {}) 231 | }) 232 | -------------------------------------------------------------------------------- /test/issue-44.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Parametric and static with shared prefix / 1', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/woo', (req, res, params) => { 15 | t.assert.fail('we should not be here') 16 | }) 17 | 18 | findMyWay.on('GET', '/:param', (req, res, params) => { 19 | t.assert.equal(params.param, 'winter') 20 | }) 21 | 22 | findMyWay.lookup({ method: 'GET', url: '/winter', headers: {} }, null) 23 | }) 24 | 25 | test('Parametric and static with shared prefix / 2', t => { 26 | t.plan(1) 27 | const findMyWay = FindMyWay({ 28 | defaultRoute: (req, res) => { 29 | t.assert.fail('Should not be defaultRoute') 30 | } 31 | }) 32 | 33 | findMyWay.on('GET', '/woo', (req, res, params) => { 34 | t.assert.ok('we should be here') 35 | }) 36 | 37 | findMyWay.on('GET', '/:param', (req, res, params) => { 38 | t.assert.fail('we should not be here') 39 | }) 40 | 41 | findMyWay.lookup({ method: 'GET', url: '/woo', headers: {} }, null) 42 | }) 43 | 44 | test('Parametric and static with shared prefix (nested)', t => { 45 | t.plan(1) 46 | const findMyWay = FindMyWay({ 47 | defaultRoute: (req, res) => { 48 | t.assert.ok('We should be here') 49 | } 50 | }) 51 | 52 | findMyWay.on('GET', '/woo', (req, res, params) => { 53 | t.assert.fail('we should not be here') 54 | }) 55 | 56 | findMyWay.on('GET', '/:param', (req, res, params) => { 57 | t.assert.fail('we should not be here') 58 | }) 59 | 60 | findMyWay.lookup({ method: 'GET', url: '/winter/coming', headers: {} }, null) 61 | }) 62 | 63 | test('Parametric and static with shared prefix and different suffix', t => { 64 | t.plan(1) 65 | const findMyWay = FindMyWay({ 66 | defaultRoute: (req, res) => { 67 | t.assert.fail('We should not be here') 68 | } 69 | }) 70 | 71 | findMyWay.on('GET', '/example/shared/nested/test', (req, res, params) => { 72 | t.assert.fail('We should not be here') 73 | }) 74 | 75 | findMyWay.on('GET', '/example/:param/nested/other', (req, res, params) => { 76 | t.assert.ok('We should be here') 77 | }) 78 | 79 | findMyWay.lookup({ method: 'GET', url: '/example/shared/nested/other', headers: {} }, null) 80 | }) 81 | 82 | test('Parametric and static with shared prefix (with wildcard)', t => { 83 | t.plan(1) 84 | const findMyWay = FindMyWay({ 85 | defaultRoute: (req, res) => { 86 | t.assert.fail('Should not be defaultRoute') 87 | } 88 | }) 89 | 90 | findMyWay.on('GET', '/woo', (req, res, params) => { 91 | t.assert.fail('we should not be here') 92 | }) 93 | 94 | findMyWay.on('GET', '/:param', (req, res, params) => { 95 | t.assert.equal(params.param, 'winter') 96 | }) 97 | 98 | findMyWay.on('GET', '/*', (req, res, params) => { 99 | t.assert.fail('we should not be here') 100 | }) 101 | 102 | findMyWay.lookup({ method: 'GET', url: '/winter', headers: {} }, null) 103 | }) 104 | 105 | test('Parametric and static with shared prefix (nested with wildcard)', t => { 106 | t.plan(1) 107 | const findMyWay = FindMyWay({ 108 | defaultRoute: (req, res) => { 109 | t.assert.fail('Should not be defaultRoute') 110 | } 111 | }) 112 | 113 | findMyWay.on('GET', '/woo', (req, res, params) => { 114 | t.assert.fail('we should not be here') 115 | }) 116 | 117 | findMyWay.on('GET', '/:param', (req, res, params) => { 118 | t.assert.fail('we should not be here') 119 | }) 120 | 121 | findMyWay.on('GET', '/*', (req, res, params) => { 122 | t.assert.equal(params['*'], 'winter/coming') 123 | }) 124 | 125 | findMyWay.lookup({ method: 'GET', url: '/winter/coming', headers: {} }, null) 126 | }) 127 | 128 | test('Parametric and static with shared prefix (nested with split)', t => { 129 | t.plan(1) 130 | const findMyWay = FindMyWay({ 131 | defaultRoute: (req, res) => { 132 | t.assert.fail('we should not be here') 133 | } 134 | }) 135 | 136 | findMyWay.on('GET', '/woo', (req, res, params) => { 137 | t.assert.fail('we should not be here') 138 | }) 139 | 140 | findMyWay.on('GET', '/:param', (req, res, params) => { 141 | t.assert.equal(params.param, 'winter') 142 | }) 143 | 144 | findMyWay.on('GET', '/wo', (req, res, params) => { 145 | t.assert.fail('we should not be here') 146 | }) 147 | 148 | findMyWay.lookup({ method: 'GET', url: '/winter', headers: {} }, null) 149 | }) 150 | -------------------------------------------------------------------------------- /test/issue-46.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('If the prefixLen is higher than the pathLen we should not save the wildcard child', t => { 7 | t.plan(3) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.get('/static/*', () => {}) 15 | 16 | t.assert.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) 17 | t.assert.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) 18 | t.assert.deepEqual(findMyWay.find('GET', '/static'), null) 19 | }) 20 | 21 | test('If the prefixLen is higher than the pathLen we should not save the wildcard child (mixed routes)', t => { 22 | t.plan(3) 23 | const findMyWay = FindMyWay({ 24 | defaultRoute: (req, res) => { 25 | t.assert.fail('Should not be defaultRoute') 26 | } 27 | }) 28 | 29 | findMyWay.get('/static/*', () => {}) 30 | findMyWay.get('/simple', () => {}) 31 | findMyWay.get('/simple/:bar', () => {}) 32 | findMyWay.get('/hello', () => {}) 33 | 34 | t.assert.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) 35 | t.assert.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) 36 | t.assert.deepEqual(findMyWay.find('GET', '/static'), null) 37 | }) 38 | 39 | test('If the prefixLen is higher than the pathLen we should not save the wildcard child (with a root wildcard)', t => { 40 | t.plan(3) 41 | const findMyWay = FindMyWay({ 42 | defaultRoute: (req, res) => { 43 | t.assert.fail('Should not be defaultRoute') 44 | } 45 | }) 46 | 47 | findMyWay.get('*', () => {}) 48 | findMyWay.get('/static/*', () => {}) 49 | findMyWay.get('/simple', () => {}) 50 | findMyWay.get('/simple/:bar', () => {}) 51 | findMyWay.get('/hello', () => {}) 52 | 53 | t.assert.deepEqual(findMyWay.find('GET', '/static/').params, { '*': '' }) 54 | t.assert.deepEqual(findMyWay.find('GET', '/static/hello').params, { '*': 'hello' }) 55 | t.assert.deepEqual(findMyWay.find('GET', '/static').params, { '*': '/static' }) 56 | }) 57 | 58 | test('If the prefixLen is higher than the pathLen we should not save the wildcard child (404)', t => { 59 | t.plan(4) 60 | const findMyWay = FindMyWay({ 61 | defaultRoute: (req, res) => { 62 | t.assert.fail('Should not be defaultRoute') 63 | } 64 | }) 65 | 66 | findMyWay.get('/static/*', () => {}) 67 | findMyWay.get('/simple', () => {}) 68 | findMyWay.get('/simple/:bar', () => {}) 69 | findMyWay.get('/hello', () => {}) 70 | 71 | t.assert.deepEqual(findMyWay.find('GET', '/stati'), null) 72 | t.assert.deepEqual(findMyWay.find('GET', '/staticc'), null) 73 | t.assert.deepEqual(findMyWay.find('GET', '/stati/hello'), null) 74 | t.assert.deepEqual(findMyWay.find('GET', '/staticc/hello'), null) 75 | }) 76 | -------------------------------------------------------------------------------- /test/issue-49.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | const noop = () => {} 6 | 7 | test('Defining static route after parametric - 1', t => { 8 | t.plan(3) 9 | const findMyWay = FindMyWay() 10 | 11 | findMyWay.on('GET', '/static', noop) 12 | findMyWay.on('GET', '/:param', noop) 13 | 14 | t.assert.ok(findMyWay.find('GET', '/static')) 15 | t.assert.ok(findMyWay.find('GET', '/para')) 16 | t.assert.ok(findMyWay.find('GET', '/s')) 17 | }) 18 | 19 | test('Defining static route after parametric - 2', t => { 20 | t.plan(3) 21 | const findMyWay = FindMyWay() 22 | 23 | findMyWay.on('GET', '/:param', noop) 24 | findMyWay.on('GET', '/static', noop) 25 | 26 | t.assert.ok(findMyWay.find('GET', '/static')) 27 | t.assert.ok(findMyWay.find('GET', '/para')) 28 | t.assert.ok(findMyWay.find('GET', '/s')) 29 | }) 30 | 31 | test('Defining static route after parametric - 3', t => { 32 | t.plan(4) 33 | const findMyWay = FindMyWay() 34 | 35 | findMyWay.on('GET', '/:param', noop) 36 | findMyWay.on('GET', '/static', noop) 37 | findMyWay.on('GET', '/other', noop) 38 | 39 | t.assert.ok(findMyWay.find('GET', '/static')) 40 | t.assert.ok(findMyWay.find('GET', '/para')) 41 | t.assert.ok(findMyWay.find('GET', '/s')) 42 | t.assert.ok(findMyWay.find('GET', '/o')) 43 | }) 44 | 45 | test('Defining static route after parametric - 4', t => { 46 | t.plan(4) 47 | const findMyWay = FindMyWay() 48 | 49 | findMyWay.on('GET', '/static', noop) 50 | findMyWay.on('GET', '/other', noop) 51 | findMyWay.on('GET', '/:param', noop) 52 | 53 | t.assert.ok(findMyWay.find('GET', '/static')) 54 | t.assert.ok(findMyWay.find('GET', '/para')) 55 | t.assert.ok(findMyWay.find('GET', '/s')) 56 | t.assert.ok(findMyWay.find('GET', '/o')) 57 | }) 58 | 59 | test('Defining static route after parametric - 5', t => { 60 | t.plan(4) 61 | const findMyWay = FindMyWay() 62 | 63 | findMyWay.on('GET', '/static', noop) 64 | findMyWay.on('GET', '/:param', noop) 65 | findMyWay.on('GET', '/other', noop) 66 | 67 | t.assert.ok(findMyWay.find('GET', '/static')) 68 | t.assert.ok(findMyWay.find('GET', '/para')) 69 | t.assert.ok(findMyWay.find('GET', '/s')) 70 | t.assert.ok(findMyWay.find('GET', '/o')) 71 | }) 72 | 73 | test('Should produce the same tree - 1', t => { 74 | t.plan(1) 75 | const findMyWay1 = FindMyWay() 76 | const findMyWay2 = FindMyWay() 77 | 78 | findMyWay1.on('GET', '/static', noop) 79 | findMyWay1.on('GET', '/:param', noop) 80 | 81 | findMyWay2.on('GET', '/:param', noop) 82 | findMyWay2.on('GET', '/static', noop) 83 | 84 | t.assert.equal(findMyWay1.tree, findMyWay2.tree) 85 | }) 86 | 87 | test('Should produce the same tree - 2', t => { 88 | t.plan(3) 89 | const findMyWay1 = FindMyWay() 90 | const findMyWay2 = FindMyWay() 91 | const findMyWay3 = FindMyWay() 92 | 93 | findMyWay1.on('GET', '/:param', noop) 94 | findMyWay1.on('GET', '/static', noop) 95 | findMyWay1.on('GET', '/other', noop) 96 | 97 | findMyWay2.on('GET', '/static', noop) 98 | findMyWay2.on('GET', '/:param', noop) 99 | findMyWay2.on('GET', '/other', noop) 100 | 101 | findMyWay3.on('GET', '/static', noop) 102 | findMyWay3.on('GET', '/other', noop) 103 | findMyWay3.on('GET', '/:param', noop) 104 | 105 | t.assert.equal(findMyWay1.tree, findMyWay2.tree) 106 | t.assert.equal(findMyWay2.tree, findMyWay3.tree) 107 | t.assert.equal(findMyWay1.tree, findMyWay3.tree) 108 | }) 109 | -------------------------------------------------------------------------------- /test/issue-59.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | const noop = () => {} 6 | 7 | test('single-character prefix', t => { 8 | t.plan(1) 9 | const findMyWay = FindMyWay() 10 | 11 | findMyWay.on('GET', '/b/', noop) 12 | findMyWay.on('GET', '/b/bulk', noop) 13 | 14 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 15 | }) 16 | 17 | test('multi-character prefix', t => { 18 | t.plan(1) 19 | const findMyWay = FindMyWay() 20 | 21 | findMyWay.on('GET', '/bu/', noop) 22 | findMyWay.on('GET', '/bu/bulk', noop) 23 | 24 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 25 | }) 26 | 27 | test('static / 1', t => { 28 | t.plan(1) 29 | const findMyWay = FindMyWay() 30 | 31 | findMyWay.on('GET', '/bb/', noop) 32 | findMyWay.on('GET', '/bb/bulk', noop) 33 | 34 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 35 | }) 36 | 37 | test('static / 2', t => { 38 | t.plan(2) 39 | const findMyWay = FindMyWay() 40 | 41 | findMyWay.on('GET', '/bb/ff/', noop) 42 | findMyWay.on('GET', '/bb/ff/bulk', noop) 43 | 44 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 45 | t.assert.equal(findMyWay.find('GET', '/ff/bulk'), null) 46 | }) 47 | 48 | test('static / 3', t => { 49 | t.plan(1) 50 | const findMyWay = FindMyWay() 51 | 52 | findMyWay.on('GET', '/bb/ff/', noop) 53 | findMyWay.on('GET', '/bb/ff/bulk', noop) 54 | findMyWay.on('GET', '/bb/ff/gg/bulk', noop) 55 | findMyWay.on('GET', '/bb/ff/bulk/bulk', noop) 56 | 57 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 58 | }) 59 | 60 | test('with parameter / 1', t => { 61 | t.plan(1) 62 | const findMyWay = FindMyWay() 63 | 64 | findMyWay.on('GET', '/:foo/', noop) 65 | findMyWay.on('GET', '/:foo/bulk', noop) 66 | 67 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 68 | }) 69 | 70 | test('with parameter / 2', t => { 71 | t.plan(1) 72 | const findMyWay = FindMyWay() 73 | 74 | findMyWay.on('GET', '/bb/', noop) 75 | findMyWay.on('GET', '/bb/:foo', noop) 76 | 77 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 78 | }) 79 | 80 | test('with parameter / 3', t => { 81 | t.plan(1) 82 | const findMyWay = FindMyWay() 83 | 84 | findMyWay.on('GET', '/bb/ff/', noop) 85 | findMyWay.on('GET', '/bb/ff/:foo', noop) 86 | 87 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 88 | }) 89 | 90 | test('with parameter / 4', t => { 91 | t.plan(1) 92 | const findMyWay = FindMyWay() 93 | 94 | findMyWay.on('GET', '/bb/:foo/', noop) 95 | findMyWay.on('GET', '/bb/:foo/bulk', noop) 96 | 97 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 98 | }) 99 | 100 | test('with parameter / 5', t => { 101 | t.plan(2) 102 | const findMyWay = FindMyWay() 103 | 104 | findMyWay.on('GET', '/bb/:foo/aa/', noop) 105 | findMyWay.on('GET', '/bb/:foo/aa/bulk', noop) 106 | 107 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 108 | t.assert.equal(findMyWay.find('GET', '/bb/foo/bulk'), null) 109 | }) 110 | 111 | test('with parameter / 6', t => { 112 | t.plan(3) 113 | const findMyWay = FindMyWay() 114 | 115 | findMyWay.on('GET', '/static/:parametric/static/:parametric', noop) 116 | findMyWay.on('GET', '/static/:parametric/static/:parametric/bulk', noop) 117 | 118 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 119 | t.assert.equal(findMyWay.find('GET', '/static/foo/bulk'), null) 120 | t.assert.notEqual(findMyWay.find('GET', '/static/foo/static/bulk'), null) 121 | }) 122 | 123 | test('wildcard / 1', t => { 124 | t.plan(1) 125 | const findMyWay = FindMyWay() 126 | 127 | findMyWay.on('GET', '/bb/', noop) 128 | findMyWay.on('GET', '/bb/*', noop) 129 | 130 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 131 | }) 132 | -------------------------------------------------------------------------------- /test/issue-62.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | const noop = function () {} 7 | 8 | test('issue-62', (t) => { 9 | t.plan(2) 10 | 11 | const findMyWay = FindMyWay({ allowUnsafeRegex: true }) 12 | 13 | findMyWay.on('GET', '/foo/:id(([a-f0-9]{3},?)+)', noop) 14 | 15 | t.assert.ok(!findMyWay.find('GET', '/foo/qwerty')) 16 | t.assert.ok(findMyWay.find('GET', '/foo/bac,1ea')) 17 | }) 18 | 19 | test('issue-62 - escape chars', (t) => { 20 | const findMyWay = FindMyWay() 21 | 22 | t.plan(2) 23 | 24 | findMyWay.get('/foo/:param(\\([a-f0-9]{3}\\))', noop) 25 | 26 | t.assert.ok(!findMyWay.find('GET', '/foo/abc')) 27 | t.assert.ok(findMyWay.find('GET', '/foo/(abc)', {})) 28 | }) 29 | -------------------------------------------------------------------------------- /test/issue-63.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const factory = require('../') 5 | 6 | const noop = function () {} 7 | 8 | test('issue-63', (t) => { 9 | t.plan(2) 10 | 11 | const fmw = factory() 12 | 13 | t.assert.throws(function () { 14 | fmw.on('GET', '/foo/:id(a', noop) 15 | }) 16 | 17 | try { 18 | fmw.on('GET', '/foo/:id(a', noop) 19 | t.assert.fail('should fail') 20 | } catch (err) { 21 | t.assert.equal(err.message, 'Invalid regexp expression in "/foo/:id(a"') 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/issue-67.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | const noop = () => {} 6 | 7 | test('static routes', t => { 8 | t.plan(1) 9 | const findMyWay = FindMyWay() 10 | 11 | findMyWay.on('GET', '/b/', noop) 12 | findMyWay.on('GET', '/b/bulk', noop) 13 | findMyWay.on('GET', '/b/ulk', noop) 14 | 15 | t.assert.equal(findMyWay.find('GET', '/bulk'), null) 16 | }) 17 | 18 | test('parametric routes', t => { 19 | t.plan(5) 20 | const findMyWay = FindMyWay() 21 | 22 | function foo () { } 23 | 24 | findMyWay.on('GET', '/foo/:fooParam', foo) 25 | findMyWay.on('GET', '/foo/bar/:barParam', noop) 26 | findMyWay.on('GET', '/foo/search', noop) 27 | findMyWay.on('GET', '/foo/submit', noop) 28 | 29 | t.assert.equal(findMyWay.find('GET', '/foo/awesome-parameter').handler, foo) 30 | t.assert.equal(findMyWay.find('GET', '/foo/b-first-character').handler, foo) 31 | t.assert.equal(findMyWay.find('GET', '/foo/s-first-character').handler, foo) 32 | t.assert.equal(findMyWay.find('GET', '/foo/se-prefix').handler, foo) 33 | t.assert.equal(findMyWay.find('GET', '/foo/sx-prefix').handler, foo) 34 | }) 35 | 36 | test('parametric with common prefix', t => { 37 | t.plan(1) 38 | const findMyWay = FindMyWay() 39 | 40 | findMyWay.on('GET', '/test', noop) 41 | findMyWay.on('GET', '/:test', (req, res, params) => { 42 | t.assert.deepEqual( 43 | { test: 'text' }, 44 | params 45 | ) 46 | }) 47 | findMyWay.on('GET', '/text/hello', noop) 48 | 49 | findMyWay.lookup({ url: '/text', method: 'GET', headers: {} }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/issue-93.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | const noop = () => {} 6 | 7 | test('Should keep semver store when split node', t => { 8 | t.plan(4) 9 | 10 | const findMyWay = FindMyWay() 11 | 12 | findMyWay.on('GET', '/t1', { constraints: { version: '1.0.0' } }, noop) 13 | findMyWay.on('GET', '/t2', { constraints: { version: '2.1.0' } }, noop) 14 | 15 | t.assert.ok(findMyWay.find('GET', '/t1', { version: '1.0.0' })) 16 | t.assert.ok(findMyWay.find('GET', '/t2', { version: '2.x' })) 17 | t.assert.ok(!findMyWay.find('GET', '/t1', { version: '2.x' })) 18 | t.assert.ok(!findMyWay.find('GET', '/t2', { version: '1.0.0' })) 19 | }) 20 | -------------------------------------------------------------------------------- /test/lookup-async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('should return result in the done callback', t => { 7 | t.plan(2) 8 | 9 | const router = FindMyWay() 10 | router.on('GET', '/', () => 'asyncHandlerResult') 11 | 12 | router.lookup({ method: 'GET', url: '/' }, null, (err, result) => { 13 | t.assert.equal(err, null) 14 | t.assert.equal(result, 'asyncHandlerResult') 15 | }) 16 | }) 17 | 18 | test('should return an error in the done callback', t => { 19 | t.plan(2) 20 | 21 | const router = FindMyWay() 22 | const error = new Error('ASYNC_HANDLER_ERROR') 23 | router.on('GET', '/', () => { throw error }) 24 | 25 | router.lookup({ method: 'GET', url: '/' }, null, (err, result) => { 26 | t.assert.equal(err, error) 27 | t.assert.equal(result, undefined) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/lookup.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('lookup calls route handler with no context', t => { 7 | t.plan(1) 8 | 9 | const findMyWay = FindMyWay() 10 | 11 | findMyWay.on('GET', '/example', function handle (req, res, params) { 12 | // without context, this will be the result object returned from router.find 13 | t.assert.equal(this.handler, handle) 14 | }) 15 | 16 | findMyWay.lookup({ method: 'GET', url: '/example', headers: {} }, null) 17 | }) 18 | 19 | test('lookup calls route handler with context as scope', t => { 20 | t.plan(1) 21 | 22 | const findMyWay = FindMyWay() 23 | 24 | const ctx = { foo: 'bar' } 25 | 26 | findMyWay.on('GET', '/example', function handle (req, res, params) { 27 | t.assert.equal(this, ctx) 28 | }) 29 | 30 | findMyWay.lookup({ method: 'GET', url: '/example', headers: {} }, null, ctx) 31 | }) 32 | 33 | test('lookup calls default route handler with no context', t => { 34 | t.plan(1) 35 | 36 | const findMyWay = FindMyWay({ 37 | defaultRoute (req, res) { 38 | // without context, the default route's scope is the router itself 39 | t.assert.equal(this, findMyWay) 40 | } 41 | }) 42 | 43 | findMyWay.lookup({ method: 'GET', url: '/example', headers: {} }, null) 44 | }) 45 | 46 | test('lookup calls default route handler with context as scope', t => { 47 | t.plan(1) 48 | 49 | const ctx = { foo: 'bar' } 50 | 51 | const findMyWay = FindMyWay({ 52 | defaultRoute (req, res) { 53 | t.assert.equal(this, ctx) 54 | } 55 | }) 56 | 57 | findMyWay.lookup({ method: 'GET', url: '/example', headers: {} }, null, ctx) 58 | }) 59 | -------------------------------------------------------------------------------- /test/matching-order.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('Matching order', t => { 7 | t.plan(3) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/foo/bar/static', { constraints: { host: 'test' } }, () => {}) 11 | findMyWay.on('GET', '/foo/bar/*', () => {}) 12 | findMyWay.on('GET', '/foo/:param/static', () => {}) 13 | 14 | t.assert.deepEqual(findMyWay.find('GET', '/foo/bar/static', { host: 'test' }).params, {}) 15 | t.assert.deepEqual(findMyWay.find('GET', '/foo/bar/static').params, { '*': 'static' }) 16 | t.assert.deepEqual(findMyWay.find('GET', '/foo/value/static').params, { param: 'value' }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/max-param-length.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('maxParamLength default value is 500', t => { 7 | t.plan(1) 8 | 9 | const findMyWay = FindMyWay() 10 | t.assert.equal(findMyWay.maxParamLength, 100) 11 | }) 12 | 13 | test('maxParamLength should set the maximum length for a parametric route', t => { 14 | t.plan(1) 15 | 16 | const findMyWay = FindMyWay({ maxParamLength: 10 }) 17 | findMyWay.on('GET', '/test/:param', () => {}) 18 | t.assert.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) 19 | }) 20 | 21 | test('maxParamLength should set the maximum length for a parametric (regex) route', t => { 22 | t.plan(1) 23 | 24 | const findMyWay = FindMyWay({ maxParamLength: 10 }) 25 | findMyWay.on('GET', '/test/:param(^\\d+$)', () => {}) 26 | 27 | t.assert.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) 28 | }) 29 | 30 | test('maxParamLength should set the maximum length for a parametric (multi) route', t => { 31 | t.plan(1) 32 | 33 | const findMyWay = FindMyWay({ maxParamLength: 10 }) 34 | findMyWay.on('GET', '/test/:param-bar', () => {}) 35 | t.assert.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) 36 | }) 37 | 38 | test('maxParamLength should set the maximum length for a parametric (regex with suffix) route', t => { 39 | t.plan(1) 40 | 41 | const findMyWay = FindMyWay({ maxParamLength: 10 }) 42 | findMyWay.on('GET', '/test/:param(^\\w{3})bar', () => {}) 43 | t.assert.deepEqual(findMyWay.find('GET', '/test/123456789abcd'), null) 44 | }) 45 | -------------------------------------------------------------------------------- /test/null-object.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const { NullObject } = require('../lib/null-object') 5 | 6 | test('NullObject', t => { 7 | t.plan(2) 8 | const nullObject = new NullObject() 9 | t.assert.ok(nullObject instanceof NullObject) 10 | t.assert.ok(typeof nullObject === 'object') 11 | }) 12 | 13 | test('has no methods from generic Object class', t => { 14 | function getAllPropertyNames (obj) { 15 | const props = [] 16 | 17 | do { 18 | Object.getOwnPropertyNames(obj).forEach(function (prop) { 19 | if (props.indexOf(prop) === -1) { 20 | props.push(prop) 21 | } 22 | }) 23 | } while (obj = Object.getPrototypeOf(obj)) // eslint-disable-line 24 | 25 | return props 26 | } 27 | const propertyNames = getAllPropertyNames({}) 28 | t.plan(propertyNames.length + 1) 29 | 30 | const nullObject = new NullObject() 31 | 32 | for (const propertyName of propertyNames) { 33 | t.assert.ok(!(propertyName in nullObject), propertyName) 34 | } 35 | t.assert.equal(getAllPropertyNames(nullObject).length, 0) 36 | }) 37 | -------------------------------------------------------------------------------- /test/on-bad-url.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('If onBadUrl is defined, then a bad url should be handled differently (find)', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | }, 12 | onBadUrl: (path, req, res) => { 13 | t.assert.equal(path, '/%world', { todo: 'this is not executed' }) 14 | } 15 | }) 16 | 17 | findMyWay.on('GET', '/hello/:id', (req, res) => { 18 | t.assert.fail('Should not be here') 19 | }) 20 | 21 | const handle = findMyWay.find('GET', '/hello/%world') 22 | t.assert.notDeepStrictEqual(handle, null) 23 | }) 24 | 25 | test('If onBadUrl is defined, then a bad url should be handled differently (lookup)', t => { 26 | t.plan(1) 27 | const findMyWay = FindMyWay({ 28 | defaultRoute: (req, res) => { 29 | t.assert.fail('Should not be defaultRoute') 30 | }, 31 | onBadUrl: (path, req, res) => { 32 | t.assert.equal(path, '/hello/%world') 33 | } 34 | }) 35 | 36 | findMyWay.on('GET', '/hello/:id', (req, res) => { 37 | t.assert.fail('Should not be here') 38 | }) 39 | 40 | findMyWay.lookup({ method: 'GET', url: '/hello/%world', headers: {} }, null) 41 | }) 42 | 43 | test('If onBadUrl is not defined, then we should call the defaultRoute (find)', t => { 44 | t.plan(1) 45 | const findMyWay = FindMyWay({ 46 | defaultRoute: (req, res) => { 47 | t.assert.fail('Should not be defaultRoute') 48 | } 49 | }) 50 | 51 | findMyWay.on('GET', '/hello/:id', (req, res) => { 52 | t.assert.fail('Should not be here') 53 | }) 54 | 55 | const handle = findMyWay.find('GET', '/hello/%world') 56 | t.assert.equal(handle, null) 57 | }) 58 | 59 | test('If onBadUrl is not defined, then we should call the defaultRoute (lookup)', t => { 60 | t.plan(1) 61 | const findMyWay = FindMyWay({ 62 | defaultRoute: (req, res) => { 63 | t.assert.ok('Everything fine') 64 | } 65 | }) 66 | 67 | findMyWay.on('GET', '/hello/:id', (req, res) => { 68 | t.assert.fail('Should not be here') 69 | }) 70 | 71 | findMyWay.lookup({ method: 'GET', url: '/hello/%world', headers: {} }, null) 72 | }) 73 | -------------------------------------------------------------------------------- /test/optional-params.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('Test route with optional parameter', (t) => { 7 | t.plan(2) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: (req, res) => { 10 | t.assert.fail('Should not be defaultRoute') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/a/:param/b/:optional?', (req, res, params) => { 15 | if (params.optional) { 16 | t.assert.equal(params.optional, 'foo') 17 | } else { 18 | t.assert.equal(params.optional, undefined) 19 | } 20 | }) 21 | 22 | findMyWay.lookup({ method: 'GET', url: '/a/foo-bar/b', headers: {} }, null) 23 | findMyWay.lookup({ method: 'GET', url: '/a/foo-bar/b/foo', headers: {} }, null) 24 | }) 25 | 26 | test('Test for duplicate route with optional param', (t) => { 27 | t.plan(1) 28 | const findMyWay = FindMyWay({ 29 | defaultRoute: (req, res) => { 30 | t.assert.fail('Should not be defaultRoute') 31 | } 32 | }) 33 | 34 | findMyWay.on('GET', '/foo/:bar?', (req, res, params) => {}) 35 | 36 | try { 37 | findMyWay.on('GET', '/foo', (req, res, params) => {}) 38 | t.assert.fail('method is already declared for route with optional param') 39 | } catch (e) { 40 | t.assert.equal(e.message, 'Method \'GET\' already declared for route \'/foo\' with constraints \'{}\'') 41 | } 42 | }) 43 | 44 | test('Test for param with ? not at the end', (t) => { 45 | t.plan(1) 46 | const findMyWay = FindMyWay({ 47 | defaultRoute: (req, res) => { 48 | t.assert.fail('Should not be defaultRoute') 49 | } 50 | }) 51 | 52 | try { 53 | findMyWay.on('GET', '/foo/:bar?/baz', (req, res, params) => {}) 54 | t.assert.fail('Optional Param in the middle of the path is not allowed') 55 | } catch (e) { 56 | t.assert.equal(e.message, 'Optional Parameter needs to be the last parameter of the path') 57 | } 58 | }) 59 | 60 | test('Multi parametric route with optional param', (t) => { 61 | t.plan(2) 62 | const findMyWay = FindMyWay({ 63 | defaultRoute: (req, res) => { 64 | t.assert.fail('Should not be defaultRoute') 65 | } 66 | }) 67 | 68 | findMyWay.on('GET', '/a/:p1-:p2?', (req, res, params) => { 69 | if (params.p1 && params.p2) { 70 | t.assert.equal(params.p1, 'foo-bar') 71 | t.assert.equal(params.p2, 'baz') 72 | } 73 | }) 74 | 75 | findMyWay.lookup({ method: 'GET', url: '/a/foo-bar-baz', headers: {} }, null) 76 | findMyWay.lookup({ method: 'GET', url: '/a', headers: {} }, null) 77 | }) 78 | 79 | test('Optional Parameter with ignoreTrailingSlash = true', (t) => { 80 | t.plan(4) 81 | const findMyWay = FindMyWay({ 82 | ignoreTrailingSlash: true, 83 | defaultRoute: (req, res) => { 84 | t.assert.fail('Should not be defaultRoute') 85 | } 86 | }) 87 | 88 | findMyWay.on('GET', '/test/hello/:optional?', (req, res, params) => { 89 | if (params.optional) { 90 | t.assert.equal(params.optional, 'foo') 91 | } else { 92 | t.assert.equal(params.optional, undefined) 93 | } 94 | }) 95 | 96 | findMyWay.lookup({ method: 'GET', url: '/test/hello/', headers: {} }, null) 97 | findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) 98 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo', headers: {} }, null) 99 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo/', headers: {} }, null) 100 | }) 101 | 102 | test('Optional Parameter with ignoreTrailingSlash = false', (t) => { 103 | t.plan(4) 104 | const findMyWay = FindMyWay({ 105 | ignoreTrailingSlash: false, 106 | defaultRoute: (req, res) => { 107 | t.assert.equal(req.url, '/test/hello/foo/') 108 | } 109 | }) 110 | 111 | findMyWay.on('GET', '/test/hello/:optional?', (req, res, params) => { 112 | if (req.url === '/test/hello/') { 113 | t.assert.deepEqual(params, { optional: '' }) 114 | } else if (req.url === '/test/hello') { 115 | t.assert.deepEqual(params, {}) 116 | } else if (req.url === '/test/hello/foo') { 117 | t.assert.deepEqual(params, { optional: 'foo' }) 118 | } 119 | }) 120 | 121 | findMyWay.lookup({ method: 'GET', url: '/test/hello/', headers: {} }, null) 122 | findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) 123 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo', headers: {} }, null) 124 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo/', headers: {} }, null) 125 | }) 126 | 127 | test('Optional Parameter with ignoreDuplicateSlashes = true', (t) => { 128 | t.plan(4) 129 | const findMyWay = FindMyWay({ 130 | ignoreDuplicateSlashes: true, 131 | defaultRoute: (req, res) => { 132 | t.assert.fail('Should not be defaultRoute') 133 | } 134 | }) 135 | 136 | findMyWay.on('GET', '/test/hello/:optional?', (req, res, params) => { 137 | if (params.optional) { 138 | t.assert.equal(params.optional, 'foo') 139 | } else { 140 | t.assert.equal(params.optional, undefined) 141 | } 142 | }) 143 | 144 | findMyWay.lookup({ method: 'GET', url: '/test//hello', headers: {} }, null) 145 | findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) 146 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo', headers: {} }, null) 147 | findMyWay.lookup({ method: 'GET', url: '/test//hello//foo', headers: {} }, null) 148 | }) 149 | 150 | test('Optional Parameter with ignoreDuplicateSlashes = false', (t) => { 151 | t.plan(4) 152 | const findMyWay = FindMyWay({ 153 | ignoreDuplicateSlashes: false, 154 | defaultRoute: (req, res) => { 155 | if (req.url === '/test//hello') { 156 | t.assert.deepEqual(req.params, undefined) 157 | } else if (req.url === '/test//hello/foo') { 158 | t.assert.deepEqual(req.params, undefined) 159 | } 160 | } 161 | }) 162 | 163 | findMyWay.on('GET', '/test/hello/:optional?', (req, res, params) => { 164 | if (req.url === '/test/hello/') { 165 | t.assert.deepEqual(params, { optional: '' }) 166 | } else if (req.url === '/test/hello') { 167 | t.assert.deepEqual(params, {}) 168 | } else if (req.url === '/test/hello/foo') { 169 | t.assert.deepEqual(params, { optional: 'foo' }) 170 | } 171 | }) 172 | 173 | findMyWay.lookup({ method: 'GET', url: '/test//hello', headers: {} }, null) 174 | findMyWay.lookup({ method: 'GET', url: '/test/hello', headers: {} }, null) 175 | findMyWay.lookup({ method: 'GET', url: '/test/hello/foo', headers: {} }, null) 176 | findMyWay.lookup({ method: 'GET', url: '/test//hello/foo', headers: {} }, null) 177 | }) 178 | 179 | test('deregister a route with optional param', (t) => { 180 | t.plan(4) 181 | const findMyWay = FindMyWay({ 182 | defaultRoute: (req, res) => { 183 | t.assert.fail('Should not be defaultRoute') 184 | } 185 | }) 186 | 187 | findMyWay.on('GET', '/a/:param/b/:optional?', (req, res, params) => {}) 188 | 189 | t.assert.ok(findMyWay.find('GET', '/a/:param/b')) 190 | t.assert.ok(findMyWay.find('GET', '/a/:param/b/:optional')) 191 | 192 | findMyWay.off('GET', '/a/:param/b/:optional?') 193 | 194 | t.assert.ok(!findMyWay.find('GET', '/a/:param/b')) 195 | t.assert.ok(!findMyWay.find('GET', '/a/:param/b/:optional')) 196 | }) 197 | 198 | test('optional parameter on root', (t) => { 199 | t.plan(2) 200 | const findMyWay = FindMyWay({ 201 | defaultRoute: (req, res) => { 202 | t.assert.fail('Should not be defaultRoute') 203 | } 204 | }) 205 | 206 | findMyWay.on('GET', '/:optional?', (req, res, params) => { 207 | if (params.optional) { 208 | t.assert.equal(params.optional, 'foo') 209 | } else { 210 | t.assert.equal(params.optional, undefined) 211 | } 212 | }) 213 | 214 | findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null) 215 | findMyWay.lookup({ method: 'GET', url: '/foo', headers: {} }, null) 216 | }) 217 | -------------------------------------------------------------------------------- /test/params-collisions.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('..') 5 | 6 | test('should setup parametric and regexp node', t => { 7 | t.plan(2) 8 | 9 | const findMyWay = FindMyWay() 10 | 11 | const paramHandler = () => {} 12 | const regexpHandler = () => {} 13 | 14 | findMyWay.on('GET', '/foo/:bar', paramHandler) 15 | findMyWay.on('GET', '/foo/:bar(123)', regexpHandler) 16 | 17 | t.assert.equal(findMyWay.find('GET', '/foo/value').handler, paramHandler) 18 | t.assert.equal(findMyWay.find('GET', '/foo/123').handler, regexpHandler) 19 | }) 20 | 21 | test('should setup parametric and multi-parametric node', t => { 22 | t.plan(2) 23 | 24 | const findMyWay = FindMyWay() 25 | 26 | const paramHandler = () => {} 27 | const regexpHandler = () => {} 28 | 29 | findMyWay.on('GET', '/foo/:bar', paramHandler) 30 | findMyWay.on('GET', '/foo/:bar.png', regexpHandler) 31 | 32 | t.assert.equal(findMyWay.find('GET', '/foo/value').handler, paramHandler) 33 | t.assert.equal(findMyWay.find('GET', '/foo/value.png').handler, regexpHandler) 34 | }) 35 | 36 | test('should throw when set upping two parametric nodes', t => { 37 | t.plan(1) 38 | 39 | const findMyWay = FindMyWay() 40 | findMyWay.on('GET', '/foo/:bar', () => {}) 41 | 42 | t.assert.throws(() => findMyWay.on('GET', '/foo/:baz', () => {})) 43 | }) 44 | 45 | test('should throw when set upping two regexp nodes', t => { 46 | t.plan(1) 47 | 48 | const findMyWay = FindMyWay() 49 | findMyWay.on('GET', '/foo/:bar(123)', () => {}) 50 | 51 | t.assert.throws(() => findMyWay.on('GET', '/foo/:bar(456)', () => {})) 52 | }) 53 | 54 | test('should set up two parametric nodes with static ending', t => { 55 | t.plan(2) 56 | 57 | const findMyWay = FindMyWay() 58 | 59 | const paramHandler1 = () => {} 60 | const paramHandler2 = () => {} 61 | 62 | findMyWay.on('GET', '/foo/:bar.png', paramHandler1) 63 | findMyWay.on('GET', '/foo/:bar.jpeg', paramHandler2) 64 | 65 | t.assert.equal(findMyWay.find('GET', '/foo/value.png').handler, paramHandler1) 66 | t.assert.equal(findMyWay.find('GET', '/foo/value.jpeg').handler, paramHandler2) 67 | }) 68 | 69 | test('should set up two regexp nodes with static ending', t => { 70 | t.plan(2) 71 | 72 | const findMyWay = FindMyWay() 73 | 74 | const paramHandler1 = () => {} 75 | const paramHandler2 = () => {} 76 | 77 | findMyWay.on('GET', '/foo/:bar(123).png', paramHandler1) 78 | findMyWay.on('GET', '/foo/:bar(456).jpeg', paramHandler2) 79 | 80 | t.assert.equal(findMyWay.find('GET', '/foo/123.png').handler, paramHandler1) 81 | t.assert.equal(findMyWay.find('GET', '/foo/456.jpeg').handler, paramHandler2) 82 | }) 83 | 84 | test('node with longer static suffix should have higher priority', t => { 85 | t.plan(2) 86 | 87 | const findMyWay = FindMyWay() 88 | 89 | const paramHandler1 = () => {} 90 | const paramHandler2 = () => {} 91 | 92 | findMyWay.on('GET', '/foo/:bar.png', paramHandler1) 93 | findMyWay.on('GET', '/foo/:bar.png.png', paramHandler2) 94 | 95 | t.assert.equal(findMyWay.find('GET', '/foo/value.png').handler, paramHandler1) 96 | t.assert.equal(findMyWay.find('GET', '/foo/value.png.png').handler, paramHandler2) 97 | }) 98 | 99 | test('node with longer static suffix should have higher priority', t => { 100 | t.plan(2) 101 | 102 | const findMyWay = FindMyWay() 103 | 104 | const paramHandler1 = () => {} 105 | const paramHandler2 = () => {} 106 | 107 | findMyWay.on('GET', '/foo/:bar.png.png', paramHandler2) 108 | findMyWay.on('GET', '/foo/:bar.png', paramHandler1) 109 | 110 | t.assert.equal(findMyWay.find('GET', '/foo/value.png').handler, paramHandler1) 111 | t.assert.equal(findMyWay.find('GET', '/foo/value.png.png').handler, paramHandler2) 112 | }) 113 | 114 | test('should set up regexp node and node with static ending', t => { 115 | t.plan(2) 116 | 117 | const regexHandler = () => {} 118 | const multiParamHandler = () => {} 119 | 120 | const findMyWay = FindMyWay() 121 | findMyWay.on('GET', '/foo/:bar(123)', regexHandler) 122 | findMyWay.on('GET', '/foo/:bar(123).jpeg', multiParamHandler) 123 | 124 | t.assert.equal(findMyWay.find('GET', '/foo/123.jpeg').handler, multiParamHandler) 125 | t.assert.equal(findMyWay.find('GET', '/foo/123').handler, regexHandler) 126 | }) 127 | -------------------------------------------------------------------------------- /test/path-params-match.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('path params match', (t) => { 7 | t.plan(24) 8 | 9 | const findMyWay = FindMyWay({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true }) 10 | 11 | const b1Path = function b1StaticPath () {} 12 | const b2Path = function b2StaticPath () {} 13 | const cPath = function cStaticPath () {} 14 | const paramPath = function parameterPath () {} 15 | 16 | findMyWay.on('GET', '/ab1', b1Path) 17 | findMyWay.on('GET', '/ab2', b2Path) 18 | findMyWay.on('GET', '/ac', cPath) 19 | findMyWay.on('GET', '/:pam', paramPath) 20 | 21 | t.assert.equal(findMyWay.find('GET', '/ab1').handler, b1Path) 22 | t.assert.equal(findMyWay.find('GET', '/ab1/').handler, b1Path) 23 | t.assert.equal(findMyWay.find('GET', '//ab1').handler, b1Path) 24 | t.assert.equal(findMyWay.find('GET', '//ab1//').handler, b1Path) 25 | t.assert.equal(findMyWay.find('GET', '/ab2').handler, b2Path) 26 | t.assert.equal(findMyWay.find('GET', '/ab2/').handler, b2Path) 27 | t.assert.equal(findMyWay.find('GET', '//ab2').handler, b2Path) 28 | t.assert.equal(findMyWay.find('GET', '//ab2//').handler, b2Path) 29 | t.assert.equal(findMyWay.find('GET', '/ac').handler, cPath) 30 | t.assert.equal(findMyWay.find('GET', '/ac/').handler, cPath) 31 | t.assert.equal(findMyWay.find('GET', '//ac').handler, cPath) 32 | t.assert.equal(findMyWay.find('GET', '//ac//').handler, cPath) 33 | t.assert.equal(findMyWay.find('GET', '/foo').handler, paramPath) 34 | t.assert.equal(findMyWay.find('GET', '/foo/').handler, paramPath) 35 | t.assert.equal(findMyWay.find('GET', '//foo').handler, paramPath) 36 | t.assert.equal(findMyWay.find('GET', '//foo//').handler, paramPath) 37 | 38 | const noTrailingSlashRet = findMyWay.find('GET', '/abcdef') 39 | t.assert.equal(noTrailingSlashRet.handler, paramPath) 40 | t.assert.deepEqual(noTrailingSlashRet.params, { pam: 'abcdef' }) 41 | 42 | const trailingSlashRet = findMyWay.find('GET', '/abcdef/') 43 | t.assert.equal(trailingSlashRet.handler, paramPath) 44 | t.assert.deepEqual(trailingSlashRet.params, { pam: 'abcdef' }) 45 | 46 | const noDuplicateSlashRet = findMyWay.find('GET', '/abcdef') 47 | t.assert.equal(noDuplicateSlashRet.handler, paramPath) 48 | t.assert.deepEqual(noDuplicateSlashRet.params, { pam: 'abcdef' }) 49 | 50 | const duplicateSlashRet = findMyWay.find('GET', '//abcdef') 51 | t.assert.equal(duplicateSlashRet.handler, paramPath) 52 | t.assert.deepEqual(duplicateSlashRet.params, { pam: 'abcdef' }) 53 | }) 54 | -------------------------------------------------------------------------------- /test/querystring.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('should sanitize the url - query', t => { 7 | t.plan(2) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/test', (req, res, params, store, query) => { 11 | t.assert.deepEqual(query, { hello: 'world' }) 12 | t.assert.ok('inside the handler') 13 | }) 14 | 15 | findMyWay.lookup({ method: 'GET', url: '/test?hello=world', headers: {} }, null) 16 | }) 17 | 18 | test('should sanitize the url - hash', t => { 19 | t.plan(2) 20 | const findMyWay = FindMyWay() 21 | 22 | findMyWay.on('GET', '/test', (req, res, params, store, query) => { 23 | t.assert.deepEqual(query, { hello: '' }) 24 | t.assert.ok('inside the handler') 25 | }) 26 | 27 | findMyWay.lookup({ method: 'GET', url: '/test#hello', headers: {} }, null) 28 | }) 29 | 30 | test('handles path and query separated by ; with useSemicolonDelimiter enabled', t => { 31 | t.plan(2) 32 | const findMyWay = FindMyWay({ 33 | useSemicolonDelimiter: true 34 | }) 35 | 36 | findMyWay.on('GET', '/test', (req, res, params, store, query) => { 37 | t.assert.deepEqual(query, { jsessionid: '123456' }) 38 | t.assert.ok('inside the handler') 39 | }) 40 | 41 | findMyWay.lookup({ method: 'GET', url: '/test;jsessionid=123456', headers: {} }, null) 42 | }) 43 | 44 | test('handles path and query separated by ? using ; in the path', t => { 45 | t.plan(2) 46 | const findMyWay = FindMyWay() 47 | 48 | findMyWay.on('GET', '/test;jsessionid=123456', (req, res, params, store, query) => { 49 | t.assert.deepEqual(query, { foo: 'bar' }) 50 | t.assert.ok('inside the handler') 51 | }) 52 | 53 | findMyWay.lookup({ method: 'GET', url: '/test;jsessionid=123456?foo=bar', headers: {} }, null) 54 | }) 55 | -------------------------------------------------------------------------------- /test/regex.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('route with matching regex', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay({ 9 | defaultRoute: () => { 10 | t.assert.fail('route not matched') 11 | } 12 | }) 13 | 14 | findMyWay.on('GET', '/test/:id(^\\d+$)', () => { 15 | t.assert.ok('regex match') 16 | }) 17 | 18 | findMyWay.lookup({ method: 'GET', url: '/test/12', headers: {} }, null) 19 | }) 20 | 21 | test('route without matching regex', t => { 22 | t.plan(1) 23 | const findMyWay = FindMyWay({ 24 | defaultRoute: () => { 25 | t.assert.ok('route not matched') 26 | } 27 | }) 28 | 29 | findMyWay.on('GET', '/test/:id(^\\d+$)', () => { 30 | t.assert.fail('regex match') 31 | }) 32 | 33 | findMyWay.lookup({ method: 'GET', url: '/test/test', headers: {} }, null) 34 | }) 35 | 36 | test('route with an extension regex 2', t => { 37 | t.plan(2) 38 | const findMyWay = FindMyWay({ 39 | defaultRoute: (req) => { 40 | t.assert.fail(`route not matched: ${req.url}`) 41 | } 42 | }) 43 | findMyWay.on('GET', '/test/S/:file(^\\S+).png', () => { 44 | t.assert.ok('regex match') 45 | }) 46 | findMyWay.on('GET', '/test/D/:file(^\\D+).png', () => { 47 | t.assert.ok('regex match') 48 | }) 49 | findMyWay.lookup({ method: 'GET', url: '/test/S/foo.png', headers: {} }, null) 50 | findMyWay.lookup({ method: 'GET', url: '/test/D/foo.png', headers: {} }, null) 51 | }) 52 | 53 | test('nested route with matching regex', t => { 54 | t.plan(1) 55 | const findMyWay = FindMyWay({ 56 | defaultRoute: () => { 57 | t.assert.fail('route not matched') 58 | } 59 | }) 60 | 61 | findMyWay.on('GET', '/test/:id(^\\d+$)/hello', () => { 62 | t.assert.ok('regex match') 63 | }) 64 | 65 | findMyWay.lookup({ method: 'GET', url: '/test/12/hello', headers: {} }, null) 66 | }) 67 | 68 | test('mixed nested route with matching regex', t => { 69 | t.plan(2) 70 | const findMyWay = FindMyWay({ 71 | defaultRoute: () => { 72 | t.assert.fail('route not matched') 73 | } 74 | }) 75 | 76 | findMyWay.on('GET', '/test/:id(^\\d+$)/hello/:world', (req, res, params) => { 77 | t.assert.equal(params.id, '12') 78 | t.assert.equal(params.world, 'world') 79 | }) 80 | 81 | findMyWay.lookup({ method: 'GET', url: '/test/12/hello/world', headers: {} }, null) 82 | }) 83 | 84 | test('mixed nested route with double matching regex', t => { 85 | t.plan(2) 86 | const findMyWay = FindMyWay({ 87 | defaultRoute: () => { 88 | t.assert.fail('route not matched') 89 | } 90 | }) 91 | 92 | findMyWay.on('GET', '/test/:id(^\\d+$)/hello/:world(^\\d+$)', (req, res, params) => { 93 | t.assert.equal(params.id, '12') 94 | t.assert.equal(params.world, '15') 95 | }) 96 | 97 | findMyWay.lookup({ method: 'GET', url: '/test/12/hello/15', headers: {} }, null) 98 | }) 99 | 100 | test('mixed nested route without double matching regex', t => { 101 | t.plan(1) 102 | const findMyWay = FindMyWay({ 103 | defaultRoute: () => { 104 | t.assert.ok('route not matched') 105 | } 106 | }) 107 | 108 | findMyWay.on('GET', '/test/:id(^\\d+$)/hello/:world(^\\d+$)', (req, res, params) => { 109 | t.assert.fail('route mathed') 110 | }) 111 | 112 | findMyWay.lookup({ method: 'GET', url: '/test/12/hello/test', headers: {} }, null) 113 | }) 114 | 115 | test('route with an extension regex', t => { 116 | t.plan(1) 117 | const findMyWay = FindMyWay({ 118 | defaultRoute: () => { 119 | t.assert.fail('route not matched') 120 | } 121 | }) 122 | 123 | findMyWay.on('GET', '/test/:file(^\\d+).png', () => { 124 | t.assert.ok('regex match') 125 | }) 126 | 127 | findMyWay.lookup({ method: 'GET', url: '/test/12.png', headers: {} }, null) 128 | }) 129 | 130 | test('route with an extension regex - no match', t => { 131 | t.plan(1) 132 | const findMyWay = FindMyWay({ 133 | defaultRoute: () => { 134 | t.assert.ok('route not matched') 135 | } 136 | }) 137 | 138 | findMyWay.on('GET', '/test/:file(^\\d+).png', () => { 139 | t.assert.fail('regex match') 140 | }) 141 | 142 | findMyWay.lookup({ method: 'GET', url: '/test/aa.png', headers: {} }, null) 143 | }) 144 | 145 | test('safe decodeURIComponent', t => { 146 | t.plan(1) 147 | const findMyWay = FindMyWay({ 148 | defaultRoute: () => { 149 | t.assert.ok('route not matched') 150 | } 151 | }) 152 | 153 | findMyWay.on('GET', '/test/:id(^\\d+$)', () => { 154 | t.assert.fail('we should not be here') 155 | }) 156 | 157 | t.assert.deepEqual( 158 | findMyWay.find('GET', '/test/hel%"Flo', {}), 159 | null 160 | ) 161 | }) 162 | 163 | test('Should check if a regex is safe to use', t => { 164 | t.plan(13) 165 | 166 | const noop = () => {} 167 | 168 | // https://github.com/substack/safe-regex/blob/master/test/regex.js 169 | const good = [ 170 | /\bOakland\b/, 171 | /\b(Oakland|San Francisco)\b/i, 172 | /^\d+1337\d+$/i, 173 | /^\d+(1337|404)\d+$/i, 174 | /^\d+(1337|404)*\d+$/i, 175 | RegExp(Array(26).join('a?') + Array(26).join('a')) 176 | ] 177 | 178 | const bad = [ 179 | /^(a?){25}(a){25}$/, 180 | RegExp(Array(27).join('a?') + Array(27).join('a')), 181 | /(x+x+)+y/, 182 | /foo|(x+x+)+y/, 183 | /(a+){10}y/, 184 | /(a+){2}y/, 185 | /(.*){1,32000}[bc]/ 186 | ] 187 | 188 | const findMyWay = FindMyWay() 189 | 190 | good.forEach(regex => { 191 | try { 192 | findMyWay.on('GET', `/test/:id(${regex.toString()})`, noop) 193 | t.assert.ok('ok') 194 | findMyWay.off('GET', `/test/:id(${regex.toString()})`) 195 | } catch (err) { 196 | t.assert.fail(err) 197 | } 198 | }) 199 | 200 | bad.forEach(regex => { 201 | try { 202 | findMyWay.on('GET', `/test/:id(${regex.toString()})`, noop) 203 | t.assert.fail('should throw') 204 | } catch (err) { 205 | t.assert.ok(err) 206 | } 207 | }) 208 | }) 209 | 210 | test('Disable safe regex check', t => { 211 | t.plan(13) 212 | 213 | const noop = () => {} 214 | 215 | // https://github.com/substack/safe-regex/blob/master/test/regex.js 216 | const good = [ 217 | /\bOakland\b/, 218 | /\b(Oakland|San Francisco)\b/i, 219 | /^\d+1337\d+$/i, 220 | /^\d+(1337|404)\d+$/i, 221 | /^\d+(1337|404)*\d+$/i, 222 | RegExp(Array(26).join('a?') + Array(26).join('a')) 223 | ] 224 | 225 | const bad = [ 226 | /^(a?){25}(a){25}$/, 227 | RegExp(Array(27).join('a?') + Array(27).join('a')), 228 | /(x+x+)+y/, 229 | /foo|(x+x+)+y/, 230 | /(a+){10}y/, 231 | /(a+){2}y/, 232 | /(.*){1,32000}[bc]/ 233 | ] 234 | 235 | const findMyWay = FindMyWay({ allowUnsafeRegex: true }) 236 | 237 | good.forEach(regex => { 238 | try { 239 | findMyWay.on('GET', `/test/:id(${regex.toString()})`, noop) 240 | t.assert.ok('ok') 241 | findMyWay.off('GET', `/test/:id(${regex.toString()})`) 242 | } catch (err) { 243 | t.assert.fail(err) 244 | } 245 | }) 246 | 247 | bad.forEach(regex => { 248 | try { 249 | findMyWay.on('GET', `/test/:id(${regex.toString()})`, noop) 250 | t.assert.ok('ok') 251 | findMyWay.off('GET', `/test/:id(${regex.toString()})`) 252 | } catch (err) { 253 | t.assert.fail(err) 254 | } 255 | }) 256 | }) 257 | 258 | test('prevent back-tracking', { timeout: 20 }, (t) => { 259 | t.plan(0) 260 | 261 | const findMyWay = FindMyWay({ 262 | defaultRoute: () => { 263 | t.assert.fail('route not matched') 264 | } 265 | }) 266 | 267 | findMyWay.on('GET', '/:foo-:bar-', (req, res, params) => {}) 268 | findMyWay.find('GET', '/' + '-'.repeat(16000) + 'a', { host: 'fastify.io' }) 269 | }) 270 | -------------------------------------------------------------------------------- /test/routes-registered.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | function initializeRoutes (router, handler, quantity) { 7 | for (const x of Array(quantity).keys()) { 8 | router.on('GET', '/test-route-' + x, handler) 9 | } 10 | return router 11 | } 12 | 13 | test('verify routes registered', t => { 14 | const assertPerTest = 5 15 | const quantity = 5 16 | // 1 (check length) + quantity of routes * quantity of tests per route 17 | t.plan(1 + (quantity * assertPerTest)) 18 | 19 | let findMyWay = FindMyWay() 20 | const defaultHandler = (req, res, params) => res.end(JSON.stringify({ hello: 'world' })) 21 | 22 | findMyWay = initializeRoutes(findMyWay, defaultHandler, quantity) 23 | t.assert.equal(findMyWay.routes.length, quantity) 24 | findMyWay.routes.forEach((route, idx) => { 25 | t.assert.equal(route.method, 'GET') 26 | t.assert.equal(route.path, '/test-route-' + idx) 27 | t.assert.deepStrictEqual(route.opts, {}) 28 | t.assert.equal(route.handler, defaultHandler) 29 | t.assert.equal(route.store, undefined) 30 | }) 31 | }) 32 | 33 | test('verify routes registered and deregister', t => { 34 | // 1 (check length) + quantity of routes * quantity of tests per route 35 | t.plan(2) 36 | 37 | let findMyWay = FindMyWay() 38 | const quantity = 2 39 | const defaultHandler = (req, res, params) => res.end(JSON.stringify({ hello: 'world' })) 40 | 41 | findMyWay = initializeRoutes(findMyWay, defaultHandler, quantity) 42 | t.assert.equal(findMyWay.routes.length, quantity) 43 | findMyWay.off('GET', '/test-route-0') 44 | t.assert.equal(findMyWay.routes.length, quantity - 1) 45 | }) 46 | -------------------------------------------------------------------------------- /test/shorthands.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const httpMethods = require('../lib/http-methods') 4 | const { describe, test } = require('node:test') 5 | const FindMyWay = require('../') 6 | 7 | describe('should support shorthand', t => { 8 | for (const i in httpMethods) { 9 | const m = httpMethods[i] 10 | const methodName = m.toLowerCase() 11 | 12 | test('`.' + methodName + '`', t => { 13 | t.plan(1) 14 | const findMyWay = FindMyWay() 15 | 16 | findMyWay[methodName]('/test', () => { 17 | t.assert.ok('inside the handler') 18 | }) 19 | 20 | findMyWay.lookup({ method: m, url: '/test', headers: {} }, null) 21 | }) 22 | } 23 | }) 24 | 25 | test('should support `.all` shorthand', t => { 26 | t.plan(11) 27 | const findMyWay = FindMyWay() 28 | 29 | findMyWay.all('/test', () => { 30 | t.assert.ok('inside the handler') 31 | }) 32 | 33 | findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) 34 | findMyWay.lookup({ method: 'DELETE', url: '/test', headers: {} }, null) 35 | findMyWay.lookup({ method: 'HEAD', url: '/test', headers: {} }, null) 36 | findMyWay.lookup({ method: 'PATCH', url: '/test', headers: {} }, null) 37 | findMyWay.lookup({ method: 'POST', url: '/test', headers: {} }, null) 38 | findMyWay.lookup({ method: 'PUT', url: '/test', headers: {} }, null) 39 | findMyWay.lookup({ method: 'OPTIONS', url: '/test', headers: {} }, null) 40 | findMyWay.lookup({ method: 'TRACE', url: '/test', headers: {} }, null) 41 | findMyWay.lookup({ method: 'CONNECT', url: '/test', headers: {} }, null) 42 | findMyWay.lookup({ method: 'COPY', url: '/test', headers: {} }, null) 43 | findMyWay.lookup({ method: 'SUBSCRIBE', url: '/test', headers: {} }, null) 44 | }) 45 | -------------------------------------------------------------------------------- /test/store.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const FindMyWay = require('../') 5 | 6 | test('handler should have the store object', t => { 7 | t.plan(1) 8 | const findMyWay = FindMyWay() 9 | 10 | findMyWay.on('GET', '/test', (req, res, params, store) => { 11 | t.assert.equal(store.hello, 'world') 12 | }, { hello: 'world' }) 13 | 14 | findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) 15 | }) 16 | 17 | test('find a store object', t => { 18 | t.plan(1) 19 | const findMyWay = FindMyWay() 20 | const fn = () => {} 21 | 22 | findMyWay.on('GET', '/test', fn, { hello: 'world' }) 23 | 24 | t.assert.deepEqual(findMyWay.find('GET', '/test'), { 25 | handler: fn, 26 | params: {}, 27 | store: { hello: 'world' }, 28 | searchParams: {} 29 | }) 30 | }) 31 | 32 | test('update the store', t => { 33 | t.plan(2) 34 | const findMyWay = FindMyWay() 35 | let bool = false 36 | 37 | findMyWay.on('GET', '/test', (req, res, params, store) => { 38 | if (!bool) { 39 | t.assert.equal(store.hello, 'world') 40 | store.hello = 'hello' 41 | bool = true 42 | findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) 43 | } else { 44 | t.assert.equal(store.hello, 'hello') 45 | } 46 | }, { hello: 'world' }) 47 | 48 | findMyWay.lookup({ method: 'GET', url: '/test', headers: {} }, null) 49 | }) 50 | -------------------------------------------------------------------------------- /test/types/router.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from 'tsd' 2 | import Router from '../../' 3 | import { Http2ServerRequest, Http2ServerResponse } from 'http2' 4 | import { IncomingMessage, ServerResponse } from 'http' 5 | 6 | let http1Req!: IncomingMessage; 7 | let http1Res!: ServerResponse; 8 | let http2Req!: Http2ServerRequest; 9 | let http2Res!: Http2ServerResponse; 10 | let ctx!: { req: IncomingMessage; res: ServerResponse }; 11 | let done!: (err: Error | null, result: any) => void; 12 | 13 | // HTTP1 14 | { 15 | let handler!: Router.Handler 16 | const router = Router({ 17 | ignoreTrailingSlash: true, 18 | ignoreDuplicateSlashes: true, 19 | allowUnsafeRegex: false, 20 | caseSensitive: false, 21 | maxParamLength: 42, 22 | querystringParser: (queryString) => {}, 23 | defaultRoute (http1Req, http1Res) {}, 24 | onBadUrl (path, http1Req, http1Res) {}, 25 | constraints: { 26 | foo: { 27 | name: 'foo', 28 | mustMatchWhenDerived: true, 29 | storage () { 30 | return { 31 | get (version) { return handler }, 32 | set (version, handler) {} 33 | } 34 | }, 35 | deriveConstraint(req) { return '1.0.0' }, 36 | validate(value) { if (typeof value === "string") { throw new Error("invalid")} } 37 | } 38 | } 39 | }) 40 | expectType>(router) 41 | 42 | expectType(router.on('GET', '/', () => {})) 43 | expectType(router.on(['GET', 'POST'], '/', () => {})) 44 | expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {})) 45 | expectType(router.on('GET', '/', () => {}, {})) 46 | expectType(router.on('GET', '/', {constraints: { version: '1.0.0' }}, () => {}, {})) 47 | 48 | expectType(router.get('/', () => {})) 49 | expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {})) 50 | expectType(router.get('/', () => {}, {})) 51 | expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {})) 52 | 53 | expectType(router.off('GET', '/')) 54 | expectType(router.off(['GET', 'POST'], '/')) 55 | 56 | expectType(router.lookup(http1Req, http1Res)) 57 | expectType(router.lookup(http1Req, http1Res, done)); 58 | expectType(router.lookup(http1Req, http1Res, ctx, done)); 59 | expectType | null>(router.find('GET', '/')) 60 | expectType | null>(router.find('GET', '/', {})) 61 | expectType | null>(router.find('GET', '/', {version: '1.0.0'})) 62 | 63 | expectType | null>(router.findRoute('GET', '/')); 64 | expectType | null>(router.findRoute('GET', '/', {})); 65 | expectType | null>(router.findRoute('GET', '/', {version: '1.0.0'})); 66 | 67 | expectType(router.reset()) 68 | expectType(router.prettyPrint()) 69 | expectType(router.prettyPrint({ method: 'GET' })) 70 | expectType(router.prettyPrint({ commonPrefix: false })) 71 | expectType(router.prettyPrint({ commonPrefix: true })) 72 | expectType(router.prettyPrint({ includeMeta: true })) 73 | expectType(router.prettyPrint({ includeMeta: ['test', Symbol('test')] })) 74 | } 75 | 76 | // HTTP2 77 | { 78 | const constraints: { [key: string]: Router.ConstraintStrategy } = { 79 | foo: { 80 | name: 'foo', 81 | mustMatchWhenDerived: true, 82 | storage () { 83 | return { 84 | get (version) { return handler }, 85 | set (version, handler) {} 86 | } 87 | }, 88 | deriveConstraint(req) { return '1.0.0' }, 89 | validate(value) { if (typeof value === "string") { throw new Error("invalid")} } 90 | } 91 | } 92 | 93 | let handler!: Router.Handler 94 | const router = Router({ 95 | ignoreTrailingSlash: true, 96 | ignoreDuplicateSlashes: true, 97 | allowUnsafeRegex: false, 98 | caseSensitive: false, 99 | maxParamLength: 42, 100 | querystringParser: (queryString) => {}, 101 | defaultRoute (http1Req, http1Res) {}, 102 | onBadUrl (path, http1Req, http1Res) {}, 103 | constraints 104 | }) 105 | expectType>(router) 106 | 107 | expectType(router.on('GET', '/', () => {})) 108 | expectType(router.on(['GET', 'POST'], '/', () => {})) 109 | expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {})) 110 | expectType(router.on('GET', '/', () => {}, {})) 111 | expectType(router.on('GET', '/', { constraints: { version: '1.0.0' }}, () => {}, {})) 112 | 113 | expectType(router.addConstraintStrategy(constraints.foo)) 114 | 115 | expectType(router.get('/', () => {})) 116 | expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {})) 117 | expectType(router.get('/', () => {}, {})) 118 | expectType(router.get('/', { constraints: { version: '1.0.0' }}, () => {}, {})) 119 | 120 | expectType(router.off('GET', '/')) 121 | expectType(router.off(['GET', 'POST'], '/')) 122 | 123 | expectType(router.lookup(http2Req, http2Res)) 124 | expectType(router.lookup(http2Req, http2Res, done)); 125 | expectType(router.lookup(http2Req, http2Res, ctx, done)); 126 | expectType | null>(router.find('GET', '/', {})) 127 | expectType | null>(router.find('GET', '/', {version: '1.0.0', host: 'fastify.io'})) 128 | 129 | expectType(router.reset()) 130 | expectType(router.prettyPrint()) 131 | 132 | } 133 | 134 | // Custom Constraint 135 | { 136 | let handler!: Router.Handler 137 | 138 | interface AcceptAndContentType { accept?: string, contentType?: string } 139 | 140 | const customConstraintWithObject: Router.ConstraintStrategy = { 141 | name: "customConstraintWithObject", 142 | deriveConstraint(req: Router.Req, ctx: Context | undefined): AcceptAndContentType { 143 | return { 144 | accept: req.headers.accept, 145 | contentType: req.headers["content-type"] 146 | } 147 | }, 148 | validate(value: unknown): void {}, 149 | storage () { 150 | return { 151 | get (version) { return handler }, 152 | set (version, handler) {} 153 | } 154 | } 155 | } 156 | 157 | const storageWithObject = customConstraintWithObject.storage() 158 | const acceptAndContentType: AcceptAndContentType = { accept: 'application/json', contentType: 'application/xml' } 159 | 160 | expectType(customConstraintWithObject.deriveConstraint(http1Req, http1Res)) 161 | expectType | null>(storageWithObject.get(acceptAndContentType)); 162 | expectType(storageWithObject.set(acceptAndContentType, () => {})); 163 | 164 | const customConstraintWithDefault: Router.ConstraintStrategy = { 165 | name: "customConstraintWithObject", 166 | deriveConstraint(req: Router.Req, ctx: Context | undefined): string { 167 | return req.headers.accept ?? '' 168 | }, 169 | storage () { 170 | return { 171 | get (version) { return handler }, 172 | set (version, handler) {} 173 | } 174 | } 175 | } 176 | 177 | const storageWithDefault = customConstraintWithDefault.storage() 178 | 179 | expectType(customConstraintWithDefault.deriveConstraint(http1Req, http1Res)) 180 | expectType | null>(storageWithDefault.get('')); 181 | expectType(storageWithDefault.set('', () => {})); 182 | } 183 | --------------------------------------------------------------------------------