Merge branch 'main' into versions/3.0.0

# Conflicts:
#	package-lock.json
#	test/integration/Identity.test.ts
#	test/integration/RepresentationConverter.test.ts
This commit is contained in:
Joachim Van Herwegen 2022-01-25 11:44:24 +01:00
commit 90a6460c8d
39 changed files with 1832 additions and 1808 deletions

View File

@ -83,6 +83,10 @@ module.exports = {
'unicorn/no-fn-reference-in-iterator': 'off',
'unicorn/no-object-as-default-parameter': 'off',
'unicorn/numeric-separators-style': 'off',
// At function only supported in Node v16.6.0
'unicorn/prefer-at': 'off',
// Does not make sense for more complex cases
'unicorn/prefer-object-from-entries': 'off',
// Can get ugly with large single statements
'unicorn/prefer-ternary': 'off',
'yield-star-spacing': [ 'error', 'after' ],

View File

@ -1,15 +1,22 @@
#### 📁 Related issues
<!--
Things to check before submitting a pull request:
* Label this PR with the correct semver label (if you have permission to do so).
Reference any relevant issues here. Closing keywords only have an effect when targeting the main branch. If there are no related issues, you must first create an issue through https://github.com/solid/community-server/issues/new/choose
-->
#### ✍️ Description
<!-- Describe the relevant changes in this PR. Also add notes that might be relevant for code reviewers. -->
### ✅ PR check list
Before this pull request can be merged, a core maintainer will check whether
* [ ] this PR is labeled with the correct semver label
- semver.patch: Backwards compatible bug fixes.
- semver.minor: Backwards compatible feature additions.
- semver.major: Breaking changes. This includes changing interfaces or configuration behaviour.
* Target the correct branch. Patch updates can target main, other changes should target the latest versions/* branch.
* Update the RELEASE_NOTES.md document in case of relevant feature or config changes.
-->
* [ ] the correct branch is targeted. Patch updates can target main, other changes should target the latest versions/* branch.
* [ ] the RELEASE_NOTES.md document in case of relevant feature or config changes.
#### Related issues
<!-- Reference any relevant issues here. Closing keywords only have an effect when targeting the main branch. -->
#### Description
<!-- Describe the relevant changes in this PR. Also add notes that might be relevant for code reviewers. -->
<!-- Try to check these to the best of your abilities before opening the PR -->

View File

@ -139,6 +139,46 @@ jobs:
github-token: ${{ secrets.github_token }}
parallel-finished: true
docker:
needs:
- lint
- test-unit
- test-integration
- test-integration-windows
- validate-components
# Only run on tags starting with v prefix for now -- extra push need for triggering CI again
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: |
solidproject/community-server
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
github-token: ${{ secrets.github_token }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
docs:
runs-on: ubuntu-latest
steps:

View File

@ -44,6 +44,6 @@ jobs:
-v "$(pwd)"/reports/css:/reports
--env-file=./test/deploy/conformance.env
--network="host"
solidconformancetestbeta/conformance-test-harness
solidproject/conformance-test-harness
--output=/reports
--target=https://github.com/solid/conformance-test-harness/css

View File

@ -69,21 +69,20 @@ npm start -- # add parameters if needed
```
### 📦 Running via Docker
Docker allows you to run the server without having Node.js installed:
Docker allows you to run the server without having Node.js installed. Images are built on each tagged version and hosted on [Docker Hub](https://hub.docker.com/r/solidproject/community-server).
```shell
# Clone the repo to get access to the configs
git clone https://github.com/solid/community-server.git
cd community-server
# Build the Docker image
docker build --rm -f Dockerfile -t css:latest .
# Run the image, serving your `~/Solid` directory on `http://localhost:3000`
docker run --rm -v ~/Solid:/data -p 3000:3000 -it css:latest
docker run --rm -v ~/Solid:/data -p 3000:3000 -it solidproject/community-server:latest
# Or use one of the built-in configurations
docker run --rm -p 3000:3000 -it css:latest -c config/default.json
docker run --rm -p 3000:3000 -it solidproject/community-server -c config/default.json
# Or use your own configuration mapped to the right directory
docker run --rm -v ~/solid-config:/config -p 3000:3000 -it css:latest -c /config/my-config.json
docker run --rm -v ~/solid-config:/config -p 3000:3000 -it solidproject/community-server -c /config/my-config.json
```
## 🔧 Configuring the server
The Community Solid Server is designed to be flexible
such that people can easily run different configurations.
@ -131,7 +130,7 @@ the [📐 architectural diagram](https://rubenverborgh.github.io/solid-server-a
can help you find your way.
If you want to help out with server development,
have a look at the [📓 developer notes](guides/developer-notes.md) and
have a look at the [📓 developer notes](https://github.com/solid/community-server/blob/main/guides/developer-notes.md) and
[🛠 good first issues](https://github.com/solid/community-server/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).

View File

@ -16,7 +16,7 @@
},
"controls": {
"BasicInteractionRoute:_controls_key": "forgotPassword",
"BasicInteractionRoute:_controls_value": "/forgotpassword"
"BasicInteractionRoute:_controls_value": "/forgotpassword/"
},
"handler": {
"@type": "ForgotPasswordHandler",

View File

@ -13,7 +13,7 @@
},
"controls": {
"BasicInteractionRoute:_controls_key": "login",
"BasicInteractionRoute:_controls_value": "/login"
"BasicInteractionRoute:_controls_value": "/login/"
},
"handler": {
"@type": "LoginHandler",

View File

@ -16,7 +16,7 @@
},
"controls": {
"BasicInteractionRoute:_controls_key": "register",
"BasicInteractionRoute:_controls_value": "/register"
"BasicInteractionRoute:_controls_value": "/register/"
},
"handler": {
"@type": "RegistrationHandler",

3308
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -77,7 +77,7 @@
"dependencies": {
"@comunica/actor-init-sparql": "^1.21.3",
"@rdfjs/data-model": "^1.2.0",
"@solid/access-token-verifier": "^1.0.1",
"@solid/access-token-verifier": "^1.1.2",
"@types/arrayify-stream": "^1.0.0",
"@types/async-lock": "^1.1.2",
"@types/bcrypt": "^5.0.0",
@ -87,7 +87,7 @@
"@types/marked": "^3.0.0",
"@types/mime-types": "^2.1.0",
"@types/n3": "^1.10.0",
"@types/node": "^15.12.5",
"@types/node": "^14.18.0",
"@types/nodemailer": "^6.4.2",
"@types/pump": "^1.1.1",
"@types/punycode": "^2.1.0",
@ -109,11 +109,11 @@
"escape-string-regexp": "^4.0.0",
"fetch-sparql-endpoint": "^2.0.1",
"handlebars": "^4.7.7",
"jose": "^3.11.6",
"jose": "^4.3.7",
"lodash.orderby": "^4.6.0",
"marked": "^3.0.0",
"mime-types": "^2.1.32",
"n3": "^1.10.0",
"n3": "^1.12.2",
"nodemailer": "^6.6.2",
"oidc-provider": "^6.31.1",
"pump": "^3.0.0",
@ -142,17 +142,17 @@
"@types/jest": "^27.0.0",
"@types/set-cookie-parser": "^2.4.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"cheerio": "^1.0.0-rc.10",
"componentsjs-generator": "^2.6.0",
"eslint": "^7.29.0",
"eslint-config-es": "^3.20.3",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^24.3.6",
"eslint": "^8.4.1",
"eslint-config-es": "4.1.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jest": "^25.3.0",
"eslint-plugin-tsdoc": "^0.2.14",
"eslint-plugin-unused-imports": "^1.1.1",
"eslint-plugin-unused-imports": "^2.0.0",
"fs-extra": "^10.0.0",
"husky": "^4.3.8",
"jest": "^27.0.6",

View File

@ -19,7 +19,7 @@ export class BearerWebIdExtractor extends CredentialsExtractor {
public async canHandle({ headers }: HttpRequest): Promise<void> {
const { authorization } = headers;
if (!authorization || !authorization.startsWith('Bearer ')) {
if (!authorization || !/^Bearer /ui.test(authorization)) {
throw new NotImplementedHttpError('No Bearer Authorization header specified.');
}
}

View File

@ -27,7 +27,7 @@ export class DPoPWebIdExtractor extends CredentialsExtractor {
public async canHandle({ headers }: HttpRequest): Promise<void> {
const { authorization } = headers;
if (!authorization || !authorization.startsWith('DPoP ')) {
if (!authorization || !/^DPoP /ui.test(authorization)) {
throw new NotImplementedHttpError('No DPoP-bound Authorization header specified.');
}
}

View File

@ -13,13 +13,13 @@ export class UnsecureWebIdExtractor extends CredentialsExtractor {
public async canHandle({ headers }: HttpRequest): Promise<void> {
const { authorization } = headers;
if (!authorization || !authorization.startsWith('WebID ')) {
if (!authorization || !/^WebID /ui.test(authorization)) {
throw new NotImplementedHttpError('No WebID Authorization header specified.');
}
}
public async handle({ headers }: HttpRequest): Promise<CredentialSet> {
const webId = /^WebID\s+(.*)/u.exec(headers.authorization!)![1];
const webId = /^WebID\s+(.*)/ui.exec(headers.authorization!)![1];
this.logger.info(`Agent unsecurely claims to be ${webId}`);
return { [CredentialGroup.agent]: { webId }};
}

View File

@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention, import/no-unresolved, tsdoc/syntax */
/* eslint-disable @typescript-eslint/naming-convention, tsdoc/syntax */
// import/no-unresolved can't handle jose imports
// tsdoc/syntax can't handle {json} parameter
import { randomBytes } from 'crypto';
import type { JWK } from 'jose/jwk/from_key_like';
import { fromKeyLike } from 'jose/jwk/from_key_like';
import { generateKeyPair } from 'jose/util/generate_key_pair';
import type { JWK } from 'jose';
import { exportJWK, generateKeyPair } from 'jose';
import type { AnyObject,
CanBePromise,
KoaContextWithOIDC,
@ -135,7 +134,7 @@ export class IdentityProviderFactory implements ProviderFactory {
// Cast necessary due to typing conflict between jose 2.x and 3.x
config.jwks = await this.generateJwks() as any;
config.cookies = {
...config.cookies ?? {},
...config.cookies,
keys: await this.generateCookieKeys(),
};
@ -154,7 +153,7 @@ export class IdentityProviderFactory implements ProviderFactory {
}
// If they are not, generate and save them
const { privateKey } = await generateKeyPair('RS256');
const jwk = await fromKeyLike(privateKey);
const jwk = await exportJWK(privateKey);
// Required for Solid authn client
jwk.alg = 'RS256';
// In node v15.12.0 the JWKS does not get accepted because the JWK is not a plain object,

View File

@ -122,11 +122,11 @@ describe('A quota server', (): void => {
const response1 = performSimplePutWithLength(testFile1, 2000);
await expect(response1).resolves.toBeDefined();
expect((await response1).status).toEqual(201);
expect((await response1).status).toBe(201);
const response2 = performSimplePutWithLength(testFile2, 2500);
await expect(response2).resolves.toBeDefined();
expect((await response2).status).toEqual(413);
expect((await response2).status).toBe(413);
});
// Test if writing in another pod is still possible
@ -135,7 +135,7 @@ describe('A quota server', (): void => {
const response1 = performSimplePutWithLength(testFile1, 2000);
await expect(response1).resolves.toBeDefined();
expect((await response1).status).toEqual(201);
expect((await response1).status).toBe(201);
});
// Both pods should not accept this request anymore
@ -145,11 +145,11 @@ describe('A quota server', (): void => {
const response1 = performSimplePutWithLength(testFile1, 2500);
await expect(response1).resolves.toBeDefined();
expect((await response1).status).toEqual(413);
expect((await response1).status).toBe(413);
const response2 = performSimplePutWithLength(testFile2, 2500);
await expect(response2).resolves.toBeDefined();
expect((await response2).status).toEqual(413);
expect((await response2).status).toBe(413);
});
});
@ -196,12 +196,12 @@ describe('A quota server', (): void => {
const response1 = performSimplePutWithLength(testFile1, 2000);
await expect(response1).resolves.toBeDefined();
const awaitedRes1 = await response1;
expect(awaitedRes1.status).toEqual(201);
expect(awaitedRes1.status).toBe(201);
const response2 = performSimplePutWithLength(testFile2, 2500);
await expect(response2).resolves.toBeDefined();
const awaitedRes2 = await response2;
expect(awaitedRes2.status).toEqual(413);
expect(awaitedRes2.status).toBe(413);
});
it('should return 413 when trying to write to any pod when global quota is exceeded.', async(): Promise<void> => {
@ -211,12 +211,12 @@ describe('A quota server', (): void => {
const response1 = performSimplePutWithLength(testFile1, 2500);
await expect(response1).resolves.toBeDefined();
const awaitedRes1 = await response1;
expect(awaitedRes1.status).toEqual(413);
expect(awaitedRes1.status).toBe(413);
const response2 = performSimplePutWithLength(testFile2, 2500);
await expect(response2).resolves.toBeDefined();
const awaitedRes2 = await response2;
expect(awaitedRes2.status).toEqual(413);
expect(awaitedRes2.status).toBe(413);
});
});
});

