mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Rewrite TokenOwnershipValidator behaviour to remove @rdfjs/fetch dependency
This commit is contained in:
parent
9d337ba80c
commit
63e88578c3
@ -5,6 +5,7 @@
|
||||
"comment": "Determines WebID ownership by requesting a specific value to be added to the WebID document",
|
||||
"@id": "urn:solid-server:auth:password:OwnershipValidator",
|
||||
"@type": "TokenOwnershipValidator",
|
||||
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||
"storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
|
||||
}
|
||||
]
|
||||
|
1208
package-lock.json
generated
1208
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -77,7 +77,6 @@
|
||||
"dependencies": {
|
||||
"@comunica/actor-init-sparql": "^1.21.3",
|
||||
"@rdfjs/data-model": "^1.2.0",
|
||||
"@rdfjs/fetch": "^2.1.0",
|
||||
"@solid/access-token-verifier": "^0.10.0",
|
||||
"@types/arrayify-stream": "^1.0.0",
|
||||
"@types/async-lock": "^1.1.2",
|
||||
@ -93,8 +92,6 @@
|
||||
"@types/pump": "^1.1.1",
|
||||
"@types/punycode": "^2.1.0",
|
||||
"@types/rdf-js": "^4.0.2",
|
||||
"@types/rdfjs__fetch": "^2.0.3",
|
||||
"@types/rdfjs__fetch-lite": "^2.0.3",
|
||||
"@types/redis": "^2.8.30",
|
||||
"@types/redlock": "^4.0.1",
|
||||
"@types/sparqljs": "^3.1.2",
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { Quad } from 'n3';
|
||||
import { DataFactory } from 'n3';
|
||||
import { v4 } from 'uuid';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||
import { fetchDataset } from '../../util/FetchUtil';
|
||||
@ -15,11 +17,13 @@ const { literal, namedNode, quad } = DataFactory;
|
||||
export class TokenOwnershipValidator extends OwnershipValidator {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly converter: RepresentationConverter;
|
||||
private readonly storage: ExpiringStorage<string, string>;
|
||||
private readonly expiration: number;
|
||||
|
||||
public constructor(storage: ExpiringStorage<string, string>, expiration = 30) {
|
||||
public constructor(converter: RepresentationConverter, storage: ExpiringStorage<string, string>, expiration = 30) {
|
||||
super();
|
||||
this.converter = converter;
|
||||
this.storage = storage;
|
||||
// Convert minutes to milliseconds
|
||||
this.expiration = expiration * 60 * 1000;
|
||||
@ -37,9 +41,7 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
||||
}
|
||||
|
||||
// Verify if the token can be found in the WebId
|
||||
const dataset = await fetchDataset(webId);
|
||||
const expectedQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
||||
if (!dataset.has(expectedQuad)) {
|
||||
if (!await this.hasToken(webId, token)) {
|
||||
this.throwError(webId, token);
|
||||
}
|
||||
this.logger.debug(`Verified ownership of ${webId}`);
|
||||
@ -60,6 +62,22 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
||||
return v4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data from the WebID to determine if the token is present.
|
||||
*/
|
||||
private async hasToken(webId: string, token: string): Promise<boolean> {
|
||||
const representation = await fetchDataset(webId, this.converter);
|
||||
const expectedQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
||||
for await (const data of representation.data) {
|
||||
const triple = data as Quad;
|
||||
if (triple.equals(expectedQuad)) {
|
||||
representation.data.destroy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error containing the description of which triple is needed for verification.
|
||||
*/
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { Store } from 'n3';
|
||||
import type { Quad } from 'n3';
|
||||
import type { Adapter, AdapterPayload } from 'oidc-provider';
|
||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
||||
import { getLoggerFor } from '../../logging/LogUtil';
|
||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||
import { fetchDataset } from '../../util/FetchUtil';
|
||||
import { OIDC } from '../../util/Vocabularies';
|
||||
import type { AdapterFactory } from './AdapterFactory';
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
@ -84,29 +84,27 @@ export class WebIdAdapter implements Adapter {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses RDF data found at a client WebID.
|
||||
* @param data - Raw data from the WebID.
|
||||
* @param id - The actual WebID.
|
||||
* @param response - Response object from the request.
|
||||
*/
|
||||
private async parseRdfWebId(data: string, id: string, response: Response): Promise<AdapterPayload> {
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType) {
|
||||
throw new Error(`No content-type received for client WebID ${id}`);
|
||||
const representation = await fetchDataset(response, this.converter, data);
|
||||
|
||||
// Find the valid redirect URIs
|
||||
const redirectUris: string[] = [];
|
||||
for await (const entry of representation.data) {
|
||||
const triple = entry as Quad;
|
||||
if (triple.predicate.equals(OIDC.terms.redirect_uris)) {
|
||||
redirectUris.push(triple.object.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to convert to quads
|
||||
const representation = new BasicRepresentation(data, contentType);
|
||||
const preferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
const converted = await this.converter.handleSafe({ representation, identifier: { path: id }, preferences });
|
||||
const quads = new Store();
|
||||
const importer = quads.import(converted.data);
|
||||
await new Promise((resolve, reject): void => {
|
||||
importer.on('end', resolve);
|
||||
importer.on('error', reject);
|
||||
});
|
||||
|
||||
// Find the valid redirect uris
|
||||
const match = quads.getObjects(id, 'http://www.w3.org/ns/solid/oidc#redirect_uris', null);
|
||||
|
||||
return {
|
||||
client_id: id,
|
||||
redirect_uris: match.map((node): string => node.value),
|
||||
redirect_uris: redirectUris,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,29 +1,54 @@
|
||||
import fetch from '@rdfjs/fetch';
|
||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
||||
import type { Dataset } from 'rdf-js';
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||
import type { Representation } from '../ldp/representation/Representation';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { createErrorMessage } from './errors/ErrorUtil';
|
||||
import type { RepresentationConverter } from '../storage/conversion/RepresentationConverter';
|
||||
import { INTERNAL_QUADS } from './ContentTypes';
|
||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||
|
||||
const logger = getLoggerFor('FetchUtil');
|
||||
|
||||
/**
|
||||
* Fetches an RDF dataset from the given URL.
|
||||
* Input can also be a Response if the request was already made.
|
||||
* In case the given Response object was already parsed its body can be passed along as a string.
|
||||
*
|
||||
* The converter will be used to convert the response body to RDF.
|
||||
*
|
||||
* Response will be a Representation with content-type internal/quads.
|
||||
*/
|
||||
export async function fetchDataset(url: string): Promise<Dataset> {
|
||||
let rawResponse: DatasetResponse<Dataset>;
|
||||
try {
|
||||
rawResponse = (await fetch(url)) as DatasetResponse<Dataset>;
|
||||
} catch (err: unknown) {
|
||||
logger.error(`Cannot fetch ${url}: ${createErrorMessage(err)}`);
|
||||
throw new Error(`Cannot fetch ${url}`);
|
||||
export async function fetchDataset(url: string, converter: RepresentationConverter): Promise<Representation>;
|
||||
export async function fetchDataset(response: Response, converter: RepresentationConverter, body?: string):
|
||||
Promise<Representation>;
|
||||
export async function fetchDataset(input: string | Response, converter: RepresentationConverter, body?: string):
|
||||
Promise<Representation> {
|
||||
let response: Response;
|
||||
if (typeof input === 'string') {
|
||||
response = await fetch(input);
|
||||
} else {
|
||||
response = input;
|
||||
}
|
||||
let dataset: Dataset;
|
||||
try {
|
||||
dataset = await rawResponse.dataset();
|
||||
} catch (err: unknown) {
|
||||
logger.error(`Could not parse RDF in ${url}: ${createErrorMessage(err)}`);
|
||||
// Keeping the error message the same to prevent leaking possible information about intranet
|
||||
throw new Error(`Cannot fetch ${url}`);
|
||||
if (!body) {
|
||||
body = await response.text();
|
||||
}
|
||||
return dataset;
|
||||
|
||||
// Keeping the error message the same everywhere to prevent leaking possible information about intranet.
|
||||
const error = new BadRequestHttpError(`Unable to access data at ${response.url}`);
|
||||
|
||||
if (response.status !== 200) {
|
||||
logger.warn(`Cannot fetch ${response.url}: ${body}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType) {
|
||||
logger.warn(`Missing content-type header from ${response.url}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Try to convert to quads
|
||||
const representation = new BasicRepresentation(body, contentType);
|
||||
const preferences = { type: { [INTERNAL_QUADS]: 1 }};
|
||||
return converter.handleSafe({ representation, identifier: { path: response.url }, preferences });
|
||||
}
|
||||
|
@ -100,6 +100,10 @@ export const MA = createUriAndTermNamespace('http://www.w3.org/ns/ma-ont#',
|
||||
'format',
|
||||
);
|
||||
|
||||
export const OIDC = createUriAndTermNamespace('http://www.w3.org/ns/solid/oidc#',
|
||||
'redirect_uris',
|
||||
);
|
||||
|
||||
export const PIM = createUriAndTermNamespace('http://www.w3.org/ns/pim/space#',
|
||||
'Storage',
|
||||
);
|
||||
|
@ -1,15 +1,14 @@
|
||||
import fetch from '@rdfjs/fetch';
|
||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { DataFactory } from 'n3';
|
||||
import type { Quad } from 'n3';
|
||||
import type { DatasetCore } from 'rdf-js';
|
||||
import { v4 } from 'uuid';
|
||||
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
|
||||
import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter';
|
||||
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
|
||||
import { SOLID } from '../../../../src/util/Vocabularies';
|
||||
const { literal, namedNode, quad } = DataFactory;
|
||||
|
||||
jest.mock('@rdfjs/fetch');
|
||||
jest.mock('cross-fetch');
|
||||
jest.mock('uuid');
|
||||
|
||||
function quadToString(qq: Quad): string {
|
||||
@ -24,26 +23,25 @@ describe('A TokenOwnershipValidator', (): void => {
|
||||
const fetchMock: jest.Mock = fetch as any;
|
||||
const webId = 'http://alice.test.com/#me';
|
||||
const token = 'randomlyGeneratedToken';
|
||||
let rawResponse: DatasetResponse<DatasetCore>;
|
||||
let dataset: DatasetCore;
|
||||
let triples: Quad[];
|
||||
const tokenTriple = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
||||
const tokenString = `${quadToString(tokenTriple)} .`;
|
||||
const converter = new RdfToQuadConverter();
|
||||
let storage: ExpiringStorage<string, string>;
|
||||
let validator: TokenOwnershipValidator;
|
||||
|
||||
function mockFetch(body: string): void {
|
||||
fetchMock.mockImplementation((url: string): any => ({
|
||||
text: (): any => body,
|
||||
url,
|
||||
status: 200,
|
||||
headers: { get: (): any => 'text/turtle' },
|
||||
}));
|
||||
}
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
const now = Date.now();
|
||||
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||
(v4 as jest.Mock).mockReturnValue(token);
|
||||
triples = [];
|
||||
|
||||
dataset = {
|
||||
has: (qq: Quad): boolean => triples.some((triple): boolean => triple.equals(qq)),
|
||||
} as any;
|
||||
|
||||
rawResponse = {
|
||||
dataset: async(): Promise<DatasetCore> => dataset,
|
||||
} as any;
|
||||
|
||||
const map = new Map<string, any>();
|
||||
storage = {
|
||||
@ -52,40 +50,41 @@ describe('A TokenOwnershipValidator', (): void => {
|
||||
delete: jest.fn().mockImplementation((key: string): any => map.delete(key)),
|
||||
} as any;
|
||||
|
||||
fetchMock.mockReturnValue(rawResponse);
|
||||
mockFetch('');
|
||||
|
||||
validator = new TokenOwnershipValidator(storage);
|
||||
validator = new TokenOwnershipValidator(converter, storage);
|
||||
});
|
||||
|
||||
it('errors if no token is stored in the storage.', async(): Promise<void> => {
|
||||
// Even if the token is in the WebId, it will error since it's not in the storage
|
||||
triples = [ tokenTriple ];
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
mockFetch(tokenString);
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
expect(fetch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('errors if the expected triple is missing.', async(): Promise<void> => {
|
||||
// First call will add the token to the storage
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
expect(fetch).toHaveBeenCalledTimes(0);
|
||||
// Second call will fetch the WebId
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('resolves if the WebId contains the verification triple.', async(): Promise<void> => {
|
||||
triples = [ tokenTriple ];
|
||||
mockFetch(tokenString);
|
||||
// First call will add the token to the storage
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
// Second call will succeed since it has the verification triple
|
||||
await expect(validator.handle({ webId })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails if the WebId contains the wrong verification triple.', async(): Promise<void> => {
|
||||
triples = [ quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal('wrongToken')) ];
|
||||
const wrongQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal('wrongToken'));
|
||||
mockFetch(`${quadToString(wrongQuad)} .`);
|
||||
// First call will add the token to the storage
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
// Second call will fail since it has the wrong verification triple
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
||||
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||
});
|
||||
});
|
||||
|
@ -10,7 +10,6 @@ jest.mock('cross-fetch');
|
||||
describe('A WebIdAdapterFactory', (): void => {
|
||||
const fetchMock: jest.Mock = fetch as any;
|
||||
const id = 'https://app.test.com/card#me';
|
||||
let data: string;
|
||||
let json: any;
|
||||
let rdf: string;
|
||||
let source: Adapter;
|
||||
@ -34,7 +33,7 @@ describe('A WebIdAdapterFactory', (): void => {
|
||||
};
|
||||
rdf = `<${id}> <http://www.w3.org/ns/solid/oidc#redirect_uris> <http://test.com>.`;
|
||||
|
||||
fetchMock.mockReturnValue({ text: (): any => data });
|
||||
fetchMock.mockImplementation((url: string): any => ({ text: (): any => '', url, status: 200 }));
|
||||
|
||||
source = {
|
||||
upsert: jest.fn(),
|
||||
@ -110,12 +109,12 @@ describe('A WebIdAdapterFactory', (): void => {
|
||||
});
|
||||
|
||||
it('errors if the client ID requests does not respond with 200.', async(): Promise<void> => {
|
||||
fetchMock.mockResolvedValueOnce({ status: 400, text: (): string => 'error' });
|
||||
fetchMock.mockResolvedValueOnce({ url: id, status: 400, text: (): string => 'error' });
|
||||
await expect(adapter.find(id)).rejects.toThrow(`Unable to access data at ${id}: error`);
|
||||
});
|
||||
|
||||
it('can handle a valid JSON-LD response.', async(): Promise<void> => {
|
||||
fetchMock.mockResolvedValueOnce({ status: 200, text: (): string => JSON.stringify(json) });
|
||||
fetchMock.mockResolvedValueOnce({ url: id, status: 200, text: (): string => JSON.stringify(json) });
|
||||
await expect(adapter.find(id)).resolves.toEqual({
|
||||
...json,
|
||||
token_endpoint_auth_method: 'none',
|
||||
@ -124,14 +123,14 @@ describe('A WebIdAdapterFactory', (): void => {
|
||||
|
||||
it('errors if there is a client_id mismatch.', async(): Promise<void> => {
|
||||
json.client_id = 'someone else';
|
||||
fetchMock.mockResolvedValueOnce({ status: 200, text: (): string => JSON.stringify(json) });
|
||||
fetchMock.mockResolvedValueOnce({ url: id, status: 200, text: (): string => JSON.stringify(json) });
|
||||
await expect(adapter.find(id)).rejects
|
||||
.toThrow('The client registration `client_id` field must match the client WebID');
|
||||
});
|
||||
|
||||
it('can handle a valid RDF response.', async(): Promise<void> => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
{ status: 200, text: (): string => rdf, headers: { get: (): any => 'text/turtle' }},
|
||||
{ url: id, status: 200, text: (): string => rdf, headers: { get: (): any => 'text/turtle' }},
|
||||
);
|
||||
await expect(adapter.find(id)).resolves.toEqual({
|
||||
client_id: id,
|
||||
@ -147,7 +146,10 @@ describe('A WebIdAdapterFactory', (): void => {
|
||||
'http://randomField': { '@value': 'this will not be there since RDF parsing only takes preset fields' },
|
||||
};
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
{ status: 200, text: (): string => JSON.stringify(json), headers: { get: (): any => 'application/ld+json' }},
|
||||
{ url: id,
|
||||
status: 200,
|
||||
text: (): string => JSON.stringify(json),
|
||||
headers: { get: (): any => 'application/ld+json' }},
|
||||
);
|
||||
await expect(adapter.find(id)).resolves.toEqual({
|
||||
client_id: id,
|
||||
@ -158,9 +160,9 @@ describe('A WebIdAdapterFactory', (): void => {
|
||||
|
||||
it('errors if there is no content-type.', async(): Promise<void> => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
{ status: 200, text: (): string => rdf, headers: { get: jest.fn() }},
|
||||
{ url: id, status: 200, text: (): string => rdf, headers: { get: jest.fn() }},
|
||||
);
|
||||
await expect(adapter.find(id)).rejects
|
||||
.toThrow(`No content-type received for client WebID ${id}`);
|
||||
.toThrow(`Unable to access data at ${id}`);
|
||||
});
|
||||
});
|
||||
|
@ -1,38 +1,55 @@
|
||||
import fetch from '@rdfjs/fetch';
|
||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
||||
import type { Dataset } from 'rdf-js';
|
||||
import arrayifyStream from 'arrayify-stream';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { DataFactory } from 'n3';
|
||||
import { RdfToQuadConverter } from '../../../src/storage/conversion/RdfToQuadConverter';
|
||||
import { fetchDataset } from '../../../src/util/FetchUtil';
|
||||
const { namedNode, quad } = DataFactory;
|
||||
|
||||
jest.mock('@rdfjs/fetch');
|
||||
jest.mock('cross-fetch');
|
||||
|
||||
describe('FetchUtil', (): void => {
|
||||
describe('#fetchDataset', (): void => {
|
||||
const fetchMock: jest.Mock = fetch as any;
|
||||
const url = 'http://test.com/foo';
|
||||
let datasetResponse: DatasetResponse<Dataset>;
|
||||
const dataset: Dataset = {} as any;
|
||||
const converter = new RdfToQuadConverter();
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
datasetResponse = {
|
||||
dataset: jest.fn().mockReturnValue(dataset),
|
||||
} as any;
|
||||
function mockFetch(body: string, status = 200): void {
|
||||
fetchMock.mockImplementation((input: string): any => ({
|
||||
text: (): any => body,
|
||||
url: input,
|
||||
status,
|
||||
headers: { get: (): any => 'text/turtle' },
|
||||
}));
|
||||
}
|
||||
|
||||
fetchMock.mockResolvedValue(datasetResponse);
|
||||
});
|
||||
|
||||
it('errors if there was an issue fetching.', async(): Promise<void> => {
|
||||
fetchMock.mockRejectedValueOnce(new Error('Invalid webId!'));
|
||||
await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}`);
|
||||
it('errors if the status code is not 200.', async(): Promise<void> => {
|
||||
mockFetch('Invalid URL!', 404);
|
||||
await expect(fetchDataset(url, converter)).rejects.toThrow(`Unable to access data at ${url}`);
|
||||
expect(fetchMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
|
||||
it('errors if there was an issue parsing the returned RDF.', async(): Promise<void> => {
|
||||
(datasetResponse.dataset as jest.Mock).mockRejectedValueOnce(new Error('Invalid RDF!'));
|
||||
await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}`);
|
||||
it('errors if there is no content-type.', async(): Promise<void> => {
|
||||
fetchMock.mockResolvedValueOnce({ url, text: (): any => '', status: 200, headers: { get: jest.fn() }});
|
||||
await expect(fetchDataset(url, converter)).rejects.toThrow(`Unable to access data at ${url}`);
|
||||
expect(fetchMock).toHaveBeenCalledWith(url);
|
||||
});
|
||||
|
||||
it('returns the resulting Dataset.', async(): Promise<void> => {
|
||||
await expect(fetchDataset(url)).resolves.toBe(dataset);
|
||||
it('returns a Representation with quads.', async(): Promise<void> => {
|
||||
mockFetch('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
|
||||
const representation = await fetchDataset(url, converter);
|
||||
await expect(arrayifyStream(representation.data)).resolves.toEqual([
|
||||
quad(namedNode('http://test.com/s'), namedNode('http://test.com/p'), namedNode('http://test.com/o')),
|
||||
]);
|
||||
});
|
||||
|
||||
it('accepts Response objects as input.', async(): Promise<void> => {
|
||||
mockFetch('<http://test.com/s> <http://test.com/p> <http://test.com/o>.');
|
||||
const response = await fetch(url);
|
||||
const body = await response.text();
|
||||
const representation = await fetchDataset(response, converter, body);
|
||||
await expect(arrayifyStream(representation.data)).resolves.toEqual([
|
||||
quad(namedNode('http://test.com/s'), namedNode('http://test.com/p'), namedNode('http://test.com/o')),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user