mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
Merge branch 'main' into versions/6.0.0
This commit is contained in:
@@ -133,6 +133,12 @@ describe('An http server with middleware', (): void => {
|
||||
expect(splitCommaSeparated(exposed)).toContain('Updates-Via');
|
||||
});
|
||||
|
||||
it('exposes the Www-Authenticate header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Www-Authenticate');
|
||||
});
|
||||
|
||||
it('sends incoming requests to the handler.', async(): Promise<void> => {
|
||||
const response = request(server).get('/').set('Host', 'test.com');
|
||||
expect(response).toBeDefined();
|
||||
|
||||
@@ -86,17 +86,20 @@ const table: [string, string, AM[], AM[] | undefined, string, string, number, nu
|
||||
[ 'PUT', 'C/R', [ AM.write ], undefined, '', TXT, 205, 201 ],
|
||||
[ 'PUT', 'C/R', [ AM.append ], [ AM.write ], '', TXT, 205, 201 ],
|
||||
|
||||
// All PATCH operations with read permissions return 401 instead of 404 if the target does not exist.
|
||||
// This is a consequence of PATCH always creating a resource in case it does not exist.
|
||||
// https://solidproject.org/TR/2021/protocol-20211217#n3-patch
|
||||
// "Start from the RDF dataset in the target document,
|
||||
// or an empty RDF dataset if the target resource does not exist yet."
|
||||
[ 'PATCH', 'C/R', [], undefined, DELETE, N3, 401, 401 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.read ], DELETE, N3, 401, 404 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.read ], DELETE, N3, 401, 401 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.append ], INSERT, N3, 205, 401 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.append ], DELETE, N3, 401, 401 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.write ], INSERT, N3, 205, 401 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.write ], DELETE, N3, 401, 401 ],
|
||||
[ 'PATCH', 'C/R', [ AM.append ], [ AM.write ], INSERT, N3, 205, 201 ],
|
||||
[ 'PATCH', 'C/R', [ AM.append ], [ AM.write ], DELETE, N3, 401, 401 ],
|
||||
// We currently return 409 instead of 404 in case a PATCH has no inserts and C/R does not exist.
|
||||
// This is an agreed upon deviation from the original table
|
||||
[ 'PATCH', 'C/R', [], [ AM.read, AM.write ], DELETE, N3, 205, 409 ],
|
||||
[ 'PATCH', 'C/R', [], [ AM.read, AM.write ], DELETE, N3, 205, 401 ],
|
||||
|
||||
[ 'DELETE', 'C/R', [], undefined, '', '', 401, 401 ],
|
||||
[ 'DELETE', 'C/R', [], [ AM.read ], '', '', 401, 404 ],
|
||||
@@ -105,8 +108,7 @@ const table: [string, string, AM[], AM[] | undefined, string, string, number, nu
|
||||
[ 'DELETE', 'C/R', [ AM.read ], undefined, '', '', 401, 404 ],
|
||||
[ 'DELETE', 'C/R', [ AM.append ], undefined, '', '', 401, 401 ],
|
||||
[ 'DELETE', 'C/R', [ AM.append ], [ AM.read ], '', '', 401, 404 ],
|
||||
// We throw a 404 instead of 401 since we don't yet check if the parent container has read permissions
|
||||
// [ 'DELETE', 'C/R', [ AM.write ], undefined, '', '', 205, 401 ],
|
||||
[ 'DELETE', 'C/R', [ AM.write ], undefined, '', '', 205, 401 ],
|
||||
[ 'DELETE', 'C/R', [ AM.write ], [ AM.read ], '', '', 401, 404 ],
|
||||
[ 'DELETE', 'C/R', [ AM.write ], [ AM.append ], '', '', 401, 401 ],
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { promises as fsPromises } from 'fs';
|
||||
import type { Stats } from 'fs';
|
||||
import fetch from 'cross-fetch';
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { ensureDir, pathExists } from 'fs-extra';
|
||||
import { joinFilePath, joinUrl } from '../../src';
|
||||
import type { App } from '../../src';
|
||||
import { getPort } from '../util/Util';
|
||||
@@ -73,7 +73,7 @@ describe('A quota server', (): void => {
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// We want to use an empty folder as on APFS/Mac folder sizes vary a lot
|
||||
const tempFolder = getTestFolder('quota-temp');
|
||||
await fsPromises.mkdir(tempFolder);
|
||||
await ensureDir(tempFolder);
|
||||
folderSizeTest = await fsPromises.stat(tempFolder);
|
||||
await removeFolder(tempFolder);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { CreateModesExtractor } from '../../../../src/authorization/permissions/CreateModesExtractor';
|
||||
import type { ModesExtractor } from '../../../../src/authorization/permissions/ModesExtractor';
|
||||
import type { AccessMap } from '../../../../src/authorization/permissions/Permissions';
|
||||
import { AccessMode } from '../../../../src/authorization/permissions/Permissions';
|
||||
import type { Operation } from '../../../../src/http/Operation';
|
||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
||||
import type { ResourceSet } from '../../../../src/storage/ResourceSet';
|
||||
import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap';
|
||||
import { compareMaps } from '../../../util/Util';
|
||||
|
||||
describe('A CreateModesExtractor', (): void => {
|
||||
const target: ResourceIdentifier = { path: 'http://example.com/foo' };
|
||||
let operation: Operation;
|
||||
let result: AccessMap;
|
||||
let resourceSet: jest.Mocked<ResourceSet>;
|
||||
let source: jest.Mocked<ModesExtractor>;
|
||||
let extractor: CreateModesExtractor;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = {
|
||||
method: 'PATCH',
|
||||
target,
|
||||
body: new BasicRepresentation(),
|
||||
preferences: {},
|
||||
};
|
||||
|
||||
result = new IdentifierSetMultiMap<AccessMode>([[ target, AccessMode.read ]]);
|
||||
|
||||
resourceSet = {
|
||||
hasResource: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
source = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(result),
|
||||
} as any;
|
||||
|
||||
extractor = new CreateModesExtractor(source, resourceSet);
|
||||
});
|
||||
|
||||
it('checks if the source can handle the input.', async(): Promise<void> => {
|
||||
await expect(extractor.canHandle(operation)).resolves.toBeUndefined();
|
||||
|
||||
source.canHandle.mockRejectedValue(new Error('bad data'));
|
||||
await expect(extractor.canHandle(operation)).rejects.toThrow('bad data');
|
||||
});
|
||||
|
||||
it('does nothing if the resource exists.', async(): Promise<void> => {
|
||||
await expect(extractor.handle(operation)).resolves.toBe(result);
|
||||
compareMaps(result, new IdentifierSetMultiMap([[ target, AccessMode.read ]]));
|
||||
});
|
||||
|
||||
it('adds the create mode if the resource does not exist.', async(): Promise<void> => {
|
||||
resourceSet.hasResource.mockResolvedValue(false);
|
||||
await expect(extractor.handle(operation)).resolves.toBe(result);
|
||||
compareMaps(result, new IdentifierSetMultiMap([[ target, AccessMode.read ], [ target, AccessMode.create ]]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { DeleteParentExtractor } from '../../../../src/authorization/permissions/DeleteParentExtractor';
|
||||
import type { ModesExtractor } from '../../../../src/authorization/permissions/ModesExtractor';
|
||||
import type { AccessMap } from '../../../../src/authorization/permissions/Permissions';
|
||||
import { AccessMode } from '../../../../src/authorization/permissions/Permissions';
|
||||
import type { Operation } from '../../../../src/http/Operation';
|
||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||
import type { ResourceSet } from '../../../../src/storage/ResourceSet';
|
||||
import type { IdentifierStrategy } from '../../../../src/util/identifiers/IdentifierStrategy';
|
||||
import { IdentifierSetMultiMap } from '../../../../src/util/map/IdentifierMap';
|
||||
|
||||
describe('A DeleteParentExtractor', (): void => {
|
||||
const baseUrl = 'http://example.com/';
|
||||
const resource = 'http://example.com/foo';
|
||||
let operation: Operation;
|
||||
let sourceMap: AccessMap;
|
||||
let source: jest.Mocked<ModesExtractor>;
|
||||
let resourceSet: jest.Mocked<ResourceSet>;
|
||||
let identifierStrategy: jest.Mocked<IdentifierStrategy>;
|
||||
let extractor: DeleteParentExtractor;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
operation = {
|
||||
target: { path: resource },
|
||||
method: 'DELETE',
|
||||
preferences: {},
|
||||
body: new BasicRepresentation(),
|
||||
};
|
||||
|
||||
sourceMap = new IdentifierSetMultiMap();
|
||||
|
||||
source = {
|
||||
canHandle: jest.fn(),
|
||||
handle: jest.fn().mockResolvedValue(sourceMap),
|
||||
} as any;
|
||||
|
||||
resourceSet = {
|
||||
hasResource: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
identifierStrategy = {
|
||||
isRootContainer: jest.fn().mockReturnValue(false),
|
||||
getParentContainer: jest.fn().mockReturnValue({ path: baseUrl }),
|
||||
} as any;
|
||||
|
||||
extractor = new DeleteParentExtractor(source, resourceSet, identifierStrategy);
|
||||
});
|
||||
|
||||
it('supports input its source supports.', async(): Promise<void> => {
|
||||
await expect(extractor.canHandle(operation)).resolves.toBeUndefined();
|
||||
|
||||
source.canHandle.mockRejectedValue(new Error('bad data'));
|
||||
await expect(extractor.canHandle(operation)).rejects.toThrow('bad data');
|
||||
});
|
||||
|
||||
it('adds read permission requirements if all conditions are met.', async(): Promise<void> => {
|
||||
sourceMap.add({ path: resource }, AccessMode.delete);
|
||||
identifierStrategy.isRootContainer.mockReturnValue(false);
|
||||
resourceSet.hasResource.mockResolvedValue(false);
|
||||
|
||||
const resultMap = await extractor.handle(operation);
|
||||
expect([ ...resultMap.entries() ]).toHaveLength(2);
|
||||
expect(resultMap.get({ path: baseUrl })).toContain(AccessMode.read);
|
||||
});
|
||||
|
||||
it('does not change the results if no delete access is required.', async(): Promise<void> => {
|
||||
sourceMap.add({ path: resource }, AccessMode.read);
|
||||
identifierStrategy.isRootContainer.mockReturnValue(false);
|
||||
resourceSet.hasResource.mockResolvedValue(false);
|
||||
|
||||
const resultMap = await extractor.handle(operation);
|
||||
expect([ ...resultMap.entries() ]).toHaveLength(1);
|
||||
expect(resultMap.get({ path: baseUrl })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not change the results if the target is the root container.', async(): Promise<void> => {
|
||||
sourceMap.add({ path: resource }, AccessMode.delete);
|
||||
identifierStrategy.isRootContainer.mockReturnValue(true);
|
||||
resourceSet.hasResource.mockResolvedValue(false);
|
||||
|
||||
const resultMap = await extractor.handle(operation);
|
||||
expect([ ...resultMap.entries() ]).toHaveLength(1);
|
||||
expect(resultMap.get({ path: baseUrl })).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not change the results if the target exists.', async(): Promise<void> => {
|
||||
sourceMap.add({ path: resource }, AccessMode.delete);
|
||||
identifierStrategy.isRootContainer.mockReturnValue(false);
|
||||
resourceSet.hasResource.mockResolvedValue(true);
|
||||
|
||||
const resultMap = await extractor.handle(operation);
|
||||
expect([ ...resultMap.entries() ]).toHaveLength(1);
|
||||
expect(resultMap.get({ path: baseUrl })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -101,6 +101,15 @@ const redis: jest.Mocked<Redis & RedisResourceLock & RedisReadWriteLock> = {
|
||||
jest.mock('ioredis', (): any => jest.fn().mockImplementation((): Redis => redis));
|
||||
|
||||
describe('A RedisLocker', (): void => {
|
||||
it('will generate keys with the given namespacePrefix.', async(): Promise<void> => {
|
||||
const identifier = { path: 'http://test.com/resource' };
|
||||
const lockerPrefixed = new RedisLocker('6379', {}, { namespacePrefix: 'MY_PREFIX' });
|
||||
await lockerPrefixed.acquire(identifier);
|
||||
const allLocksPrefixed = Object.keys(store.internal).every((key): boolean => key.startsWith('MY_PREFIX'));
|
||||
await lockerPrefixed.release(identifier);
|
||||
expect(allLocksPrefixed).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('with Read-Write logic', (): void => {
|
||||
const resource1 = { path: 'http://test.com/resource' };
|
||||
const resource2 = { path: 'http://test.com/resource2' };
|
||||
@@ -392,8 +401,8 @@ describe('A RedisLocker', (): void => {
|
||||
const emitter = new EventEmitter();
|
||||
const promise = locker.withWriteLock(resource1, (): any =>
|
||||
new Promise<void>((resolve): any => emitter.on('release', resolve)));
|
||||
await redis.releaseWriteLock(`__RW__${resource1.path}`);
|
||||
await flushPromises();
|
||||
await redis.releaseWriteLock(`__RW__${resource1.path}`);
|
||||
emitter.emit('release');
|
||||
await expect(promise).rejects.toThrow('Redis operation error detected (value was null).');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user