mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/next-major
# Conflicts: # package-lock.json # package.json
This commit is contained in:
commit
4664c64e6c
4
.github/workflows/cth-test.yml
vendored
4
.github/workflows/cth-test.yml
vendored
@ -78,13 +78,13 @@ jobs:
|
|||||||
branch-name: ${{ inputs.branch || github.head_ref }}
|
branch-name: ${{ inputs.branch || github.head_ref }}
|
||||||
- name: Save the reports
|
- name: Save the reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
|
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
|
||||||
path: reports
|
path: reports
|
||||||
- name: Save the server output
|
- name: Save the server output
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
|
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
|
||||||
path: server-output.log
|
path: server-output.log
|
||||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@ -89,6 +89,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
|
platforms: linux/amd64
|
||||||
tags: ${{ needs.docker-meta.outputs.tags }}
|
tags: ${{ needs.docker-meta.outputs.tags }}
|
||||||
labels: ${{ needs.docker-meta.outputs.labels }}
|
labels: ${{ needs.docker-meta.outputs.labels }}
|
||||||
|
2
.github/workflows/mkdocs.yml
vendored
2
.github/workflows/mkdocs.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
|||||||
needs: mkdocs-prep
|
needs: mkdocs-prep
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4.1.1
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.x
|
python-version: 3.x
|
||||||
- run: pip install mkdocs-material
|
- run: pip install mkdocs-material
|
||||||
|
3
.github/workflows/npm-test.yml
vendored
3
.github/workflows/npm-test.yml
vendored
@ -27,6 +27,7 @@ jobs:
|
|||||||
- 18.x
|
- 18.x
|
||||||
- '20.0'
|
- '20.0'
|
||||||
- 20.x
|
- 20.x
|
||||||
|
- 21.x
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
@ -58,6 +59,7 @@ jobs:
|
|||||||
node-version:
|
node-version:
|
||||||
- 18.x
|
- 18.x
|
||||||
- 20.x
|
- 20.x
|
||||||
|
- 21.x
|
||||||
env:
|
env:
|
||||||
TEST_DOCKER: true
|
TEST_DOCKER: true
|
||||||
services:
|
services:
|
||||||
@ -92,6 +94,7 @@ jobs:
|
|||||||
node-version:
|
node-version:
|
||||||
- 18.x
|
- 18.x
|
||||||
- 20.x
|
- 20.x
|
||||||
|
- 21.x
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
debug-only: true
|
debug-only: true
|
||||||
stale-issue-label: 🏚️ abandoned
|
stale-issue-label: 🏚️ abandoned
|
||||||
|
31
CHANGELOG.md
31
CHANGELOG.md
@ -3,6 +3,37 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [7.0.2](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.1...v7.0.2) (2023-11-20)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add index signature to Credentials ([86f4592](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/86f45923ba6cc696615a98a5fbc8f13a525e4745))
|
||||||
|
* Pass requestedModes metadata on 401 ([58daeb6](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/58daeb684f6e84fa0950d0ea5d9827881ba136c2))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Add query parameter to static resources ([5f85441](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/5f85441b6e23a2bd2b3e1f3ef116a4868d5f9614))
|
||||||
|
* Update resource size in ConstantConverter ([6c30a25](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/6c30a2512bd30bf52deb4526d13416016004646f))
|
||||||
|
* Prevent errors in JSON storage when data is invalid ([4318479](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/43184791545bbf6fe8a840de07616fca8f3b7f97))
|
||||||
|
* Prevent errors during migration for invalid JSON ([2f928bd](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/2f928bd2d4c8d4385bca4554ad0ed19cc5aaa770))
|
||||||
|
* Disable submit buttons until form data is loaded ([1597a5a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/1597a5a5782ca5b574c42ab3bab3d01de89ccf02))
|
||||||
|
* Undo util.js errors introduced when changing lint settings ([261c3d0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/261c3d05a6b446442f2ca5a4a0803363d1cb9021))
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
|
||||||
|
* Update to TypeScript 5.2.2 ([edbf895](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/edbf895505d625d59404b82807616eebca757040))
|
||||||
|
* Fix Dockerfile to Node v18 to prevent build issues ([9cc4a9c](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/9cc4a9ce4d786e76c54d126754271eb2ec1355a5))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* Explain storage/location import options ([01623e0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/01623e0b5c58d77add4ab3e2a1a9082897ad5948))
|
||||||
|
* Fix outdated information in IDP documentation ([#1773](https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1773)) ([15a929a](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/15a929a87e4ce00c0ed266e296405c8e4a22d4a7))
|
||||||
|
* Explain the patching store in-depth ([4d05fe4](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/4d05fe4315e282d6e1ec19af01b185cb21cab29c))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
* Replace linting configurations ([6248ed0](https://github.com/CommunitySolidServer/CommunitySolidServer/commit/6248ed093813e95255f031eb8fcc37e4d869235c))
|
||||||
|
|
||||||
## [7.0.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.0...v7.0.1) (2023-10-20)
|
## [7.0.1](https://github.com/CommunitySolidServer/CommunitySolidServer/compare/v7.0.0...v7.0.1) (2023-10-20)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
@ -22,6 +22,17 @@ Used by certain classes for internal storage.
|
|||||||
* *memory*: Store everything in memory.
|
* *memory*: Store everything in memory.
|
||||||
* *resource-store*: Store everything in a specific container in the resource store.
|
* *resource-store*: Store everything in a specific container in the resource store.
|
||||||
|
|
||||||
|
## Location
|
||||||
|
|
||||||
|
Tells the server where it can find the storage root(s).
|
||||||
|
Solid does not allow storage roots to be nested,
|
||||||
|
so usually you either have one storage root at the server with no pods,
|
||||||
|
or multiple storage roots at pod level with an inaccessible server root.
|
||||||
|
|
||||||
|
* *pod*: Indicates that the root storages are at pod level.
|
||||||
|
If subdomains are used for pods, this will also include the actual server root.
|
||||||
|
* *root*: There is only one storage root, and it is the same as the server root.
|
||||||
|
|
||||||
## Middleware
|
## Middleware
|
||||||
|
|
||||||
The chain of utility ResourceStores that needs to be passed through before reaching the backend stores.
|
The chain of utility ResourceStores that needs to be passed through before reaching the backend stores.
|
||||||
|
@ -29,6 +29,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
|
|||||||
|
|
||||||
## Using the server
|
## Using the server
|
||||||
|
|
||||||
|
* [An overview of the main features of the server](features.md)
|
||||||
* [Quickly starting the server](usage/starting-server.md)
|
* [Quickly starting the server](usage/starting-server.md)
|
||||||
* [Basic example HTTP requests](usage/example-requests.md)
|
* [Basic example HTTP requests](usage/example-requests.md)
|
||||||
* [Editing the metadata of a resource](usage/metadata.md)
|
* [Editing the metadata of a resource](usage/metadata.md)
|
||||||
|
@ -76,7 +76,7 @@ The `urn:solid-server:default:AuthResourceHttpHandler` is identical
|
|||||||
to the `urn:solid-server:default:LdpHandler` which will be discussed below,
|
to the `urn:solid-server:default:LdpHandler` which will be discussed below,
|
||||||
but only handles resources relevant for authorization.
|
but only handles resources relevant for authorization.
|
||||||
|
|
||||||
In practice this means that is your server is configured
|
In practice this means that if your server is configured
|
||||||
to use [Web Access Control](https://solidproject.org/TR/wac) for authorization,
|
to use [Web Access Control](https://solidproject.org/TR/wac) for authorization,
|
||||||
this handler will catch all requests targeting `.acl` resources.
|
this handler will catch all requests targeting `.acl` resources.
|
||||||
|
|
||||||
|
92
documentation/markdown/features.md
Normal file
92
documentation/markdown/features.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Features
|
||||||
|
|
||||||
|
Below is a non-exhaustive listing of the features available to a server instance,
|
||||||
|
depending on the chosen configuration.
|
||||||
|
The core feature of the CSS is that it uses **dependency injection** to configure its components,
|
||||||
|
so any of the features below can always be adapted or replaced with custom components if required.
|
||||||
|
It can also be used to configure dummy components for debugging, development, or experimentation purposes.
|
||||||
|
See this [tutorial](https://github.com/CommunitySolidServer/tutorials/blob/main/custom-configurations.md)
|
||||||
|
and/or this [example repository](https://github.com/CommunitySolidServer/hello-world-component)
|
||||||
|
for more information on that.
|
||||||
|
|
||||||
|
To generate configurations with some of these features enabled/disabled,
|
||||||
|
you can use the **[configuration generator](https://communitysolidserver.github.io/configuration-generator/)**.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Clients are identified based on the contents of **DPoP** tokens,
|
||||||
|
as described in the [**Solid-OIDC** specification](https://solidproject.org/TR/oidc).
|
||||||
|
|
||||||
|
The server also provides several dummy components that can be used here,
|
||||||
|
to either always identify the client as a fixed WebID,
|
||||||
|
or to allow the WebID to be set directly in the `Authorization` header.
|
||||||
|
These can be configured by changing the `ldp/authentication` import in your configuration.
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
Two authorization mechanisms are implemented for determining who has access to resources:
|
||||||
|
|
||||||
|
* **[Web Access Control](https://solidproject.org/TR/wac)**
|
||||||
|
* **[Access Control Policy](https://solidproject.org/TR/acp)**
|
||||||
|
|
||||||
|
Alternatively, the server can be configured to not have any kind of authorization and allow full access to all resources.
|
||||||
|
|
||||||
|
## Solid Protocol
|
||||||
|
|
||||||
|
The **[Solid Protocol](https://solidproject.org/TR/protocol)** is supported.
|
||||||
|
|
||||||
|
Requests to the server support **content negotiation** for common RDF formats.
|
||||||
|
|
||||||
|
Binary **[range headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)** are supported.
|
||||||
|
|
||||||
|
[`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) and `Last-Modified` headers are supported.
|
||||||
|
These can be used for **[conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests)**.
|
||||||
|
|
||||||
|
`PATCH` requests targeting RDF resources can be made with **N3 Patch** or **SPARQL Update** bodies.
|
||||||
|
|
||||||
|
The server can be configured to store data in **memory**, on the **file system**, or through a **SPARQL endpoint**.
|
||||||
|
Similarly, the locking system that is used to prevent data conflicts
|
||||||
|
can be configured to store locks in memory, on the file system, or in a Redis store, or it can be disabled.
|
||||||
|
|
||||||
|
Multiple **worker threads** can be used when starting the server.
|
||||||
|
|
||||||
|
## Account management
|
||||||
|
|
||||||
|
Accounts can be created on the server with which users can perform the following actions,
|
||||||
|
through either a **JSON** or an **HTML** API:
|
||||||
|
|
||||||
|
* Add **email/password** combinations which can be used to log in.
|
||||||
|
* Create **pods**, which are containers on the server over which the user has full control.
|
||||||
|
* Link **WebIDs** to the account. When using [Solid-OIDC](https://solidproject.org/TR/oidc),
|
||||||
|
the user can identify as any of these.
|
||||||
|
For external WebIDs, the server requires the user to add a triple as identification,
|
||||||
|
but this can be disabled if needed.
|
||||||
|
* Create **client credentials**, which can be used to authenticate without using the browser.
|
||||||
|
More information on these can be found [here](usage/client-credentials.md).
|
||||||
|
* It is possible to use Solid-OIDC to limit access to certain parts of the account API.
|
||||||
|
More information on this can be found [here](usage/identity-provider.md#access).
|
||||||
|
|
||||||
|
Using these accounts, the server can generate tokens
|
||||||
|
to support **[Solid-OIDC](https://solidproject.org/TR/oidc)** authentication.
|
||||||
|
|
||||||
|
### Pods
|
||||||
|
|
||||||
|
The server keeps track of the **pod owners**,
|
||||||
|
which is a list of WebIDs which have full control access over all resources contained within.
|
||||||
|
Owners can be added to and removed from a pod.
|
||||||
|
|
||||||
|
Pod URLs can be minted as either
|
||||||
|
**subdomain**, `http://pod.example.com/`, or **suffix**, `http://example.com/pod/`.
|
||||||
|
|
||||||
|
When starting the server, a configuration file can be provided
|
||||||
|
to immediately create one or more accounts on the server with their own pods.
|
||||||
|
See the [documentation](usage/seeding-pods.md) for more information.
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
CSS supports v0.2.0 of the **[Solid Notifications Protocol](https://solidproject.org/TR/notifications-protocol)**.
|
||||||
|
Specifically it supports the Notification Types
|
||||||
|
[WebSocketChannel2023](https://solid.github.io/notifications/websocket-channel-2023)
|
||||||
|
and [WebhookChannel2023](https://solid.github.io/notifications/webhook-channel-2023).
|
||||||
|
|
||||||
|
More documentation on notifications can be found [here](usage/notifications.md).
|
@ -26,6 +26,13 @@ Below is an example of how to call the API to generate such a token.
|
|||||||
The code below generates a token linked to your account and WebID.
|
The code below generates a token linked to your account and WebID.
|
||||||
This only needs to be done once, afterwards this token can be used for all future requests.
|
This only needs to be done once, afterwards this token can be used for all future requests.
|
||||||
|
|
||||||
|
Before doing the step below,
|
||||||
|
you already need to have an [authorization value](account/json-api.md#authorization)
|
||||||
|
that you get after logging in to your account.
|
||||||
|
In the example below the cookie value is used.
|
||||||
|
In the default server configurations,
|
||||||
|
you can log in through the [email/password API](account/json-api.md#controlspasswordlogin).
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// This assumes your server is started under http://localhost:3000/.
|
// This assumes your server is started under http://localhost:3000/.
|
||||||
// It also assumes you have already logged in and `cookie` contains a valid cookie header
|
// It also assumes you have already logged in and `cookie` contains a valid cookie header
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Besides implementing the [Solid protocol](https://solidproject.org/TR/protocol),
|
Besides implementing the [Solid protocol](https://solidproject.org/TR/protocol),
|
||||||
the community server can also be an Identity Provider (IDP), officially known as an OpenID Provider (OP),
|
the community server can also be an Identity Provider (IDP), officially known as an OpenID Provider (OP),
|
||||||
following the [Solid OIDC spec](https://solid.github.io/solid-oidc/) as much as possible.
|
following the [Solid-OIDC specification](https://solid.github.io/solid-oidc/) as much as possible.
|
||||||
|
|
||||||
It is recommended to use the latest version
|
It is recommended to use the latest version
|
||||||
of the [Solid authentication client](https://github.com/inrupt/solid-client-authn-js)
|
of the [Solid authentication client](https://github.com/inrupt/solid-client-authn-js)
|
||||||
@ -91,11 +91,11 @@ Below we go a bit deeper into the available options
|
|||||||
The `access` option allows you to set authorization restrictions on the IDP API when enabled,
|
The `access` option allows you to set authorization restrictions on the IDP API when enabled,
|
||||||
similar to how authorization works on the LDP requests on the server.
|
similar to how authorization works on the LDP requests on the server.
|
||||||
For example, if the server uses WebACL as authorization scheme,
|
For example, if the server uses WebACL as authorization scheme,
|
||||||
you can put a `.acl` resource in the `/idp/register/` container to restrict
|
you can put a `.acl` resource in the `/.account/account/` container to restrict
|
||||||
who is allowed to access the registration API.
|
who is allowed to access the account creation API.
|
||||||
Note that for everything to work there needs to be a `.acl` resource in `/idp/` when using WebACL
|
Note that for everything to work there needs to be a `.acl` resource in `/.account/` when using WebACL
|
||||||
so resources can be accessed as usual when the server starts up.
|
so resources can be accessed as usual when the server starts up.
|
||||||
Make sure you change the permissions on `/idp/.acl` so not everyone can modify those.
|
Make sure you change the permissions on `/.account/.acl` so not everyone can modify those.
|
||||||
|
|
||||||
All of the above is only relevant if you use the `restricted.json` setting for this import.
|
All of the above is only relevant if you use the `restricted.json` setting for this import.
|
||||||
When you use `public.json` the API will simply always be accessible by everyone.
|
When you use `public.json` the API will simply always be accessible by everyone.
|
||||||
@ -104,20 +104,33 @@ When you use `public.json` the API will simply always be accessible by everyone.
|
|||||||
|
|
||||||
In case you want users to be able to reset their password when they forget it,
|
In case you want users to be able to reset their password when they forget it,
|
||||||
you will need to tell the server which email server to use to send reset mails.
|
you will need to tell the server which email server to use to send reset mails.
|
||||||
`example.json` contains an example of what this looks like,
|
`example.json` contains an example of what this looks like.
|
||||||
which you will need to copy over to your base configuration and then remove the `config/identity/email` import.
|
When using this import, you can override the values with those of your own mail client
|
||||||
|
by adding the following to your `Components.js` configuration with updated values:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"comment": "The settings of your email server.",
|
||||||
|
"@type": "Override",
|
||||||
|
"overrideInstance": {
|
||||||
|
"@id": "urn:solid-server:default:EmailSender"
|
||||||
|
},
|
||||||
|
"overrideParameters": {
|
||||||
|
"@type": "BaseEmailSender",
|
||||||
|
"senderName": "Community Solid Server <solid@example.email>",
|
||||||
|
"emailConfig_host": "smtp.example.email",
|
||||||
|
"emailConfig_port": 587,
|
||||||
|
"emailConfig_auth_user": "solid@example.email",
|
||||||
|
"emailConfig_auth_pass": "NYEaCsqV7aVStRCbmC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### handler
|
### handler
|
||||||
|
|
||||||
There is only one option here. This import contains all the core components necessary to make the IDP work.
|
|
||||||
In case you need to make some changes to core IDP settings, this is where you would have to look.
|
|
||||||
|
|
||||||
### interaction
|
|
||||||
|
|
||||||
Here you determine which features of account management are available.
|
Here you determine which features of account management are available.
|
||||||
`default.json` allows everything, while `no-accounts.json` and `no-pods.json`
|
`default.json` allows everything, `disabled.json` completely disables account management,
|
||||||
disable account and pod creation respectively.
|
and the other options disable account and/or pod creation.
|
||||||
Taking one of those latter options will disable the relevant JSON APIs and HTML pages.
|
|
||||||
|
|
||||||
### pod
|
### pod
|
||||||
|
|
||||||
|
@ -77,6 +77,8 @@ extra:
|
|||||||
nav:
|
nav:
|
||||||
- Welcome:
|
- Welcome:
|
||||||
- README.md
|
- README.md
|
||||||
|
- Features:
|
||||||
|
- features.md
|
||||||
- Usage:
|
- Usage:
|
||||||
- Example request: usage/example-requests.md
|
- Example request: usage/example-requests.md
|
||||||
- Metadata: usage/metadata.md
|
- Metadata: usage/metadata.md
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
const antfu = require('@antfu/eslint-config').default;
|
const antfu = require('@antfu/eslint-config');
|
||||||
const generalConfig = require('./eslint/general');
|
const generalConfig = require('./eslint/general');
|
||||||
const testConfig = require('./eslint/test');
|
const testConfig = require('./eslint/test');
|
||||||
const typedConfig = require('./eslint/typed');
|
const typedConfig = require('./eslint/typed');
|
||||||
const unicornConfig = require('./eslint/unicorn');
|
const unicornConfig = require('./eslint/unicorn');
|
||||||
|
|
||||||
const configs = antfu(
|
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder
|
||||||
|
// See https://github.com/antfu/eslint-config/blob/7071af7024335aad319a91db41ce594ebc6a0899/src/globs.ts#L55
|
||||||
|
const index = antfu.GLOB_EXCLUDE.indexOf('**/output');
|
||||||
|
if (index < 0) {
|
||||||
|
throw new Error('Could not update GLOB_EXCLUDE. Check if antfu changed how it handles ignores.');
|
||||||
|
}
|
||||||
|
antfu.GLOB_EXCLUDE.splice(index, 1);
|
||||||
|
|
||||||
|
module.exports = antfu.default(
|
||||||
{
|
{
|
||||||
// Don't want to lint test assets, or TS snippets in markdown files
|
// Don't want to lint test assets, or TS snippets in markdown files
|
||||||
ignores: [ 'test/assets/*', '**/*.md/**/*.ts' ],
|
ignores: [ 'test/assets/*', '**/*.md/**/*.ts' ],
|
||||||
@ -35,12 +43,3 @@ const configs = antfu(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// The default ignore list contains all `output` folders, which conflicts with our src/http/output folder
|
|
||||||
// See https://github.com/antfu/eslint-config/blob/29f29f1e16d0187f5c870102f910d798acd9b874/src/globs.ts#L53
|
|
||||||
if (!configs[1].ignores.includes('**/output')) {
|
|
||||||
throw new Error('Unexpected data in config position. Check if antfu changed how it handles ignores.');
|
|
||||||
}
|
|
||||||
delete configs[1].ignores;
|
|
||||||
|
|
||||||
module.exports = configs;
|
|
||||||
|
@ -61,6 +61,8 @@ module.exports = {
|
|||||||
'style/block-spacing': 'off',
|
'style/block-spacing': 'off',
|
||||||
'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }],
|
'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }],
|
||||||
'style/generator-star-spacing': [ 'error', { before: false, after: true }],
|
'style/generator-star-spacing': [ 'error', { before: false, after: true }],
|
||||||
|
// Seems to be inconsistent in when it adds indentation and when it does not
|
||||||
|
'style/indent-binary-ops': 'off',
|
||||||
'style/member-delimiter-style': [ 'error', {
|
'style/member-delimiter-style': [ 'error', {
|
||||||
multiline: { delimiter: 'semi', requireLast: true },
|
multiline: { delimiter: 'semi', requireLast: true },
|
||||||
singleline: { delimiter: 'semi', requireLast: false },
|
singleline: { delimiter: 'semi', requireLast: false },
|
||||||
|
5534
package-lock.json
generated
5534
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@solid/community-server",
|
"name": "@solid/community-server",
|
||||||
"version": "7.0.1",
|
"version": "7.0.2",
|
||||||
"description": "Community Solid Server: an open and modular implementation of the Solid specifications",
|
"description": "Community Solid Server: an open and modular implementation of the Solid specifications",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
|
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
|
||||||
@ -143,7 +143,7 @@
|
|||||||
"yup": "^1.3.2"
|
"yup": "^1.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^1.0.0-beta.27",
|
"@antfu/eslint-config": "2.3.4",
|
||||||
"@commitlint/cli": "^18.2.0",
|
"@commitlint/cli": "^18.2.0",
|
||||||
"@commitlint/config-conventional": "^18.0.0",
|
"@commitlint/config-conventional": "^18.0.0",
|
||||||
"@inrupt/solid-client-authn-core": "^1.17.3",
|
"@inrupt/solid-client-authn-core": "^1.17.3",
|
||||||
|
@ -36,7 +36,8 @@ export class StorageDescriptionAdvertiser extends MetadataWriter {
|
|||||||
storageRoot = await this.storageStrategy.getStorageIdentifier(identifier);
|
storageRoot = await this.storageStrategy.getStorageIdentifier(identifier);
|
||||||
this.logger.debug(`Found storage root ${storageRoot.path}`);
|
this.logger.debug(`Found storage root ${storageRoot.path}`);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.error(`Unable to find storage root: ${createErrorMessage(error)}`);
|
this.logger.error(`Unable to find storage root: ${createErrorMessage(error)
|
||||||
|
}. The storage/location import in the server configuration is probably wrong.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const storageDescription = joinUrl(storageRoot.path, this.relativePath);
|
const storageDescription = joinUrl(storageRoot.path, this.relativePath);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import { JsonResourceStorage } from '../../storage/keyvalue/JsonResourceStorage';
|
import { JsonResourceStorage } from '../../storage/keyvalue/JsonResourceStorage';
|
||||||
|
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||||
import { readableToString } from '../../util/StreamUtil';
|
import { readableToString } from '../../util/StreamUtil';
|
||||||
import { LDP } from '../../util/Vocabularies';
|
import { LDP } from '../../util/Vocabularies';
|
||||||
@ -34,9 +35,14 @@ export class SingleContainerJsonStorage<T> extends JsonResourceStorage<T> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = JSON.parse(await readableToString(document.data)) as T;
|
|
||||||
const key = this.identifierToKey(documentId);
|
const key = this.identifierToKey(documentId);
|
||||||
yield [ key, json ];
|
try {
|
||||||
|
const json = JSON.parse(await readableToString(document.data)) as T;
|
||||||
|
yield [ key, json ];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Unable to parse ${path}. You should probably delete this resource manually. Error: ${
|
||||||
|
createErrorMessage(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
|
import type { Stats } from 'node:fs';
|
||||||
import { createReadStream } from 'node:fs';
|
import { createReadStream } from 'node:fs';
|
||||||
|
import { stat } from 'fs-extra';
|
||||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
|
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||||
|
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { isContainerIdentifier } from '../../util/PathUtil';
|
import { isContainerIdentifier } from '../../util/PathUtil';
|
||||||
|
import { toLiteral } from '../../util/TermUtil';
|
||||||
|
import { POSIX, XSD } from '../../util/Vocabularies';
|
||||||
import { cleanPreferences, getTypeWeight, matchesMediaType } from './ConversionUtil';
|
import { cleanPreferences, getTypeWeight, matchesMediaType } from './ConversionUtil';
|
||||||
import { RepresentationConverter } from './RepresentationConverter';
|
import { RepresentationConverter } from './RepresentationConverter';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
@ -46,6 +53,8 @@ export interface ConstantConverterOptions {
|
|||||||
* Options default to the most permissive values when not defined.
|
* Options default to the most permissive values when not defined.
|
||||||
*/
|
*/
|
||||||
export class ConstantConverter extends RepresentationConverter {
|
export class ConstantConverter extends RepresentationConverter {
|
||||||
|
private readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly filePath: string;
|
private readonly filePath: string;
|
||||||
private readonly contentType: string;
|
private readonly contentType: string;
|
||||||
private readonly options: Required<ConstantConverterOptions>;
|
private readonly options: Required<ConstantConverterOptions>;
|
||||||
@ -113,8 +122,19 @@ export class ConstantConverter extends RepresentationConverter {
|
|||||||
// Ignore the original representation
|
// Ignore the original representation
|
||||||
representation.data.destroy();
|
representation.data.destroy();
|
||||||
|
|
||||||
|
// Get the stats to have the correct size metadata
|
||||||
|
let stats: Stats;
|
||||||
|
try {
|
||||||
|
stats = await stat(this.filePath);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Unable to access ${this.filePath}: ${createErrorMessage(error)}`);
|
||||||
|
// Not giving out details in error as it contains internal server information
|
||||||
|
throw new InternalServerError(`Unable to access file used for constant conversion.`);
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new representation from the constant file
|
// Create a new representation from the constant file
|
||||||
const data = createReadStream(this.filePath, 'utf8');
|
const data = createReadStream(this.filePath, 'utf8');
|
||||||
|
representation.metadata.set(POSIX.terms.size, toLiteral(stats.size, XSD.terms.integer));
|
||||||
return new BasicRepresentation(data, representation.metadata, this.contentType);
|
return new BasicRepresentation(data, representation.metadata, this.contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
|
|||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
|
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
import { ensureTrailingSlash, isContainerIdentifier, joinUrl, trimLeadingSlashes } from '../../util/PathUtil';
|
import { ensureTrailingSlash, isContainerIdentifier, joinUrl, trimLeadingSlashes } from '../../util/PathUtil';
|
||||||
import { readableToString } from '../../util/StreamUtil';
|
import { readableToString } from '../../util/StreamUtil';
|
||||||
@ -88,8 +89,14 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
|
|||||||
yield* this.getResourceEntries({ path });
|
yield* this.getResourceEntries({ path });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const json = JSON.parse(await readableToString(representation.data)) as T;
|
try {
|
||||||
yield [ this.identifierToKey(identifier), json ];
|
const json = JSON.parse(await readableToString(representation.data)) as T;
|
||||||
|
yield [ this.identifierToKey(identifier), json ];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error(`Unable to parse ${identifier.path
|
||||||
|
}. You should probably delete this resource manually. Error: ${
|
||||||
|
createErrorMessage(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,6 @@ export function transformSafely<T = any>(
|
|||||||
source: NodeJS.ReadableStream,
|
source: NodeJS.ReadableStream,
|
||||||
{
|
{
|
||||||
transform = function(data): void {
|
transform = function(data): void {
|
||||||
// eslint-disable-next-line ts/no-invalid-this
|
|
||||||
this.push(data);
|
this.push(data);
|
||||||
},
|
},
|
||||||
flush = (): null => null,
|
flush = (): null => null,
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title><%= extractTitle(htmlBody) %></title>
|
<title><%= extractTitle(htmlBody) %></title>
|
||||||
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css" type="text/css">
|
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css?v=7" type="text/css">
|
||||||
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js"></script>
|
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js?v=7"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
this is not valid JSON
|
@ -63,6 +63,8 @@ describe('A server migrating from v6', (): void => {
|
|||||||
// Setup resources should have been migrated
|
// Setup resources should have been migrated
|
||||||
const setupDir = await readdir(joinFilePath(rootFilePath, '.internal/setup/'));
|
const setupDir = await readdir(joinFilePath(rootFilePath, '.internal/setup/'));
|
||||||
expect(setupDir).toEqual([
|
expect(setupDir).toEqual([
|
||||||
|
// Invalid JSON file was not deleted, only error was logged. Just in case its data needs to be saved.
|
||||||
|
'aW52YWxpZFJlc291cmNl$.json',
|
||||||
'current-base-url$.json',
|
'current-base-url$.json',
|
||||||
'current-server-version$.json',
|
'current-server-version$.json',
|
||||||
'setupCompleted-2.0$.json',
|
'setupCompleted-2.0$.json',
|
||||||
|
@ -310,6 +310,7 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
expect(use).toHaveBeenCalledTimes(1);
|
expect(use).toHaveBeenCalledTimes(1);
|
||||||
const middleware = use.mock.calls[0][0];
|
const middleware = use.mock.calls[0][0];
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/unbound-method
|
||||||
const oldAccept = ctx.accepts;
|
const oldAccept = ctx.accepts;
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||||
@ -325,6 +326,7 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
expect(use).toHaveBeenCalledTimes(1);
|
expect(use).toHaveBeenCalledTimes(1);
|
||||||
const middleware = use.mock.calls[0][0];
|
const middleware = use.mock.calls[0][0];
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/unbound-method
|
||||||
const oldAccept = ctx.accepts;
|
const oldAccept = ctx.accepts;
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||||
|
@ -22,8 +22,8 @@ describe('A ClusterManager', (): void => {
|
|||||||
beforeAll((): void => {
|
beforeAll((): void => {
|
||||||
Object.assign(mockCluster, {
|
Object.assign(mockCluster, {
|
||||||
fork: jest.fn().mockImplementation((): any => mockWorker),
|
fork: jest.fn().mockImplementation((): any => mockWorker),
|
||||||
on: jest.fn().mockImplementation(emitter.on),
|
on: jest.fn().mockImplementation(emitter.on.bind(emitter)),
|
||||||
emit: jest.fn().mockImplementation(emitter.emit),
|
emit: jest.fn().mockImplementation(emitter.emit.bind(emitter)),
|
||||||
isMaster: true,
|
isMaster: true,
|
||||||
isWorker: false,
|
isWorker: false,
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,7 @@ describe('A SingleContainerJsonStorage', (): void => {
|
|||||||
if (isContainerIdentifier(id)) {
|
if (isContainerIdentifier(id)) {
|
||||||
const metadata = new RepresentationMetadata(id);
|
const metadata = new RepresentationMetadata(id);
|
||||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/foo');
|
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/foo');
|
||||||
|
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bad');
|
||||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bar/');
|
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/bar/');
|
||||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/baz');
|
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/baz');
|
||||||
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/unknown');
|
metadata.add(LDP.terms.contains, 'http://example.com/.internal/accounts/unknown');
|
||||||
@ -27,14 +28,17 @@ describe('A SingleContainerJsonStorage', (): void => {
|
|||||||
if (id.path.endsWith('unknown')) {
|
if (id.path.endsWith('unknown')) {
|
||||||
throw new NotFoundHttpError();
|
throw new NotFoundHttpError();
|
||||||
}
|
}
|
||||||
return new BasicRepresentation(`{ "id": "${id.path}" }`, 'text/plain');
|
if (id.path.endsWith('bad')) {
|
||||||
|
return new BasicRepresentation(`invalid JSON`, 'application/json');
|
||||||
|
}
|
||||||
|
return new BasicRepresentation(`{ "id": "${id.path}" }`, 'application/json');
|
||||||
}),
|
}),
|
||||||
} satisfies Partial<ResourceStore> as any;
|
} satisfies Partial<ResourceStore> as any;
|
||||||
|
|
||||||
storage = new SingleContainerJsonStorage(store, baseUrl, container);
|
storage = new SingleContainerJsonStorage(store, baseUrl, container);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only iterates over the documents in the base container.', async(): Promise<void> => {
|
it('only iterates over the valid documents in the base container.', async(): Promise<void> => {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for await (const entry of storage.entries()) {
|
for await (const entry of storage.entries()) {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
@ -43,7 +47,7 @@ describe('A SingleContainerJsonStorage', (): void => {
|
|||||||
[ 'foo', { id: 'http://example.com/.internal/accounts/foo' }],
|
[ 'foo', { id: 'http://example.com/.internal/accounts/foo' }],
|
||||||
[ 'baz', { id: 'http://example.com/.internal/accounts/baz' }],
|
[ 'baz', { id: 'http://example.com/.internal/accounts/baz' }],
|
||||||
]);
|
]);
|
||||||
expect(store.getRepresentation).toHaveBeenCalledTimes(4);
|
expect(store.getRepresentation).toHaveBeenCalledTimes(5);
|
||||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
{ path: 'http://example.com/.internal/accounts/' },
|
{ path: 'http://example.com/.internal/accounts/' },
|
||||||
@ -56,11 +60,16 @@ describe('A SingleContainerJsonStorage', (): void => {
|
|||||||
);
|
);
|
||||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||||
3,
|
3,
|
||||||
{ path: 'http://example.com/.internal/accounts/baz' },
|
{ path: 'http://example.com/.internal/accounts/bad' },
|
||||||
{ type: { 'application/json': 1 }},
|
{ type: { 'application/json': 1 }},
|
||||||
);
|
);
|
||||||
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||||
4,
|
4,
|
||||||
|
{ path: 'http://example.com/.internal/accounts/baz' },
|
||||||
|
{ type: { 'application/json': 1 }},
|
||||||
|
);
|
||||||
|
expect(store.getRepresentation).toHaveBeenNthCalledWith(
|
||||||
|
5,
|
||||||
{ path: 'http://example.com/.internal/accounts/unknown' },
|
{ path: 'http://example.com/.internal/accounts/unknown' },
|
||||||
{ type: { 'application/json': 1 }},
|
{ type: { 'application/json': 1 }},
|
||||||
);
|
);
|
||||||
|
@ -819,7 +819,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
const auxResourceID = { path: `${root}resource.dummy` };
|
const auxResourceID = { path: `${root}resource.dummy` };
|
||||||
accessor.data[resourceID.path] = representation;
|
accessor.data[resourceID.path] = representation;
|
||||||
accessor.data[auxResourceID.path] = representation;
|
accessor.data[auxResourceID.path] = representation;
|
||||||
const deleteFn = accessor.deleteResource;
|
const deleteFn = accessor.deleteResource.bind(accessor);
|
||||||
jest.spyOn(accessor, 'deleteResource')
|
jest.spyOn(accessor, 'deleteResource')
|
||||||
.mockImplementation(async(identifier: ResourceIdentifier): Promise<void> => {
|
.mockImplementation(async(identifier: ResourceIdentifier): Promise<void> => {
|
||||||
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
|
||||||
@ -862,6 +862,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
|
|
||||||
it('should rethrow any unexpected errors from validateIdentifier.', async(): Promise<void> => {
|
it('should rethrow any unexpected errors from validateIdentifier.', async(): Promise<void> => {
|
||||||
const resourceID = { path: `${root}resource` };
|
const resourceID = { path: `${root}resource` };
|
||||||
|
// eslint-disable-next-line jest/unbound-method
|
||||||
const originalMetaData = accessor.getMetadata;
|
const originalMetaData = accessor.getMetadata;
|
||||||
jest.spyOn(accessor, 'getMetadata').mockImplementation(async(): Promise<any> => {
|
jest.spyOn(accessor, 'getMetadata').mockImplementation(async(): Promise<any> => {
|
||||||
throw new Error('error');
|
throw new Error('error');
|
||||||
|
@ -33,7 +33,7 @@ describe('A LockingResourceStore', (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const readable = guardedStreamFrom([ 1, 2, 3 ]);
|
const readable = guardedStreamFrom([ 1, 2, 3 ]);
|
||||||
const { destroy } = readable;
|
const destroy = readable.destroy.bind(readable);
|
||||||
jest.spyOn(readable, 'destroy').mockImplementation((error): any => destroy.call(readable, error));
|
jest.spyOn(readable, 'destroy').mockImplementation((error): any => destroy.call(readable, error));
|
||||||
source = {
|
source = {
|
||||||
getRepresentation: jest.fn((): any => addOrder('getRepresentation', { data: readable } as Representation)),
|
getRepresentation: jest.fn((): any => addOrder('getRepresentation', { data: readable } as Representation)),
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import fsExtra from 'fs-extra';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { ConstantConverterOptions } from '../../../../src/storage/conversion/ConstantConverter';
|
import type { ConstantConverterOptions } from '../../../../src/storage/conversion/ConstantConverter';
|
||||||
import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
|
import { ConstantConverter } from '../../../../src/storage/conversion/ConstantConverter';
|
||||||
import { CONTENT_TYPE } from '../../../../src/util/Vocabularies';
|
import { CONTENT_TYPE, POSIX } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any);
|
const createReadStream = jest.spyOn(fs, 'createReadStream').mockReturnValue('file contents' as any);
|
||||||
|
const stat = jest.spyOn(fsExtra, 'stat').mockReturnValue({ size: 100 } as any);
|
||||||
|
|
||||||
describe('A ConstantConverter', (): void => {
|
describe('A ConstantConverter', (): void => {
|
||||||
const identifier = { path: 'identifier' };
|
const identifier = { path: 'identifier' };
|
||||||
@ -13,6 +15,7 @@ describe('A ConstantConverter', (): void => {
|
|||||||
let converter: ConstantConverter;
|
let converter: ConstantConverter;
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
|
jest.clearAllMocks();
|
||||||
options = { container: true, document: true, minQuality: 1, enabledMediaRanges: [ '*/*' ], disabledMediaRanges: []};
|
options = { container: true, document: true, minQuality: 1, enabledMediaRanges: [ '*/*' ], disabledMediaRanges: []};
|
||||||
converter = new ConstantConverter('abc/def/index.html', 'text/html', options);
|
converter = new ConstantConverter('abc/def/index.html', 'text/html', options);
|
||||||
});
|
});
|
||||||
@ -99,7 +102,7 @@ describe('A ConstantConverter', (): void => {
|
|||||||
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('replaces the representation of a supported request.', async(): Promise<void> => {
|
it('replaces the representation of a supported request and replaces the size.', async(): Promise<void> => {
|
||||||
const preferences = { type: { 'text/html': 1 }};
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||||
@ -114,9 +117,28 @@ describe('A ConstantConverter', (): void => {
|
|||||||
expect(createReadStream).toHaveBeenCalledWith('abc/def/index.html', 'utf8');
|
expect(createReadStream).toHaveBeenCalledWith('abc/def/index.html', 'utf8');
|
||||||
|
|
||||||
expect(converted.metadata.contentType).toBe('text/html');
|
expect(converted.metadata.contentType).toBe('text/html');
|
||||||
|
expect(converted.metadata.get(POSIX.terms.size)?.value).toBe('100');
|
||||||
await expect(arrayifyStream(converted.data)).resolves.toEqual([ 'file contents' ]);
|
await expect(arrayifyStream(converted.data)).resolves.toEqual([ 'file contents' ]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws an internal error if the file cannot be accessed.', async(): Promise<void> => {
|
||||||
|
const preferences = { type: { 'text/html': 1 }};
|
||||||
|
const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
|
||||||
|
const representation = { metadata, data: { destroy: jest.fn() }} as any;
|
||||||
|
const args = { identifier, representation, preferences };
|
||||||
|
|
||||||
|
await expect(converter.canHandle(args)).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-misused-promises
|
||||||
|
stat.mockImplementation(async(): Promise<never> => {
|
||||||
|
throw new Error('file not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(converter.handle(args)).rejects.toThrow('Unable to access file used for constant conversion.');
|
||||||
|
expect(representation.data.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createReadStream).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('defaults to the most permissive options.', async(): Promise<void> => {
|
it('defaults to the most permissive options.', async(): Promise<void> => {
|
||||||
const preferences = { type: { 'text/html': 0.1 }};
|
const preferences = { type: { 'text/html': 0.1 }};
|
||||||
const metadata = new RepresentationMetadata();
|
const metadata = new RepresentationMetadata();
|
||||||
|
@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../../../../src/http/representation/Res
|
|||||||
import { JsonResourceStorage } from '../../../../src/storage/keyvalue/JsonResourceStorage';
|
import { JsonResourceStorage } from '../../../../src/storage/keyvalue/JsonResourceStorage';
|
||||||
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
|
||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
||||||
import { isContainerIdentifier } from '../../../../src/util/PathUtil';
|
import { isContainerIdentifier, joinUrl } from '../../../../src/util/PathUtil';
|
||||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||||
import { LDP } from '../../../../src/util/Vocabularies';
|
import { LDP } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
@ -112,6 +112,9 @@ describe('A JsonResourceStorage', (): void => {
|
|||||||
data.set(containerIdentifier, '');
|
data.set(containerIdentifier, '');
|
||||||
data.set(subContainerIdentifier, '');
|
data.set(subContainerIdentifier, '');
|
||||||
|
|
||||||
|
// Manually setting invalid data which will be ignored
|
||||||
|
data.set(joinUrl(containerIdentifier, 'badData'), 'invalid JSON');
|
||||||
|
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for await (const entry of storage.entries()) {
|
for await (const entry of storage.entries()) {
|
||||||
entries.push(entry);
|
entries.push(entry);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user