Merge branch 'main' into versions/next-major

# Conflicts:
#	package-lock.json
#	package.json
This commit is contained in:
Joachim Van Herwegen 2024-01-04 10:53:01 +01:00
commit 4664c64e6c
32 changed files with 1908 additions and 3966 deletions

View File

@ -78,13 +78,13 @@ jobs:
branch-name: ${{ inputs.branch || github.head_ref }}
- name: Save the reports
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
path: reports
- name: Save the server output
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
path: server-output.log

View File

@ -89,6 +89,6 @@ jobs:
with:
context: .
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm/v8,linux/arm64/v8
platforms: linux/amd64
tags: ${{ needs.docker-meta.outputs.tags }}
labels: ${{ needs.docker-meta.outputs.labels }}

View File

@ -44,7 +44,7 @@ jobs:
needs: mkdocs-prep
steps:
- uses: actions/checkout@v4.1.1
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material

View File

@ -27,6 +27,7 @@ jobs:
- 18.x
- '20.0'
- 20.x
- 21.x
timeout-minutes: 15
steps:
- name: Use Node.js ${{ matrix.node-version }}
@ -58,6 +59,7 @@ jobs:
node-version:
- 18.x
- 20.x
- 21.x
env:
TEST_DOCKER: true
services:
@ -92,6 +94,7 @@ jobs:
node-version:
- 18.x
- 20.x
- 21.x
timeout-minutes: 20
steps:
- name: Use Node.js ${{ matrix.node-version }}

View File

@ -10,7 +10,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@v9
with:
debug-only: true
stale-issue-label: 🏚️ abandoned

View File

@ -3,6 +3,37 @@
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)
### Fixes

View File

@ -22,6 +22,17 @@ Used by certain classes for internal storage.
* *memory*: Store everything in memory.
* *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
The chain of utility ResourceStores that needs to be passed through before reaching the backend stores.

View File

@ -29,6 +29,7 @@ the [changelog](https://github.com/CommunitySolidServer/CommunitySolidServer/blo
## Using the server
* [An overview of the main features of the server](features.md)
* [Quickly starting the server](usage/starting-server.md)
* [Basic example HTTP requests](usage/example-requests.md)
* [Editing the metadata of a resource](usage/metadata.md)

View File

@ -76,7 +76,7 @@ The `urn:solid-server:default:AuthResourceHttpHandler` is identical
to the `urn:solid-server:default:LdpHandler` which will be discussed below,
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,
this handler will catch all requests targeting `.acl` resources.

View 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).

View File

@ -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.
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
// 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

View File

@ -2,7 +2,7 @@
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),
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
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,
similar to how authorization works on the LDP requests on the server.
For example, if the server uses WebACL as authorization scheme,
you can put a `.acl` resource in the `/idp/register/` container to restrict
who is allowed to access the registration API.
Note that for everything to work there needs to be a `.acl` resource in `/idp/` when using WebACL
you can put a `.acl` resource in the `/.account/account/` container to restrict
who is allowed to access the account creation API.
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.
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.
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,
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,
which you will need to copy over to your base configuration and then remove the `config/identity/email` import.
`example.json` contains an example of what this looks like.
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
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.
`default.json` allows everything, while `no-accounts.json` and `no-pods.json`
disable account and pod creation respectively.
Taking one of those latter options will disable the relevant JSON APIs and HTML pages.
`default.json` allows everything, `disabled.json` completely disables account management,
and the other options disable account and/or pod creation.
### pod

View File

@ -77,6 +77,8 @@ extra:
nav:
- Welcome:
- README.md
- Features:
- features.md
- Usage:
- Example request: usage/example-requests.md
- Metadata: usage/metadata.md

View File

@ -1,10 +1,18 @@
const antfu = require('@antfu/eslint-config').default;
const antfu = require('@antfu/eslint-config');
const generalConfig = require('./eslint/general');
const testConfig = require('./eslint/test');
const typedConfig = require('./eslint/typed');
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
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;

View File

@ -61,6 +61,8 @@ module.exports = {
'style/block-spacing': 'off',
'style/brace-style': [ 'error', '1tbs', { allowSingleLine: false }],
'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', {
multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'semi', requireLast: false },

5534
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"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",
"license": "MIT",
"homepage": "https://github.com/CommunitySolidServer/CommunitySolidServer#readme",
@ -143,7 +143,7 @@
"yup": "^1.3.2"
},
"devDependencies": {
"@antfu/eslint-config": "^1.0.0-beta.27",
"@antfu/eslint-config": "2.3.4",
"@commitlint/cli": "^18.2.0",
"@commitlint/config-conventional": "^18.0.0",
"@inrupt/solid-client-authn-core": "^1.17.3",

View File

@ -36,7 +36,8 @@ export class StorageDescriptionAdvertiser extends MetadataWriter {
storageRoot = await this.storageStrategy.getStorageIdentifier(identifier);
this.logger.debug(`Found storage root ${storageRoot.path}`);
} 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;
}
const storageDescription = joinUrl(storageRoot.path, this.relativePath);

