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",
|
"comment": "Determines WebID ownership by requesting a specific value to be added to the WebID document",
|
||||||
"@id": "urn:solid-server:auth:password:OwnershipValidator",
|
"@id": "urn:solid-server:auth:password:OwnershipValidator",
|
||||||
"@type": "TokenOwnershipValidator",
|
"@type": "TokenOwnershipValidator",
|
||||||
|
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" },
|
||||||
"storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
|
"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": {
|
"dependencies": {
|
||||||
"@comunica/actor-init-sparql": "^1.21.3",
|
"@comunica/actor-init-sparql": "^1.21.3",
|
||||||
"@rdfjs/data-model": "^1.2.0",
|
"@rdfjs/data-model": "^1.2.0",
|
||||||
"@rdfjs/fetch": "^2.1.0",
|
|
||||||
"@solid/access-token-verifier": "^0.10.0",
|
"@solid/access-token-verifier": "^0.10.0",
|
||||||
"@types/arrayify-stream": "^1.0.0",
|
"@types/arrayify-stream": "^1.0.0",
|
||||||
"@types/async-lock": "^1.1.2",
|
"@types/async-lock": "^1.1.2",
|
||||||
@ -93,8 +92,6 @@
|
|||||||
"@types/pump": "^1.1.1",
|
"@types/pump": "^1.1.1",
|
||||||
"@types/punycode": "^2.1.0",
|
"@types/punycode": "^2.1.0",
|
||||||
"@types/rdf-js": "^4.0.2",
|
"@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/redis": "^2.8.30",
|
||||||
"@types/redlock": "^4.0.1",
|
"@types/redlock": "^4.0.1",
|
||||||
"@types/sparqljs": "^3.1.2",
|
"@types/sparqljs": "^3.1.2",
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
import type { Quad } from 'n3';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
|
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||||
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
import { fetchDataset } from '../../util/FetchUtil';
|
import { fetchDataset } from '../../util/FetchUtil';
|
||||||
@ -15,11 +17,13 @@ const { literal, namedNode, quad } = DataFactory;
|
|||||||
export class TokenOwnershipValidator extends OwnershipValidator {
|
export class TokenOwnershipValidator extends OwnershipValidator {
|
||||||
protected readonly logger = getLoggerFor(this);
|
protected readonly logger = getLoggerFor(this);
|
||||||
|
|
||||||
|
private readonly converter: RepresentationConverter;
|
||||||
private readonly storage: ExpiringStorage<string, string>;
|
private readonly storage: ExpiringStorage<string, string>;
|
||||||
private readonly expiration: number;
|
private readonly expiration: number;
|
||||||
|
|
||||||
public constructor(storage: ExpiringStorage<string, string>, expiration = 30) {
|
public constructor(converter: RepresentationConverter, storage: ExpiringStorage<string, string>, expiration = 30) {
|
||||||
super();
|
super();
|
||||||
|
this.converter = converter;
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
// Convert minutes to milliseconds
|
// Convert minutes to milliseconds
|
||||||
this.expiration = expiration * 60 * 1000;
|
this.expiration = expiration * 60 * 1000;
|
||||||
@ -37,9 +41,7 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify if the token can be found in the WebId
|
// Verify if the token can be found in the WebId
|
||||||
const dataset = await fetchDataset(webId);
|
if (!await this.hasToken(webId, token)) {
|
||||||
const expectedQuad = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
|
||||||
if (!dataset.has(expectedQuad)) {
|
|
||||||
this.throwError(webId, token);
|
this.throwError(webId, token);
|
||||||
}
|
}
|
||||||
this.logger.debug(`Verified ownership of ${webId}`);
|
this.logger.debug(`Verified ownership of ${webId}`);
|
||||||
@ -60,6 +62,22 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
|||||||
return v4();
|
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.
|
* Throws an error containing the description of which triple is needed for verification.
|
||||||
*/
|
*/
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import type { Response } from 'cross-fetch';
|
import type { Response } from 'cross-fetch';
|
||||||
import { fetch } 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 type { Adapter, AdapterPayload } from 'oidc-provider';
|
||||||
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
|
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
|
||||||
import { INTERNAL_QUADS } from '../../util/ContentTypes';
|
|
||||||
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
import { createErrorMessage } from '../../util/errors/ErrorUtil';
|
||||||
|
import { fetchDataset } from '../../util/FetchUtil';
|
||||||
|
import { OIDC } from '../../util/Vocabularies';
|
||||||
import type { AdapterFactory } from './AdapterFactory';
|
import type { AdapterFactory } from './AdapterFactory';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
@ -84,29 +84,27 @@ export class WebIdAdapter implements Adapter {
|
|||||||
return payload;
|
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> {
|
private async parseRdfWebId(data: string, id: string, response: Response): Promise<AdapterPayload> {
|
||||||
const contentType = response.headers.get('content-type');
|
const representation = await fetchDataset(response, this.converter, data);
|
||||||
if (!contentType) {
|
|
||||||
throw new Error(`No content-type received for client WebID ${id}`);
|
// 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 {
|
return {
|
||||||
client_id: id,
|
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 { Response } from 'cross-fetch';
|
||||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
import { fetch } from 'cross-fetch';
|
||||||
import type { Dataset } from 'rdf-js';
|
import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
|
||||||
|
import type { Representation } from '../ldp/representation/Representation';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
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');
|
const logger = getLoggerFor('FetchUtil');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches an RDF dataset from the given URL.
|
* 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> {
|
export async function fetchDataset(url: string, converter: RepresentationConverter): Promise<Representation>;
|
||||||
let rawResponse: DatasetResponse<Dataset>;
|
export async function fetchDataset(response: Response, converter: RepresentationConverter, body?: string):
|
||||||
try {
|
Promise<Representation>;
|
||||||
rawResponse = (await fetch(url)) as DatasetResponse<Dataset>;
|
export async function fetchDataset(input: string | Response, converter: RepresentationConverter, body?: string):
|
||||||
} catch (err: unknown) {
|
Promise<Representation> {
|
||||||
logger.error(`Cannot fetch ${url}: ${createErrorMessage(err)}`);
|
let response: Response;
|
||||||
throw new Error(`Cannot fetch ${url}`);
|
if (typeof input === 'string') {
|
||||||
|
response = await fetch(input);
|
||||||
|
} else {
|
||||||
|
response = input;
|
||||||
}
|
}
|
||||||
let dataset: Dataset;
|
if (!body) {
|
||||||
try {
|
body = await response.text();
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
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',
|
'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#',
|
export const PIM = createUriAndTermNamespace('http://www.w3.org/ns/pim/space#',
|
||||||
'Storage',
|
'Storage',
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import fetch from '@rdfjs/fetch';
|
import { fetch } from 'cross-fetch';
|
||||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { Quad } from 'n3';
|
import type { Quad } from 'n3';
|
||||||
import type { DatasetCore } from 'rdf-js';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
|
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
|
||||||
|
import { RdfToQuadConverter } from '../../../../src/storage/conversion/RdfToQuadConverter';
|
||||||
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
|
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
|
||||||
import { SOLID } from '../../../../src/util/Vocabularies';
|
import { SOLID } from '../../../../src/util/Vocabularies';
|
||||||
const { literal, namedNode, quad } = DataFactory;
|
const { literal, namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
jest.mock('@rdfjs/fetch');
|
jest.mock('cross-fetch');
|
||||||
jest.mock('uuid');
|
jest.mock('uuid');
|
||||||
|
|
||||||
function quadToString(qq: Quad): string {
|
function quadToString(qq: Quad): string {
|
||||||
@ -24,26 +23,25 @@ describe('A TokenOwnershipValidator', (): void => {
|
|||||||
const fetchMock: jest.Mock = fetch as any;
|
const fetchMock: jest.Mock = fetch as any;
|
||||||
const webId = 'http://alice.test.com/#me';
|
const webId = 'http://alice.test.com/#me';
|
||||||
const token = 'randomlyGeneratedToken';
|
const token = 'randomlyGeneratedToken';
|
||||||
let rawResponse: DatasetResponse<DatasetCore>;
|
|
||||||
let dataset: DatasetCore;
|
|
||||||
let triples: Quad[];
|
|
||||||
const tokenTriple = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
const tokenTriple = quad(namedNode(webId), SOLID.terms.oidcIssuerRegistrationToken, literal(token));
|
||||||
|
const tokenString = `${quadToString(tokenTriple)} .`;
|
||||||
|
const converter = new RdfToQuadConverter();
|
||||||
let storage: ExpiringStorage<string, string>;
|
let storage: ExpiringStorage<string, string>;
|
||||||
let validator: TokenOwnershipValidator;
|
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> => {
|
beforeEach(async(): Promise<void> => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
jest.spyOn(Date, 'now').mockReturnValue(now);
|
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||||
(v4 as jest.Mock).mockReturnValue(token);
|
(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>();
|
const map = new Map<string, any>();
|
||||||
storage = {
|
storage = {
|
||||||
@ -52,40 +50,41 @@ describe('A TokenOwnershipValidator', (): void => {
|
|||||||
delete: jest.fn().mockImplementation((key: string): any => map.delete(key)),
|
delete: jest.fn().mockImplementation((key: string): any => map.delete(key)),
|
||||||
} as any;
|
} 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> => {
|
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
|
// Even if the token is in the WebId, it will error since it's not in the storage
|
||||||
triples = [ tokenTriple ];
|
mockFetch(tokenString);
|
||||||
await expect(validator.handle({ webId })).rejects.toThrow(quadToString(tokenTriple));
|
await expect(validator.handle({ webId })).rejects.toThrow(tokenString);
|
||||||
expect(fetch).toHaveBeenCalledTimes(0);
|
expect(fetch).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if the expected triple is missing.', async(): Promise<void> => {
|
it('errors if the expected triple is missing.', async(): Promise<void> => {
|
||||||
// First call will add the token to the storage
|
// 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);
|
expect(fetch).toHaveBeenCalledTimes(0);
|
||||||
// Second call will fetch the WebId
|
// 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);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves if the WebId contains the verification triple.', async(): Promise<void> => {
|
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
|
// 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
|
// Second call will succeed since it has the verification triple
|
||||||
await expect(validator.handle({ webId })).resolves.toBeUndefined();
|
await expect(validator.handle({ webId })).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails if the WebId contains the wrong verification triple.', async(): Promise<void> => {
|
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
|
// 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
|
// 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 => {
|
describe('A WebIdAdapterFactory', (): void => {
|
||||||
const fetchMock: jest.Mock = fetch as any;
|
const fetchMock: jest.Mock = fetch as any;
|
||||||
const id = 'https://app.test.com/card#me';
|
const id = 'https://app.test.com/card#me';
|
||||||
let data: string;
|
|
||||||
let json: any;
|
let json: any;
|
||||||
let rdf: string;
|
let rdf: string;
|
||||||
let source: Adapter;
|
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>.`;
|
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 = {
|
source = {
|
||||||
upsert: jest.fn(),
|
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> => {
|
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`);
|
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> => {
|
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({
|
await expect(adapter.find(id)).resolves.toEqual({
|
||||||
...json,
|
...json,
|
||||||
token_endpoint_auth_method: 'none',
|
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> => {
|
it('errors if there is a client_id mismatch.', async(): Promise<void> => {
|
||||||
json.client_id = 'someone else';
|
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
|
await expect(adapter.find(id)).rejects
|
||||||
.toThrow('The client registration `client_id` field must match the client WebID');
|
.toThrow('The client registration `client_id` field must match the client WebID');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can handle a valid RDF response.', async(): Promise<void> => {
|
it('can handle a valid RDF response.', async(): Promise<void> => {
|
||||||
fetchMock.mockResolvedValueOnce(
|
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({
|
await expect(adapter.find(id)).resolves.toEqual({
|
||||||
client_id: id,
|
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' },
|
'http://randomField': { '@value': 'this will not be there since RDF parsing only takes preset fields' },
|
||||||
};
|
};
|
||||||
fetchMock.mockResolvedValueOnce(
|
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({
|
await expect(adapter.find(id)).resolves.toEqual({
|
||||||
client_id: id,
|
client_id: id,
|
||||||
@ -158,9 +160,9 @@ describe('A WebIdAdapterFactory', (): void => {
|
|||||||
|
|
||||||
it('errors if there is no content-type.', async(): Promise<void> => {
|
it('errors if there is no content-type.', async(): Promise<void> => {
|
||||||
fetchMock.mockResolvedValueOnce(
|
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
|
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 arrayifyStream from 'arrayify-stream';
|
||||||
import type { DatasetResponse } from '@rdfjs/fetch-lite';
|
import { fetch } from 'cross-fetch';
|
||||||
import type { Dataset } from 'rdf-js';
|
import { DataFactory } from 'n3';
|
||||||
|
import { RdfToQuadConverter } from '../../../src/storage/conversion/RdfToQuadConverter';
|
||||||
import { fetchDataset } from '../../../src/util/FetchUtil';
|
import { fetchDataset } from '../../../src/util/FetchUtil';
|
||||||
|
const { namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
jest.mock('@rdfjs/fetch');
|
jest.mock('cross-fetch');
|
||||||
|
|
||||||
describe('FetchUtil', (): void => {
|
describe('FetchUtil', (): void => {
|
||||||
describe('#fetchDataset', (): void => {
|
describe('#fetchDataset', (): void => {
|
||||||
const fetchMock: jest.Mock = fetch as any;
|
const fetchMock: jest.Mock = fetch as any;
|
||||||
const url = 'http://test.com/foo';
|
const url = 'http://test.com/foo';
|
||||||
let datasetResponse: DatasetResponse<Dataset>;
|
const converter = new RdfToQuadConverter();
|
||||||
const dataset: Dataset = {} as any;
|
|
||||||
|
|
||||||
beforeEach(async(): Promise<void> => {
|
function mockFetch(body: string, status = 200): void {
|
||||||
datasetResponse = {
|
fetchMock.mockImplementation((input: string): any => ({
|
||||||
dataset: jest.fn().mockReturnValue(dataset),
|
text: (): any => body,
|
||||||
} as any;
|
url: input,
|
||||||
|
status,
|
||||||
|
headers: { get: (): any => 'text/turtle' },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
fetchMock.mockResolvedValue(datasetResponse);
|
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}`);
|
||||||
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}`);
|
|
||||||
expect(fetchMock).toHaveBeenCalledWith(url);
|
expect(fetchMock).toHaveBeenCalledWith(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if there was an issue parsing the returned RDF.', async(): Promise<void> => {
|
it('errors if there is no content-type.', async(): Promise<void> => {
|
||||||
(datasetResponse.dataset as jest.Mock).mockRejectedValueOnce(new Error('Invalid RDF!'));
|
fetchMock.mockResolvedValueOnce({ url, text: (): any => '', status: 200, headers: { get: jest.fn() }});
|
||||||
await expect(fetchDataset(url)).rejects.toThrow(`Cannot fetch ${url}`);
|
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> => {
|
it('returns a Representation with quads.', async(): Promise<void> => {
|
||||||
await expect(fetchDataset(url)).resolves.toBe(dataset);
|
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