mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/6.0.0
This commit is contained in:
commit
b09bf66ad7
90
.github/workflows/cth-test.yml
vendored
Normal file
90
.github/workflows/cth-test.yml
vendored
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
name: Conformance test harness
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: latest
|
||||||
|
description: Version of the Conformance Test Harness that has to be tested against
|
||||||
|
branch:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: branch to run tests against (used for matrix strategy)
|
||||||
|
ignore_failures:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: Return succes even if there are failures?
|
||||||
|
required: false
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: latest
|
||||||
|
description: Version of the Conformance Test Harness that has to be tested against.
|
||||||
|
ignore_failures:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: Return succes even if there are failures?
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Run the Conformance Test Harness and upload output as artifacts to GitHub
|
||||||
|
conformance:
|
||||||
|
continue-on-error: ${{ inputs.ignore_failures }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- name: Use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
- name: Check out the project
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.branch || github.ref }}
|
||||||
|
- name: Install dependencies and run build scripts
|
||||||
|
run: npm ci
|
||||||
|
- name: Start the server in the background
|
||||||
|
run: npm start > server-output.log &
|
||||||
|
- name: Create the necessary folders
|
||||||
|
run: mkdir -p reports/css
|
||||||
|
- name: Pull the conformance harness docker
|
||||||
|
run: "docker pull solidproject/conformance-test-harness:${{ inputs.version }}"
|
||||||
|
- name: Wait until the server has started
|
||||||
|
run: >
|
||||||
|
curl --output /dev/null --silent --head --fail --retry 30
|
||||||
|
--retry-connrefused --retry-delay 0 -k http://localhost:3000/
|
||||||
|
- name: Create users
|
||||||
|
run: npx ts-node test/deploy/createAccountCredentials.ts http://localhost:3000/ >> test/deploy/conformance.env
|
||||||
|
- name: Run the test harness
|
||||||
|
run: >
|
||||||
|
docker run -i --rm
|
||||||
|
-v "$(pwd)"/reports/css:/reports
|
||||||
|
--env-file=./test/deploy/conformance.env
|
||||||
|
--network="host"
|
||||||
|
solidproject/conformance-test-harness
|
||||||
|
--skip-teardown
|
||||||
|
--output=/reports
|
||||||
|
--target=https://github.com/solid/conformance-test-harness/css
|
||||||
|
# Steps below use `always()` to make sure logs get uploaded in case the CTH errors
|
||||||
|
- name: Sanitize branch name for artifact upload
|
||||||
|
id: sanitize
|
||||||
|
if: always()
|
||||||
|
uses: yeouchien/sanitize-branch-name-action@v1
|
||||||
|
with:
|
||||||
|
branch-name: ${{ inputs.branch || github.head_ref }}
|
||||||
|
- name: Save the reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
|
||||||
|
path: reports
|
||||||
|
- name: Save the server output
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
|
||||||
|
path: server-output.log
|
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
@ -7,14 +7,24 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- '**'
|
concurrency:
|
||||||
|
group: ${{ github.head_ref || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
npm-test:
|
npm-test:
|
||||||
# Run the tests in every case
|
# Run the tests in every case
|
||||||
uses: ./.github/workflows/npm-test.yml
|
uses: ./.github/workflows/npm-test.yml
|
||||||
|
|
||||||
|
cth-test:
|
||||||
|
# Run the Conformance Test Harness on PRs targeting main or versions/
|
||||||
|
if: github.event_name == 'pull_request' && ( github.base_ref == 'main' || startsWith( github.base_ref, 'versions/' ) )
|
||||||
|
uses: ./.github/workflows/cth-test.yml
|
||||||
|
with:
|
||||||
|
ignore_failures: true
|
||||||
|
version: 1.1.7 # The latest version that CSS is confirmed to pass
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
# Build docker containers on version tag, push to main and push to versions/
|
# Build docker containers on version tag, push to main and push to versions/
|
||||||
needs: npm-test
|
needs: npm-test
|
||||||
|
63
.github/workflows/schedule.yml
vendored
63
.github/workflows/schedule.yml
vendored
@ -3,69 +3,16 @@ name: Conformance test harness
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '23 5 * * *'
|
- cron: '23 5 * * *'
|
||||||
push:
|
workflow_dispatch:
|
||||||
branches:
|
|
||||||
- test/conformance
|
|
||||||
jobs:
|
jobs:
|
||||||
conformance:
|
conformance:
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
branch:
|
branch:
|
||||||
- 'main'
|
- 'main'
|
||||||
- 'versions/6.0.0'
|
- 'versions/6.0.0'
|
||||||
timeout-minutes: 10
|
uses: ./.github/workflows/cth-test.yml
|
||||||
steps:
|
with:
|
||||||
- name: Use Node.js 16.x
|
branch: ${{ matrix.branch }}
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16.x
|
|
||||||
- name: Check out the project
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ matrix.branch }}
|
|
||||||
- name: Install dependencies and run build scripts
|
|
||||||
run: npm ci
|
|
||||||
- name: Start the server in the background
|
|
||||||
run: npm start > server-output.log &
|
|
||||||
- name: Create the necessary folders
|
|
||||||
run: mkdir -p reports/css
|
|
||||||
- name: Pull the conformance harness docker
|
|
||||||
run: docker pull solidproject/conformance-test-harness
|
|
||||||
- name: Wait until the server has started
|
|
||||||
run: |
|
|
||||||
until $(curl --output /dev/null --silent --head --fail -k http://localhost:3000/); do
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
- name: Create users
|
|
||||||
run: npx ts-node test/deploy/createAccountCredentials.ts http://localhost:3000/ >> test/deploy/conformance.env
|
|
||||||
- name: Run the test harness
|
|
||||||
run: >
|
|
||||||
docker run -i --rm
|
|
||||||
-v "$(pwd)"/reports/css:/reports
|
|
||||||
--env-file=./test/deploy/conformance.env
|
|
||||||
--network="host"
|
|
||||||
solidproject/conformance-test-harness
|
|
||||||
--skip-teardown
|
|
||||||
--output=/reports
|
|
||||||
--target=https://github.com/solid/conformance-test-harness/css
|
|
||||||
# Steps below use `always()` to make sure logs get uploaded in case the CTH errors
|
|
||||||
- name: Sanitize branch name for artifact upload
|
|
||||||
id: sanitize
|
|
||||||
if: always()
|
|
||||||
uses: yeouchien/sanitize-branch-name-action@v1
|
|
||||||
with:
|
|
||||||
branch-name: ${{ matrix.branch }}
|
|
||||||
- name: Save the reports
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} reports
|
|
||||||
path: reports
|
|
||||||
- name: Save the server output
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: ${{ steps.sanitize.outputs.sanitized-branch-name }} server output
|
|
||||||
path: server-output.log
|
|
||||||
|
26
README.md
26
README.md
@ -122,19 +122,19 @@ by passing parameters to the server command.
|
|||||||
These parameters give you direct access
|
These parameters give you direct access
|
||||||
to some commonly used settings:
|
to some commonly used settings:
|
||||||
|
|
||||||
| parameter name | default value | description |
|
| parameter name | default value | description |
|
||||||
|------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `--port, -p` | `3000` | The TCP port on which the server should listen. |
|
| `--port, -p` | `3000` | The TCP port on which the server should listen. |
|
||||||
| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`. |
|
| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`. |
|
||||||
| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. |
|
| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. |
|
||||||
| `--config, -c` | `@css:config/default.json` | The configuration for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` |
|
| `--config, -c` | `@css:config/default.json` | The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` |
|
||||||
| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. |
|
| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. |
|
||||||
| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. |
|
| `--sparqlEndpoint, -s` | | URL of the SPARQL endpoint, when using a quadstore-based configuration. |
|
||||||
| `--showStackTrace, -t` | false | Enables detailed logging on error output. |
|
| `--showStackTrace, -t` | false | Enables detailed logging on error output. |
|
||||||
| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. Only relevant when using `@css:config/dynamic.json`. |
|
| `--podConfigJson` | `./pod-config.json` | Path to the file that keeps track of dynamic Pod configurations. Only relevant when using `@css:config/dynamic.json`. |
|
||||||
| `--seededPodConfigJson`| | Path to the file that keeps track of seeded Pod configurations. |
|
| `--seededPodConfigJson`| | Path to the file that keeps track of seeded Pod configurations. |
|
||||||
| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. |
|
| `--mainModulePath, -m` | | Path from where Components.js will start its lookup when initializing configurations. |
|
||||||
| `--workers, -w` | `1` | Run in multithreaded mode using workers. Special values are `-1` (scale to `num_cores-1`), `0` (scale to `num_cores`) and 1 (singlethreaded). |
|
| `--workers, -w` | `1` | Run in multithreaded mode using workers. Special values are `-1` (scale to `num_cores-1`), `0` (scale to `num_cores`) and 1 (singlethreaded). |
|
||||||
|
|
||||||
### 🔀 Multithreading
|
### 🔀 Multithreading
|
||||||
|
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"alias": "c",
|
"alias": "c",
|
||||||
"requiresArg": true,
|
"requiresArg": true,
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"describe": "The configuration for the server. The default only stores data in memory; to persist to your filesystem, use @css:config/file.json."
|
"describe": "The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use @css:config/file.json."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@comunica/context-entries": "^2.2.0",
|
||||||
"@comunica/query-sparql": "^2.2.1",
|
"@comunica/query-sparql": "^2.2.1",
|
||||||
"@rdfjs/types": "^1.1.0",
|
"@rdfjs/types": "^1.1.0",
|
||||||
"@solid/access-token-verifier": "^2.0.3",
|
"@solid/access-token-verifier": "^2.0.3",
|
||||||
@ -10922,9 +10923,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/jose": {
|
"node_modules/jose": {
|
||||||
"version": "4.8.3",
|
"version": "4.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz",
|
||||||
"integrity": "sha512-7rySkpW78d8LBp4YU70Wb7+OTgE3OwAALNVZxhoIhp4Kscp+p/fBkdpxGAMKxvCAMV4QfXBU9m6l9nX/vGwd2g==",
|
"integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/panva"
|
"url": "https://github.com/sponsors/panva"
|
||||||
}
|
}
|
||||||
@ -23976,9 +23977,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jose": {
|
"jose": {
|
||||||
"version": "4.8.3",
|
"version": "4.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.9.2.tgz",
|
||||||
"integrity": "sha512-7rySkpW78d8LBp4YU70Wb7+OTgE3OwAALNVZxhoIhp4Kscp+p/fBkdpxGAMKxvCAMV4QfXBU9m6l9nX/vGwd2g=="
|
"integrity": "sha512-EqKvu2PqJCD3Jrg3PvcYZVS7D21qMVLSYMDAFcOdGUEOpJSLNtJO7NjLANvu3SYHVl6pdP2ff7ve6EZW2nX7Nw=="
|
||||||
},
|
},
|
||||||
"js-tokens": {
|
"js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
@ -99,6 +99,7 @@
|
|||||||
"templates"
|
"templates"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@comunica/context-entries": "^2.2.0",
|
||||||
"@comunica/query-sparql": "^2.2.1",
|
"@comunica/query-sparql": "^2.2.1",
|
||||||
"@rdfjs/types": "^1.1.0",
|
"@rdfjs/types": "^1.1.0",
|
||||||
"@solid/access-token-verifier": "^2.0.3",
|
"@solid/access-token-verifier": "^2.0.3",
|
||||||
@ -193,7 +194,6 @@
|
|||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
"set-cookie-parser": "^2.5.1",
|
"set-cookie-parser": "^2.5.1",
|
||||||
"simple-git": "^3.12.0",
|
"simple-git": "^3.12.0",
|
||||||
"commit-and-tag-version": "^10.1.0",
|
|
||||||
"supertest": "^6.2.4",
|
"supertest": "^6.2.4",
|
||||||
"ts-jest": "^27.1.5",
|
"ts-jest": "^27.1.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import type { TLSSocket } from 'tls';
|
import type { TLSSocket } from 'tls';
|
||||||
import type { WebSocket } from 'ws';
|
import type { WebSocket } from 'ws';
|
||||||
|
import type { SingleThreaded } from '../init/cluster/SingleThreaded';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
import { WebSocketHandler } from '../server/WebSocketHandler';
|
import { WebSocketHandler } from '../server/WebSocketHandler';
|
||||||
@ -119,7 +120,7 @@ class WebSocketListener extends EventEmitter {
|
|||||||
* Provides live update functionality following
|
* Provides live update functionality following
|
||||||
* the Solid WebSockets API Spec solid-0.1
|
* the Solid WebSockets API Spec solid-0.1
|
||||||
*/
|
*/
|
||||||
export class UnsecureWebSocketsProtocol extends WebSocketHandler {
|
export class UnsecureWebSocketsProtocol extends WebSocketHandler implements SingleThreaded {
|
||||||
private readonly logger = getLoggerFor(this);
|
private readonly logger = getLoggerFor(this);
|
||||||
private readonly listeners = new Set<WebSocketListener>();
|
private readonly listeners = new Set<WebSocketListener>();
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import type { Operation } from '../../http/Operation';
|
|||||||
import type { ErrorHandler } from '../../http/output/error/ErrorHandler';
|
import type { ErrorHandler } from '../../http/output/error/ErrorHandler';
|
||||||
import type { ResponseWriter } from '../../http/output/ResponseWriter';
|
import type { ResponseWriter } from '../../http/output/ResponseWriter';
|
||||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||||
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||||
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
||||||
@ -77,6 +78,8 @@ const COOKIES_KEY = 'cookie-secret';
|
|||||||
* Routes will be updated based on the `baseUrl` and `oidcPath`.
|
* Routes will be updated based on the `baseUrl` and `oidcPath`.
|
||||||
*/
|
*/
|
||||||
export class IdentityProviderFactory implements ProviderFactory {
|
export class IdentityProviderFactory implements ProviderFactory {
|
||||||
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
private readonly config: Configuration;
|
private readonly config: Configuration;
|
||||||
private readonly adapterFactory: AdapterFactory;
|
private readonly adapterFactory: AdapterFactory;
|
||||||
private readonly baseUrl: string;
|
private readonly baseUrl: string;
|
||||||
@ -136,9 +139,44 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
const provider = new Provider(this.baseUrl, config);
|
const provider = new Provider(this.baseUrl, config);
|
||||||
provider.proxy = true;
|
provider.proxy = true;
|
||||||
|
|
||||||
|
this.captureErrorResponses(provider);
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the `configureErrors` function below, we configure the `renderError` function of the provider configuration.
|
||||||
|
* This function is called by the OIDC provider library to render errors,
|
||||||
|
* but only does this if the accept header is HTML.
|
||||||
|
* Otherwise, it just returns the error object iself as a JSON object.
|
||||||
|
* See https://github.com/panva/node-oidc-provider/blob/0fcc112e0a95b3b2dae4eba6da812253277567c9/lib/shared/error_handler.js#L48-L52.
|
||||||
|
*
|
||||||
|
* In this function we override the `ctx.accepts` function
|
||||||
|
* to make the above code think HTML is always requested there.
|
||||||
|
* This way we have full control over error representation as configured in `configureErrors`.
|
||||||
|
* We still check the accept headers ourselves so there still is content negotiation on the output,
|
||||||
|
* the client will not simply always receive HTML.
|
||||||
|
*
|
||||||
|
* Should this part of the OIDC library code ever change, our function will break,
|
||||||
|
* at which point behaviour will simply revert to what it was before.
|
||||||
|
*/
|
||||||
|
private captureErrorResponses(provider: Provider): void {
|
||||||
|
provider.use(async(ctx, next): Promise<void> => {
|
||||||
|
const accepts = ctx.accepts.bind(ctx);
|
||||||
|
|
||||||
|
// Using `any` typings to make sure we support all different versions of `ctx.accepts`
|
||||||
|
ctx.accepts = (...types: any[]): any => {
|
||||||
|
// Make sure we only override our specific case
|
||||||
|
if (types.length === 2 && types[0] === 'json' && types[1] === 'html') {
|
||||||
|
return 'html';
|
||||||
|
}
|
||||||
|
return accepts(...types);
|
||||||
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a configuration by copying the internal configuration
|
* Creates a configuration by copying the internal configuration
|
||||||
* and adding the adapter, default audience and jwks/cookie keys.
|
* and adding the adapter, default audience and jwks/cookie keys.
|
||||||
@ -340,14 +378,19 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
// Doesn't really matter which type it is since all relevant fields are optional
|
// Doesn't really matter which type it is since all relevant fields are optional
|
||||||
const oidcError = error as errors.OIDCProviderError;
|
const oidcError = error as errors.OIDCProviderError;
|
||||||
|
|
||||||
|
let detailedError = error.message;
|
||||||
|
if (oidcError.error_description) {
|
||||||
|
detailedError += ` - ${oidcError.error_description}`;
|
||||||
|
}
|
||||||
|
if (oidcError.error_detail) {
|
||||||
|
detailedError += ` - ${oidcError.error_detail}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`OIDC request failed: ${detailedError}`);
|
||||||
|
|
||||||
// OIDC library hides extra details in these fields
|
// OIDC library hides extra details in these fields
|
||||||
if (this.showStackTrace) {
|
if (this.showStackTrace) {
|
||||||
if (oidcError.error_description) {
|
error.message = detailedError;
|
||||||
error.message += ` - ${oidcError.error_description}`;
|
|
||||||
}
|
|
||||||
if (oidcError.error_detail) {
|
|
||||||
oidcError.message += ` - ${oidcError.error_detail}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also change the error message in the stack trace
|
// Also change the error message in the stack trace
|
||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
|
@ -21,7 +21,7 @@ const DEFAULT_CLI_RESOLVER = 'urn:solid-server-app-setup:default:CliResolver';
|
|||||||
const DEFAULT_APP = 'urn:solid-server:default:App';
|
const DEFAULT_APP = 'urn:solid-server:default:App';
|
||||||
|
|
||||||
const CORE_CLI_PARAMETERS = {
|
const CORE_CLI_PARAMETERS = {
|
||||||
config: { type: 'string', alias: 'c', default: DEFAULT_CONFIG, requiresArg: true },
|
config: { type: 'array', alias: 'c', default: [ DEFAULT_CONFIG ], requiresArg: true },
|
||||||
loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS },
|
loggingLevel: { type: 'string', alias: 'l', default: 'info', requiresArg: true, choices: LOG_LEVELS },
|
||||||
mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
|
mainModulePath: { type: 'string', alias: 'm', requiresArg: true },
|
||||||
} as const;
|
} as const;
|
||||||
@ -48,13 +48,13 @@ export class AppRunner {
|
|||||||
* The values in `variableBindings` take priority over those in `shorthand`.
|
* The values in `variableBindings` take priority over those in `shorthand`.
|
||||||
*
|
*
|
||||||
* @param loaderProperties - Components.js loader properties.
|
* @param loaderProperties - Components.js loader properties.
|
||||||
* @param configFile - Path to the server config file.
|
* @param configFile - Path to the server config file(s).
|
||||||
* @param variableBindings - Bindings of Components.js variables.
|
* @param variableBindings - Bindings of Components.js variables.
|
||||||
* @param shorthand - Shorthand values that need to be resolved.
|
* @param shorthand - Shorthand values that need to be resolved.
|
||||||
*/
|
*/
|
||||||
public async run(
|
public async run(
|
||||||
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
||||||
configFile: string,
|
configFile: string | string[],
|
||||||
variableBindings?: VariableBindings,
|
variableBindings?: VariableBindings,
|
||||||
shorthand?: Shorthand,
|
shorthand?: Shorthand,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -75,13 +75,13 @@ export class AppRunner {
|
|||||||
* The values in `variableBindings` take priority over those in `shorthand`.
|
* The values in `variableBindings` take priority over those in `shorthand`.
|
||||||
*
|
*
|
||||||
* @param loaderProperties - Components.js loader properties.
|
* @param loaderProperties - Components.js loader properties.
|
||||||
* @param configFile - Path to the server config file.
|
* @param configFile - Path to the server config file(s).
|
||||||
* @param variableBindings - Bindings of Components.js variables.
|
* @param variableBindings - Bindings of Components.js variables.
|
||||||
* @param shorthand - Shorthand values that need to be resolved.
|
* @param shorthand - Shorthand values that need to be resolved.
|
||||||
*/
|
*/
|
||||||
public async create(
|
public async create(
|
||||||
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
loaderProperties: IComponentsManagerBuilderOptions<App>,
|
||||||
configFile: string,
|
configFile: string | string[],
|
||||||
variableBindings?: VariableBindings,
|
variableBindings?: VariableBindings,
|
||||||
shorthand?: Shorthand,
|
shorthand?: Shorthand,
|
||||||
): Promise<App> {
|
): Promise<App> {
|
||||||
@ -152,16 +152,16 @@ export class AppRunner {
|
|||||||
typeChecking: false,
|
typeChecking: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = resolveAssetPath(params.config);
|
const configs = params.config.map(resolveAssetPath);
|
||||||
|
|
||||||
// Create the Components.js manager used to build components from the provided config
|
// Create the Components.js manager used to build components from the provided config
|
||||||
let componentsManager: ComponentsManager<any>;
|
let componentsManager: ComponentsManager<any>;
|
||||||
try {
|
try {
|
||||||
componentsManager = await this.createComponentsManager(loaderProperties, config);
|
componentsManager = await this.createComponentsManager(loaderProperties, configs);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Print help of the expected core CLI parameters
|
// Print help of the expected core CLI parameters
|
||||||
const help = await yargv.getHelp();
|
const help = await yargv.getHelp();
|
||||||
this.resolveError(`${help}\n\nCould not build the config files from ${config}`, error);
|
this.resolveError(`${help}\n\nCould not build the config files from ${configs}`, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the CLI components and use them to generate values for the Components.js variables
|
// Build the CLI components and use them to generate values for the Components.js variables
|
||||||
@ -176,10 +176,12 @@ export class AppRunner {
|
|||||||
*/
|
*/
|
||||||
public async createComponentsManager<T>(
|
public async createComponentsManager<T>(
|
||||||
loaderProperties: IComponentsManagerBuilderOptions<T>,
|
loaderProperties: IComponentsManagerBuilderOptions<T>,
|
||||||
configFile: string,
|
configFile: string | string[],
|
||||||
): Promise<ComponentsManager<T>> {
|
): Promise<ComponentsManager<T>> {
|
||||||
const componentsManager = await ComponentsManager.build(loaderProperties);
|
const componentsManager = await ComponentsManager.build(loaderProperties);
|
||||||
await componentsManager.configRegistry.register(configFile);
|
for (const config of Array.isArray(configFile) ? configFile : [ configFile ]) {
|
||||||
|
await componentsManager.configRegistry.register(config);
|
||||||
|
}
|
||||||
return componentsManager;
|
return componentsManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,25 +1,61 @@
|
|||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
|
import { KeysRdfParseJsonLd } from '@comunica/context-entries';
|
||||||
import type { NamedNode } from '@rdfjs/types';
|
import type { NamedNode } from '@rdfjs/types';
|
||||||
|
import fetch from 'cross-fetch';
|
||||||
|
import { readJsonSync } from 'fs-extra';
|
||||||
|
import { FetchDocumentLoader } from 'jsonld-context-parser';
|
||||||
|
import type { IJsonLdContext } from 'jsonld-context-parser';
|
||||||
import rdfParser from 'rdf-parse';
|
import rdfParser from 'rdf-parse';
|
||||||
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 { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
|
import { resolveAssetPath } from '../../util/PathUtil';
|
||||||
import { pipeSafely } from '../../util/StreamUtil';
|
import { pipeSafely } from '../../util/StreamUtil';
|
||||||
import { PREFERRED_PREFIX_TERM, SOLID_META } from '../../util/Vocabularies';
|
import { PREFERRED_PREFIX_TERM, SOLID_META } from '../../util/Vocabularies';
|
||||||
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First checks if a context is stored locally before letting the super class do a fetch.
|
||||||
|
*/
|
||||||
|
class ContextDocumentLoader extends FetchDocumentLoader {
|
||||||
|
private readonly contexts: Record<string, IJsonLdContext>;
|
||||||
|
|
||||||
|
public constructor(contexts: Record<string, string>) {
|
||||||
|
super(fetch);
|
||||||
|
this.contexts = {};
|
||||||
|
for (const [ key, path ] of Object.entries(contexts)) {
|
||||||
|
this.contexts[key] = readJsonSync(resolveAssetPath(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async load(url: string): Promise<IJsonLdContext> {
|
||||||
|
if (url in this.contexts) {
|
||||||
|
return this.contexts[url];
|
||||||
|
}
|
||||||
|
return super.load(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts most major RDF serializations to `internal/quads`.
|
* Converts most major RDF serializations to `internal/quads`.
|
||||||
|
*
|
||||||
|
* Custom contexts can be defined to be used when parsing JSON-LD.
|
||||||
|
* The keys of the object should be the URL of the context,
|
||||||
|
* and the values the file path of the contexts to use when the JSON-LD parser would fetch the given context.
|
||||||
|
* We use filepaths because embedding them directly into the configurations breaks Components.js.
|
||||||
*/
|
*/
|
||||||
export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
|
export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
|
||||||
public constructor() {
|
private readonly documentLoader: ContextDocumentLoader;
|
||||||
|
|
||||||
|
public constructor(contexts: Record<string, string> = {}) {
|
||||||
const inputTypes = rdfParser.getContentTypes()
|
const inputTypes = rdfParser.getContentTypes()
|
||||||
// ContentType application/json MAY NOT be converted to Quad.
|
// ContentType application/json MAY NOT be converted to Quad.
|
||||||
.then((types): string[] => types.filter((type): boolean => type !== 'application/json'));
|
.then((types): string[] => types.filter((type): boolean => type !== 'application/json'));
|
||||||
super(inputTypes, INTERNAL_QUADS);
|
super(inputTypes, INTERNAL_QUADS);
|
||||||
|
this.documentLoader = new ContextDocumentLoader(contexts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ representation, identifier }: RepresentationConverterArgs): Promise<Representation> {
|
public async handle({ representation, identifier }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
@ -27,7 +63,12 @@ export class RdfToQuadConverter extends BaseTypedRepresentationConverter {
|
|||||||
const rawQuads = rdfParser.parse(representation.data, {
|
const rawQuads = rdfParser.parse(representation.data, {
|
||||||
contentType: representation.metadata.contentType!,
|
contentType: representation.metadata.contentType!,
|
||||||
baseIRI: identifier.path,
|
baseIRI: identifier.path,
|
||||||
})
|
// All extra keys get passed in the Comunica ActionContext
|
||||||
|
// and this is the key that is used to define the document loader.
|
||||||
|
// See https://github.com/rubensworks/rdf-parse.js/blob/master/lib/RdfParser.ts
|
||||||
|
// and https://github.com/comunica/comunica/blob/master/packages/actor-rdf-parse-jsonld/lib/ActorRdfParseJsonLd.ts
|
||||||
|
[KeysRdfParseJsonLd.documentLoader.name]: this.documentLoader,
|
||||||
|
} as any)
|
||||||
// This works only for those cases where the data stream has been completely read before accessing the metadata.
|
// This works only for those cases where the data stream has been completely read before accessing the metadata.
|
||||||
// Eg. the PATCH operation, which is the main case why we store the prefixes in metadata here if there are any.
|
// Eg. the PATCH operation, which is the main case why we store the prefixes in metadata here if there are any.
|
||||||
// See also https://github.com/CommunitySolidServer/CommunitySolidServer/issues/126
|
// See also https://github.com/CommunitySolidServer/CommunitySolidServer/issues/126
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
<h1>Authorize</h1>
|
<h1>An application is requesting access</h1>
|
||||||
<p id="webId">Your WebID: </p>
|
<p>
|
||||||
<p>The following client wants to do authorized requests in your name:</p>
|
Your WebID is <strong id="webId"></strong>
|
||||||
<ul id="clientInfo">
|
</p>
|
||||||
</ul>
|
<p>
|
||||||
|
Do you trust this application
|
||||||
|
to read and write data on your behalf?
|
||||||
|
</p>
|
||||||
|
<dl id="client"></dl>
|
||||||
<form method="post" id="mainForm">
|
<form method="post" id="mainForm">
|
||||||
<p class="error" id="error"></p>
|
<p class="error" id="error"></p>
|
||||||
|
|
||||||
@ -15,52 +19,54 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<button autofocus type="submit" name="submit">Consent</button>
|
<button id="authorize" type="submit" autofocus>Authorize</button>
|
||||||
<button onclick="logOut(event)">Log in with a different account</button>
|
<button id="cancel" type="button">Cancel</button>
|
||||||
|
<button id="switch" type="button" class="alternate">Use a different WebID</button>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
async function logOut(e) {
|
// Wire up elements
|
||||||
e.preventDefault();
|
const elements = getElements();
|
||||||
|
elements.cancel.addEventListener('click', logOut);
|
||||||
|
elements.switch.addEventListener('click', logOut);
|
||||||
|
addPostListener('mainForm', 'error', '', () => {
|
||||||
|
throw new Error('Expected a location field in the response.');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retrieve and display client information
|
||||||
|
(async() => {
|
||||||
|
const res = await fetch('', { headers: { accept: 'application/json' } });
|
||||||
|
const { webId, client } = await res.json();
|
||||||
|
showWebId(webId);
|
||||||
|
showClientInfo('Name', client.client_name);
|
||||||
|
showClientInfo('ID', client.client_id);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Updates the user's WebID in the UI
|
||||||
|
function showWebId(webId) {
|
||||||
|
elements.webId.innerText = webId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds client info to the UI
|
||||||
|
function showClientInfo(label, value) {
|
||||||
|
if (value) {
|
||||||
|
const dt = document.createElement('dt');
|
||||||
|
const dd = document.createElement('dd')
|
||||||
|
dt.innerText = label;
|
||||||
|
dd.innerText = value;
|
||||||
|
elements.client.append(dt, dd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs the user out
|
||||||
|
async function logOut(event) {
|
||||||
const res = await fetch('', {
|
const res = await fetch('', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ logOut: true }),
|
body: JSON.stringify({ logOut: true }),
|
||||||
});
|
});
|
||||||
const json = await res.json();
|
const body = await res.json();
|
||||||
location.href = json.location;
|
location.href = body.location;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
|
||||||
function addWebId(webId) {
|
|
||||||
const p = document.getElementById('webId');
|
|
||||||
const strong = document.createElement('strong')
|
|
||||||
strong.appendChild(document.createTextNode(webId));
|
|
||||||
p.appendChild(strong);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientInfo = document.getElementById('clientInfo');
|
|
||||||
function addClientInfo(text, value) {
|
|
||||||
if (value) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
const strong = document.createElement('strong')
|
|
||||||
strong.appendChild(document.createTextNode(value));
|
|
||||||
li.appendChild(document.createTextNode(`${text}: `));
|
|
||||||
li.appendChild(strong);
|
|
||||||
clientInfo.appendChild(li);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the client information
|
|
||||||
(async() => {
|
|
||||||
const res = await fetch('', { headers: { accept: 'application/json' } });
|
|
||||||
const { webId, client } = await res.json();
|
|
||||||
addWebId(webId);
|
|
||||||
addClientInfo('Name', client.client_name);
|
|
||||||
addClientInfo('ID', client.client_id);
|
|
||||||
})()
|
|
||||||
|
|
||||||
addPostListener('mainForm', 'error', '', () => { throw new Error('Expected a location field in the response.') });
|
|
||||||
</script>
|
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Returns an object that maps IDs to the corresponding element.
|
||||||
|
*
|
||||||
|
* @param ...ids - IDs of the element (empty to retrieve all elements)
|
||||||
|
*/
|
||||||
|
function getElements(...ids) {
|
||||||
|
ids = ids.length ? ids : [...document.querySelectorAll("[id]")].map(e => e.id);
|
||||||
|
return Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acquires all data from the given form and POSTs it as JSON to the target URL.
|
* Acquires all data from the given form and POSTs it as JSON to the target URL.
|
||||||
* In case of failure this function will throw an error.
|
* In case of failure this function will throw an error.
|
||||||
|
@ -138,6 +138,20 @@ ul ul, ol ul {
|
|||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
padding: 0 0 0 2em;
|
||||||
|
margin: .75em 0 0;
|
||||||
|
}
|
||||||
|
dl dt {
|
||||||
|
font-weight: bold;
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
|
dl dt:after {
|
||||||
|
content: ": ";
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
}
|
}
|
||||||
@ -207,12 +221,30 @@ button {
|
|||||||
border: 0px;
|
border: 0px;
|
||||||
background-color: var(--solid-purple);
|
background-color: var(--solid-purple);
|
||||||
color: white;
|
color: white;
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--solid-blue);
|
background-color: var(--solid-blue);
|
||||||
}
|
}
|
||||||
|
button[type=submit] {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.alternate {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: .5em 0;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
color: var(--solid-purple);
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
button.alternate:hover {
|
||||||
|
color: var(--solid-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, button:focus {
|
||||||
|
outline: var(--solid-blue) solid 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
form p.actions {
|
form p.actions {
|
||||||
margin: .5em 0 1em 11em;
|
margin: .5em 0 1em 11em;
|
||||||
@ -222,6 +254,9 @@ form p.error {
|
|||||||
color: #ad0f0f;
|
color: #ad0f0f;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
form p.error:empty {
|
||||||
|
display :none;
|
||||||
|
}
|
||||||
|
|
||||||
form ul.actions {
|
form ul.actions {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
7
test/assets/contexts/test.jsonld
Normal file
7
test/assets/contexts/test.jsonld
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"@context": {
|
||||||
|
"@version": 1.1,
|
||||||
|
"test": "http://example.com/context#",
|
||||||
|
"testPredicate": { "@id": "test:predicate" }
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ describe('An instantiated CliResolver', (): void => {
|
|||||||
'-s', 's',
|
'-s', 's',
|
||||||
'-w', '2',
|
'-w', '2',
|
||||||
]);
|
]);
|
||||||
expect(shorthand.config).toBe('c');
|
expect(shorthand.config).toEqual([ 'c' ]);
|
||||||
expect(shorthand.mainModulePath).toBe('m');
|
expect(shorthand.mainModulePath).toBe('m');
|
||||||
expect(shorthand.loggingLevel).toBe('l');
|
expect(shorthand.loggingLevel).toBe('l');
|
||||||
expect(shorthand.baseUrl).toBe('b');
|
expect(shorthand.baseUrl).toBe('b');
|
||||||
|
@ -621,9 +621,13 @@ describe('A Solid server with IDP', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct error output.', async(): Promise<void> => {
|
it('should return correct error output.', async(): Promise<void> => {
|
||||||
const res = await fetch(`${baseUrl}.oidc/auth`);
|
const res = await fetch(`${baseUrl}.oidc/foo`, { headers: { accept: 'application/json' }});
|
||||||
expect(res.status).toBe(400);
|
expect(res.status).toBe(404);
|
||||||
await expect(res.text()).resolves.toContain('InvalidRequest: invalid_request');
|
const json = await res.json();
|
||||||
|
expect(json.name).toBe(`InvalidRequest`);
|
||||||
|
expect(json.message).toBe(`invalid_request - unrecognized route or not allowed method (GET on /.oidc/foo)`);
|
||||||
|
expect(json.statusCode).toBe(404);
|
||||||
|
expect(json.stack).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import type * as Koa from 'koa';
|
||||||
import type { errors, Configuration, KoaContextWithOIDC } from 'oidc-provider';
|
import type { errors, Configuration, KoaContextWithOIDC } from 'oidc-provider';
|
||||||
import type { ErrorHandler } from '../../../../src/http/output/error/ErrorHandler';
|
import type { ErrorHandler } from '../../../../src/http/output/error/ErrorHandler';
|
||||||
import type { ResponseWriter } from '../../../../src/http/output/ResponseWriter';
|
import type { ResponseWriter } from '../../../../src/http/output/ResponseWriter';
|
||||||
@ -14,7 +15,8 @@ import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
|||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
jest.mock('oidc-provider', (): any => ({
|
jest.mock('oidc-provider', (): any => ({
|
||||||
Provider: jest.fn().mockImplementation((issuer: string, config: Configuration): any => ({ issuer, config })),
|
Provider: jest.fn().mockImplementation((issuer: string, config: Configuration): any =>
|
||||||
|
({ issuer, config, use: jest.fn() })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
@ -58,6 +60,7 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
request: {
|
request: {
|
||||||
href: 'http://example.com/idp/',
|
href: 'http://example.com/idp/',
|
||||||
},
|
},
|
||||||
|
accepts: jest.fn().mockReturnValue('type'),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
interactionHandler = {
|
interactionHandler = {
|
||||||
@ -146,10 +149,11 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Test the renderError function
|
// Test the renderError function
|
||||||
await expect((config.renderError as any)(ctx, {}, 'error!')).resolves.toBeUndefined();
|
const error = new Error('error!');
|
||||||
|
await expect((config.renderError as any)(ctx, {}, error)).resolves.toBeUndefined();
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe)
|
expect(errorHandler.handleSafe)
|
||||||
.toHaveBeenLastCalledWith({ error: 'error!', request: ctx.req });
|
.toHaveBeenLastCalledWith({ error, request: ctx.req });
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
||||||
});
|
});
|
||||||
@ -230,4 +234,35 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
expect(error.message).toBe('bad data - more info - more details');
|
expect(error.message).toBe('bad data - more info - more details');
|
||||||
expect(error.stack).toContain('Error: bad data - more info - more details');
|
expect(error.stack).toContain('Error: bad data - more info - more details');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds middleware to make the OIDC provider think the request wants HTML.', async(): Promise<void> => {
|
||||||
|
const provider = await factory.getProvider() as any;
|
||||||
|
const use = provider.use as jest.Mock<ReturnType<Koa['use']>, Parameters<Koa['use']>>;
|
||||||
|
expect(use).toHaveBeenCalledTimes(1);
|
||||||
|
const middleware = use.mock.calls[0][0];
|
||||||
|
|
||||||
|
const oldAccept = ctx.accepts;
|
||||||
|
const next = jest.fn();
|
||||||
|
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||||
|
expect(next).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ctx.accepts('json', 'html')).toBe('html');
|
||||||
|
expect(oldAccept).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not modify the context accepts function in other cases.', async(): Promise<void> => {
|
||||||
|
const provider = await factory.getProvider() as any;
|
||||||
|
const use = provider.use as jest.Mock<ReturnType<Koa['use']>, Parameters<Koa['use']>>;
|
||||||
|
expect(use).toHaveBeenCalledTimes(1);
|
||||||
|
const middleware = use.mock.calls[0][0];
|
||||||
|
|
||||||
|
const oldAccept = ctx.accepts;
|
||||||
|
const next = jest.fn();
|
||||||
|
await expect(middleware(ctx, next)).resolves.toBeUndefined();
|
||||||
|
expect(next).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
expect(ctx.accepts('something')).toBe('type');
|
||||||
|
expect(oldAccept).toHaveBeenCalledTimes(1);
|
||||||
|
expect(oldAccept).toHaveBeenLastCalledWith('something');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -281,6 +281,37 @@ describe('AppRunner', (): void => {
|
|||||||
expect(app.start).toHaveBeenCalledTimes(0);
|
expect(app.start).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can apply multiple configurations.', async(): Promise<void> => {
|
||||||
|
const params = [
|
||||||
|
'node', 'script',
|
||||||
|
'-c', 'config1.json', 'config2.json',
|
||||||
|
];
|
||||||
|
await expect(new AppRunner().createCli(params)).resolves.toBe(app);
|
||||||
|
|
||||||
|
expect(ComponentsManager.build).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ComponentsManager.build).toHaveBeenCalledWith({
|
||||||
|
dumpErrorState: true,
|
||||||
|
logLevel: 'info',
|
||||||
|
mainModulePath: joinFilePath(__dirname, '../../../'),
|
||||||
|
typeChecking: false,
|
||||||
|
});
|
||||||
|
expect(manager.configRegistry.register).toHaveBeenCalledTimes(2);
|
||||||
|
expect(manager.configRegistry.register).toHaveBeenNthCalledWith(1, '/var/cwd/config1.json');
|
||||||
|
expect(manager.configRegistry.register).toHaveBeenNthCalledWith(2, '/var/cwd/config2.json');
|
||||||
|
expect(manager.instantiate).toHaveBeenCalledTimes(2);
|
||||||
|
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
|
||||||
|
expect(cliExtractor.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(cliExtractor.handleSafe).toHaveBeenCalledWith(params);
|
||||||
|
expect(shorthandResolver.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(shorthandResolver.handleSafe).toHaveBeenCalledWith(defaultParameters);
|
||||||
|
expect(manager.instantiate).toHaveBeenNthCalledWith(1, 'urn:solid-server-app-setup:default:CliResolver', {});
|
||||||
|
expect(manager.instantiate).toHaveBeenNthCalledWith(2,
|
||||||
|
'urn:solid-server:default:App',
|
||||||
|
{ variables: defaultVariables });
|
||||||
|
expect(app.clusterManager.isSingleThreaded()).toBeFalsy();
|
||||||
|
expect(app.start).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('uses the default process.argv in case none are provided.', async(): Promise<void> => {
|
it('uses the default process.argv in case none are provided.', async(): Promise<void> => {
|
||||||
const { argv } = process;
|
const { argv } = process;
|
||||||
const argvParameters = [
|
const argvParameters = [
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'jest-rdf';
|
import 'jest-rdf';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import arrayifyStream from 'arrayify-stream';
|
import arrayifyStream from 'arrayify-stream';
|
||||||
|
import fetch, { Headers } from 'cross-fetch';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import rdfParser from 'rdf-parse';
|
import rdfParser from 'rdf-parse';
|
||||||
import { PREFERRED_PREFIX_TERM, SOLID_META } from '../../../../src';
|
import { PREFERRED_PREFIX_TERM, SOLID_META } from '../../../../src';
|
||||||
@ -14,10 +15,25 @@ import { INTERNAL_QUADS } from '../../../../src/util/ContentTypes';
|
|||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
const { namedNode, triple, literal, quad } = DataFactory;
|
const { namedNode, triple, literal, quad } = DataFactory;
|
||||||
|
|
||||||
describe('A RdfToQuadConverter', (): void => {
|
// All of this is necessary to not break the cross-fetch imports that happen in `rdf-parse`
|
||||||
const converter = new RdfToQuadConverter();
|
jest.mock('cross-fetch', (): any => {
|
||||||
const identifier: ResourceIdentifier = { path: 'path' };
|
const mock = jest.fn();
|
||||||
|
// Require the original module to not be mocked...
|
||||||
|
const originalFetch = jest.requireActual('cross-fetch');
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
__esModule: true,
|
||||||
|
...originalFetch,
|
||||||
|
fetch: mock,
|
||||||
|
default: mock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Not mocking `fs` since this breaks the `rdf-parser` library
|
||||||
|
describe('A RdfToQuadConverter', (): void => {
|
||||||
|
const fetchMock: jest.Mock = fetch as any;
|
||||||
|
const converter = new RdfToQuadConverter();
|
||||||
|
const identifier: ResourceIdentifier = { path: 'http://example.com/resource' };
|
||||||
it('supports serializing as quads.', async(): Promise<void> => {
|
it('supports serializing as quads.', async(): Promise<void> => {
|
||||||
const types = rdfParser.getContentTypes()
|
const types = rdfParser.getContentTypes()
|
||||||
.then((inputTypes): string[] => inputTypes.filter((type): boolean => type !== 'application/json'));
|
.then((inputTypes): string[] => inputTypes.filter((type): boolean => type !== 'application/json'));
|
||||||
@ -123,4 +139,43 @@ describe('A RdfToQuadConverter', (): void => {
|
|||||||
expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
|
expect(result.metadata.contentType).toEqual(INTERNAL_QUADS);
|
||||||
await expect(arrayifyStream(result.data)).rejects.toThrow(BadRequestHttpError);
|
await expect(arrayifyStream(result.data)).rejects.toThrow(BadRequestHttpError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can use locally stored contexts.', async(): Promise<void> => {
|
||||||
|
const fetchedContext = {
|
||||||
|
'@context': {
|
||||||
|
'@version': 1.1,
|
||||||
|
test: 'http://example.com/context2#',
|
||||||
|
testPredicate2: { '@id': 'test:predicate2' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// This depends on the fields needed by the `jsonld-context-parser` so could break if library changes
|
||||||
|
fetchMock.mockResolvedValueOnce({
|
||||||
|
json: (): any => fetchedContext,
|
||||||
|
status: 200,
|
||||||
|
ok: true,
|
||||||
|
headers: new Headers({ 'content-type': 'application/ld+json' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextConverter = new RdfToQuadConverter(
|
||||||
|
{ 'http://example.com/context.jsonld': '@css:test/assets/contexts/test.jsonld' },
|
||||||
|
);
|
||||||
|
const jsonld = {
|
||||||
|
'@context': [ 'http://example.com/context.jsonld', 'http://example.com/context2.jsonld' ],
|
||||||
|
'@id': 'http://example.com/resource',
|
||||||
|
testPredicate: 123,
|
||||||
|
testPredicate2: 456,
|
||||||
|
};
|
||||||
|
const representation = new BasicRepresentation(JSON.stringify(jsonld), 'application/ld+json');
|
||||||
|
const preferences: RepresentationPreferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||||
|
const result = await contextConverter.handle({ identifier, representation, preferences });
|
||||||
|
await expect(arrayifyStream(result.data)).resolves.toEqualRdfQuadArray([ triple(
|
||||||
|
namedNode('http://example.com/resource'),
|
||||||
|
namedNode('http://example.com/context#predicate'),
|
||||||
|
literal(123),
|
||||||
|
), triple(
|
||||||
|
namedNode('http://example.com/resource'),
|
||||||
|
namedNode('http://example.com/context2#predicate2'),
|
||||||
|
literal(456),
|
||||||
|
) ]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user