View File

@ -1,5 +1,6 @@
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { JsonResourceStorage } from '../../storage/keyvalue/JsonResourceStorage';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
import { isContainerIdentifier } from '../../util/PathUtil';
import { readableToString } from '../../util/StreamUtil';
import { LDP } from '../../util/Vocabularies';
@ -34,9 +35,14 @@ export class SingleContainerJsonStorage<T> extends JsonResourceStorage<T> {
continue;
}
const json = JSON.parse(await readableToString(document.data)) as T;
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)}`);
}
}
}
}

View File

@ -1,8 +1,15 @@
import type { Stats } from 'node:fs';
import { createReadStream } from 'node:fs';
import { stat } from 'fs-extra';
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
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 { isContainerIdentifier } from '../../util/PathUtil';
import { toLiteral } from '../../util/TermUtil';
import { POSIX, XSD } from '../../util/Vocabularies';
import { cleanPreferences, getTypeWeight, matchesMediaType } from './ConversionUtil';
import { RepresentationConverter } from './RepresentationConverter';
import type { RepresentationConverterArgs } from './RepresentationConverter';
@ -46,6 +53,8 @@ export interface ConstantConverterOptions {
* Options default to the most permissive values when not defined.
*/
export class ConstantConverter extends RepresentationConverter {
private readonly logger = getLoggerFor(this);
private readonly filePath: string;
private readonly contentType: string;
private readonly options: Required<ConstantConverterOptions>;
@ -113,8 +122,19 @@ export class ConstantConverter extends RepresentationConverter {
// Ignore the original representation
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
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);
}
}

View File

@ -2,6 +2,7 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
import type { Representation } from '../../http/representation/Representation';
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../../logging/LogUtil';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
import { ensureTrailingSlash, isContainerIdentifier, joinUrl, trimLeadingSlashes } from '../../util/PathUtil';
import { readableToString } from '../../util/StreamUtil';
@ -88,8 +89,14 @@ export class JsonResourceStorage<T> implements KeyValueStorage<string, T> {
yield* this.getResourceEntries({ path });
}
} else {
const json = JSON.parse(await readableToString(representation.data)) as T;
yield [ this.identifierToKey(identifier), json ];
try {
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)}`);
}
}
}
}

View File

