Skip to content

Commit 83c01f8

Browse files
committed
test: add unit tests + coverage
1 parent dea13c7 commit 83c01f8

File tree

6 files changed

+421
-0
lines changed

6 files changed

+421
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [x] make https://github.com/woutslabbinck/community-server/tree/feat/shape-support work in this directory
66
- [x] override link header attempt
77
- [ ] make the tests work
8+
- [ ] add README.md
89
- [ ] check everything at https://trello.com/c/JzMVInXw/81-shape-support-pr-after-metadata-editing
910
- [ ] ask feedback Joachim (zal pas na Januari zijn)
1011
- [ ] publish op npm: https://www.npmjs.com/org/community-solid-server

jest.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ module.exports = {
1212
'js',
1313
],
1414
testEnvironment: 'node',
15+
collectCoverage: true,
16+
coverageReporters: [ 'text', 'lcov' ],
17+
coveragePathIgnorePatterns: [
18+
'/dist/',
19+
'/node_modules/',
20+
'/test/',
21+
],
1522
// Make sure our tests have enough time to start a server
1623
testTimeout: 60000,
1724
};

jest.coverage.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const jestConfig = require('./jest.config');
2+
3+
module.exports = {
4+
...jestConfig,
5+
coverageThreshold: {
6+
'./src': {
7+
branches: 100,
8+
functions: 100,
9+
lines: 100,
10+
statements: 100,
11+
},
12+
},
13+
};
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { DataFactory } from 'n3';
2+
import type { AuxiliaryStrategy } from '@solid/community-server'
3+
import { BasicRepresentation } from '@solid/community-server'
4+
import type { Representation } from '@solid/community-server'
5+
import type { RepresentationConverter } from '@solid/community-server'
6+
import type { ResourceStore } from '@solid/community-server'
7+
import { ShapeValidationStore } from '../../../src/storage/ShapeValidationStore';
8+
import type { ShapeValidator } from '../../../src/storage/validators/ShapeValidator';
9+
import { INTERNAL_QUADS } from '@solid/community-server'
10+
import { BadRequestHttpError } from '@solid/community-server'
11+
import { SingleRootIdentifierStrategy } from '@solid/community-server'
12+
import { guardedStreamFrom } from '@solid/community-server'
13+
import { LDP } from '../../../src/util/Vocabularies';
14+
import { SimpleSuffixStrategy } from '../../util/SimpleSuffixStrategy';
15+
import namedNode = DataFactory.namedNode;
16+
import quad = DataFactory.quad;
17+
18+
describe('ShapeValidationStore', (): void => {
19+
let validator: ShapeValidator;
20+
let metadataStrategy: AuxiliaryStrategy;
21+
let source: ResourceStore;
22+
let converter: RepresentationConverter;
23+
const root = 'http://test.com/';
24+
const identifierStrategy = new SingleRootIdentifierStrategy(root);
25+
const metaSuffix = '.meta';
26+
let store: ShapeValidationStore;
27+
28+
beforeEach((): void => {
29+
source = {
30+
getRepresentation: jest.fn(async(): Promise<any> => new BasicRepresentation()),
31+
addResource: jest.fn(async(): Promise<any> => 'add'),
32+
setRepresentation: jest.fn(async(): Promise<any> => 'set'),
33+
} as unknown as ResourceStore;
34+
35+
metadataStrategy = new SimpleSuffixStrategy(metaSuffix);
36+
converter = {
37+
handleSafe: jest.fn(),
38+
canHandle: jest.fn(),
39+
handle: jest.fn(),
40+
};
41+
validator = {
42+
handleSafe: jest.fn(),
43+
canHandle: jest.fn(),
44+
handle: jest.fn(),
45+
};
46+
store = new ShapeValidationStore(source, identifierStrategy, metadataStrategy, converter, validator);
47+
});
48+
49+
describe('adding a Resource', (): void => {
50+
it('calls the validator with the correct arguments.', async(): Promise<void> => {
51+
const resourceID = { path: root };
52+
const representation = new BasicRepresentation();
53+
const parentRepresentation = new BasicRepresentation();
54+
source.getRepresentation = jest.fn().mockReturnValue(parentRepresentation);
55+
56+
await expect(store.addResource(resourceID, representation)).resolves.toBe('add');
57+
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
58+
expect(validator.handleSafe).toHaveBeenCalledWith({ parentRepresentation, representation });
59+
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
60+
expect(source.getRepresentation).toHaveBeenLastCalledWith(resourceID, {});
61+
expect(source.addResource).toHaveBeenCalledTimes(1);
62+
expect(source.addResource).toHaveBeenLastCalledWith(resourceID, representation, undefined);
63+
});
64+
});
65+
66+
describe('setting a Representation', (): void => {
67+
let parentRepresentation: Representation;
68+
let representation: Representation;
69+
const shapeURL = `${root}shape`;
70+
const containerURL = `${root}container/`;
71+
const metadataURL = containerURL + metaSuffix;
72+
const shapeConstraintQuad = quad(namedNode(containerURL), LDP.terms.constrainedBy, namedNode(shapeURL));
73+
const shapeConstraintQuad2 = quad(namedNode(containerURL), LDP.terms.constrainedBy, namedNode(`${shapeURL}1`));
74+
let metadataRepresentation: Representation =
75+
76+
beforeEach((): void => {
77+
representation = new BasicRepresentation();
78+
79+
parentRepresentation = new BasicRepresentation();
80+
source.getRepresentation = jest.fn().mockReturnValue(parentRepresentation);
81+
82+
metadataRepresentation = new BasicRepresentation(
83+
guardedStreamFrom([ shapeConstraintQuad ]),
84+
{ path: metadataURL },
85+
INTERNAL_QUADS,
86+
);
87+
});
88+
89+
it('calls the source setRepresentation when the resource ID is the root.', async(): Promise<void> => {
90+
const resourceID = { path: root };
91+
92+
await expect(store.setRepresentation(resourceID, representation)).resolves.toBe('set');
93+
});
94+
95+
it('calls the validator with the correct arguments.', async(): Promise<void> => {
96+
const resourceID = { path: `${root}resource.ttl` };
97+
98+
await expect(store.setRepresentation(resourceID, representation)).resolves.toBe('set');
99+
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
100+
expect(validator.handleSafe).toHaveBeenCalledWith({ parentRepresentation, representation });
101+
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
102+
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: root }, {});
103+
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
104+
expect(source.setRepresentation).toHaveBeenLastCalledWith(resourceID, representation, undefined);
105+
});
106+
107+
it('errors when the validation fails.', async(): Promise<void> => {
108+
const resourceID = { path: `${root}resource.ttl` };
109+
validator.handleSafe = jest.fn().mockRejectedValue(new BadRequestHttpError());
110+
111+
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(BadRequestHttpError);
112+
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
113+
expect(validator.handleSafe).toHaveBeenCalledWith({ parentRepresentation, representation });
114+
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
115+
expect(source.getRepresentation).toHaveBeenLastCalledWith({ path: root }, {});
116+
});
117+
118+
it('allows adding a shape constraint when none is currently present.', async(): Promise<void> => {
119+
const resourceID = { path: containerURL + metaSuffix };
120+
121+
converter.handleSafe = jest.fn().mockReturnValueOnce(metadataRepresentation).mockReturnValueOnce(
122+
new BasicRepresentation(guardedStreamFrom([ ]), resourceID, INTERNAL_QUADS),
123+
);
124+
125+
await expect(store.setRepresentation(resourceID, representation)).resolves.toBe('set');
126+
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
127+
expect(validator.handleSafe).toHaveBeenCalledWith({ parentRepresentation, representation });
128+
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
129+
expect(source.getRepresentation).toHaveBeenNthCalledWith(1, resourceID, {});
130+
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
131+
expect(source.setRepresentation).toHaveBeenLastCalledWith(resourceID, representation, undefined);
132+
});
133+
134+
it('errors when adding multiple shape constraints.', async(): Promise<void> => {
135+
const resourceID = { path: containerURL + metaSuffix };
136+
137+
converter.handleSafe = jest.fn().mockReturnValueOnce(
138+
new BasicRepresentation(guardedStreamFrom([
139+
shapeConstraintQuad, shapeConstraintQuad2,
140+
]), resourceID, INTERNAL_QUADS),
141+
).mockReturnValueOnce(
142+
new BasicRepresentation(guardedStreamFrom([
143+
]), resourceID, INTERNAL_QUADS),
144+
);
145+
146+
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
147+
new BadRequestHttpError('A container can only be constrained by at most one shape resource.'),
148+
);
149+
expect(source.getRepresentation).toHaveBeenCalledTimes(1);
150+
expect(source.getRepresentation).toHaveBeenCalledWith(resourceID, {});
151+
});
152+
153+
it('errors when adding a shape constraint when resources are already present in the container.',
154+
async(): Promise<void> => {
155+
const resourceID = { path: containerURL + metaSuffix };
156+
converter.handleSafe = jest.fn().mockReturnValueOnce(
157+
new BasicRepresentation(guardedStreamFrom([
158+
shapeConstraintQuad,
159+
quad(namedNode(containerURL), LDP.terms.contains, namedNode(`${containerURL}resource`)),
160+
]), resourceID, INTERNAL_QUADS),
161+
).mockReturnValueOnce(
162+
new BasicRepresentation(guardedStreamFrom([
163+
]), resourceID, INTERNAL_QUADS),
164+
);
165+
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(new BadRequestHttpError(
166+
'A container can only be constrained when there are no resources present in that container.',
167+
));
168+
});
169+
170+
it('allows adding the same shape constraint that was already present and some resources are present.',
171+
async(): Promise<void> => {
172+
const resourceID = { path: containerURL + metaSuffix };
173+
174+
converter.handleSafe = jest.fn().mockReturnValueOnce(
175+
new BasicRepresentation(guardedStreamFrom([
176+
shapeConstraintQuad,
177+
quad(namedNode(containerURL), LDP.terms.contains, namedNode(`${containerURL}resource`)),
178+
]), resourceID, INTERNAL_QUADS),
179+
).mockReturnValueOnce(metadataRepresentation);
180+
181+
await expect(store.setRepresentation(resourceID, representation)).resolves.toBe('set');
182+
expect(validator.handleSafe).toHaveBeenCalledTimes(1);
183+
expect(validator.handleSafe).toHaveBeenCalledWith({ parentRepresentation, representation });
184+
expect(source.getRepresentation).toHaveBeenCalledTimes(2);
185+
expect(source.getRepresentation).toHaveBeenNthCalledWith(1, resourceID, {});
186+
expect(source.setRepresentation).toHaveBeenCalledTimes(1);
187+
expect(source.setRepresentation).toHaveBeenLastCalledWith(resourceID, representation, undefined);
188+
});
189+
190+
it('errors when changing the shape constraint when some resources are present.', async(): Promise<void> => {
191+
const resourceID = { path: containerURL + metaSuffix };
192+
193+
converter.handleSafe = jest.fn().mockReturnValueOnce(
194+
new BasicRepresentation(guardedStreamFrom([
195+
shapeConstraintQuad2,
196+
quad(namedNode(containerURL), LDP.terms.contains, namedNode(`${containerURL}resource`)),
197+
]), resourceID, INTERNAL_QUADS),
198+
).mockReturnValueOnce(metadataRepresentation);
199+
await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(new BadRequestHttpError(
200+
'A container can only be constrained when there are no resources present in that container.',
201+
));
202+
});
203+
});
204+
});
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { DataFactory } from 'n3';
2+
import type { AuxiliaryStrategy } from '@solid/community-server';
3+
import {BasicRepresentation, RDF} from '@solid/community-server';
4+
import type { Representation } from '@solid/community-server';
5+
import { RepresentationMetadata } from '@solid/community-server';
6+
import type { ResourceIdentifier } from '@solid/community-server';
7+
import type { RepresentationConverter } from '@solid/community-server';
8+
import { ShaclValidator } from '../../../../src/storage/validators/ShaclValidator';
9+
import type { ShapeValidatorInput } from '../../../../src/storage/validators/ShapeValidator';
10+
import { INTERNAL_QUADS } from '@solid/community-server';
11+
import { BadRequestHttpError } from '@solid/community-server';
12+
import { InternalServerError } from '@solid/community-server';
13+
import { NotImplementedHttpError } from '@solid/community-server';
14+
import { fetchDataset } from '@solid/community-server';
15+
import { guardedStreamFrom } from '@solid/community-server';
16+
import { LDP, SH } from '../../../../src/util/Vocabularies';
17+
import { SimpleSuffixStrategy } from '../../../util/SimpleSuffixStrategy';
18+
const { namedNode, quad, literal } = DataFactory;
19+
20+
jest.mock('@solid/community-server/dist/util/FetchUtil', (): any => ({
21+
fetchDataset: jest.fn<string, any>(),
22+
}));
23+
24+
describe('ShaclValidator', (): void => {
25+
const root = 'http://example.org/';
26+
const shapeUrl = `${root}shape`;
27+
const auxiliarySuffix = '.dummy';
28+
let shape: Representation;
29+
let parentRepresentation: Representation;
30+
let representationToValidate: Representation;
31+
let converter: RepresentationConverter;
32+
let validator: ShaclValidator;
33+
let auxiliaryStrategy: AuxiliaryStrategy;
34+
let input: ShapeValidatorInput;
35+
36+
beforeEach((): void => {
37+
const containerMetadata: RepresentationMetadata = new RepresentationMetadata({ path: root });
38+
containerMetadata.addQuad(namedNode(root), LDP.terms.constrainedBy, namedNode(shapeUrl));
39+
parentRepresentation = new BasicRepresentation();
40+
parentRepresentation.metadata = containerMetadata;
41+
representationToValidate = new BasicRepresentation(guardedStreamFrom([
42+
quad(namedNode('http://example.org/a'), RDF.terms.type, namedNode('http://example.org/c')),
43+
quad(namedNode('http://example.org/a'), namedNode('http://xmlns.com/foaf/0.1/name'), literal('Test')),
44+
]), INTERNAL_QUADS);
45+
46+
const shapeIdentifier: ResourceIdentifier = { path: `${shapeUrl}` };
47+
shape = new BasicRepresentation(guardedStreamFrom([
48+
quad(namedNode('http://example.org/exampleshape'), RDF.terms.type, namedNode('http://www.w3.org/ns/shacl#NodeShape')),
49+
quad(namedNode('http://example.org/exampleshape'), SH.terms.targetClass, namedNode('http://example.org/c')),
50+
quad(namedNode('http://example.org/exampleshape'), namedNode('http://www.w3.org/ns/shacl#property'), namedNode('http://example.org/property')),
51+
quad(namedNode('http://example.org/property'), namedNode('http://www.w3.org/ns/shacl#path'), namedNode('http://xmlns.com/foaf/0.1/name')),
52+
quad(namedNode('http://example.org/property'), namedNode('http://www.w3.org/ns/shacl#minCount'), literal(1)),
53+
quad(namedNode('http://example.org/property'), namedNode('http://www.w3.org/ns/shacl#maxCount'), literal(1)),
54+
quad(namedNode('http://example.org/property'), namedNode('http://www.w3.org/ns/shacl#datatype'), namedNode('http://www.w3.org/2001/XMLSchema#string')),
55+
]), shapeIdentifier, INTERNAL_QUADS);
56+
57+
converter = {
58+
handleSafe: jest.fn((): Promise<Representation> => Promise.resolve(representationToValidate)),
59+
canHandle: jest.fn(),
60+
handle: jest.fn(),
61+
};
62+
63+
auxiliaryStrategy = new SimpleSuffixStrategy(auxiliarySuffix);
64+
validator = new ShaclValidator(converter, auxiliaryStrategy);
65+
66+
input = {
67+
parentRepresentation,
68+
representation: representationToValidate,
69+
};
70+
(fetchDataset as jest.Mock).mockReturnValue(Promise.resolve(shape));
71+
});
72+
73+
afterEach((): void => {
74+
jest.clearAllMocks();
75+
});
76+
77+
it('throws error if the parent container is not constrained by a shape.', async(): Promise<void> => {
78+
input.parentRepresentation = new BasicRepresentation();
79+
await expect(validator.canHandle(input)).rejects.toThrow(Error);
80+
});
81+
82+
it('does not validate when the parent container is not constrained by a shape.', async(): Promise<void> => {
83+
input.parentRepresentation = new BasicRepresentation();
84+
await expect(validator.handleSafe(input)).resolves.toBeUndefined();
85+
expect(converter.handleSafe).toHaveBeenCalledTimes(0);
86+
expect(fetchDataset).toHaveBeenCalledTimes(0);
87+
});
88+
89+
it('fetches the shape and validates the representation.', async(): Promise<void> => {
90+
await expect(validator.handleSafe(input)).resolves.toBeUndefined();
91+
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
92+
expect(fetchDataset).toHaveBeenCalledTimes(1);
93+
expect(fetchDataset).toHaveBeenLastCalledWith(shapeUrl);
94+
});
95+
96+
it('throws error when the converter fails.', async(): Promise<void> => {
97+
const error = new BadRequestHttpError('error');
98+
converter.handleSafe = jest.fn().mockImplementation((): void => {
99+
throw error;
100+
});
101+
await expect(validator.handleSafe(input)).rejects.toThrow(error);
102+
});
103+
104+
it('throws error when the converter fails due to non-RDF input to validate.', async(): Promise<void> => {
105+
converter.handleSafe = jest.fn().mockImplementationOnce((): void => {
106+
throw new NotImplementedHttpError('error');
107+
});
108+
await expect(validator.handleSafe(input)).rejects.toThrow(BadRequestHttpError);
109+
});
110+
111+
it('throws error when the converter fails due empty input to validate.', async(): Promise<void> => {
112+
// This happens in the case that an attempt is made to add a new container within the constrained container.
113+
converter.handleSafe = jest.fn().mockImplementationOnce((): void => {
114+
throw new InternalServerError('error');
115+
});
116+
await expect(validator.handleSafe(input)).rejects.toThrow(BadRequestHttpError);
117+
});
118+
119+
it('does not execute validation when the target resource is an auxiliary resource.', async(): Promise<void> => {
120+
input.representation.metadata.identifier = namedNode(root + auxiliarySuffix);
121+
122+
await expect(validator.handleSafe(input)).resolves.toBeUndefined();
123+
});
124+
125+
it('throws error when the data does not conform to the shape.', async(): Promise<void> => {
126+
converter.handleSafe = jest.fn((): Promise<Representation> => Promise.resolve(
127+
new BasicRepresentation(guardedStreamFrom([
128+
quad(namedNode('http://example.org/a'), RDF.terms.type, namedNode('http://example.org/c')),
129+
quad(namedNode('http://example.org/a'), namedNode('http://xmlns.com/foaf/0.1/name'), literal(5)),
130+
]), INTERNAL_QUADS),
131+
));
132+
133+
await expect(validator.handleSafe(input)).rejects.toThrow(BadRequestHttpError);
134+
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
135+
expect(fetchDataset).toHaveBeenCalledTimes(1);
136+
expect(fetchDataset).toHaveBeenLastCalledWith(shapeUrl);
137+
});
138+
139+
it('throws error when no nodes not conform to any of the target classes of the shape.', async(): Promise<void> => {
140+
converter.handleSafe = jest.fn((): Promise<Representation> => Promise.resolve(
141+
new BasicRepresentation(guardedStreamFrom([
142+
quad(namedNode('http://example.org/a'), namedNode('http://xmlns.com/foaf/0.1/name'), literal('Test')),
143+
]), INTERNAL_QUADS),
144+
));
145+
146+
await expect(validator.handleSafe(input)).rejects.toThrow(BadRequestHttpError);
147+
expect(converter.handleSafe).toHaveBeenCalledTimes(1);
148+
expect(fetchDataset).toHaveBeenCalledTimes(1);
149+
expect(fetchDataset).toHaveBeenLastCalledWith(shapeUrl);
150+
});
151+
});

0 commit comments

Comments
 (0)