View File

@ -53,7 +53,7 @@ describe('A BasicRequestParser with simple input parsers', (): void => {
metadata: expect.any(RepresentationMetadata),
},
});
expect(result.body?.metadata.contentType).toEqual('text/turtle');
expect(result.body?.metadata.contentType).toBe('text/turtle');
await expect(arrayifyStream(result.body.data)).resolves.toEqual(
[ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ],

View File

@ -62,6 +62,21 @@ describe('A BearerWebIdExtractor', (): void => {
});
});
describe('on a request with Authorization and a lowercase Bearer token', (): void => {
const request = {
method: 'GET',
headers: {
authorization: 'bearer token-1234',
},
} as any as HttpRequest;
it('calls the Bearer verifier with the correct parameters.', async(): Promise<void> => {
await webIdExtractor.handleSafe(request);
expect(solidTokenVerifier).toHaveBeenCalledTimes(1);
expect(solidTokenVerifier).toHaveBeenCalledWith('bearer token-1234');
});
});
describe('when verification throws an error', (): void => {
const request = {
method: 'GET',

View File

@ -90,6 +90,22 @@ describe('A DPoPWebIdExtractor', (): void => {
});
});
describe('on a request with Authorization specifying DPoP in lowercase', (): void => {
const request = {
method: 'GET',
headers: {
authorization: 'dpop token-1234',
dpop: 'token-5678',
},
} as any as HttpRequest;
it('calls the target extractor with the correct parameters.', async(): Promise<void> => {
await webIdExtractor.handleSafe(request);
expect(targetExtractor.handle).toHaveBeenCalledTimes(1);
expect(targetExtractor.handle).toHaveBeenCalledWith({ request });
});
});
describe('when verification throws an error', (): void => {
const request = {
method: 'GET',

View File

@ -20,9 +20,15 @@ describe('An UnsecureWebIdExtractor', (): void => {
await expect(result).rejects.toThrow('No WebID Authorization header specified.');
});
it('returns the authorization header as WebID if there is one.', async(): Promise<void> => {
it('returns the authorization header as WebID if specified.', async(): Promise<void> => {
const headers = { authorization: 'WebID http://alice.example/card#me' };
const result = extractor.handleSafe({ headers } as HttpRequest);
await expect(result).resolves.toEqual({ [CredentialGroup.agent]: { webId: 'http://alice.example/card#me' }});
});
it('returns the authorization header as WebID if specified with a lowercase token.', async(): Promise<void> => {
const headers = { authorization: 'webid http://alice.example/card#me' };
const result = extractor.handleSafe({ headers } as HttpRequest);
await expect(result).resolves.toEqual({ [CredentialGroup.agent]: { webId: 'http://alice.example/card#me' }});
});
});

View File

@ -49,7 +49,7 @@ describe('A BasicResponseWriter', (): void => {
response.on('end', (): void => {
expect(response._isEndCalled()).toBeTruthy();
expect(response._getStatusCode()).toBe(201);
expect(response._getData()).toEqual('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
expect(response._getData()).toBe('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
resolve();
});
});

View File

@ -35,7 +35,7 @@ describe('A RepresentationMetadata', (): void => {
describe('constructor', (): void => {
it('creates a blank node if no identifier was given.', async(): Promise<void> => {
metadata = new RepresentationMetadata();
expect(metadata.identifier.termType).toEqual('BlankNode');
expect(metadata.identifier.termType).toBe('BlankNode');
expect(metadata.quads()).toHaveLength(0);
});
@ -51,19 +51,19 @@ describe('A RepresentationMetadata', (): void => {
it('converts string to content type.', async(): Promise<void> => {
metadata = new RepresentationMetadata('text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
expect(metadata.contentType).toBe('text/turtle');
metadata = new RepresentationMetadata({ path: 'identifier' }, 'text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
expect(metadata.contentType).toBe('text/turtle');
metadata = new RepresentationMetadata(new RepresentationMetadata(), 'text/turtle');
expect(metadata.contentType).toEqual('text/turtle');
expect(metadata.contentType).toBe('text/turtle');
});
it('stores the content-length correctly.', async(): Promise<void> => {
metadata = new RepresentationMetadata();
metadata.contentLength = 50;
expect(metadata.contentLength).toEqual(50);
expect(metadata.contentLength).toBe(50);
metadata = new RepresentationMetadata();
metadata.contentLength = undefined;
@ -285,7 +285,7 @@ describe('A RepresentationMetadata', (): void => {
expect(metadata.contentType).toBeUndefined();
metadata.contentType = 'a/b';
expect(metadata.get(CONTENT_TYPE)).toEqualRdfTerm(literal('a/b'));
expect(metadata.contentType).toEqual('a/b');
expect(metadata.contentType).toBe('a/b');
metadata.contentType = undefined;
expect(metadata.contentType).toBeUndefined();
});

View File

@ -98,7 +98,7 @@ describe('An IdentityProviderFactory', (): void => {
expect(config.jwks).toEqual({ keys: [ expect.objectContaining({ kty: 'RSA' }) ]});
expect(config.routes).toEqual(routes);
expect((config.interactions?.url as any)()).toEqual('/idp/');
expect((config.interactions?.url as any)()).toBe('/idp/');
expect((config.audiences as any)(null, null, {}, 'access_token')).toBe('solid');
expect((config.audiences as any)(null, null, { clientId: 'clientId' }, 'client_credentials')).toBe('clientId');

View File

@ -11,12 +11,12 @@ describe('LogUtil', (): void => {
it('allows creating a lazy logger for a string label.', async(): Promise<void> => {
expect(getLoggerFor('MyLabel')).toBeInstanceOf(LazyLogger);
expect((getLoggerFor('MyLabel') as any).label).toEqual('MyLabel');
expect((getLoggerFor('MyLabel') as any).label).toBe('MyLabel');
});
it('allows creating a lazy logger for a class instance.', async(): Promise<void> => {
expect(getLoggerFor(new VoidLogger())).toBeInstanceOf(LazyLogger);
expect((getLoggerFor(new VoidLogger()) as any).label).toEqual('VoidLogger');
expect((getLoggerFor(new VoidLogger()) as any).label).toBe('VoidLogger');
});
it('allows setting the global logger factory.', async(): Promise<void> => {

View File

@ -13,7 +13,7 @@ describe('WinstonLoggerFactory', (): void => {
const logger = factory.createLogger('MyLabel');
expect(logger).toBeInstanceOf(WinstonLogger);
const innerLogger: Logger = (logger as any).logger;
expect(innerLogger.level).toEqual('debug');
expect(innerLogger.level).toBe('debug');
expect(innerLogger.format).toBeTruthy();
expect(innerLogger.transports).toHaveLength(1);
});

View File

@ -180,7 +180,7 @@ describe('A DataAccessorBasedStore', (): void => {
const result = await store.getRepresentation(resourceID);
expect(result).toMatchObject({ binary: true });
expect(await arrayifyStream(result.data)).toEqual([ resourceData ]);
expect(result.metadata.contentType).toEqual('text/plain');
expect(result.metadata.contentType).toBe('text/plain');
expect(result.metadata.get('AUXILIARY')?.value).toBe(auxiliaryStrategy.getAuxiliaryIdentifier(resourceID).path);
});
@ -690,7 +690,7 @@ describe('A DataAccessorBasedStore', (): void => {
{ path: root },
]);
expect(accessor.data[`${root}resource`]).toBeUndefined();
expect(accessor.data[`${root}resource.dummy`]).not.toBeUndefined();
expect(accessor.data[`${root}resource.dummy`]).toBeDefined();
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenLastCalledWith(
'Error deleting auxiliary resource http://test.com/resource.dummy: auxiliary error!',

View File

@ -109,16 +109,16 @@ describe('ConversionUtil', (): void => {
describe('#matchesMediaPreferences', (): void => {
it('returns false if there are no matches.', async(): Promise<void> => {
const preferences: ValuePreferences = { 'a/x': 1, 'b/x': 0.5, 'c/x': 0 };
expect(matchesMediaPreferences('c/x', preferences)).toEqual(false);
expect(matchesMediaPreferences('c/x', preferences)).toBe(false);
});
it('returns true if there are matches.', async(): Promise<void> => {
const preferences: ValuePreferences = { 'a/x': 1, 'b/x': 0.5, 'c/x': 0 };
expect(matchesMediaPreferences('b/x', preferences)).toEqual(true);
expect(matchesMediaPreferences('b/x', preferences)).toBe(true);
});
it('matches anything if there are no preferences.', async(): Promise<void> => {
expect(matchesMediaPreferences('a/a')).toEqual(true);
expect(matchesMediaPreferences('a/a')).toBe(true);
});
it('does not match internal types if not in the preferences.', async(): Promise<void> => {
@ -157,7 +157,7 @@ describe('ConversionUtil', (): void => {
describe('#preferencesToString', (): void => {
it('returns a string serialization.', async(): Promise<void> => {
const preferences: ValuePreferences = { 'a/*': 1, 'b/b': 0.8, 'c/c': 0 };
expect(preferencesToString(preferences)).toEqual('a/*:1,b/b:0.8,c/c:0');
expect(preferencesToString(preferences)).toBe('a/*:1,b/b:0.8,c/c:0');
});
});
});

View File

@ -55,7 +55,7 @@ describe('A QuadToRdfConverter', (): void => {
binary: true,
metadata: expect.any(RepresentationMetadata),
});
expect(result.metadata.contentType).toEqual('text/turtle');
expect(result.metadata.contentType).toBe('text/turtle');
await expect(readableToString(result.data)).resolves.toEqual(
`<http://test.com/s> <http://test.com/p> <http://test.com/o>.
`,
@ -73,7 +73,7 @@ describe('A QuadToRdfConverter', (): void => {
metadata);
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
const result = await converter.handle({ identifier, representation, preferences });
expect(result.metadata.contentType).toEqual('text/turtle');
expect(result.metadata.contentType).toBe('text/turtle');
await expect(readableToString(result.data)).resolves.toEqual(
`@prefix dc: <http://purl.org/dc/terms/>.
@prefix test: <http://test.com/>.
@ -92,7 +92,7 @@ test:s dc:modified test:o.
metadata);
const preferences: RepresentationPreferences = { type: { 'text/turtle': 1 }};
const result = await converter.handle({ identifier, representation, preferences });
expect(result.metadata.contentType).toEqual('text/turtle');
expect(result.metadata.contentType).toBe('text/turtle');
await expect(readableToString(result.data)).resolves.toEqual(
`<> <#abc> <def/ghi>.
`,
@ -113,7 +113,7 @@ test:s dc:modified test:o.
binary: true,
metadata: expect.any(RepresentationMetadata),
});
expect(result.metadata.contentType).toEqual('application/ld+json');
expect(result.metadata.contentType).toBe('application/ld+json');
await expect(readableToString(result.data)).resolves.toEqual(
`[
{

View File

@ -43,7 +43,7 @@ describe('A WrappedExpiringStorage', (): void => {
it('returns data if it has not expired.', async(): Promise<void> => {
source.get.mockResolvedValueOnce(createExpires('data!', tomorrow));
await expect(storage.get('key')).resolves.toEqual('data!');
await expect(storage.get('key')).resolves.toBe('data!');
});
it('deletes expired data when trying to get it.', async(): Promise<void> => {

View File

@ -119,7 +119,7 @@ describe('A FileSizeReporter', (): void => {
it('should return the content-length.', async(): Promise<void> => {
const metadata = new RepresentationMetadata();
metadata.contentLength = 100;
await expect(fileSizeReporter.estimateSize(metadata)).resolves.toEqual(100);
await expect(fileSizeReporter.estimateSize(metadata)).resolves.toBe(100);
});
it(
'should return undefined if no content-length is present in the metadata.',

View File

@ -25,17 +25,17 @@ import {
describe('PathUtil', (): void => {
describe('#normalizeFilePath', (): void => {
it('normalizes POSIX paths.', async(): Promise<void> => {
expect(normalizeFilePath('/foo/bar/../baz')).toEqual('/foo/baz');
expect(normalizeFilePath('/foo/bar/../baz')).toBe('/foo/baz');
});
it('normalizes Windows paths.', async(): Promise<void> => {
expect(normalizeFilePath('c:\\foo\\bar\\..\\baz')).toEqual('c:/foo/baz');
expect(normalizeFilePath('c:\\foo\\bar\\..\\baz')).toBe('c:/foo/baz');
});
});
describe('#joinFilePath', (): void => {
it('joins POSIX paths.', async(): Promise<void> => {
expect(joinFilePath('/foo/bar/', '..', '/baz')).toEqual('/foo/baz');
expect(joinFilePath('/foo/bar/', '..', '/baz')).toBe('/foo/baz');
});
it('joins Windows paths.', async(): Promise<void> => {
@ -45,11 +45,11 @@ describe('PathUtil', (): void => {
describe('#absoluteFilePath', (): void => {
it('does not change absolute posix paths.', async(): Promise<void> => {
expect(absoluteFilePath('/foo/bar/')).toEqual('/foo/bar/');
expect(absoluteFilePath('/foo/bar/')).toBe('/foo/bar/');
});
it('converts absolute win32 paths to posix paths.', async(): Promise<void> => {
expect(absoluteFilePath('C:\\foo\\bar')).toEqual('C:/foo/bar');
expect(absoluteFilePath('C:\\foo\\bar')).toBe('C:/foo/bar');
});
it('makes relative paths absolute.', async(): Promise<void> => {
@ -59,70 +59,70 @@ describe('PathUtil', (): void => {
describe('#ensureTrailingSlash', (): void => {
it('makes sure there is always exactly 1 slash.', async(): Promise<void> => {
expect(ensureTrailingSlash('http://test.com')).toEqual('http://test.com/');
expect(ensureTrailingSlash('http://test.com/')).toEqual('http://test.com/');
expect(ensureTrailingSlash('http://test.com//')).toEqual('http://test.com/');
expect(ensureTrailingSlash('http://test.com///')).toEqual('http://test.com/');
expect(ensureTrailingSlash('http://test.com')).toBe('http://test.com/');
expect(ensureTrailingSlash('http://test.com/')).toBe('http://test.com/');
expect(ensureTrailingSlash('http://test.com//')).toBe('http://test.com/');
expect(ensureTrailingSlash('http://test.com///')).toBe('http://test.com/');
});
});
describe('#trimTrailingSlashes', (): void => {
it('removes all trailing slashes.', async(): Promise<void> => {
expect(trimTrailingSlashes('http://test.com')).toEqual('http://test.com');
expect(trimTrailingSlashes('http://test.com/')).toEqual('http://test.com');
expect(trimTrailingSlashes('http://test.com//')).toEqual('http://test.com');
expect(trimTrailingSlashes('http://test.com///')).toEqual('http://test.com');
expect(trimTrailingSlashes('http://test.com')).toBe('http://test.com');
expect(trimTrailingSlashes('http://test.com/')).toBe('http://test.com');
expect(trimTrailingSlashes('http://test.com//')).toBe('http://test.com');
expect(trimTrailingSlashes('http://test.com///')).toBe('http://test.com');
});
});
describe('#getExtension', (): void => {
it('returns the extension of a path.', async(): Promise<void> => {
expect(getExtension('/a/b.txt')).toEqual('txt');
expect(getExtension('/a/btxt')).toEqual('');
expect(getExtension('/a/b.txt')).toBe('txt');
expect(getExtension('/a/btxt')).toBe('');
});
});
describe('#toCanonicalUriPath', (): void => {
it('encodes only the necessary parts.', async(): Promise<void> => {
expect(toCanonicalUriPath('/a%20path&/name')).toEqual('/a%20path%26/name');
expect(toCanonicalUriPath('/a%20path&/name')).toBe('/a%20path%26/name');
});
it('leaves the query string untouched.', async(): Promise<void> => {
expect(toCanonicalUriPath('/a%20path&/name?abc=def&xyz')).toEqual('/a%20path%26/name?abc=def&xyz');
expect(toCanonicalUriPath('/a%20path&/name?abc=def&xyz')).toBe('/a%20path%26/name?abc=def&xyz');
});
});
describe('#decodeUriPathComponents', (): void => {
it('decodes all parts of a path.', async(): Promise<void> => {
expect(decodeUriPathComponents('/a%20path&/name')).toEqual('/a path&/name');
expect(decodeUriPathComponents('/a%20path&/name')).toBe('/a path&/name');
});
it('leaves the query string untouched.', async(): Promise<void> => {
expect(decodeUriPathComponents('/a%20path&/name?abc=def&xyz')).toEqual('/a path&/name?abc=def&xyz');
expect(decodeUriPathComponents('/a%20path&/name?abc=def&xyz')).toBe('/a path&/name?abc=def&xyz');
});
});
describe('#encodeUriPathComponents', (): void => {
it('encodes all parts of a path.', async(): Promise<void> => {
expect(encodeUriPathComponents('/a%20path&/name')).toEqual('/a%2520path%26/name');
expect(encodeUriPathComponents('/a%20path&/name')).toBe('/a%2520path%26/name');
});
it('leaves the query string untouched.', async(): Promise<void> => {
expect(encodeUriPathComponents('/a%20path&/name?abc=def&xyz')).toEqual('/a%2520path%26/name?abc=def&xyz');
expect(encodeUriPathComponents('/a%20path&/name?abc=def&xyz')).toBe('/a%2520path%26/name?abc=def&xyz');
});
});
describe('#isContainerPath', (): void => {
it('returns true if the path ends with a slash.', async(): Promise<void> => {
expect(isContainerPath('/a/b')).toEqual(false);
expect(isContainerPath('/a/b/')).toEqual(true);
expect(isContainerPath('/a/b')).toBe(false);
expect(isContainerPath('/a/b/')).toBe(true);
});
});
describe('#isContainerIdentifier', (): void => {
it('works af isContainerPath but for identifiers.', async(): Promise<void> => {
expect(isContainerIdentifier({ path: '/a/b' })).toEqual(false);
expect(isContainerIdentifier({ path: '/a/b/' })).toEqual(true);
expect(isContainerIdentifier({ path: '/a/b' })).toBe(false);
expect(isContainerIdentifier({ path: '/a/b/' })).toBe(true);
});
});
@ -159,8 +159,8 @@ describe('PathUtil', (): void => {
const regex = createSubdomainRegexp('http://test.com/foo/');
expect(regex.exec('http://test.com/foo/')![1]).toBeUndefined();
expect(regex.exec('http://test.com/foo/bar')![1]).toBeUndefined();
expect(regex.exec('http://alice.test.com/foo/')![1]).toEqual('alice');
expect(regex.exec('http://alice.bob.test.com/foo/')![1]).toEqual('alice.bob');
expect(regex.exec('http://alice.test.com/foo/')![1]).toBe('alice');
expect(regex.exec('http://alice.bob.test.com/foo/')![1]).toBe('alice.bob');
expect(regex.exec('http://test.com/')).toBeNull();
expect(regex.exec('http://alicetest.com/foo/')).toBeNull();
});

View File

@ -9,23 +9,23 @@ describe('PromiseUtil', (): void => {
const resultInfinite = new Promise<boolean>((): void => {});
it('returns false if no promise is provided.', async(): Promise<void> => {
await expect(promiseSome([])).resolves.toEqual(false);
await expect(promiseSome([])).resolves.toBe(false);
});
it('returns false if no promise returns true.', async(): Promise<void> => {
await expect(promiseSome([ resultFalse, resultFalse, resultFalse ])).resolves.toEqual(false);
await expect(promiseSome([ resultFalse, resultFalse, resultFalse ])).resolves.toBe(false);
});
it('returns true if at least a promise returns true.', async(): Promise<void> => {
await expect(promiseSome([ resultFalse, resultTrue, resultFalse ])).resolves.toEqual(true);
await expect(promiseSome([ resultFalse, resultTrue, resultFalse ])).resolves.toBe(true);
});
it('does not propagate errors.', async(): Promise<void> => {
await expect(promiseSome([ resultError, resultFalse, resultFalse ])).resolves.toEqual(false);
await expect(promiseSome([ resultError, resultFalse, resultFalse ])).resolves.toBe(false);
});
it('works with a combination of promises.', async(): Promise<void> => {
await expect(promiseSome([ resultError, resultTrue, resultInfinite ])).resolves.toEqual(true);
await expect(promiseSome([ resultError, resultTrue, resultInfinite ])).resolves.toBe(true);
});
});
});

View File

@ -23,7 +23,7 @@ describe('StreamUtil', (): void => {
describe('#readableToString', (): void => {
it('concatenates all elements of a Readable.', async(): Promise<void> => {
const stream = Readable.from([ 'a', 'b', 'c' ]);
await expect(readableToString(stream)).resolves.toEqual('abc');
await expect(readableToString(stream)).resolves.toBe('abc');
});
});
@ -77,7 +77,7 @@ describe('StreamUtil', (): void => {
const input = Readable.from([ 'data' ]);
const output = new PassThrough();
const piped = pipeSafely(input, output);
await expect(readableToString(piped)).resolves.toEqual('data');
await expect(readableToString(piped)).resolves.toBe('data');
});
it('pipes errors from one stream to the other.', async(): Promise<void> => {

View File

@ -4,14 +4,14 @@ describe('An BadRequestHttpError', (): void => {
it('has status code 400.', async(): Promise<void> => {
const error = new BadRequestHttpError('test');
expect(error.statusCode).toEqual(400);
expect(error.message).toEqual('test');
expect(error.name).toEqual('BadRequestHttpError');
expect(error.statusCode).toBe(400);
expect(error.message).toBe('test');
expect(error.name).toBe('BadRequestHttpError');
});
it('has a default message if none was provided.', async(): Promise<void> => {
const error = new BadRequestHttpError();
expect(error.message).toEqual('The given input is not supported by the server configuration.');
expect(error.message).toBe('The given input is not supported by the server configuration.');
});
});

View File

@ -8,7 +8,7 @@ describe('A StaticHandler', (): void => {
it('returns the stored value.', async(): Promise<void> => {
const handler = new StaticHandler('apple');
await expect(handler.handle()).resolves.toEqual('apple');
await expect(handler.handle()).resolves.toBe('apple');
});
it('returns undefined if there is no stored value.', async(): Promise<void> => {

View File

@ -57,7 +57,7 @@ describe('A WaterfallHandler', (): void => {
it('handles data if a handler supports it.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerTrue ]);
await expect(handler.handle('test')).resolves.toEqual('test');
await expect(handler.handle('test')).resolves.toBe('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});
@ -71,7 +71,7 @@ describe('A WaterfallHandler', (): void => {
it('only calls the canHandle function once of its handlers when handleSafe is called.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerTrue ]);
await expect(handler.handleSafe('test')).resolves.toEqual('test');
await expect(handler.handleSafe('test')).resolves.toBe('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});

View File

@ -1,5 +1,4 @@
import { EventEmitter } from 'events';
// eslint-disable-next-line import/default
import redis from 'redis';
import Redlock from 'redlock';
import type { Lock } from 'redlock';

View File

@ -25,7 +25,6 @@ describe('A SingleThreadedResourceLocker', (): void => {
await expect(locker.release(identifier)).rejects.toThrow(InternalServerError);
});
/* eslint-disable jest/valid-expect-in-promise */
it('blocks lock acquisition until they are released.', async(): Promise<void> => {
const results: number[] = [];
const lock1 = locker.acquire(identifier);

View File

@ -21,7 +21,7 @@ describe('A ChainedTemplateEngine', (): void => {
});
it('chains the engines.', async(): Promise<void> => {
await expect(engine.render(contents, template)).resolves.toEqual('body2');
await expect(engine.render(contents, template)).resolves.toBe('body2');
expect(engines[0].render).toHaveBeenCalledTimes(1);
expect(engines[0].render).toHaveBeenLastCalledWith(contents, template);
expect(engines[1].render).toHaveBeenCalledTimes(1);
@ -30,7 +30,7 @@ describe('A ChainedTemplateEngine', (): void => {
it('can use a different field to pass along the body.', async(): Promise<void> => {
engine = new ChainedTemplateEngine(engines, 'different');
await expect(engine.render(contents, template)).resolves.toEqual('body2');
await expect(engine.render(contents, template)).resolves.toBe('body2');
expect(engines[0].render).toHaveBeenCalledTimes(1);
expect(engines[0].render).toHaveBeenLastCalledWith(contents, template);
expect(engines[1].render).toHaveBeenCalledTimes(1);

View File

@ -38,7 +38,7 @@ export function getPort(name: typeof portNames[number]): number {
export function describeIf(envFlag: string, name: string, fn: () => void): void {
const flag = `TEST_${envFlag.toUpperCase()}`;
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
// eslint-disable-next-line jest/valid-describe, jest/valid-title, jest/no-disabled-tests
// eslint-disable-next-line jest/valid-describe-callback, jest/valid-title, jest/no-disabled-tests
return enabled ? describe(name, fn) : describe.skip(name, fn);
}