diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 942501c73..6b9aba65b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,7 +14,7 @@ updates: interval: "daily" time: "03:35" timezone: "Europe/Brussels" - target-branch: "versions/6.0.0" + target-branch: "versions/next-major" ignore: # Ignore minor and patch version updates - dependency-name: "*" diff --git a/.github/workflows/cth-test.yml b/.github/workflows/cth-test.yml index b58380e85..6c033531b 100644 --- a/.github/workflows/cth-test.yml +++ b/.github/workflows/cth-test.yml @@ -42,7 +42,7 @@ jobs: with: node-version: 16.x - name: Check out the project - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 with: ref: ${{ inputs.branch || github.ref }} - name: Install dependencies and run build scripts diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a59a58b02..786e13307 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,7 +21,7 @@ jobs: tags: ${{ steps.meta-main.outputs.tags || steps.meta-version.outputs.tags }} steps: - name: Checkout - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main') name: Docker meta edge and version tag id: meta-main @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx diff --git a/.github/workflows/mkdocs.yml b/.github/workflows/mkdocs.yml index 1a7d8918e..a0de1486f 100644 --- a/.github/workflows/mkdocs.yml +++ b/.github/workflows/mkdocs.yml @@ -21,7 +21,7 @@ jobs: outputs: major: ${{ steps.tagged_version.outputs.major || steps.current_version.outputs.major }} steps: - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v3.5.3 - uses: actions/setup-node@v3 with: node-version: '16.x' @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest needs: mkdocs-prep steps: - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v3.5.3 - uses: actions/setup-python@v4 with: python-version: 3.x @@ -63,7 +63,7 @@ jobs: needs: [mkdocs-prep, mkdocs] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v3.5.3 - uses: actions/setup-node@v3 with: node-version: '16.x' diff --git a/.github/workflows/npm-test.yml b/.github/workflows/npm-test.yml index 00e32d132..cae1eb0e0 100644 --- a/.github/workflows/npm-test.yml +++ b/.github/workflows/npm-test.yml @@ -7,7 +7,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v3.5.3 - uses: actions/setup-node@v3 with: node-version: '16.x' @@ -38,7 +38,7 @@ jobs: - name: Ensure line endings are consistent run: git config --global core.autocrlf input - name: Check out repository - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install dependencies and run build scripts run: npm ci - name: Type-check tests @@ -81,7 +81,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Check out repository - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install dependencies and run build scripts run: npm ci - name: Run integration tests @@ -105,7 +105,7 @@ jobs: - name: Ensure line endings are consistent run: git config --global core.autocrlf input - name: Check out repository - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install dependencies and run build scripts run: npm ci - name: Run integration tests @@ -127,7 +127,7 @@ jobs: with: node-version: '16.x' - name: Check out repository - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install dependencies and run build scripts run: npm ci - name: Run deploy tests diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index 33244eec3..b40e1433f 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -12,7 +12,7 @@ jobs: matrix: branch: - 'main' - - 'versions/6.0.0' + - 'versions/next-major' uses: ./.github/workflows/cth-test.yml with: branch: ${{ matrix.branch }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1172d6fd1..c01553ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. +## [6.0.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v6.0.0...v6.0.1) (2023-06-15) + +### Fixes + +* Use correct type for Webhook notifications ([c0a881b](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/c0a881b9809d3a551c4cdf63bbd89ce57f3fff8d)) +* Make root storage subject of storage description ([9584ab7](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/9584ab7549ecf7ab20fe1e6db28f3c900d9a5392)) +* Prevent illegal file paths from being generated ([fdee4b3](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/fdee4b334fa456746e9d2097284321a6c1fa2362)) + ## [6.0.0](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v6.0.0-alpha.0...v6.0.0) (2023-05-02) ### Features diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 31d6f6517..6e7a05827 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,3 +1,9 @@ -# Code of conduct +# Code of Conduct -We follow and adhere to the Solid [Code of Conduct](https://github.com/solid/process/blob/main/code-of-conduct.md). +For our Code of Conduct, we follow and adhere to the Solid [Code of Conduct](https://github.com/solid/process/blob/main/code-of-conduct.md), +but with a different Committee, which should be contacted in the case of violations. + +The Committee consists of the following people: + +* Joachim Van Herwegen +* Ruben Verborgh diff --git a/config/http/handler/default.json b/config/http/handler/default.json index b5be5b6ad..5d1740737 100644 --- a/config/http/handler/default.json +++ b/config/http/handler/default.json @@ -12,6 +12,7 @@ "handlers": [ { "@id": "urn:solid-server:default:Middleware" }, { + "@id": "urn:solid-server:default:BaseHttpHandler", "@type": "WaterfallHandler", "handlers": [ { "@id": "urn:solid-server:default:StaticAssetHandler" }, diff --git a/config/http/handler/simple.json b/config/http/handler/simple.json index 39bf25009..da53d12d7 100644 --- a/config/http/handler/simple.json +++ b/config/http/handler/simple.json @@ -11,6 +11,7 @@ "handlers": [ { "@id": "urn:solid-server:default:Middleware" }, { + "@id": "urn:solid-server:default:BaseHttpHandler", "@type": "WaterfallHandler", "handlers": [ { "@id": "urn:solid-server:default:StaticAssetHandler" }, diff --git a/config/http/notifications/webhooks/handler.json b/config/http/notifications/webhooks/handler.json index 8b2f9a221..ff864ba4b 100644 --- a/config/http/notifications/webhooks/handler.json +++ b/config/http/notifications/webhooks/handler.json @@ -2,10 +2,10 @@ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@graph": [ { - "comment": "Handles the generation and serialization of notifications for WebHookChannel2023.", + "comment": "Handles the generation and serialization of notifications for WebhookChannel2023.", "@id": "urn:solid-server:default:WebHookNotificationHandler", "@type": "TypedNotificationHandler", - "type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023", + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", "source": { "@type": "ComposedNotificationHandler", "generator": { "@id": "urn:solid-server:default:BaseNotificationGenerator" }, @@ -14,7 +14,7 @@ } }, { - "comment": "Emits serialized notifications through HTTP requests to the WebHook.", + "comment": "Emits serialized notifications through HTTP requests to the Webhook.", "@id": "urn:solid-server:default:WebHookEmitter", "@type": "WebHookEmitter", "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, diff --git a/config/http/notifications/webhooks/routes.json b/config/http/notifications/webhooks/routes.json index 0acbf15a8..fe1b0dcac 100644 --- a/config/http/notifications/webhooks/routes.json +++ b/config/http/notifications/webhooks/routes.json @@ -5,7 +5,7 @@ "@id": "urn:solid-server:default:WebHookRoute", "@type": "RelativePathInteractionRoute", "base": { "@id": "urn:solid-server:default:NotificationRoute" }, - "relativePath": "/WebHookChannel2023/" + "relativePath": "/WebhookChannel2023/" }, { "@id": "urn:solid-server:default:WebHookWebIdRoute", @@ -15,11 +15,11 @@ }, { - "comment": "Handles the WebHookChannel2023 WebID.", + "comment": "Handles the WebhookChannel2023 WebID.", "@id": "urn:solid-server:default:WebHookWebId", "@type": "OperationRouterHandler", "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, - "allowedPathNames": [ "/WebHookChannel2023/webId$" ], + "allowedPathNames": [ "/WebhookChannel2023/webId$" ], "handler": { "@type": "WebHookWebId", "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" } diff --git a/config/http/notifications/webhooks/subscription.json b/config/http/notifications/webhooks/subscription.json index bdde8e92b..335a1c113 100644 --- a/config/http/notifications/webhooks/subscription.json +++ b/config/http/notifications/webhooks/subscription.json @@ -2,12 +2,12 @@ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@graph": [ { - "comment": "Handles the subscriptions targeting a WebHookChannel2023.", + "comment": "Handles the subscriptions targeting a WebhookChannel2023.", "@id": "urn:solid-server:default:WebHookRouter", "@type": "OperationRouterHandler", "baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }, "allowedMethods": [ "HEAD", "GET", "POST" ], - "allowedPathNames": [ "/WebHookChannel2023/$" ], + "allowedPathNames": [ "/WebhookChannel2023/$" ], "handler": { "@id": "urn:solid-server:default:WebHookSubscriber", "@type": "NotificationSubscriber", @@ -20,7 +20,7 @@ } }, { - "comment": "Contains all the metadata relevant for a WebHookChannel2023.", + "comment": "Contains all the metadata relevant for a WebhookChannel2023.", "@id": "urn:solid-server:default:WebHookChannel2023Type", "@type": "WebhookChannel2023Type", "route": { "@id": "urn:solid-server:default:WebHookRoute" }, diff --git a/config/quota-file.json b/config/quota-file.json index 55569a742..b3884c98a 100644 --- a/config/quota-file.json +++ b/config/quota-file.json @@ -38,6 +38,7 @@ "comment": "A server that stores its resources on disk while enforcing quota." }, { + "comment": "Sets the maximum size of a single pod to 7KB.", "@id": "urn:solid-server:default:QuotaStrategy", "@type": "PodQuotaStrategy", "limit_amount": 7000, diff --git a/config/util/representation-conversion/converters/rdf-to-quad.json b/config/util/representation-conversion/converters/rdf-to-quad.json index 605fbb7fa..51030cce9 100644 --- a/config/util/representation-conversion/converters/rdf-to-quad.json +++ b/config/util/representation-conversion/converters/rdf-to-quad.json @@ -2,13 +2,17 @@ "@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^6.0.0/components/context.jsonld", "@graph": [ { - "comment": "Converts many RDF serialization to Quad objects.", + "comment": "Converts many RDF serialization to Quad objects. Caching notification related contexts to prevent too many requests.", "@id": "urn:solid-server:default:RdfToQuadConverter", "@type": "RdfToQuadConverter", "contexts": [ { "RdfToQuadConverter:_contexts_key": "https://www.w3.org/ns/solid/notification/v1", "RdfToQuadConverter:_contexts_value": "@css:templates/contexts/notification.jsonld" + }, + { + "RdfToQuadConverter:_contexts_key": "https://www.w3.org/ns/activitystreams", + "RdfToQuadConverter:_contexts_value": "@css:templates/contexts/activitystreams.jsonld" } ] } diff --git a/documentation/markdown/contributing/release.md b/documentation/markdown/contributing/release.md index b921e5dad..80ecb6cf9 100644 --- a/documentation/markdown/contributing/release.md +++ b/documentation/markdown/contributing/release.md @@ -1,10 +1,10 @@ -# Releasing a new version +# Releasing a new major version This is only relevant if you are a developer with push access responsible for doing a new release. Steps to follow: -* Merge `main` into `versions/x.0.0`. +* Merge `main` into `versions/next-major`. * Verify if there are issues when upgrading an existing installation to the new version. * Can the data still be accessed? * Does authentication still work? @@ -15,7 +15,7 @@ Steps to follow: * Automatically updates Components.js references to the new version. Committed with `chore(release): Update configs to vx.0.0`. * Updates the `package.json`, and generates the new entries in `CHANGELOG.md`. - Commited with `chore(release): Release version vx.0.0 of the npm package` + Commits with `chore(release): Release version vx.0.0 of the npm package` * Optionally run `npx commit-and-tag-version -r major --dry-run` to preview the commands that will be run and the changes to `CHANGELOG.md`. * The `postrelease` script will now prompt you to manually edit the `CHANGELOG.md`. @@ -24,19 +24,25 @@ Steps to follow: Documentation can be removed. * Press any key in your terminal when your changes are ready. * The `postrelease` script will amend the release commit, create an annotated tag and push changes to origin. -* Merge `versions/x.0.0` into `main` and push. +* Merge `versions/next-major` into `main` and push. * Do a GitHub release. * `npm publish` - * Check if there is a `next` tag that needs to be replaced. + * `npm dist-tag add @solid/community-server@x.0.0 next` * Rename the `versions/x.0.0` branch to the next version. -* Update `.github/workflows/schedule.yml` and `.github/dependabot.yml` to point at the new branch. * Potentially upgrade dependent repositories: * Recipes at * Tutorials at + * Generator at + * Hello world component at -Changes when doing a pre-release of a major version: +## Changes when doing a pre-release * Version with `npm run release -- -r major --prerelease alpha` -* Do not merge `versions/x.0.0` into `main`. +* Do not merge `versions/next-major` into `main`. * Publish with `npm publish --tag next`. * Do not update the branch or anything related. + +## Changes when doing a minor release + +* Version with `npm run release -- -r minor` +* Do not merge `versions/next-major` into `main`. diff --git a/documentation/markdown/usage/notifications.md b/documentation/markdown/usage/notifications.md index b089c1c41..2aa445a38 100644 --- a/documentation/markdown/usage/notifications.md +++ b/documentation/markdown/usage/notifications.md @@ -27,7 +27,7 @@ Doing a GET to `http://localhost:3000/.well-known/solid` then gives the followin a ; notify:subscription , - . + . notify:channelType notify:WebSocketChannel2023 ; notify:feature notify:accept , @@ -35,8 +35,8 @@ Doing a GET to `http://localhost:3000/.well-known/solid` then gives the followin notify:rate , notify:startAt , notify:state . - - notify:channelType notify:WebHookChannel2023; + + notify:channelType notify:WebhookChannel2023; notify:feature notify:accept , notify:endAt , notify:rate , @@ -61,7 +61,7 @@ Requests without `Read` permission will be rejected. There are currently up to two supported ways to get notifications in CSS, depending on your configuration: the notification channel types [`WebSocketChannel2023`](https://solid.github.io/notifications/websocket-channel-2023); -and [`WebHookChannel2023`](https://solid.github.io/notifications/webhook-channel-2023). +and [`WebhookChannel2023`](https://solid.github.io/notifications/webhook-channel-2023). ### WebSockets @@ -98,30 +98,30 @@ const ws = new WebSocket(receiveFrom); ws.on('message', (notification) => console.log(notification)); ``` -### WebHooks +### Webhooks Similar to the WebSocket subscription, below is sample JSON-LD -that would be sent to `http://localhost:3000/.notifications/WebHookChannel2023/`: +that would be sent to `http://localhost:3000/.notifications/WebhookChannel2023/`: ```json { "@context": [ "https://www.w3.org/ns/solid/notification/v1" ], - "type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023", + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", "topic": "http://localhost:3000/foo", "sendTo": "https://example.com/webhook" } ``` Note that this document has an additional `sendTo` field. -This is the WebHook URL of your server, the URL to which you want the notifications to be sent. +This is the Webhook URL of your server, the URL to which you want the notifications to be sent. The response would then be something like this: ```json { "@context": [ "https://www.w3.org/ns/solid/notification/v1" ], - "id": "http://localhost:3000/.notifications/WebHookChannel2023/eeaf2c17-699a-4e53-8355-e91d13807e5f", - "type": "http://www.w3.org/ns/solid/notifications#WebHookChannel2023", + "id": "http://localhost:3000/.notifications/WebhookChannel2023/eeaf2c17-699a-4e53-8355-e91d13807e5f", + "type": "http://www.w3.org/ns/solid/notifications#WebhookChannel2023", "topic": "http://localhost:3000/foo", "sendTo": "https://example.com/webhook" } diff --git a/jest.config.js b/jest.config.js index ea437384f..3c080fa6d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -39,7 +39,8 @@ module.exports = { 'js', ], testEnvironment: 'node', - setupFilesAfterEnv: [ 'jest-rdf', '/test/util/SetupTests.ts' ], + globalSetup: '/test/util/SetupTests.ts', + setupFilesAfterEnv: [ 'jest-rdf' ], collectCoverage: false, // See https://github.com/matthieubosquet/ts-dpop/issues/13 moduleNameMapper: { diff --git a/package-lock.json b/package-lock.json index fd2562621..5b7227119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solid/community-server", - "version": "6.0.0", + "version": "6.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@solid/community-server", - "version": "6.0.0", + "version": "6.0.1", "license": "MIT", "dependencies": { "@comunica/context-entries": "^2.6.8", @@ -15179,9 +15179,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -27505,9 +27505,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wordwrap": { diff --git a/package.json b/package.json index 840e6afef..61b881ad5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solid/community-server", - "version": "6.0.0", + "version": "6.0.1", "description": "Community Solid Server: an open and modular implementation of the Solid specifications", "keywords": [ "solid", diff --git a/src/http/output/metadata/AllowAcceptHeaderWriter.ts b/src/http/output/metadata/AllowAcceptHeaderWriter.ts index 268f6aec7..7e2a5b4ae 100644 --- a/src/http/output/metadata/AllowAcceptHeaderWriter.ts +++ b/src/http/output/metadata/AllowAcceptHeaderWriter.ts @@ -50,10 +50,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter { // POST is only allowed on containers. // Metadata only has the resource URI in case it has resource metadata. - if (this.isPostAllowed(metadata)) { + if (!this.isPostAllowed(metadata)) { allowedMethods.delete('POST'); } + if (!this.isPutAllowed(metadata)) { + allowedMethods.delete('PUT'); + } + if (!this.isDeleteAllowed(metadata)) { allowedMethods.delete('DELETE'); } @@ -76,7 +80,14 @@ export class AllowAcceptHeaderWriter extends MetadataWriter { * otherwise it is just a blank node. */ private isPostAllowed(metadata: RepresentationMetadata): boolean { - return metadata.has(RDF.terms.type, LDP.terms.Resource) && !isContainerPath(metadata.identifier.value); + return !metadata.has(RDF.terms.type, LDP.terms.Resource) || isContainerPath(metadata.identifier.value); + } + + /** + * PUT is not allowed on existing containers. + */ + private isPutAllowed(metadata: RepresentationMetadata): boolean { + return !metadata.has(RDF.terms.type, LDP.terms.Resource) || !isContainerPath(metadata.identifier.value); } /** diff --git a/src/server/description/StorageDescriptionHandler.ts b/src/server/description/StorageDescriptionHandler.ts index 27169d658..244a9904f 100644 --- a/src/server/description/StorageDescriptionHandler.ts +++ b/src/server/description/StorageDescriptionHandler.ts @@ -1,6 +1,7 @@ import { OkResponseDescription } from '../../http/output/response/OkResponseDescription'; import type { ResponseDescription } from '../../http/output/response/ResponseDescription'; import { BasicRepresentation } from '../../http/representation/BasicRepresentation'; +import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier'; import type { ResourceStore } from '../../storage/ResourceStore'; import { INTERNAL_QUADS } from '../../util/ContentTypes'; import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError'; @@ -32,7 +33,7 @@ export class StorageDescriptionHandler extends OperationHttpHandler { if (method !== 'GET') { throw new MethodNotAllowedHttpError([ method ], `Only GET requests can target the storage description.`); } - const container = { path: ensureTrailingSlash(target.path.slice(0, -this.path.length)) }; + const container = this.getStorageIdentifier(target); const representation = await this.store.getRepresentation(container, {}); representation.data.destroy(); if (!representation.metadata.has(RDF.terms.type, PIM.terms.Storage)) { @@ -43,10 +44,17 @@ export class StorageDescriptionHandler extends OperationHttpHandler { } public async handle({ operation: { target }}: OperationHttpHandlerInput): Promise { - const quads = await this.describer.handle(target); + const quads = await this.describer.handle(this.getStorageIdentifier(target)); const representation = new BasicRepresentation(quads, INTERNAL_QUADS); return new OkResponseDescription(representation.metadata, representation.data); } + + /** + * Determine the identifier of the root storage based on the identifier of the root storage description resource. + */ + protected getStorageIdentifier(descriptionIdentifier: ResourceIdentifier): ResourceIdentifier { + return { path: ensureTrailingSlash(descriptionIdentifier.path.slice(0, -this.path.length)) }; + } } diff --git a/src/server/notifications/WebHookChannel2023/WebhookChannel2023Type.ts b/src/server/notifications/WebHookChannel2023/WebhookChannel2023Type.ts index d05839391..ce5c0e7b9 100644 --- a/src/server/notifications/WebHookChannel2023/WebhookChannel2023Type.ts +++ b/src/server/notifications/WebHookChannel2023/WebhookChannel2023Type.ts @@ -14,7 +14,7 @@ export interface WebhookChannel2023 extends NotificationChannel { /** * The "WebHookChannel2023" type. */ - type: typeof NOTIFY.WebHookChannel2023; + type: typeof NOTIFY.WebhookChannel2023; /** * Where the notifications have to be sent. */ @@ -22,7 +22,7 @@ export interface WebhookChannel2023 extends NotificationChannel { } export function isWebHook2023Channel(channel: NotificationChannel): channel is WebhookChannel2023 { - return channel.type === NOTIFY.WebHookChannel2023; + return channel.type === NOTIFY.WebhookChannel2023; } /** @@ -47,7 +47,7 @@ export class WebhookChannel2023Type extends BaseChannelType { */ public constructor(route: InteractionRoute, webIdRoute: InteractionRoute, stateHandler: StateHandler, features?: string[]) { - super(NOTIFY.terms.WebHookChannel2023, + super(NOTIFY.terms.WebhookChannel2023, route, features, [{ path: NOTIFY.sendTo, minCount: 1, maxCount: 1 }]); @@ -62,7 +62,7 @@ export class WebhookChannel2023Type extends BaseChannelType { return { ...channel, - type: NOTIFY.WebHookChannel2023, + type: NOTIFY.WebhookChannel2023, sendTo: sendTo.value, }; } diff --git a/src/util/HeaderUtil.ts b/src/util/HeaderUtil.ts index 915ed6879..5845d765e 100644 --- a/src/util/HeaderUtil.ts +++ b/src/util/HeaderUtil.ts @@ -140,6 +140,9 @@ const mediaRange = new RegExp(`${tchar.source}+/${tchar.source}+`, 'u'); * Replaces all double quoted strings in the input string with `"0"`, `"1"`, etc. * @param input - The Accept header string. * + * @throws {@link BadRequestHttpError} + * Thrown if invalid characters are detected in a quoted string. + * * @returns The transformed string and a map with keys `"0"`, etc. and values the original string that was there. */ export function transformQuotedStrings(input: string): { result: string; replacements: Record } { @@ -163,6 +166,8 @@ export function transformQuotedStrings(input: string): { result: string; replace * Splits the input string on commas, trims all parts and filters out empty ones. * * @param input - Input header string. + * + * @returns An array of trimmed strings. */ export function splitAndClean(input: string): string[] { return input.split(',') @@ -175,44 +180,67 @@ export function splitAndClean(input: string): string[] { * * @param qvalue - Input qvalue string (so "q=...."). * - * @throws {@link BadRequestHttpError} - * Thrown on invalid syntax. + * @returns true if q value is valid, false otherwise. */ -function testQValue(qvalue: string): void { - if (!/^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue)) { - logger.warn(`Invalid q value: ${qvalue}`); - throw new BadRequestHttpError( - `Invalid q value: ${qvalue} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, - ); +function isValidQValue(qvalue: string): boolean { + return /^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue); +} + +/** + * Converts a qvalue to a number. + * Returns 1 if the value is not a valid number or 1 if it is more than 1. + * Returns 0 if the value is negative. + * Otherwise, the parsed value is returned. + * + * @param qvalue - Value to convert. + */ +function parseQValue(qvalue: string): number { + const result = Number(qvalue); + if (Number.isNaN(result) || result >= 1) { + return 1; + } + if (result < 0) { + return 0; + } + return result; +} + +/** + * Logs a warning to indicate there was an invalid value. + * Throws a {@link BadRequestHttpError} in case `strict` is `true`. + * + * @param message - Message to log and potentially put in the error. + * @param strict - `true` if an error needs to be thrown. + */ +function handleInvalidValue(message: string, strict: boolean): void | never { + logger.warn(message); + if (strict) { + throw new BadRequestHttpError(message); } } /** - * Parses a list of split parameters and checks their validity. + * Parses a list of split parameters and checks their validity. Parameters with invalid + * syntax are ignored and not returned. * * @param parameters - A list of split parameters (token [ "=" ( token / quoted-string ) ]) * @param replacements - The double quoted strings that need to be replaced. - * - * - * @throws {@link BadRequestHttpError} - * Thrown on invalid parameter syntax. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * * @returns An array of name/value objects corresponding to the parameters. */ -export function parseParameters(parameters: string[], replacements: Record): +export function parseParameters(parameters: string[], replacements: Record, strict = false): { name: string; value: string }[] { - return parameters.map((param): { name: string; value: string } => { + return parameters.reduce<{ name: string; value: string }[]>((acc, param): { name: string; value: string }[] => { const [ name, rawValue ] = param.split('=').map((str): string => str.trim()); // Test replaced string for easier check // parameter = token "=" ( token / quoted-string ) // second part is optional for certain parameters if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) { - logger.warn(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue}`); - throw new BadRequestHttpError( - `Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` + - `does not match (token ( "=" ( token / quoted-string ))?). `, - ); + handleInvalidValue(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` + + `does not match (token ( "=" ( token / quoted-string ))?). `, strict); + return acc; } let value = rawValue; @@ -220,8 +248,9 @@ export function parseParameters(parameters: string[], replacements: Record): Accept { +function parseAcceptPart(part: string, replacements: Record, strict: boolean): Accept | undefined { const [ range, ...parameters ] = part.split(';').map((param): string => param.trim()); // No reason to test differently for * since we don't check if the type exists if (!mediaRange.test(range)) { - logger.warn(`Invalid Accept range: ${range}`); - throw new BadRequestHttpError( - `Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, + handleInvalidValue( + `Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, strict, ); + return; } let weight = 1; @@ -258,13 +287,16 @@ function parseAcceptPart(part: string, replacements: Record): Ac if (name === 'q') { // Extension parameters appear after the q value map = extensionParams; - testQValue(value); - weight = Number.parseFloat(value); + if (!isValidQValue(value)) { + handleInvalidValue(`Invalid q value for range ${range}: ${value + } does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict); + } + weight = parseQValue(value); } else { if (!value && map !== extensionParams) { - logger.warn(`Invalid Accept parameter ${name}`); - throw new BadRequestHttpError(`Invalid Accept parameter ${name}: ` + - `Accept parameter values are not optional when preceding the q value`); + handleInvalidValue(`Invalid Accept parameter ${name}: ` + + `Accept parameter values are not optional when preceding the q value`, strict); + return; } map[name] = value || ''; } @@ -282,14 +314,13 @@ function parseAcceptPart(part: string, replacements: Record): Ac /** * Parses an Accept-* header where each part is only a value and a weight, so roughly /.*(q=.*)?/ separated by commas. + * The returned weights default to 1 if no q value is found or the q value is invalid. * @param input - Input header string. - * - * @throws {@link BadRequestHttpError} - * Thrown on invalid qvalue syntax. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * * @returns An array of ranges and weights. */ -function parseNoParameters(input: string): AcceptHeader[] { +function parseNoParameters(input: string, strict = false): AcceptHeader[] { const parts = splitAndClean(input); return parts.map((part): AcceptHeader => { @@ -297,12 +328,15 @@ function parseNoParameters(input: string): AcceptHeader[] { const result = { range, weight: 1 }; if (qvalue) { if (!qvalue.startsWith('q=')) { - logger.warn(`Only q parameters are allowed in ${input}`); - throw new BadRequestHttpError(`Only q parameters are allowed in ${input}`); + handleInvalidValue(`Only q parameters are allowed in ${input}`, strict); + return result; } const val = qvalue.slice(2); - testQValue(val); - result.weight = Number.parseFloat(val); + if (!isValidQValue(val)) { + handleInvalidValue(`Invalid q value for range ${range}: ${val + } does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict); + } + result.weight = parseQValue(val); } return result; }).sort((left, right): number => right.weight - left.weight); @@ -314,17 +348,25 @@ function parseNoParameters(input: string): AcceptHeader[] { * Parses an Accept header string. * * @param input - The Accept header string. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * - * @throws {@link BadRequestHttpError} - * Thrown on invalid header syntax. - * - * @returns An array of {@link Accept} objects, sorted by weight. + * @returns An array of {@link Accept} objects, sorted by weight. Accept parts + * with invalid syntax are ignored and removed from the returned array. */ -export function parseAccept(input: string): Accept[] { +export function parseAccept(input: string, strict = false): Accept[] { // Quoted strings could prevent split from having correct results const { result, replacements } = transformQuotedStrings(input); + return splitAndClean(result) - .map((part): Accept => parseAcceptPart(part, replacements)) + .reduce((acc, part): Accept[] => { + const partOrUndef = parseAcceptPart(part, replacements, strict); + + if (partOrUndef !== undefined) { + acc.push(partOrUndef); + } + + return acc; + }, []) .sort((left, right): number => right.weight - left.weight); } @@ -332,70 +374,65 @@ export function parseAccept(input: string): Accept[] { * Parses an Accept-Charset header string. * * @param input - The Accept-Charset header string. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * - * @throws {@link BadRequestHttpError} - * Thrown on invalid header syntax. - * - * @returns An array of {@link AcceptCharset} objects, sorted by weight. + * @returns An array of {@link AcceptCharset} objects, sorted by weight. Invalid ranges + * are ignored and not returned. */ -export function parseAcceptCharset(input: string): AcceptCharset[] { +export function parseAcceptCharset(input: string, strict = false): AcceptCharset[] { const results = parseNoParameters(input); - results.forEach((result): void => { + return results.filter((result): boolean => { if (!token.test(result.range)) { - logger.warn(`Invalid Accept-Charset range: ${result.range}`); - throw new BadRequestHttpError( - `Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`, + handleInvalidValue( + `Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`, strict, ); + return false; } + return true; }); - return results; } /** * Parses an Accept-Encoding header string. * * @param input - The Accept-Encoding header string. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * - * @throws {@link BadRequestHttpError} - * Thrown on invalid header syntax. - * - * @returns An array of {@link AcceptEncoding} objects, sorted by weight. + * @returns An array of {@link AcceptEncoding} objects, sorted by weight. Invalid ranges + * are ignored and not returned. */ -export function parseAcceptEncoding(input: string): AcceptEncoding[] { +export function parseAcceptEncoding(input: string, strict = false): AcceptEncoding[] { const results = parseNoParameters(input); - results.forEach((result): void => { + return results.filter((result): boolean => { if (!token.test(result.range)) { - logger.warn(`Invalid Accept-Encoding range: ${result.range}`); - throw new BadRequestHttpError(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`); + handleInvalidValue(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`, strict); + return false; } + return true; }); - return results; } /** * Parses an Accept-Language header string. * * @param input - The Accept-Language header string. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * - * @throws {@link BadRequestHttpError} - * Thrown on invalid header syntax. - * - * @returns An array of {@link AcceptLanguage} objects, sorted by weight. + * @returns An array of {@link AcceptLanguage} objects, sorted by weight. Invalid ranges + * are ignored and not returned. */ -export function parseAcceptLanguage(input: string): AcceptLanguage[] { +export function parseAcceptLanguage(input: string, strict = false): AcceptLanguage[] { const results = parseNoParameters(input); - results.forEach((result): void => { + return results.filter((result): boolean => { // (1*8ALPHA *("-" 1*8alphanum)) / "*" if (result.range !== '*' && !/^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/u.test(result.range)) { - logger.warn( - `Invalid Accept-Language range: ${result.range}`, - ); - throw new BadRequestHttpError( - `Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`, + handleInvalidValue( + `Invalid Accept-Language range: ${result.range} does not match ((1*8ALPHA *("-" 1*8alphanum)) / "*")`, strict, ); + return false; } + return true; }); - return results; } // eslint-disable-next-line max-len @@ -405,24 +442,21 @@ const rfc1123Date = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr| * Parses an Accept-DateTime header string. * * @param input - The Accept-DateTime header string. + * @param strict - Determines if invalid values throw errors (`true`) or log warnings (`false`). Defaults to `false`. * - * @returns An array with a single {@link AcceptDatetime} object. + * @returns An array with a single {@link AcceptDatetime} object, + * or an empty array if a range in an invalid format is detected. */ -export function parseAcceptDateTime(input: string): AcceptDatetime[] { - const results: AcceptDatetime[] = []; +export function parseAcceptDateTime(input: string, strict = false): AcceptDatetime[] { const range = input.trim(); - if (range) { - if (!rfc1123Date.test(range)) { - logger.warn( - `Invalid Accept-DateTime range: ${range}`, - ); - throw new BadRequestHttpError( - `Invalid Accept-DateTime range: ${range} does not match the RFC1123 format`, - ); - } - results.push({ range, weight: 1 }); + if (!range) { + return []; } - return results; + if (!rfc1123Date.test(range)) { + handleInvalidValue(`Invalid Accept-DateTime range: ${range} does not match the RFC1123 format`, strict); + return []; + } + return [{ range, weight: 1 }]; } /** diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts index 3cad06ff9..f09a10dce 100644 --- a/src/util/PathUtil.ts +++ b/src/util/PathUtil.ts @@ -155,16 +155,31 @@ export function toCanonicalUriPath(path: string): string { encodeURIComponent(decodeURIComponent(part))); } +// Characters not allowed in a Windows file path +const forbiddenSymbols = { + '<': '%3C', + '>': '%3E', + ':': '%3A', + '"': '%22', + '|': '%7C', + '?': '%3F', + // `*` does not get converted by `encodeUriComponent` + '*': '%2A', +} as const; +const forbiddenRegex = new RegExp(`[${Object.keys(forbiddenSymbols).join('')}]`, 'ug'); /** * This function is used when converting a URI to a file path. Decodes all components of a URI path, * with the exception of encoded slash characters, as this would lead to unexpected file locations * being targeted (resulting in erroneous behaviour of the file based backend). + * Characters that would result in an illegal file path remain percent encoded. * * @param path - The path to decode the URI path components of. * @returns A decoded copy of the provided URI path (ignoring encoded slash characters). */ export function decodeUriPathComponents(path: string): string { - return transformPathComponents(path, decodeURIComponent); + return transformPathComponents(path, (part): string => decodeURIComponent(part) + // The characters replaced below result in illegal Windows file paths so need to be encoded + .replace(forbiddenRegex, (val): string => forbiddenSymbols[val as keyof typeof forbiddenSymbols])); } /** diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index 772bf0ef7..71f04ffa1 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -210,7 +210,7 @@ export const NOTIFY = createVocabulary('http://www.w3.org/ns/solid/notifications 'topic', 'webhookAuth', - 'WebHookChannel2023', + 'WebhookChannel2023', 'WebSocketChannel2023', ); diff --git a/templates/contexts/activitystreams.jsonld b/templates/contexts/activitystreams.jsonld new file mode 100644 index 000000000..037026ca4 --- /dev/null +++ b/templates/contexts/activitystreams.jsonld @@ -0,0 +1,379 @@ +{ + "@context": { + "@vocab": "_:", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "as": "https://www.w3.org/ns/activitystreams#", + "ldp": "http://www.w3.org/ns/ldp#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "id": "@id", + "type": "@type", + "Accept": "as:Accept", + "Activity": "as:Activity", + "IntransitiveActivity": "as:IntransitiveActivity", + "Add": "as:Add", + "Announce": "as:Announce", + "Application": "as:Application", + "Arrive": "as:Arrive", + "Article": "as:Article", + "Audio": "as:Audio", + "Block": "as:Block", + "Collection": "as:Collection", + "CollectionPage": "as:CollectionPage", + "Relationship": "as:Relationship", + "Create": "as:Create", + "Delete": "as:Delete", + "Dislike": "as:Dislike", + "Document": "as:Document", + "Event": "as:Event", + "Follow": "as:Follow", + "Flag": "as:Flag", + "Group": "as:Group", + "Ignore": "as:Ignore", + "Image": "as:Image", + "Invite": "as:Invite", + "Join": "as:Join", + "Leave": "as:Leave", + "Like": "as:Like", + "Link": "as:Link", + "Mention": "as:Mention", + "Note": "as:Note", + "Object": "as:Object", + "Offer": "as:Offer", + "OrderedCollection": "as:OrderedCollection", + "OrderedCollectionPage": "as:OrderedCollectionPage", + "Organization": "as:Organization", + "Page": "as:Page", + "Person": "as:Person", + "Place": "as:Place", + "Profile": "as:Profile", + "Question": "as:Question", + "Reject": "as:Reject", + "Remove": "as:Remove", + "Service": "as:Service", + "TentativeAccept": "as:TentativeAccept", + "TentativeReject": "as:TentativeReject", + "Tombstone": "as:Tombstone", + "Undo": "as:Undo", + "Update": "as:Update", + "Video": "as:Video", + "View": "as:View", + "Listen": "as:Listen", + "Read": "as:Read", + "Move": "as:Move", + "Travel": "as:Travel", + "IsFollowing": "as:IsFollowing", + "IsFollowedBy": "as:IsFollowedBy", + "IsContact": "as:IsContact", + "IsMember": "as:IsMember", + "subject": { + "@id": "as:subject", + "@type": "@id" + }, + "relationship": { + "@id": "as:relationship", + "@type": "@id" + }, + "actor": { + "@id": "as:actor", + "@type": "@id" + }, + "attributedTo": { + "@id": "as:attributedTo", + "@type": "@id" + }, + "attachment": { + "@id": "as:attachment", + "@type": "@id" + }, + "bcc": { + "@id": "as:bcc", + "@type": "@id" + }, + "bto": { + "@id": "as:bto", + "@type": "@id" + }, + "cc": { + "@id": "as:cc", + "@type": "@id" + }, + "context": { + "@id": "as:context", + "@type": "@id" + }, + "current": { + "@id": "as:current", + "@type": "@id" + }, + "first": { + "@id": "as:first", + "@type": "@id" + }, + "generator": { + "@id": "as:generator", + "@type": "@id" + }, + "icon": { + "@id": "as:icon", + "@type": "@id" + }, + "image": { + "@id": "as:image", + "@type": "@id" + }, + "inReplyTo": { + "@id": "as:inReplyTo", + "@type": "@id" + }, + "items": { + "@id": "as:items", + "@type": "@id" + }, + "instrument": { + "@id": "as:instrument", + "@type": "@id" + }, + "orderedItems": { + "@id": "as:items", + "@type": "@id", + "@container": "@list" + }, + "last": { + "@id": "as:last", + "@type": "@id" + }, + "location": { + "@id": "as:location", + "@type": "@id" + }, + "next": { + "@id": "as:next", + "@type": "@id" + }, + "object": { + "@id": "as:object", + "@type": "@id" + }, + "oneOf": { + "@id": "as:oneOf", + "@type": "@id" + }, + "anyOf": { + "@id": "as:anyOf", + "@type": "@id" + }, + "closed": { + "@id": "as:closed", + "@type": "xsd:dateTime" + }, + "origin": { + "@id": "as:origin", + "@type": "@id" + }, + "accuracy": { + "@id": "as:accuracy", + "@type": "xsd:float" + }, + "prev": { + "@id": "as:prev", + "@type": "@id" + }, + "preview": { + "@id": "as:preview", + "@type": "@id" + }, + "replies": { + "@id": "as:replies", + "@type": "@id" + }, + "result": { + "@id": "as:result", + "@type": "@id" + }, + "audience": { + "@id": "as:audience", + "@type": "@id" + }, + "partOf": { + "@id": "as:partOf", + "@type": "@id" + }, + "tag": { + "@id": "as:tag", + "@type": "@id" + }, + "target": { + "@id": "as:target", + "@type": "@id" + }, + "to": { + "@id": "as:to", + "@type": "@id" + }, + "url": { + "@id": "as:url", + "@type": "@id" + }, + "altitude": { + "@id": "as:altitude", + "@type": "xsd:float" + }, + "content": "as:content", + "contentMap": { + "@id": "as:content", + "@container": "@language" + }, + "name": "as:name", + "nameMap": { + "@id": "as:name", + "@container": "@language" + }, + "duration": { + "@id": "as:duration", + "@type": "xsd:duration" + }, + "endTime": { + "@id": "as:endTime", + "@type": "xsd:dateTime" + }, + "height": { + "@id": "as:height", + "@type": "xsd:nonNegativeInteger" + }, + "href": { + "@id": "as:href", + "@type": "@id" + }, + "hreflang": "as:hreflang", + "latitude": { + "@id": "as:latitude", + "@type": "xsd:float" + }, + "longitude": { + "@id": "as:longitude", + "@type": "xsd:float" + }, + "mediaType": "as:mediaType", + "published": { + "@id": "as:published", + "@type": "xsd:dateTime" + }, + "radius": { + "@id": "as:radius", + "@type": "xsd:float" + }, + "rel": "as:rel", + "startIndex": { + "@id": "as:startIndex", + "@type": "xsd:nonNegativeInteger" + }, + "startTime": { + "@id": "as:startTime", + "@type": "xsd:dateTime" + }, + "summary": "as:summary", + "summaryMap": { + "@id": "as:summary", + "@container": "@language" + }, + "totalItems": { + "@id": "as:totalItems", + "@type": "xsd:nonNegativeInteger" + }, + "units": "as:units", + "updated": { + "@id": "as:updated", + "@type": "xsd:dateTime" + }, + "width": { + "@id": "as:width", + "@type": "xsd:nonNegativeInteger" + }, + "describes": { + "@id": "as:describes", + "@type": "@id" + }, + "formerType": { + "@id": "as:formerType", + "@type": "@id" + }, + "deleted": { + "@id": "as:deleted", + "@type": "xsd:dateTime" + }, + "inbox": { + "@id": "ldp:inbox", + "@type": "@id" + }, + "outbox": { + "@id": "as:outbox", + "@type": "@id" + }, + "following": { + "@id": "as:following", + "@type": "@id" + }, + "followers": { + "@id": "as:followers", + "@type": "@id" + }, + "streams": { + "@id": "as:streams", + "@type": "@id" + }, + "preferredUsername": "as:preferredUsername", + "endpoints": { + "@id": "as:endpoints", + "@type": "@id" + }, + "uploadMedia": { + "@id": "as:uploadMedia", + "@type": "@id" + }, + "proxyUrl": { + "@id": "as:proxyUrl", + "@type": "@id" + }, + "liked": { + "@id": "as:liked", + "@type": "@id" + }, + "oauthAuthorizationEndpoint": { + "@id": "as:oauthAuthorizationEndpoint", + "@type": "@id" + }, + "oauthTokenEndpoint": { + "@id": "as:oauthTokenEndpoint", + "@type": "@id" + }, + "provideClientKey": { + "@id": "as:provideClientKey", + "@type": "@id" + }, + "signClientKey": { + "@id": "as:signClientKey", + "@type": "@id" + }, + "sharedInbox": { + "@id": "as:sharedInbox", + "@type": "@id" + }, + "Public": { + "@id": "as:Public", + "@type": "@id" + }, + "source": "as:source", + "likes": { + "@id": "as:likes", + "@type": "@id" + }, + "shares": { + "@id": "as:shares", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + } + } +} diff --git a/test/deploy/validate-configs.sh b/test/deploy/validate-configs.sh index 9364e8b73..4fddc5b4e 100755 --- a/test/deploy/validate-configs.sh +++ b/test/deploy/validate-configs.sh @@ -25,7 +25,7 @@ else fi printf " - %s\n" "${CONFIG_ARRAY[@]}" -mkdir -p test/tmp/data +mkdir -p test/tmp/data echo "$TEST_NAME - Building and installing package" npm pack --loglevel warn npm install -g solid-community-server-*.tgz --loglevel warn @@ -72,7 +72,7 @@ run_server_with_config () { cat test/tmp/"$CONFIG_NAME" else echo "$TEST_NAME($CONFIG_NAME) - Attempting HTTP access to the server" - if curl -sfkI -X GET --retry 15 --retry-connrefused --retry-delay 1 $CSS_BASE_URL > test/tmp/"$CONFIG_NAME"-curl; then + if curl -sfkI -X GET --retry 15 --retry-connrefused --retry-delay 5 $CSS_BASE_URL > test/tmp/"$CONFIG_NAME"-curl; then echo "$TEST_NAME($CONFIG_NAME) - SUCCESS: server reached" FAILURE=0 else diff --git a/test/integration/WebHookChannel2023.test.ts b/test/integration/WebHookChannel2023.test.ts index 41f1853da..3a78d128a 100644 --- a/test/integration/WebHookChannel2023.test.ts +++ b/test/integration/WebHookChannel2023.test.ts @@ -5,7 +5,6 @@ import { createRemoteJWKSet, jwtVerify } from 'jose'; import type { NamedNode } from 'n3'; import { DataFactory, Parser, Store } from 'n3'; import type { App } from '../../src/init/App'; - import { matchesAuthorizationScheme } from '../../src/util/HeaderUtil'; import { joinUrl, trimTrailingSlashes } from '../../src/util/PathUtil'; import { readJsonStream } from '../../src/util/StreamUtil'; @@ -21,14 +20,13 @@ import { removeFolder, } from './Config'; import quad = DataFactory.quad; -import namedNode = DataFactory.namedNode; const port = getPort('WebHookChannel2023'); const baseUrl = `http://localhost:${port}/`; const clientPort = getPort('WebHookChannel2023-client'); const target = `http://localhost:${clientPort}/`; const webId = 'http://example.com/card/#me'; -const notificationType = NOTIFY.WebHookChannel2023; +const notificationType = NOTIFY.WebhookChannel2023; const rootFilePath = getTestFolder('WebHookChannel2023'); const stores: [string, any][] = [ @@ -37,8 +35,7 @@ const stores: [string, any][] = [ teardown: jest.fn(), }], [ 'on-disk storage', { - // Switch to file locker after https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1452 - configs: [ 'storage/backend/file.json', 'util/resource-locker/memory.json' ], + configs: [ 'storage/backend/file.json', 'util/resource-locker/file.json' ], teardown: async(): Promise => removeFolder(rootFilePath), }], ]; @@ -97,9 +94,9 @@ describe.each(stores)('A server supporting WebHookChannel2023 using %s', (name, const quads = new Store(new Parser().parse(await response.text())); // Find the notification channel for websockets - const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null); + const subscriptions = quads.getObjects(null, NOTIFY.terms.subscription, null); const webhookSubscriptions = subscriptions.filter((channel): boolean => quads.has( - quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebHookChannel2023`)), + quad(channel as NamedNode, NOTIFY.terms.channelType, NOTIFY.terms.WebhookChannel2023), )); expect(webhookSubscriptions).toHaveLength(1); subscriptionUrl = webhookSubscriptions[0].value; diff --git a/test/integration/WebSocketChannel2023.test.ts b/test/integration/WebSocketChannel2023.test.ts index 4d896fd1e..768ef4513 100644 --- a/test/integration/WebSocketChannel2023.test.ts +++ b/test/integration/WebSocketChannel2023.test.ts @@ -30,8 +30,7 @@ const stores: [string, any][] = [ teardown: jest.fn(), }], [ 'on-disk storage', { - // Switch to file locker after https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1452 - configs: [ 'storage/backend/file.json', 'util/resource-locker/memory.json' ], + configs: [ 'storage/backend/file.json', 'util/resource-locker/file.json' ], teardown: async(): Promise => removeFolder(rootFilePath), }], ]; @@ -86,9 +85,9 @@ describe.each(stores)('A server supporting WebSocketChannel2023 using %s', (name const quads = new Store(new Parser().parse(await response.text())); // Find the notification channel for websockets - const subscriptions = quads.getObjects(storageDescriptionUrl, NOTIFY.terms.subscription, null); + const subscriptions = quads.getObjects(null, NOTIFY.terms.subscription, null); const websocketSubscriptions = subscriptions.filter((channel): boolean => quads.has( - quad(channel as NamedNode, NOTIFY.terms.channelType, namedNode(`${NOTIFY.namespace}WebSocketChannel2023`)), + quad(channel as NamedNode, NOTIFY.terms.channelType, NOTIFY.terms.WebSocketChannel2023), )); expect(websocketSubscriptions).toHaveLength(1); subscriptionUrl = websocketSubscriptions[0].value; diff --git a/test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts b/test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts index f8e526843..ed4c50467 100644 --- a/test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts +++ b/test/unit/http/output/metadata/AllowAcceptHeaderWriter.test.ts @@ -49,36 +49,33 @@ describe('An AllowAcceptHeaderWriter', (): void => { expect(headers['accept-post']).toBeUndefined(); }); - it('returns all methods for an empty container.', async(): Promise => { + it('returns all methods except PUT for an empty container.', async(): Promise => { await expect(writer.handleSafe({ response, metadata: emptyContainer })).resolves.toBeUndefined(); const headers = response.getHeaders(); expect(typeof headers.allow).toBe('string'); expect(new Set((headers.allow as string).split(', '))) - .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', 'DELETE' ])); + .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH', 'DELETE' ])); expect(headers['accept-patch']).toBe('text/n3, application/sparql-update'); - expect(headers['accept-put']).toBe('*/*'); expect(headers['accept-post']).toBe('*/*'); }); - it('returns all methods except DELETE for a non-empty container.', async(): Promise => { + it('returns all methods except PUT/DELETE for a non-empty container.', async(): Promise => { await expect(writer.handleSafe({ response, metadata: fullContainer })).resolves.toBeUndefined(); const headers = response.getHeaders(); expect(typeof headers.allow).toBe('string'); expect(new Set((headers.allow as string).split(', '))) - .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ])); + .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ])); expect(headers['accept-patch']).toBe('text/n3, application/sparql-update'); - expect(headers['accept-put']).toBe('*/*'); expect(headers['accept-post']).toBe('*/*'); }); - it('returns all methods except DELETE for a storage container.', async(): Promise => { + it('returns all methods except PUT/DELETE for a storage container.', async(): Promise => { await expect(writer.handleSafe({ response, metadata: storageContainer })).resolves.toBeUndefined(); const headers = response.getHeaders(); expect(typeof headers.allow).toBe('string'); expect(new Set((headers.allow as string).split(', '))) - .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'PUT', 'POST', 'PATCH' ])); + .toEqual(new Set([ 'OPTIONS', 'GET', 'HEAD', 'POST', 'PATCH' ])); expect(headers['accept-patch']).toBe('text/n3, application/sparql-update'); - expect(headers['accept-put']).toBe('*/*'); expect(headers['accept-post']).toBe('*/*'); }); diff --git a/test/unit/logging/WinstonLoggerFactory.test.ts b/test/unit/logging/WinstonLoggerFactory.test.ts index a25497f04..d2dd90647 100644 --- a/test/unit/logging/WinstonLoggerFactory.test.ts +++ b/test/unit/logging/WinstonLoggerFactory.test.ts @@ -1,12 +1,24 @@ import { PassThrough } from 'stream'; import type { Logger } from 'winston'; +import type * as Transport from 'winston-transport'; import { WinstonLogger } from '../../../src/logging/WinstonLogger'; import { WinstonLoggerFactory } from '../../../src/logging/WinstonLoggerFactory'; +const now = new Date(); +jest.useFakeTimers(); +jest.setSystemTime(now); + describe('WinstonLoggerFactory', (): void => { let factory: WinstonLoggerFactory; + let transport: jest.Mocked; + beforeEach(async(): Promise => { factory = new WinstonLoggerFactory('debug'); + + // Create a dummy log transport + transport = new PassThrough({ objectMode: true }) as any; + transport.write = jest.fn(); + transport.log = jest.fn(); }); it('creates WinstonLoggers.', async(): Promise => { @@ -19,10 +31,6 @@ describe('WinstonLoggerFactory', (): void => { }); it('allows WinstonLoggers to be invoked.', async(): Promise => { - // Create a dummy log transport - const transport: any = new PassThrough({ objectMode: true }); - transport.write = jest.fn(); - transport.log = jest.fn(); (factory as any).createTransports = (): any => [ transport ]; // Create logger, and log @@ -30,15 +38,39 @@ describe('WinstonLoggerFactory', (): void => { logger.log('debug', 'my message'); expect(transport.write).toHaveBeenCalledTimes(1); + // Need to check level like this as it has color tags + const { level } = transport.write.mock.calls[0][0]; expect(transport.write).toHaveBeenCalledWith({ label: 'MyLabel', - level: expect.stringContaining('debug'), + level, message: 'my message', - timestamp: expect.any(String), - metadata: expect.any(Object), + timestamp: now.toISOString(), + metadata: {}, [Symbol.for('level')]: 'debug', [Symbol.for('splat')]: [ undefined ], - [Symbol.for('message')]: expect.any(String), + [Symbol.for('message')]: `${now.toISOString()} [MyLabel] {W-???} ${level}: my message`, }); }); + + it('allows extra metadata when logging to indicate the thread.', async(): Promise => { + (factory as any).createTransports = (): any => [ transport ]; + + // Create logger, and log + const logger = factory.createLogger('MyLabel'); + logger.log('debug', 'my message', { isPrimary: true, pid: 0 }); + + expect(transport.write).toHaveBeenCalledTimes(1); + // Need to check level like this as it has color tags + const { level } = transport.write.mock.calls[0][0]; + expect(transport.write).toHaveBeenCalledWith(expect.objectContaining({ + label: 'MyLabel', + level, + message: 'my message', + timestamp: now.toISOString(), + metadata: { isPrimary: true, pid: 0 }, + [Symbol.for('level')]: 'debug', + [Symbol.for('splat')]: [ undefined ], + [Symbol.for('message')]: `${now.toISOString()} [MyLabel] {Primary} ${level}: my message`, + })); + }); }); diff --git a/test/unit/server/description/StorageDescriptionHandler.test.ts b/test/unit/server/description/StorageDescriptionHandler.test.ts index 54f1409e0..6703cdaaa 100644 --- a/test/unit/server/description/StorageDescriptionHandler.test.ts +++ b/test/unit/server/description/StorageDescriptionHandler.test.ts @@ -83,8 +83,8 @@ describe('A StorageDescriptionHandler', (): void => { expect(result.metadata?.contentType).toBe('internal/quads'); expect(result.data).toBeDefined(); const quads = await readableToQuads(result.data!); - expect(quads.countQuads(operation.target.path, RDF.terms.type, PIM.terms.Storage, null)).toBe(1); + expect(quads.countQuads('http://example.com/', RDF.terms.type, PIM.terms.Storage, null)).toBe(1); expect(describer.handle).toHaveBeenCalledTimes(1); - expect(describer.handle).toHaveBeenLastCalledWith(operation.target); + expect(describer.handle).toHaveBeenLastCalledWith({ path: 'http://example.com/' }); }); }); diff --git a/test/unit/server/notifications/WebHookChannel2023/WebHookChannel2023Type.test.ts b/test/unit/server/notifications/WebHookChannel2023/WebHookChannel2023Type.test.ts index 3ffbbaaaa..dce22558b 100644 --- a/test/unit/server/notifications/WebHookChannel2023/WebHookChannel2023Type.test.ts +++ b/test/unit/server/notifications/WebHookChannel2023/WebHookChannel2023Type.test.ts @@ -43,14 +43,14 @@ describe('A WebhookChannel2023Type', (): void => { beforeEach(async(): Promise => { data = new Store(); - data.addQuad(quad(subject, RDF.terms.type, NOTIFY.terms.WebHookChannel2023)); + data.addQuad(quad(subject, RDF.terms.type, NOTIFY.terms.WebhookChannel2023)); data.addQuad(quad(subject, NOTIFY.terms.topic, namedNode(topic))); data.addQuad(quad(subject, NOTIFY.terms.sendTo, namedNode(sendTo))); const id = 'http://example.com/webhooks/4c9b88c1-7502-4107-bb79-2a3a590c7aa3'; channel = { id, - type: NOTIFY.WebHookChannel2023, + type: NOTIFY.WebhookChannel2023, topic: 'https://storage.example/resource', sendTo, }; @@ -79,7 +79,7 @@ describe('A WebhookChannel2023Type', (): void => { CONTEXT_NOTIFICATION, ], id: channel.id, - type: NOTIFY.WebHookChannel2023, + type: NOTIFY.WebhookChannel2023, sendTo, topic, sender: 'http://example.com/webhooks/webid', diff --git a/test/unit/server/notifications/WebHookChannel2023/WebHookEmitter.test.ts b/test/unit/server/notifications/WebHookChannel2023/WebHookEmitter.test.ts index b1047deeb..4ba0a0019 100644 --- a/test/unit/server/notifications/WebHookChannel2023/WebHookEmitter.test.ts +++ b/test/unit/server/notifications/WebHookChannel2023/WebHookEmitter.test.ts @@ -45,7 +45,7 @@ describe('A WebHookEmitter', (): void => { const channel: WebhookChannel2023 = { id: 'id', topic: 'http://example.com/foo', - type: NOTIFY.WebHookChannel2023, + type: NOTIFY.WebhookChannel2023, sendTo: 'http://example.org/somewhere-else', }; diff --git a/test/unit/util/HeaderUtil.test.ts b/test/unit/util/HeaderUtil.test.ts index e8eb77f74..049d2ac05 100644 --- a/test/unit/util/HeaderUtil.test.ts +++ b/test/unit/util/HeaderUtil.test.ts @@ -53,32 +53,63 @@ describe('HeaderUtil', (): void => { ]); }); - it('rejects Accept Headers with invalid types.', async(): Promise => { - expect((): any => parseAccept('*')).toThrow('Invalid Accept range:'); - expect((): any => parseAccept('"bad"/text')).toThrow('Invalid Accept range:'); - expect((): any => parseAccept('*/\\bad')).toThrow('Invalid Accept range:'); - expect((): any => parseAccept('*/*')).not.toThrow('Invalid Accept range:'); + it('ignores Accept Headers with invalid types.', async(): Promise => { + expect(parseAccept('*')).toEqual([]); + expect(parseAccept('"bad"/text')).toEqual([]); + expect(parseAccept('*/\\bad')).toEqual([]); + expect(parseAccept('*/*')).toEqual([{ + parameters: { extension: {}, mediaType: {}}, range: '*/*', weight: 1, + }]); }); - it('rejects Accept Headers with invalid q values.', async(): Promise => { - expect((): any => parseAccept('a/b; q=text')).toThrow('Invalid q value:'); - expect((): any => parseAccept('a/b; q=0.1234')).toThrow('Invalid q value:'); - expect((): any => parseAccept('a/b; q=1.1')).toThrow('Invalid q value:'); - expect((): any => parseAccept('a/b; q=1.000')).not.toThrow(); - expect((): any => parseAccept('a/b; q=0.123')).not.toThrow(); + it('ignores the weight of Accept Headers with q values it can not parse.', async(): Promise => { + expect(parseAccept('a/b; q=text')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + // Invalid Q value but can be parsed + expect(parseAccept('a/b; q=0.1234')).toEqual([{ + range: 'a/b', weight: 0.1234, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=1.1')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=1.000')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=-5')).toEqual([{ + range: 'a/b', weight: 0, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=0.123')).toEqual([{ + range: 'a/b', weight: 0.123, parameters: { extension: {}, mediaType: {}}, + }]); }); - it('rejects Accept Headers with invalid parameters.', async(): Promise => { - expect((): any => parseAccept('a/b; a')).toThrow('Invalid Accept parameter'); - expect((): any => parseAccept('a/b; a=\\')).toThrow('Invalid parameter value'); - expect((): any => parseAccept('a/b; q=1 ; a=\\')).toThrow('Invalid parameter value'); - expect((): any => parseAccept('a/b; q=1 ; a')).not.toThrow('Invalid Accept parameter'); + it('ignores Accept Headers with invalid parameters.', async(): Promise => { + expect(parseAccept('a/b; a')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; a=\\')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=1 ; a=\\')).toEqual([{ + range: 'a/b', weight: 1, parameters: { extension: {}, mediaType: {}}, + }]); + expect(parseAccept('a/b; q=1 ; a')).toEqual([{ + // eslint-disable-next-line id-length + range: 'a/b', weight: 1, parameters: { extension: { a: '' }, mediaType: {}}, + }]); }); it('rejects Accept Headers with quoted parameters.', async(): Promise => { expect((): any => parseAccept('a/b; a="\\""')).not.toThrow(); expect((): any => parseAccept('a/b; a="\\\u007F"')).toThrow('Invalid quoted string in header:'); }); + + it('rejects invalid values when strict mode is enabled.', async(): Promise => { + expect((): any => parseAccept('"bad"/text', true)).toThrow(BadRequestHttpError); + expect((): any => parseAccept('a/b; q=text', true)).toThrow(BadRequestHttpError); + expect((): any => parseAccept('a/b; a', true)).toThrow(BadRequestHttpError); + }); }); describe('#parseCharset', (): void => { @@ -89,10 +120,14 @@ describe('HeaderUtil', (): void => { ]); }); - it('rejects invalid Accept-Charset Headers.', async(): Promise => { - expect((): any => parseAcceptCharset('a/b')).toThrow('Invalid Accept-Charset range:'); - expect((): any => parseAcceptCharset('a; q=text')).toThrow('Invalid q value:'); - expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); + it('ignores invalid Accept-Charset Headers.', async(): Promise => { + expect(parseAcceptCharset('a/b')).toEqual([]); + expect(parseAcceptCharset('a; q=text')).toEqual([{ range: 'a', weight: 1 }]); + expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]); + }); + + it('rejects invalid values when strict mode is enabled.', async(): Promise => { + expect((): any => parseAcceptCharset('a/b', true)).toThrow(BadRequestHttpError); }); }); @@ -109,10 +144,14 @@ describe('HeaderUtil', (): void => { ]); }); - it('rejects invalid Accept-Encoding Headers.', async(): Promise => { - expect((): any => parseAcceptEncoding('a/b')).toThrow('Invalid Accept-Encoding range:'); - expect((): any => parseAcceptEncoding('a; q=text')).toThrow('Invalid q value:'); - expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); + it('ignores invalid Accept-Encoding Headers.', async(): Promise => { + expect(parseAcceptEncoding('a/b')).toEqual([]); + expect(parseAcceptEncoding('a; q=text')).toEqual([{ range: 'a', weight: 1 }]); + expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]); + }); + + it('rejects invalid values when strict mode is enabled.', async(): Promise => { + expect((): any => parseAcceptEncoding('a/b', true)).toThrow(BadRequestHttpError); }); }); @@ -125,16 +164,20 @@ describe('HeaderUtil', (): void => { ]); }); - it('rejects invalid Accept-Language Headers.', async(): Promise => { - expect((): any => parseAcceptLanguage('a/b')).toThrow('Invalid Accept-Language range:'); - expect((): any => parseAcceptLanguage('05-a')).toThrow('Invalid Accept-Language range:'); - expect((): any => parseAcceptLanguage('a--05')).toThrow('Invalid Accept-Language range:'); - expect((): any => parseAcceptLanguage('a-"a"')).toThrow('Invalid Accept-Language range:'); - expect((): any => parseAcceptLanguage('a-05')).not.toThrow('Invalid Accept-Language range:'); - expect((): any => parseAcceptLanguage('a-b-c-d')).not.toThrow('Invalid Accept-Language range:'); + it('ignores invalid Accept-Language Headers.', async(): Promise => { + expect(parseAcceptLanguage('a/b')).toEqual([]); + expect(parseAcceptLanguage('05-a')).toEqual([]); + expect(parseAcceptLanguage('a--05')).toEqual([]); + expect(parseAcceptLanguage('a-"a"')).toEqual([]); + expect(parseAcceptLanguage('a-05')).toEqual([{ range: 'a-05', weight: 1 }]); + expect(parseAcceptLanguage('a-b-c-d')).toEqual([{ range: 'a-b-c-d', weight: 1 }]); - expect((): any => parseAcceptLanguage('a; q=text')).toThrow('Invalid q value:'); - expect((): any => parseAcceptCharset('a; c=d')).toThrow('Only q parameters are allowed'); + expect(parseAcceptLanguage('a; q=text')).toEqual([{ range: 'a', weight: 1 }]); + expect(parseAcceptCharset('a; c=d')).toEqual([{ range: 'a', weight: 1 }]); + }); + + it('rejects invalid values when strict mode is enabled.', async(): Promise => { + expect((): any => parseAcceptLanguage('a/b', true)).toThrow(BadRequestHttpError); }); }); @@ -150,9 +193,13 @@ describe('HeaderUtil', (): void => { expect(parseAcceptDateTime(' ')).toEqual([]); }); - it('rejects invalid Accept-DateTime Headers.', async(): Promise => { - expect((): any => parseAcceptDateTime('a/b')).toThrow('Invalid Accept-DateTime range:'); - expect((): any => parseAcceptDateTime('30 May 2007')).toThrow('Invalid Accept-DateTime range:'); + it('ignores invalid Accept-DateTime Headers.', async(): Promise => { + expect(parseAcceptDateTime('a/b')).toEqual([]); + expect(parseAcceptDateTime('30 May 2007')).toEqual([]); + }); + + it('rejects invalid values when strict mode is enabled.', async(): Promise => { + expect((): any => parseAcceptLanguage('a/b', true)).toThrow(BadRequestHttpError); }); }); diff --git a/test/unit/util/PathUtil.test.ts b/test/unit/util/PathUtil.test.ts index 4eb681a26..01816bb76 100644 --- a/test/unit/util/PathUtil.test.ts +++ b/test/unit/util/PathUtil.test.ts @@ -96,7 +96,7 @@ describe('PathUtil', (): void => { describe('#toCanonicalUriPath', (): void => { it('encodes only the necessary parts.', (): void => { - expect(toCanonicalUriPath('/a%20path&/name')).toBe('/a%20path%26/name'); + expect(toCanonicalUriPath('/a%20path&*/name')).toBe('/a%20path%26*/name'); }); it('leaves the query string untouched.', (): void => { @@ -138,6 +138,11 @@ describe('PathUtil', (): void => { expect(decodeUriPathComponents('/a%25252Fb')).toBe('/a%25252Fb'); expect(decodeUriPathComponents('/a%2525252Fb')).toBe('/a%2525252Fb'); }); + + it('ensures illegal path characters are encoded.', async(): Promise => { + expect(decodeUriPathComponents('/a { diff --git a/test/util/NotificationUtil.ts b/test/util/NotificationUtil.ts index 3abc4e54d..a82c87450 100644 --- a/test/util/NotificationUtil.ts +++ b/test/util/NotificationUtil.ts @@ -2,7 +2,7 @@ import { fetch } from 'cross-fetch'; /** * Subscribes to a notification channel. - * @param type - The type of the notification channel, e.g., "NOTIFY.WebHookChannel2023". + * @param type - The type of the notification channel, e.g., "NOTIFY.WebhookChannel2023". * @param webId - The WebID to spoof in the authorization header. This assumes the config uses the debug auth import. * @param subscriptionUrl - The subscription URL to which the request needs to be sent. * @param topic - The topic to subscribe to. diff --git a/test/util/SetupTests.ts b/test/util/SetupTests.ts index cdb4cffe4..3e6b48978 100644 --- a/test/util/SetupTests.ts +++ b/test/util/SetupTests.ts @@ -1,17 +1,24 @@ import { setGlobalLoggerFactory } from '../../src/logging/LogUtil'; import { WinstonLoggerFactory } from '../../src/logging/WinstonLoggerFactory'; +import { getTestFolder, removeFolder } from '../integration/Config'; +// Jest global setup requires a single function to be exported +export default async function(): Promise { // Set the main logger -const level = process.env.LOGLEVEL ?? 'off'; -const loggerFactory = new WinstonLoggerFactory(level); -setGlobalLoggerFactory(loggerFactory); + const level = process.env.LOGLEVEL ?? 'off'; + const loggerFactory = new WinstonLoggerFactory(level); + setGlobalLoggerFactory(loggerFactory); -// Also set the logger factory of transpiled JS modules -// (which are instantiated by Components.js) -try { - // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports - const dist = require('../../dist/logging/LogUtil'); - dist.setGlobalLoggerFactory(loggerFactory); -} catch { - // Ignore + // Also set the logger factory of transpiled JS modules + // (which are instantiated by Components.js) + try { + // eslint-disable-next-line global-require,@typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports + const dist = require('../../dist/logging/LogUtil'); + dist.setGlobalLoggerFactory(loggerFactory); + } catch { + // Ignore + } + + // Clean up the test folder to prevent issues with remaining files from previous tests + await removeFolder(getTestFolder('')); }