diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 90d45b008..2dcf55cd3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,5 +18,6 @@ Before this pull request can be merged, a core maintainer will check whether - semver.major: Breaking changes. This includes changing interfaces or configuration behaviour. * [ ] the correct branch is targeted. Patch updates can target main, other changes should target the latest versions/* branch. * [ ] the RELEASE_NOTES.md document in case of relevant feature or config changes. + * [ ] any relevant documentation was updated to reflect the changes in this PR. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e78f7e35e..e05b19a5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: - '16.0' - '16.x' - '17.x' - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 @@ -83,7 +83,7 @@ jobs: image: redis ports: - 6379:6379 - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 @@ -104,7 +104,7 @@ jobs: - '12.x' - '14.x' - '16.x' - timeout-minutes: 10 + timeout-minutes: 15 steps: - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 diff --git a/README.md b/README.md index 596dec561..2dbecf6f8 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ or if you want to try a specific [branch](https://www.npmjs.com/) of the code, you can use: ```shell git clone https://github.com/CommunitySolidServer/CommunitySolidServer.git -cd community-server +cd CommunitySolidServer npm ci npm start -- # add parameters if needed ``` @@ -74,7 +74,7 @@ Docker allows you to run the server without having Node.js installed. Images are ```shell # Clone the repo to get access to the configs git clone https://github.com/CommunitySolidServer/CommunitySolidServer.git -cd community-server +cd CommunitySolidServer # Run the image, serving your `~/Solid` directory on `http://localhost:3000` docker run --rm -v ~/Solid:/data -p 3000:3000 -it solidproject/community-server:latest # Or use one of the built-in configurations diff --git a/config/http/handler/handlers/redirect.json b/config/http/handler/handlers/redirect.json new file mode 100644 index 000000000..4a715487b --- /dev/null +++ b/config/http/handler/handlers/redirect.json @@ -0,0 +1,20 @@ +{ + "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^3.0.0/components/context.jsonld", + "@graph": [ + { + "comment": "Example handler to configure redirect patterns.", + "@id": "urn:solid-server:default:RedirectHandler", + "@type": "RedirectingHttpHandler", + "redirects": [ + { + "RedirectingHttpHandler:_redirects_key": "/from/(.*)", + "RedirectingHttpHandler:_redirects_value": "/to/$1" + } + ], + "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, + "targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" }, + "responseWriter": { "@id": "urn:solid-server:default:ResponseWriter" }, + "statusCode": "303" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 96f20485c..fcc482a84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -87,9 +87,11 @@ "eslint": "^8.8.0", "eslint-config-es": "4.1.0", "eslint-import-resolver-typescript": "^2.5.0", - "eslint-plugin-import": "^2.25.4", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.25.2", "eslint-plugin-jest": "^26.0.0", "eslint-plugin-tsdoc": "^0.2.14", + "eslint-plugin-unicorn": "^37.0.1", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^4.3.8", "jest": "^27.4.7", @@ -172,7 +174,6 @@ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz", "integrity": "sha512-mUqYa46lgWqHKQ33Q6LNCGp/wPR3eqOYTUixHFsfrSQqRxH0+WOzca75iEjFr5RDGH1dDz622LaHhLOzOuQRUA==", "dev": true, - "peer": true, "dependencies": { "eslint-scope": "^5.1.1", "eslint-visitor-keys": "^2.1.0", @@ -191,7 +192,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true, - "peer": true, "bin": { "semver": "bin/semver.js" } @@ -5064,13 +5064,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", - "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.18.0.tgz", + "integrity": "sha512-C0CZML6NyRDj+ZbMqh9FnPscg2PrzSaVQg3IpTmpe0NURMVBXlghGZgMYqBw07YW73i0MCqSDqv2SbywnCS8jQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/visitor-keys": "5.10.2" + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/visitor-keys": "5.18.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5081,9 +5081,9 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/types": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", - "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.18.0.tgz", + "integrity": "sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5094,12 +5094,12 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", - "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz", + "integrity": "sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/types": "5.18.0", "eslint-visitor-keys": "^3.0.0" }, "engines": { @@ -5111,9 +5111,9 @@ } }, "node_modules/@typescript-eslint/scope-manager/node_modules/eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5160,15 +5160,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", - "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.18.0.tgz", + "integrity": "sha512-+hFGWUMMri7OFY26TsOlGa+zgjEy1ssEipxpLjtl4wSll8zy85x0GrUSju/FHdKfVorZPYJLkF3I4XPtnCTewA==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.10.2", - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/typescript-estree": "5.10.2", + "@typescript-eslint/scope-manager": "5.18.0", + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/typescript-estree": "5.18.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -5184,9 +5184,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", - "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.18.0.tgz", + "integrity": "sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5197,13 +5197,13 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", - "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz", + "integrity": "sha512-wa+2VAhOPpZs1bVij9e5gyVu60ReMi/KuOx4LKjGx2Y3XTNUDJgQ+5f77D49pHtqef/klglf+mibuHs9TrPxdQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/visitor-keys": "5.10.2", + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/visitor-keys": "5.18.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -5224,12 +5224,12 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", - "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz", + "integrity": "sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/types": "5.18.0", "eslint-visitor-keys": "^3.0.0" }, "engines": { @@ -5241,9 +5241,9 @@ } }, "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5895,7 +5895,6 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", "dev": true, - "peer": true, "engines": { "node": ">=6" }, @@ -6194,7 +6193,6 @@ "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", "dev": true, - "peer": true, "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -6207,7 +6205,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true, - "peer": true, "engines": { "node": ">=0.8.0" } @@ -7495,7 +7492,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", "dev": true, - "peer": true, "dependencies": { "escape-string-regexp": "^1.0.5", "ignore": "^5.0.5" @@ -7515,7 +7511,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true, - "peer": true, "engines": { "node": ">=0.8.0" } @@ -7592,15 +7587,15 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.0.0.tgz", - "integrity": "sha512-Fvs0YgJ/nw9FTrnqTuMGVrkozkd07jkQzWm0ajqyHlfcsdkxGfAuv30fgfWHOnHiCr9+1YQ365CcDX7vrNhqQg==", + "version": "26.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.3.tgz", + "integrity": "sha512-Pju+T7MFpo5VFhFlwrkK/9jRUu18r2iugvgyrWOnnGRaVTFFmFXp+xFJpHyqmjjLmGJPKLeEFLVTAxezkApcpQ==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.10.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -7723,7 +7718,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-37.0.1.tgz", "integrity": "sha512-E1jq5u9ojnadisJcPi+hMXTGSiIzkIUMDvWsBudsCGXvKUB2aNSU2TcfyW2/jAS5A4ryBXfzxLykMxX1EdluSQ==", "dev": true, - "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.9", "ci-info": "^3.2.0", @@ -7799,7 +7793,6 @@ "resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-2.3.2.tgz", "integrity": "sha512-3ydhqFpuV7x1M9EK52BPNj6V0Kwu0KKkcIAfpUhwHbR8ocRln/oUHgfxQupY8O1h4Qv/POHDumb/BwwNfxbtnA==", "dev": true, - "peer": true, "dependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", @@ -9586,7 +9579,6 @@ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.1.0.tgz", "integrity": "sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==", "dev": true, - "peer": true, "dependencies": { "builtin-modules": "^3.0.0" }, @@ -11631,8 +11623,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz", "integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/n3": { "version": "1.16.0", @@ -12636,7 +12627,6 @@ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, - "peer": true, "engines": { "node": ">=4" } @@ -13225,7 +13215,6 @@ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", "dev": true, - "peer": true, "bin": { "regexp-tree": "bin/regexp-tree" } @@ -13433,7 +13422,6 @@ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", "dev": true, - "peer": true, "dependencies": { "regexp-tree": "~0.1.1" } @@ -15163,7 +15151,6 @@ "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz", "integrity": "sha512-mUqYa46lgWqHKQ33Q6LNCGp/wPR3eqOYTUixHFsfrSQqRxH0+WOzca75iEjFr5RDGH1dDz622LaHhLOzOuQRUA==", "dev": true, - "peer": true, "requires": { "eslint-scope": "^5.1.1", "eslint-visitor-keys": "^2.1.0", @@ -15174,8 +15161,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "peer": true + "dev": true } } }, @@ -19123,35 +19109,35 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.2.tgz", - "integrity": "sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.18.0.tgz", + "integrity": "sha512-C0CZML6NyRDj+ZbMqh9FnPscg2PrzSaVQg3IpTmpe0NURMVBXlghGZgMYqBw07YW73i0MCqSDqv2SbywnCS8jQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/visitor-keys": "5.10.2" + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/visitor-keys": "5.18.0" }, "dependencies": { "@typescript-eslint/types": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", - "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.18.0.tgz", + "integrity": "sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==", "dev": true }, "@typescript-eslint/visitor-keys": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", - "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz", + "integrity": "sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/types": "5.18.0", "eslint-visitor-keys": "^3.0.0" } }, "eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true } } @@ -19178,33 +19164,33 @@ } }, "@typescript-eslint/utils": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.2.tgz", - "integrity": "sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.18.0.tgz", + "integrity": "sha512-+hFGWUMMri7OFY26TsOlGa+zgjEy1ssEipxpLjtl4wSll8zy85x0GrUSju/FHdKfVorZPYJLkF3I4XPtnCTewA==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.10.2", - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/typescript-estree": "5.10.2", + "@typescript-eslint/scope-manager": "5.18.0", + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/typescript-estree": "5.18.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, "dependencies": { "@typescript-eslint/types": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.2.tgz", - "integrity": "sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.18.0.tgz", + "integrity": "sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.2.tgz", - "integrity": "sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz", + "integrity": "sha512-wa+2VAhOPpZs1bVij9e5gyVu60ReMi/KuOx4LKjGx2Y3XTNUDJgQ+5f77D49pHtqef/klglf+mibuHs9TrPxdQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.10.2", - "@typescript-eslint/visitor-keys": "5.10.2", + "@typescript-eslint/types": "5.18.0", + "@typescript-eslint/visitor-keys": "5.18.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -19213,19 +19199,19 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.10.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.2.tgz", - "integrity": "sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz", + "integrity": "sha512-Hf+t+dJsjAKpKSkg3EHvbtEpFFb/1CiOHnvI8bjHgOD4/wAw3gKrA0i94LrbekypiZVanJu3McWJg7rWDMzRTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.10.2", + "@typescript-eslint/types": "5.18.0", "eslint-visitor-keys": "^3.0.0" } }, "eslint-visitor-keys": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz", - "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", "dev": true } } @@ -19732,8 +19718,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", - "dev": true, - "peer": true + "dev": true }, "bytes": { "version": "3.1.0", @@ -19951,7 +19936,6 @@ "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=", "dev": true, - "peer": true, "requires": { "escape-string-regexp": "^1.0.5" }, @@ -19960,8 +19944,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "peer": true + "dev": true } } }, @@ -21022,7 +21005,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz", "integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==", "dev": true, - "peer": true, "requires": { "escape-string-regexp": "^1.0.5", "ignore": "^5.0.5" @@ -21032,8 +21014,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "peer": true + "dev": true } } }, @@ -21099,9 +21080,9 @@ } }, "eslint-plugin-jest": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.0.0.tgz", - "integrity": "sha512-Fvs0YgJ/nw9FTrnqTuMGVrkozkd07jkQzWm0ajqyHlfcsdkxGfAuv30fgfWHOnHiCr9+1YQ365CcDX7vrNhqQg==", + "version": "26.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.1.3.tgz", + "integrity": "sha512-Pju+T7MFpo5VFhFlwrkK/9jRUu18r2iugvgyrWOnnGRaVTFFmFXp+xFJpHyqmjjLmGJPKLeEFLVTAxezkApcpQ==", "dev": true, "requires": { "@typescript-eslint/utils": "^5.10.0" @@ -21193,7 +21174,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-37.0.1.tgz", "integrity": "sha512-E1jq5u9ojnadisJcPi+hMXTGSiIzkIUMDvWsBudsCGXvKUB2aNSU2TcfyW2/jAS5A4ryBXfzxLykMxX1EdluSQ==", "dev": true, - "peer": true, "requires": { "@babel/helper-validator-identifier": "^7.14.9", "ci-info": "^3.2.0", @@ -21242,7 +21222,6 @@ "resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-2.3.2.tgz", "integrity": "sha512-3ydhqFpuV7x1M9EK52BPNj6V0Kwu0KKkcIAfpUhwHbR8ocRln/oUHgfxQupY8O1h4Qv/POHDumb/BwwNfxbtnA==", "dev": true, - "peer": true, "requires": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", @@ -22520,7 +22499,6 @@ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.1.0.tgz", "integrity": "sha512-OV7JjAgOTfAFJmHZLvpSTb4qi0nIILDV1gWPYDnDJUTNFM5aGlRAhk4QcT8i7TuAleeEV5Fdkqn3t4mS+Q11fg==", "dev": true, - "peer": true, "requires": { "builtin-modules": "^3.0.0" } @@ -24109,8 +24087,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz", "integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==", - "dev": true, - "peer": true + "dev": true }, "n3": { "version": "1.16.0", @@ -24841,8 +24818,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, - "peer": true + "dev": true }, "prelude-ls": { "version": "1.2.1", @@ -25335,8 +25311,7 @@ "version": "0.1.24", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", - "dev": true, - "peer": true + "dev": true }, "regexp.prototype.flags": { "version": "1.3.1", @@ -25469,7 +25444,6 @@ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", "dev": true, - "peer": true, "requires": { "regexp-tree": "~0.1.1" } diff --git a/package.json b/package.json index 0f8ee55fc..2ff26ab73 100644 --- a/package.json +++ b/package.json @@ -150,9 +150,11 @@ "eslint": "^8.8.0", "eslint-config-es": "4.1.0", "eslint-import-resolver-typescript": "^2.5.0", - "eslint-plugin-import": "^2.25.4", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-import": "^2.25.2", "eslint-plugin-jest": "^26.0.0", "eslint-plugin-tsdoc": "^0.2.14", + "eslint-plugin-unicorn": "^37.0.1", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^4.3.8", "jest": "^27.4.7", diff --git a/src/index.ts b/src/index.ts index 949379706..74c37a402 100644 --- a/src/index.ts +++ b/src/index.ts @@ -260,6 +260,7 @@ export * from './server/HttpResponse'; export * from './server/HttpServerFactory'; export * from './server/OperationHttpHandler'; export * from './server/ParsingHttpHandler'; +export * from './server/RedirectingHttpHandler'; export * from './server/WebSocketHandler'; export * from './server/WebSocketServerFactory'; diff --git a/src/server/RedirectingHttpHandler.ts b/src/server/RedirectingHttpHandler.ts new file mode 100644 index 000000000..059157f8a --- /dev/null +++ b/src/server/RedirectingHttpHandler.ts @@ -0,0 +1,109 @@ +import type { TargetExtractor } from '../http/input/identifier/TargetExtractor'; +import { RedirectResponseDescription } from '../http/output/response/RedirectResponseDescription'; +import type { ResponseWriter } from '../http/output/ResponseWriter'; +import { getLoggerFor } from '../logging/LogUtil'; +import { FoundHttpError } from '../util/errors/FoundHttpError'; +import { MovedPermanentlyHttpError } from '../util/errors/MovedPermanentlyHttpError'; +import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError'; +import { PermanentRedirectHttpError } from '../util/errors/PermanentRedirectHttpError'; +import type { RedirectHttpError } from '../util/errors/RedirectHttpError'; +import { SeeOtherHttpError } from '../util/errors/SeeOtherHttpError'; +import { TemporaryRedirectHttpError } from '../util/errors/TemporaryRedirectHttpError'; +import { getRelativeUrl, joinUrl } from '../util/PathUtil'; +import type { HttpHandlerInput } from './HttpHandler'; +import { HttpHandler } from './HttpHandler'; +import type { HttpRequest } from './HttpRequest'; + +const redirectErrorFactories: Record<301 | 302 | 303 | 307 | 308, (location: string) => RedirectHttpError> = { + 301: (location: string): RedirectHttpError => new MovedPermanentlyHttpError(location), + 302: (location: string): RedirectHttpError => new FoundHttpError(location), + 303: (location: string): RedirectHttpError => new SeeOtherHttpError(location), + 307: (location: string): RedirectHttpError => new TemporaryRedirectHttpError(location), + 308: (location: string): RedirectHttpError => new PermanentRedirectHttpError(location), +}; + +/** + * Handler that redirects paths matching given patterns + * to their corresponding URL, substituting selected groups. + */ +export class RedirectingHttpHandler extends HttpHandler { + private readonly logger = getLoggerFor(this); + private readonly redirects: { + regex: RegExp; + redirectPattern: string; + }[]; + + /** + * Creates a handler for the provided redirects. + * @param redirects - A mapping between URL patterns. + * @param targetExtractor - To extract the target from the request. + * @param responseWriter - To write the redirect to the response. + * @param statusCode - Desired 30x redirection code (defaults to 308). + */ + public constructor( + redirects: Record, + private readonly baseUrl: string, + private readonly targetExtractor: TargetExtractor, + private readonly responseWriter: ResponseWriter, + private readonly statusCode: 301 | 302 | 303 | 307 | 308 = 308, + ) { + super(); + + // Create an array of (regexp, redirect) pairs + this.redirects = Object.keys(redirects).map( + (pattern): { regex: RegExp; redirectPattern: string } => ({ + regex: new RegExp(pattern, 'u'), + redirectPattern: redirects[pattern], + }), + ); + } + + public async canHandle({ request }: HttpHandlerInput): Promise { + // Try to find redirect for target URL + await this.findRedirect(request); + } + + public async handle({ request, response }: HttpHandlerInput): Promise { + // Try to find redirect for target URL + const redirect = await this.findRedirect(request); + + // Send redirect response + this.logger.info(`Redirecting ${request.url} to ${redirect}`); + const result = new RedirectResponseDescription(redirectErrorFactories[this.statusCode](redirect)); + await this.responseWriter.handleSafe({ response, result }); + } + + private async findRedirect(request: HttpRequest): Promise { + // Retrieve target relative to base URL + const target = await getRelativeUrl(this.baseUrl, request, this.targetExtractor); + + // Get groups and redirect of first matching pattern + let result; + for (const { regex, redirectPattern } of this.redirects) { + const match = regex.exec(target); + if (match) { + result = { match, redirectPattern }; + break; + } + } + + // Only return if a redirect is configured for the requested URL + if (!result) { + throw new NotImplementedHttpError(`No redirect configured for ${target}`); + } + + // Build redirect URL from regexp result + const { match, redirectPattern } = result; + const redirect = match.reduce( + (prev, param, index): string => prev.replace(`$${index}`, param), + redirectPattern, + ); + + // Don't redirect if target is already correct + if (redirect === target) { + throw new NotImplementedHttpError('Target is already correct.'); + } + + return /^(?:[a-z]+:)?\/\//ui.test(redirect) ? redirect : joinUrl(this.baseUrl, redirect); + } +} diff --git a/src/util/errors/FoundHttpError.ts b/src/util/errors/FoundHttpError.ts index a88aef609..a1693f4c2 100644 --- a/src/util/errors/FoundHttpError.ts +++ b/src/util/errors/FoundHttpError.ts @@ -6,6 +6,7 @@ const BaseHttpError = generateRedirectHttpErrorClass(302, 'FoundHttpError'); /** * Error used for resources that have been moved temporarily. + * Methods other than GET may or may not be changed to GET in subsequent requests. */ export class FoundHttpError extends BaseHttpError { public constructor(location: string, message?: string, options?: HttpErrorOptions) { diff --git a/src/util/errors/MovedPermanentlyHttpError.ts b/src/util/errors/MovedPermanentlyHttpError.ts index 1ae86f45a..6d165aa02 100644 --- a/src/util/errors/MovedPermanentlyHttpError.ts +++ b/src/util/errors/MovedPermanentlyHttpError.ts @@ -6,6 +6,7 @@ const BaseHttpError = generateRedirectHttpErrorClass(301, 'MovedPermanentlyHttpE /** * Error used for resources that have been moved permanently. + * Methods other than GET may or may not be changed to GET in subsequent requests. */ export class MovedPermanentlyHttpError extends BaseHttpError { public constructor(location: string, message?: string, options?: HttpErrorOptions) { diff --git a/src/util/errors/PermanentRedirectHttpError.ts b/src/util/errors/PermanentRedirectHttpError.ts new file mode 100644 index 000000000..294a074f0 --- /dev/null +++ b/src/util/errors/PermanentRedirectHttpError.ts @@ -0,0 +1,15 @@ +import type { HttpErrorOptions } from './HttpError'; +import { generateRedirectHttpErrorClass } from './RedirectHttpError'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const BaseHttpError = generateRedirectHttpErrorClass(308, 'PermanentRedirectHttpError'); + +/** + * Error used for resources that have been moved permanently. + * Method and body should not be changed in subsequent requests. + */ +export class PermanentRedirectHttpError extends BaseHttpError { + public constructor(location: string, message?: string, options?: HttpErrorOptions) { + super(location, message, options); + } +} diff --git a/src/util/errors/SeeOtherHttpError.ts b/src/util/errors/SeeOtherHttpError.ts new file mode 100644 index 000000000..54b6c3401 --- /dev/null +++ b/src/util/errors/SeeOtherHttpError.ts @@ -0,0 +1,16 @@ +import type { HttpErrorOptions } from './HttpError'; +import { generateRedirectHttpErrorClass } from './RedirectHttpError'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const BaseHttpError = generateRedirectHttpErrorClass(303, 'SeeOtherHttpError'); + +/** + * Error used to redirect not to the requested resource itself, but to another page, + * for example a representation of a real-world object. + * The method used to display this redirected page is always GET. + */ +export class SeeOtherHttpError extends BaseHttpError { + public constructor(location: string, message?: string, options?: HttpErrorOptions) { + super(location, message, options); + } +} diff --git a/src/util/errors/TemporaryRedirectHttpError.ts b/src/util/errors/TemporaryRedirectHttpError.ts new file mode 100644 index 000000000..3cb0b1925 --- /dev/null +++ b/src/util/errors/TemporaryRedirectHttpError.ts @@ -0,0 +1,15 @@ +import type { HttpErrorOptions } from './HttpError'; +import { generateRedirectHttpErrorClass } from './RedirectHttpError'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const BaseHttpError = generateRedirectHttpErrorClass(307, 'TemporaryRedirectHttpError'); + +/** + * Error used for resources that have been moved temporarily. + * Method and body should not be changed in subsequent requests. + */ +export class TemporaryRedirectHttpError extends BaseHttpError { + public constructor(location: string, message?: string, options?: HttpErrorOptions) { + super(location, message, options); + } +} diff --git a/test/unit/server/RedirectingHttpHandler.test.ts b/test/unit/server/RedirectingHttpHandler.test.ts new file mode 100644 index 000000000..585bf7e28 --- /dev/null +++ b/test/unit/server/RedirectingHttpHandler.test.ts @@ -0,0 +1,97 @@ +import type { TargetExtractor } from '../../../src/http/input/identifier/TargetExtractor'; +import type { ResponseWriter } from '../../../src/http/output/ResponseWriter'; +import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier'; +import type { HttpRequest } from '../../../src/server/HttpRequest'; +import type { HttpResponse } from '../../../src/server/HttpResponse'; +import { RedirectingHttpHandler } from '../../../src/server/RedirectingHttpHandler'; +import { joinUrl } from '../../../src/util/PathUtil'; +import { SOLID_HTTP } from '../../../src/util/Vocabularies'; + +describe('A RedirectingHttpHandler', (): void => { + const baseUrl = 'http://test.com/'; + const request = { method: 'GET' } as HttpRequest; + const response = {} as HttpResponse; + let targetExtractor: jest.Mocked; + let responseWriter: jest.Mocked; + let handler: RedirectingHttpHandler; + + beforeEach(async(): Promise => { + targetExtractor = { + handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })), + } as any; + + responseWriter = { handleSafe: jest.fn() } as any; + + handler = new RedirectingHttpHandler({ + '/one': '/two', + '/from/(.*)': 'http://to/t$1', + '/f([aeiou]+)/b([aeiou]+)r': '/f$2/b$1r', + '/s(.)me': '/s$1me', + }, baseUrl, targetExtractor, responseWriter); + }); + + afterEach(jest.clearAllMocks); + + it('does not handle requests without URL.', async(): Promise => { + await expect(handler.canHandle({ request, response })) + .rejects.toThrow('Url must be a string. Received undefined'); + await expect(handler.handle({ request, response })) + .rejects.toThrow('Url must be a string. Received undefined'); + }); + + it('does not handle requests with unconfigured URLs.', async(): Promise => { + request.url = '/other'; + await expect(handler.canHandle({ request, response })) + .rejects.toThrow('No redirect configured for /other'); + await expect(handler.handle({ request, response })) + .rejects.toThrow('No redirect configured for /other'); + }); + + it('does not handle requests redirecting to their own target URL.', async(): Promise => { + request.url = '/same'; + await expect(handler.canHandle({ request, response })) + .rejects.toThrow('Target is already correct.'); + await expect(handler.handle({ request, response })) + .rejects.toThrow('Target is already correct.'); + }); + + it('handles requests to a known URL.', async(): Promise => { + request.url = '/one'; + + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ + response, + result: expect.objectContaining({ statusCode: 308 }), + }); + const { metadata } = responseWriter.handleSafe.mock.calls[0][0].result; + expect(metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(joinUrl(baseUrl, '/two')); + }); + + it('handles correctly substitutes group patterns.', async(): Promise => { + request.url = '/fa/boor'; + + await handler.handle({ request, response }); + const { metadata } = responseWriter.handleSafe.mock.calls[0][0].result; + expect(metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(joinUrl(baseUrl, '/foo/bar')); + }); + + it('redirects to an absolute url if provided.', async(): Promise => { + request.url = '/from/here'; + + await handler.handle({ request, response }); + const { metadata } = responseWriter.handleSafe.mock.calls[0][0].result; + expect(metadata?.get(SOLID_HTTP.terms.location)?.value).toBe('http://to/there'); + }); + + it.each([ 301, 302, 303, 307, 308 ])('redirects with the provided status code: %i.', async(code): Promise => { + request.url = '/one'; + (handler as any).statusCode = code; + + await handler.handle({ request, response }); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ + response, + result: expect.objectContaining({ statusCode: code }), + }); + }); +}); diff --git a/test/unit/util/errors/RedirectHttpError.test.ts b/test/unit/util/errors/RedirectHttpError.test.ts index 6a2a106bd..643e95dce 100644 --- a/test/unit/util/errors/RedirectHttpError.test.ts +++ b/test/unit/util/errors/RedirectHttpError.test.ts @@ -2,8 +2,11 @@ import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError'; import type { HttpErrorOptions } from '../../../../src/util/errors/HttpError'; import { generateHttpErrorUri } from '../../../../src/util/errors/HttpError'; import { MovedPermanentlyHttpError } from '../../../../src/util/errors/MovedPermanentlyHttpError'; +import { PermanentRedirectHttpError } from '../../../../src/util/errors/PermanentRedirectHttpError'; import { RedirectHttpError } from '../../../../src/util/errors/RedirectHttpError'; import type { RedirectHttpErrorClass } from '../../../../src/util/errors/RedirectHttpError'; +import { SeeOtherHttpError } from '../../../../src/util/errors/SeeOtherHttpError'; +import { TemporaryRedirectHttpError } from '../../../../src/util/errors/TemporaryRedirectHttpError'; // Used to make sure the RedirectHttpError constructor also gets called in a test. class FixedRedirectHttpError extends RedirectHttpError { @@ -20,6 +23,9 @@ describe('RedirectHttpError', (): void => { [ 'RedirectHttpError', 0, FixedRedirectHttpError ], [ 'MovedPermanentlyHttpError', 301, MovedPermanentlyHttpError ], [ 'FoundHttpError', 302, FoundHttpError ], + [ 'SeeOtherHttpError', 303, SeeOtherHttpError ], + [ 'TemporaryRedirectHttpError', 307, TemporaryRedirectHttpError ], + [ 'PermanentRedirectHttpError', 308, PermanentRedirectHttpError ], ]; describe.each(errors)('%s', (name, statusCode, constructor): void => {