feat: Update templates and generators to support ACP

This commit is contained in:
Joachim Van Herwegen
2022-08-19 11:12:02 +02:00
parent 728617ac77
commit 40f2c8ea42
41 changed files with 800 additions and 271 deletions

View File

@@ -63,7 +63,7 @@ describe('An AcpReader', (): void => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:resource <./>;
acp:accessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;
@@ -89,7 +89,7 @@ describe('An AcpReader', (): void => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:resource <./>;
acp:memberAccessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;
@@ -109,7 +109,7 @@ describe('An AcpReader', (): void => {
const target = { path: joinUrl(baseUrl, 'foo') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:resource <./>;
acp:accessControl [ acp:apply _:controlPolicy ];
acp:memberAccessControl [ acp:apply _:readPolicy ].
_:readPolicy
@@ -122,7 +122,7 @@ describe('An AcpReader', (): void => {
`, baseUrl);
dataMap[target.path] = toQuads(`
[]
acp:resource <./foo> ;
acp:resource <./foo>;
acp:accessControl [ acp:apply _:appendPolicy ].
_:appendPolicy
acp:allow acl:Append;
@@ -143,7 +143,7 @@ describe('An AcpReader', (): void => {
const target2 = { path: joinUrl(baseUrl, 'foo/bar') };
dataMap[baseUrl] = toQuads(`
[]
acp:resource <./> ;
acp:resource <./>;
acp:memberAccessControl [ acp:apply _:policy ].
_:policy
acp:allow acl:Read;

View File

@@ -1,18 +1,20 @@
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
import { BaseResourcesGenerator } from '../../../../src/pods/generate/BaseResourcesGenerator';
import type {
FileIdentifierMapper,
FileIdentifierMapperFactory,
ResourceLink,
} from '../../../../src/storage/mapping/FileIdentifierMapper';
import type { ResourceStore } from '../../../../src/storage/ResourceStore';
import { ensureTrailingSlash, trimTrailingSlashes } from '../../../../src/util/PathUtil';
import { asyncToArray } from '../../../../src/util/IterableUtil';
import { ensureTrailingSlash, joinFilePath, trimTrailingSlashes } from '../../../../src/util/PathUtil';
import { readableToQuads, readableToString } from '../../../../src/util/StreamUtil';
import { HandlebarsTemplateEngine } from '../../../../src/util/templates/HandlebarsTemplateEngine';
import { SimpleSuffixStrategy } from '../../../util/SimpleSuffixStrategy';
import { mockFileSystem } from '../../../util/Util';
jest.mock('fs');
jest.mock('fs-extra');
class DummyFactory implements FileIdentifierMapperFactory {
public async create(base: string, rootFilePath: string): Promise<FileIdentifierMapper> {
@@ -36,20 +38,12 @@ class DummyFactory implements FileIdentifierMapperFactory {
}
}
async function genToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
const arr: T[] = [];
for await (const result of iterable) {
arr.push(result);
}
return arr;
}
describe('A TemplatedResourcesGenerator', (): void => {
describe('A BaseResourcesGenerator', (): void => {
const rootFilePath = '/templates/pod';
// Using handlebars engine since it's smaller than any possible dummy
const metadataStrategy = new SimpleSuffixStrategy('.meta');
let store: jest.Mocked<ResourceStore>;
let generator: TemplatedResourcesGenerator;
let generator: BaseResourcesGenerator;
let cache: { data: any };
const template = '<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.';
const location = { path: 'http://test.com/alice/' };
@@ -61,8 +55,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
hasResource: jest.fn(),
} as any;
generator = new TemplatedResourcesGenerator({
templateFolder: rootFilePath,
generator = new BaseResourcesGenerator({
factory: new DummyFactory(),
templateEngine: new HandlebarsTemplateEngine('http://test.com/'),
metadataStrategy,
@@ -72,7 +65,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
it('fills in a template with the given options.', async(): Promise<void> => {
cache.data = { 'template.hbs': template };
const result = await genToArray(generator.generate(location, { webId }));
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
const id = { path: `${location.path}template` };
expect(identifiers).toEqual([ location, id ]);
@@ -86,7 +79,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
it('creates the necessary containers.', async(): Promise<void> => {
cache.data = { container: { container: { 'template.hbs': template }}};
const result = await genToArray(generator.generate(location, { webId }));
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
const id = { path: `${location.path}container/container/template` };
expect(identifiers).toEqual([
@@ -103,7 +96,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
it('copies the file stream directly if no template extension is found.', async(): Promise<void> => {
cache.data = { noTemplate: template };
const result = await genToArray(generator.generate(location, { webId }));
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
const id = { path: `${location.path}noTemplate` };
expect(identifiers).toEqual([ location, id ]);
@@ -119,7 +112,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
cache.data = { '.meta': meta, container: { 'template.meta': meta, template }};
// Not using options since our dummy template generator generates invalid turtle
const result = await genToArray(generator.generate(location, { webId }));
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
expect(identifiers).toEqual([
location,
@@ -158,7 +151,7 @@ describe('A TemplatedResourcesGenerator', (): void => {
cache.data = { '.meta': meta };
store.hasResource = jest.fn().mockResolvedValue(true);
const result = await genToArray(generator.generate(location, { webId }));
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
expect(identifiers).toEqual([
{ path: `${location.path}.meta` },
@@ -169,4 +162,20 @@ describe('A TemplatedResourcesGenerator', (): void => {
expect(expQuads).toHaveLength(1);
expect(expQuads[0].object.value).toBe('metadata');
});
it('returns no results if the target folder does not exist.', async(): Promise<void> => {
const result = await asyncToArray(generator.generate(joinFilePath(rootFilePath, 'nope'), location, { webId }));
expect(result).toHaveLength(0);
});
it('makes sure the results are sorted.', async(): Promise<void> => {
cache.data = { 'template2.hbs': template, 'template1.hbs': template };
const result = await asyncToArray(generator.generate(rootFilePath, location, { webId }));
const identifiers = result.map((res): ResourceIdentifier => res.identifier);
expect(identifiers).toEqual([
location,
{ path: `${location.path}template1` },
{ path: `${location.path}template2` },
]);
});
});

View File

@@ -0,0 +1,26 @@
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import type { Resource } from '../../../../src/pods/generate/ResourcesGenerator';
import { StaticFolderGenerator } from '../../../../src/pods/generate/StaticFolderGenerator';
import type { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
describe('A StaticFolderGenerator', (): void => {
const location: ResourceIdentifier = { path: 'http://example.com/foo' };
const options = { foo: 'bar' };
const folder = '/data/templates/';
let source: jest.Mocked<TemplatedResourcesGenerator>;
const response: AsyncIterable<Resource> = {} as any;
let generator: StaticFolderGenerator;
beforeEach(async(): Promise<void> => {
source = {
generate: jest.fn().mockReturnValue(response),
};
generator = new StaticFolderGenerator(source, folder);
});
it('calls the source generator with the stored template folder.', async(): Promise<void> => {
expect(generator.generate(location, options)).toBe(response);
expect(source.generate).toHaveBeenCalledTimes(1);
expect(source.generate).toHaveBeenLastCalledWith(folder, location, options);
});
});

View File

@@ -0,0 +1,90 @@
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { Resource } from '../../../../src/pods/generate/ResourcesGenerator';
import { SubfolderResourcesGenerator } from '../../../../src/pods/generate/SubfolderResourcesGenerator';
import type { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
import { asyncToArray } from '../../../../src/util/IterableUtil';
async function* yieldResources(resources: Resource[]): AsyncIterable<Resource> {
yield* resources;
}
function createResource(path: string): Resource {
const identifier = { path };
const representation = new BasicRepresentation('data', 'text/plain');
return { identifier, representation };
}
describe('A SubfolderResourcesGenerator', (): void => {
const templateFolder = '/data/templates/';
const identifier = { path: 'http://example.com/foo' };
const options = { foo: 'bar' };
const subfolders = [ 'base', 'empty', 'extra' ];
let baseResources: Resource[];
let extraResources: Resource[];
let source: jest.Mocked<TemplatedResourcesGenerator>;
let generator: SubfolderResourcesGenerator;
beforeEach(async(): Promise<void> => {
baseResources = [];
extraResources = [];
source = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
generate: jest.fn((folder, loc, opt): AsyncIterable<Resource> => {
if (folder.endsWith('base')) {
return yieldResources(baseResources);
}
if (folder.endsWith('extra')) {
return yieldResources(extraResources);
}
return yieldResources([]);
}),
};
generator = new SubfolderResourcesGenerator(source, subfolders);
});
it('merges the results of the subfolders into one sorted result.', async(): Promise<void> => {
baseResources = [ createResource('a'), createResource('c'), createResource('d'), createResource('f') ];
extraResources = [ createResource('b'), createResource('e'), createResource('g') ];
const resources = await asyncToArray(generator.generate(templateFolder, identifier, options));
expect(resources.map((resource): string => resource.identifier.path)).toEqual([
'a', 'b', 'c', 'd', 'e', 'f', 'g',
]);
expect(source.generate).toHaveBeenCalledTimes(3);
expect(source.generate).toHaveBeenNthCalledWith(1, '/data/templates/base', identifier, options);
expect(source.generate).toHaveBeenNthCalledWith(2, '/data/templates/empty', identifier, options);
expect(source.generate).toHaveBeenNthCalledWith(3, '/data/templates/extra', identifier, options);
});
it('keeps the first result in case of duplicate identifiers.', async(): Promise<void> => {
const resource1 = createResource('foo');
const resource2 = createResource('foo');
baseResources = [ createResource('b'), resource1, createResource('g') ];
extraResources = [ createResource('a'), resource2, createResource('h') ];
const resources = await asyncToArray(generator.generate(templateFolder, identifier, options));
expect(resources.map((resource): string => resource.identifier.path)).toEqual([
'a', 'b', 'foo', 'g', 'h',
]);
expect(resources[2]).toBe(resource1);
expect(resource2.representation.data.destroyed).toBe(true);
});
it('correctly sorts containers.', async(): Promise<void> => {
baseResources = [ createResource('/'), createResource('/container/'),
createResource('/container/foo.acl'), createResource('README.acl') ];
extraResources = [ createResource('/'), createResource('/container/'),
createResource('/container/foo'), createResource('README') ];
const resources = await asyncToArray(generator.generate(templateFolder, identifier, options));
expect(resources.map((resource): string => resource.identifier.path)).toEqual([
'/',
'/container/',
'/container/foo',
'/container/foo.acl',
'README',
'README.acl',
]);
});
});

View File

@@ -1,4 +1,4 @@
import { concat, filter, find, map, reduce } from '../../../src/util/IterableUtil';
import { asyncToArray, concat, filter, find, map, reduce, sortedAsyncMerge } from '../../../src/util/IterableUtil';
describe('IterableUtil', (): void => {
describe('#map', (): void => {
@@ -50,4 +50,32 @@ describe('IterableUtil', (): void => {
expect((): number => reduce(input, (acc, cur): number => acc + cur)).toThrow(TypeError);
});
});
describe('#sortedAsyncMerge', (): void => {
it('sorts the iterables.', async(): Promise<void> => {
// eslint-disable-next-line unicorn/consistent-function-scoping
async function* left(): AsyncIterator<number> {
yield* [ 1, 3, 5, 7, 9 ];
}
// eslint-disable-next-line unicorn/consistent-function-scoping
async function* right(): AsyncIterator<number> {
yield* [ 0, 2, 3, 4 ];
}
await expect(asyncToArray(sortedAsyncMerge([ left(), right() ]))).resolves
.toEqual([ 0, 1, 2, 3, 3, 4, 5, 7, 9 ]);
});
it('accepts a custom comparator.', async(): Promise<void> => {
// eslint-disable-next-line unicorn/consistent-function-scoping
async function* left(): AsyncIterator<string> {
yield* [ 'apple', 'citrus', 'date' ];
}
// eslint-disable-next-line unicorn/consistent-function-scoping
async function* right(): AsyncIterator<string> {
yield* [ 'banana', 'donut' ];
}
await expect(asyncToArray(sortedAsyncMerge([ left(), right() ]))).resolves
.toEqual([ 'apple', 'banana', 'citrus', 'date', 'donut' ]);
});
});
});