@ -144,7 +144,6 @@ export function transformSafely<T = any>(
source: NodeJS.ReadableStream,
{
transform = function(data): void {
// eslint-disable-next-line ts/no-invalid-this
this.push(data);
},
flush = (): null => null,

View File

@ -4,8 +4,8 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><%= extractTitle(htmlBody) %></title>
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css" type="text/css">
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js"></script>
<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?v=7"></script>
</head>
<body>
<header>

View File

@ -0,0 +1 @@
this is not valid JSON

View File

@ -63,6 +63,8 @@ describe('A server migrating from v6', (): void => {
// Setup resources should have been migrated
const setupDir = await readdir(joinFilePath(rootFilePath, '.internal/setup/'));
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-server-version$.json',
'setupCompleted-2.0$.json',

View File

@ -310,6 +310,7 @@ describe('An IdentityProviderFactory', (): void => {
expect(use).toHaveBeenCalledTimes(1);
const middleware = use.mock.calls[0][0];
// eslint-disable-next-line jest/unbound-method
const oldAccept = ctx.accepts;
const next = jest.fn();
await expect(middleware(ctx, next)).resolves.toBeUndefined();
@ -325,6 +326,7 @@ describe('An IdentityProviderFactory', (): void => {
expect(use).toHaveBeenCalledTimes(1);
const middleware = use.mock.calls[0][0];
// eslint-disable-next-line jest/unbound-method
const oldAccept = ctx.accepts;
const next = jest.fn();
await expect(middleware(ctx, next)).resolves.toBeUndefined();

View File

@ -22,8 +22,8 @@ describe('A ClusterManager', (): void => {
beforeAll((): void => {
Object.assign(mockCluster, {
fork: jest.fn().mockImplementation((): any => mockWorker),
on: jest.fn().mockImplementation(emitter.on),
emit: jest.fn().mockImplementation(emitter.emit),
on: jest.fn().mockImplementation(emitter.on.bind(emitter)),
emit: jest.fn().mockImplementation(emitter.emit.bind(emitter)),
isMaster: true,
isWorker: false,
});

View File

@ -19,6 +19,7 @@ describe('A SingleContainerJsonStorage', (): void => {
if (isContainerIdentifier(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/bad');
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/unknown');
@ -27,14 +28,17 @@ describe('A SingleContainerJsonStorage', (): void => {
if (id.path.endsWith('unknown')) {
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;
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 = [];
for await (const entry of storage.entries()) {
entries.push(entry);
@ -43,7 +47,7 @@ describe('A SingleContainerJsonStorage', (): void => {
[ 'foo', { id: 'http://example.com/.internal/accounts/foo' }],
[ 'baz', { id: 'http://example.com/.internal/accounts/baz' }],
]);
expect(store.getRepresentation).toHaveBeenCalledTimes(4);
expect(store.getRepresentation).toHaveBeenCalledTimes(5);
expect(store.getRepresentation).toHaveBeenNthCalledWith(
1,
{ path: 'http://example.com/.internal/accounts/' },
@ -56,11 +60,16 @@ describe('A SingleContainerJsonStorage', (): void => {
);
expect(store.getRepresentation).toHaveBeenNthCalledWith(
3,
{ path: 'http://example.com/.internal/accounts/baz' },
{ path: 'http://example.com/.internal/accounts/bad' },
{ type: { 'application/json': 1 }},
);
expect(store.getRepresentation).toHaveBeenNthCalledWith(
4,
{ path: 'http://example.com/.internal/accounts/baz' },
{ type: { 'application/json': 1 }},
);
expect(store.getRepresentation).toHaveBeenNthCalledWith(
5,
{ path: 'http://example.com/.internal/accounts/unknown' },
{ type: { 'application/json': 1 }},
);

View File

@ -819,7 +819,7 @@ describe('A DataAccessorBasedStore', (): void => {
const auxResourceID = { path: `${root}resource.dummy` };
accessor.data[resourceID.path] = representation;
accessor.data[auxResourceID.path] = representation;
const deleteFn = accessor.deleteResource;
const deleteFn = accessor.deleteResource.bind(accessor);
jest.spyOn(accessor, 'deleteResource')
.mockImplementation(async(identifier: ResourceIdentifier): Promise<void> => {
if (auxiliaryStrategy.isAuxiliaryIdentifier(identifier)) {
@ -862,6 +862,7 @@ describe('A DataAccessorBasedStore', (): void => {
it('should rethrow any unexpected errors from validateIdentifier.', async(): Promise<void> => {
const resourceID = { path: `${root}resource` };
// eslint-disable-next-line jest/unbound-method
const originalMetaData = accessor.getMetadata;
jest.spyOn(accessor, 'getMetadata').mockImplementation(async(): Promise<any> => {
throw new Error('error');

View File

@ -33,7 +33,7 @@ describe('A LockingResourceStore', (): void => {
}
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));
source = {
getRepresentation: jest.fn((): any => addOrder('getRepresentation', { data: readable } as Representation)),

View File

@ -1,11 +1,13 @@
import fs from 'node:fs';
import fsExtra from 'fs-extra';
import arrayifyStream from 'arrayify-stream';
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
import type { ConstantConverterOptions } 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 stat = jest.spyOn(fsExtra, 'stat').mockReturnValue({ size: 100 } as any);
describe('A ConstantConverter', (): void => {
const identifier = { path: 'identifier' };
@ -13,6 +15,7 @@ describe('A ConstantConverter', (): void => {
let converter: ConstantConverter;
beforeEach(async(): Promise<void> => {
jest.clearAllMocks();
options = { container: true, document: true, minQuality: 1, enabledMediaRanges: [ '*/*' ], disabledMediaRanges: []};
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();
});
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 metadata = new RepresentationMetadata({ [CONTENT_TYPE]: 'text/turtle' });
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(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' ]);
});
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> => {
const preferences = { type: { 'text/html': 0.1 }};
const metadata = new RepresentationMetadata();

View File

@ -5,7 +5,7 @@ import type { ResourceIdentifier } from '../../../../src/http/representation/Res
import { JsonResourceStorage } from '../../../../src/storage/keyvalue/JsonResourceStorage';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
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 { LDP } from '../../../../src/util/Vocabularies';
@ -112,6 +112,9 @@ describe('A JsonResourceStorage', (): void => {
data.set(containerIdentifier, '');
data.set(subContainerIdentifier, '');
// Manually setting invalid data which will be ignored
data.set(joinUrl(containerIdentifier, 'badData'), 'invalid JSON');
const entries = [];
for await (const entry of storage.entries()) {
entries.push(entry);