mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add support for pod owners
This commit is contained in:
@@ -31,6 +31,7 @@ describe('A server with account management', (): void => {
|
||||
};
|
||||
let passwordResource: string;
|
||||
let pod: string;
|
||||
let podResource: string;
|
||||
let webId: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
@@ -243,7 +244,7 @@ describe('A server with account management', (): void => {
|
||||
expect(json.podResource).toBeDefined();
|
||||
expect(json.webId).toBeDefined();
|
||||
expect(json.webIdResource).toBeDefined();
|
||||
({ pod, webId } = json);
|
||||
({ pod, webId, podResource } = json);
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.pod, { headers: { cookie }});
|
||||
@@ -254,6 +255,82 @@ describe('A server with account management', (): void => {
|
||||
expect((await res.json()).webIdLinks[webId]).toBeDefined();
|
||||
});
|
||||
|
||||
it('can not remove the last owner of a pod.', async(): Promise<void> => {
|
||||
const res = await fetch(podResource, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId, remove: true }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
await expect(res.text()).resolves.toContain('Unable to remove the last owner of a pod.');
|
||||
});
|
||||
|
||||
it('can add an owner to a pod.', async(): Promise<void> => {
|
||||
let res = await fetch(podResource, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId: 'http://example.com/other/webID', visible: true }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Verify that the new owner was added
|
||||
res = await fetch(podResource, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).owners).toEqual([
|
||||
{ webId, visible: false },
|
||||
{ webId: 'http://example.com/other/webID', visible: true },
|
||||
]);
|
||||
|
||||
// Verify only the new owner is exposed through a link header
|
||||
res = await fetch(pod);
|
||||
expect(res.status).toBe(200);
|
||||
const owners = res.headers.get('link')?.split(',')
|
||||
.filter((header): boolean => header.includes('rel="http://www.w3.org/ns/solid/terms#owner"'))
|
||||
.map((header): string => /<([^>]+)>/u.exec(header)![1]);
|
||||
expect(owners).toEqual([ 'http://example.com/other/webID' ]);
|
||||
});
|
||||
|
||||
it('can update the visibility of an existing pod owner.', async(): Promise<void> => {
|
||||
let res = await fetch(podResource, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId, visible: true }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Verify that the visibility was changed
|
||||
res = await fetch(podResource, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).owners).toEqual([
|
||||
{ webId, visible: true },
|
||||
{ webId: 'http://example.com/other/webID', visible: true },
|
||||
]);
|
||||
|
||||
// Verify both WebIDs are now visible
|
||||
res = await fetch(pod);
|
||||
expect(res.status).toBe(200);
|
||||
const owners = res.headers.get('link')?.split(',')
|
||||
.filter((header): boolean => header.includes('rel="http://www.w3.org/ns/solid/terms#owner"'))
|
||||
.map((header): string => /<([^>]+)>/u.exec(header)![1]);
|
||||
expect(owners).toEqual([ webId, 'http://example.com/other/webID' ]);
|
||||
});
|
||||
|
||||
it('can remove an owner from a pod.', async(): Promise<void> => {
|
||||
let res = await fetch(podResource, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId: 'http://example.com/other/webID', remove: true }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Verify that the new owner was added
|
||||
res = await fetch(podResource, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).owners).toEqual([
|
||||
{ webId, visible: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not store any data if creating a pod fails on the same account.', async(): Promise<void> => {
|
||||
const oldPods = (await (await fetch(controls.account.pod, { headers: { cookie }})).json()).pods;
|
||||
const oldWebIdLinks = (await (await fetch(controls.account.webId, { headers: { cookie }})).json()).webIdLinks;
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { AccessMap } from '../../../src/authorization/permissions/Permissio
|
||||
import { AuxiliaryIdentifierStrategy } from '../../../src/http/auxiliary/AuxiliaryIdentifierStrategy';
|
||||
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
||||
import { PodStore } from '../../../src/identity/interaction/pod/util/PodStore';
|
||||
import { WebIdStore } from '../../../src/identity/interaction/webid/util/WebIdStore';
|
||||
import type { StorageLocationStrategy } from '../../../src/server/description/StorageLocationStrategy';
|
||||
import { IdentifierMap, IdentifierSetMultiMap } from '../../../src/util/map/IdentifierMap';
|
||||
import { compareMaps } from '../../util/Util';
|
||||
@@ -18,7 +17,6 @@ describe('An OwnerPermissionReader', (): void => {
|
||||
let identifier: ResourceIdentifier;
|
||||
let requestedModes: AccessMap;
|
||||
let podStore: jest.Mocked<PodStore>;
|
||||
let webIdStore: jest.Mocked<WebIdStore>;
|
||||
let aclStrategy: jest.Mocked<AuxiliaryIdentifierStrategy>;
|
||||
let storageStrategy: jest.Mocked<StorageLocationStrategy>;
|
||||
let reader: OwnerPermissionReader;
|
||||
@@ -31,13 +29,10 @@ describe('An OwnerPermissionReader', (): void => {
|
||||
requestedModes = new IdentifierSetMultiMap([[ identifier, AclMode.control ]]) as any;
|
||||
|
||||
podStore = {
|
||||
findAccount: jest.fn().mockResolvedValue(accountId),
|
||||
findByBaseUrl: jest.fn().mockResolvedValue(accountId),
|
||||
getOwners: jest.fn().mockResolvedValue([{ webId: owner, visible: false }]),
|
||||
} satisfies Partial<PodStore> as any;
|
||||
|
||||
webIdStore = {
|
||||
findLinks: jest.fn().mockResolvedValue([{ id: '???', webId: owner }]),
|
||||
} satisfies Partial<WebIdStore> as any;
|
||||
|
||||
aclStrategy = {
|
||||
isAuxiliaryIdentifier: jest.fn((id): boolean => id.path.endsWith('.acl')),
|
||||
} satisfies Partial<AuxiliaryIdentifierStrategy> as any;
|
||||
@@ -46,7 +41,7 @@ describe('An OwnerPermissionReader', (): void => {
|
||||
getStorageIdentifier: jest.fn().mockResolvedValue(podBaseUrl),
|
||||
};
|
||||
|
||||
reader = new OwnerPermissionReader(podStore, webIdStore, aclStrategy, storageStrategy);
|
||||
reader = new OwnerPermissionReader(podStore, aclStrategy, storageStrategy);
|
||||
});
|
||||
|
||||
it('returns empty permissions for non-ACL resources.', async(): Promise<void> => {
|
||||
@@ -64,8 +59,13 @@ describe('An OwnerPermissionReader', (): void => {
|
||||
compareMaps(await reader.handle({ credentials, requestedModes }), new IdentifierMap());
|
||||
});
|
||||
|
||||
it('returns empty permissions if there is no pod owner.', async(): Promise<void> => {
|
||||
podStore.findAccount.mockResolvedValueOnce(undefined);
|
||||
it('returns empty permissions if there is no pod object.', async(): Promise<void> => {
|
||||
podStore.findByBaseUrl.mockResolvedValueOnce(undefined);
|
||||
compareMaps(await reader.handle({ credentials, requestedModes }), new IdentifierMap());
|
||||
});
|
||||
|
||||
it('returns empty permissions if there are no pod owners.', async(): Promise<void> => {
|
||||
podStore.getOwners.mockResolvedValueOnce(undefined);
|
||||
compareMaps(await reader.handle({ credentials, requestedModes }), new IdentifierMap());
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { PodIdRoute } from '../../../../../src/identity/interaction/pod/PodIdRoute';
|
||||
import { UpdateOwnerHandler } from '../../../../../src/identity/interaction/pod/UpdateOwnerHandler';
|
||||
import { PodStore } from '../../../../../src/identity/interaction/pod/util/PodStore';
|
||||
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
|
||||
|
||||
describe('An UpdateOwnerHandler', (): void => {
|
||||
const id = 'id';
|
||||
const accountId = 'accountId';
|
||||
const baseUrl = 'http://example.com/profile/';
|
||||
const webId = 'http://example.org/profile/card#me';
|
||||
const target = { path: 'http://example.org/account/pod/123/' };
|
||||
let store: jest.Mocked<PodStore>;
|
||||
let route: jest.Mocked<PodIdRoute>;
|
||||
let handler: UpdateOwnerHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
store = {
|
||||
get: jest.fn().mockResolvedValue({ baseUrl, accountId }),
|
||||
getOwners: jest.fn().mockResolvedValue([{ webId, visible: true }]),
|
||||
updateOwner: jest.fn(),
|
||||
removeOwner: jest.fn(),
|
||||
} satisfies Partial<PodStore> as any;
|
||||
|
||||
route = {
|
||||
getPath: jest.fn().mockReturnValue(''),
|
||||
matchPath: jest.fn().mockReturnValue({ accountId, podId: id }),
|
||||
};
|
||||
|
||||
handler = new UpdateOwnerHandler(store, route);
|
||||
});
|
||||
|
||||
it('requires specific input fields and returns all owners.', async(): Promise<void> => {
|
||||
await expect(handler.getView({ accountId, target } as any)).resolves.toEqual({
|
||||
json: {
|
||||
baseUrl,
|
||||
owners: [{ webId, visible: true }],
|
||||
fields: {
|
||||
webId: { required: true, type: 'string' },
|
||||
visible: { required: false, type: 'boolean' },
|
||||
remove: { required: false, type: 'boolean' },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(store.get).toHaveBeenCalledTimes(1);
|
||||
expect(store.get).toHaveBeenLastCalledWith(id);
|
||||
expect(store.getOwners).toHaveBeenCalledTimes(1);
|
||||
expect(store.getOwners).toHaveBeenLastCalledWith(id);
|
||||
});
|
||||
|
||||
it('can update the owner visibility.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ accountId, target, json: { webId, visible: true }} as any))
|
||||
.resolves.toEqual({ json: {}});
|
||||
expect(store.get).toHaveBeenCalledTimes(1);
|
||||
expect(store.get).toHaveBeenLastCalledWith(id);
|
||||
expect(store.updateOwner).toHaveBeenCalledTimes(1);
|
||||
expect(store.updateOwner).toHaveBeenLastCalledWith(id, webId, true);
|
||||
expect(store.removeOwner).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('can remove an owner.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ accountId, target, json: { webId, remove: true }} as any))
|
||||
.resolves.toEqual({ json: {}});
|
||||
expect(store.get).toHaveBeenCalledTimes(1);
|
||||
expect(store.get).toHaveBeenLastCalledWith(id);
|
||||
expect(store.updateOwner).toHaveBeenCalledTimes(0);
|
||||
expect(store.removeOwner).toHaveBeenCalledTimes(1);
|
||||
expect(store.removeOwner).toHaveBeenLastCalledWith(id, webId);
|
||||
});
|
||||
|
||||
it('throws a 404 if the authenticated accountId is not the owner.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ target, json: { webId, remove: true }, accountId: 'otherId' } as any))
|
||||
.rejects.toThrow(NotFoundHttpError);
|
||||
expect(store.get).toHaveBeenCalledTimes(1);
|
||||
expect(store.get).toHaveBeenLastCalledWith(id);
|
||||
expect(store.updateOwner).toHaveBeenCalledTimes(0);
|
||||
expect(store.removeOwner).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,18 @@ import {
|
||||
import { BasePodStore } from '../../../../../../src/identity/interaction/pod/util/BasePodStore';
|
||||
import type { PodManager } from '../../../../../../src/pods/PodManager';
|
||||
import type { PodSettings } from '../../../../../../src/pods/settings/PodSettings';
|
||||
import { BadRequestHttpError } from '../../../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../../../src/util/errors/InternalServerError';
|
||||
|
||||
const STORAGE_TYPE = 'pod';
|
||||
const OWNER_TYPE = 'owner';
|
||||
|
||||
describe('A BasePodStore', (): void => {
|
||||
const accountId = 'accountId';
|
||||
const id = 'id';
|
||||
const baseUrl = 'http://example.com/foo/';
|
||||
const settings: PodSettings = { webId: 'http://example.com/card#me', base: { path: baseUrl }};
|
||||
const webId = 'http://example.com/card#me';
|
||||
const settings: PodSettings = { webId, base: { path: baseUrl }};
|
||||
let storage: jest.Mocked<AccountLoginStorage<any>>;
|
||||
let manager: jest.Mocked<PodManager>;
|
||||
let store: BasePodStore;
|
||||
@@ -23,7 +26,9 @@ describe('A BasePodStore', (): void => {
|
||||
defineType: jest.fn().mockResolvedValue({}),
|
||||
createIndex: jest.fn().mockResolvedValue({}),
|
||||
create: jest.fn().mockResolvedValue({ id, baseUrl, accountId }),
|
||||
get: jest.fn().mockResolvedValue({ id, baseUrl, accountId }),
|
||||
find: jest.fn().mockResolvedValue([{ id, baseUrl, accountId }]),
|
||||
setField: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} satisfies Partial<AccountLoginStorage<any>> as any;
|
||||
|
||||
@@ -36,20 +41,26 @@ describe('A BasePodStore', (): void => {
|
||||
|
||||
it('defines the type and indexes in the storage.', async(): Promise<void> => {
|
||||
await expect(store.handle()).resolves.toBeUndefined();
|
||||
expect(storage.defineType).toHaveBeenCalledTimes(1);
|
||||
expect(storage.defineType).toHaveBeenLastCalledWith(STORAGE_TYPE, {
|
||||
expect(storage.defineType).toHaveBeenCalledTimes(2);
|
||||
expect(storage.defineType).toHaveBeenCalledWith(STORAGE_TYPE, {
|
||||
baseUrl: 'string',
|
||||
accountId: `id:${ACCOUNT_TYPE}`,
|
||||
}, false);
|
||||
expect(storage.createIndex).toHaveBeenCalledTimes(2);
|
||||
expect(storage.defineType).toHaveBeenCalledWith(OWNER_TYPE, {
|
||||
webId: 'string',
|
||||
visible: 'boolean',
|
||||
podId: `id:${STORAGE_TYPE}`,
|
||||
}, false);
|
||||
expect(storage.createIndex).toHaveBeenCalledTimes(3);
|
||||
expect(storage.createIndex).toHaveBeenCalledWith(STORAGE_TYPE, 'accountId');
|
||||
expect(storage.createIndex).toHaveBeenCalledWith(STORAGE_TYPE, 'baseUrl');
|
||||
expect(storage.createIndex).toHaveBeenCalledWith(OWNER_TYPE, 'podId');
|
||||
});
|
||||
|
||||
it('can only initialize once.', async(): Promise<void> => {
|
||||
await expect(store.handle()).resolves.toBeUndefined();
|
||||
await expect(store.handle()).resolves.toBeUndefined();
|
||||
expect(storage.defineType).toHaveBeenCalledTimes(1);
|
||||
expect(storage.defineType).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('throws an error if defining the type goes wrong.', async(): Promise<void> => {
|
||||
@@ -59,8 +70,9 @@ describe('A BasePodStore', (): void => {
|
||||
|
||||
it('calls the pod manager to create a pod.', async(): Promise<void> => {
|
||||
await expect(store.create(accountId, settings, false)).resolves.toBe(id);
|
||||
expect(storage.create).toHaveBeenCalledTimes(1);
|
||||
expect(storage.create).toHaveBeenLastCalledWith(STORAGE_TYPE, { accountId, baseUrl });
|
||||
expect(storage.create).toHaveBeenCalledTimes(2);
|
||||
expect(storage.create).toHaveBeenNthCalledWith(1, STORAGE_TYPE, { accountId, baseUrl });
|
||||
expect(storage.create).toHaveBeenNthCalledWith(2, OWNER_TYPE, { podId: id, webId, visible: false });
|
||||
expect(manager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(manager.createPod).toHaveBeenLastCalledWith(settings, false);
|
||||
});
|
||||
@@ -68,14 +80,28 @@ describe('A BasePodStore', (): void => {
|
||||
it('reverts the storage changes if something goes wrong.', async(): Promise<void> => {
|
||||
manager.createPod.mockRejectedValueOnce(new Error('bad data'));
|
||||
await expect(store.create(accountId, settings, false)).rejects.toThrow('Pod creation failed: bad data');
|
||||
expect(storage.create).toHaveBeenCalledTimes(1);
|
||||
expect(storage.create).toHaveBeenLastCalledWith(STORAGE_TYPE, { accountId, baseUrl });
|
||||
expect(storage.create).toHaveBeenCalledTimes(2);
|
||||
expect(storage.create).toHaveBeenNthCalledWith(1, STORAGE_TYPE, { accountId, baseUrl });
|
||||
expect(storage.create).toHaveBeenNthCalledWith(2, OWNER_TYPE, { podId: id, webId, visible: false });
|
||||
expect(manager.createPod).toHaveBeenCalledTimes(1);
|
||||
expect(manager.createPod).toHaveBeenLastCalledWith(settings, false);
|
||||
expect(storage.delete).toHaveBeenCalledTimes(1);
|
||||
expect(storage.delete).toHaveBeenLastCalledWith(STORAGE_TYPE, id);
|
||||
});
|
||||
|
||||
it('returns the pod information.', async(): Promise<void> => {
|
||||
await expect(store.get(id)).resolves.toEqual({ baseUrl, accountId });
|
||||
expect(storage.get).toHaveBeenCalledTimes(1);
|
||||
expect(storage.get).toHaveBeenLastCalledWith(STORAGE_TYPE, id);
|
||||
});
|
||||
|
||||
it('returns undefined if there is no matching pod.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValueOnce(undefined);
|
||||
await expect(store.get(id)).resolves.toBeUndefined();
|
||||
expect(storage.get).toHaveBeenCalledTimes(1);
|
||||
expect(storage.get).toHaveBeenLastCalledWith(STORAGE_TYPE, id);
|
||||
});
|
||||
|
||||
it('can find all the pods for an account.', async(): Promise<void> => {
|
||||
await expect(store.findPods(accountId)).resolves.toEqual([{ id, baseUrl }]);
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
@@ -83,15 +109,75 @@ describe('A BasePodStore', (): void => {
|
||||
});
|
||||
|
||||
it('can find the account that created a pod.', async(): Promise<void> => {
|
||||
await expect(store.findAccount(baseUrl)).resolves.toEqual(accountId);
|
||||
await expect(store.findByBaseUrl(baseUrl)).resolves.toEqual({ accountId, id });
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(STORAGE_TYPE, { baseUrl });
|
||||
});
|
||||
|
||||
it('returns undefined if there is no associated account.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([]);
|
||||
await expect(store.findAccount(baseUrl)).resolves.toBeUndefined();
|
||||
await expect(store.findByBaseUrl(baseUrl)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(STORAGE_TYPE, { baseUrl });
|
||||
});
|
||||
|
||||
it('can return all the owners of a pod.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([{ id: 'id1', webId, visible: true }]);
|
||||
await expect(store.getOwners(id)).resolves.toEqual([{ webId, visible: true }]);
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id });
|
||||
});
|
||||
|
||||
it('returns undefined if there are no owners.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([]);
|
||||
await expect(store.getOwners(id)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id });
|
||||
});
|
||||
|
||||
it('creates a new owner if the update target does not exist yet.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([]);
|
||||
await expect(store.updateOwner(id, webId, true)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id, webId });
|
||||
expect(storage.create).toHaveBeenCalledTimes(1);
|
||||
expect(storage.create).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id, webId, visible: true });
|
||||
expect(storage.setField).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('updates the existing object if there already is an owner with this WebID.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([{ id: 'id1', webId, visible: false }]);
|
||||
await expect(store.updateOwner(id, webId, true)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id, webId });
|
||||
expect(storage.create).toHaveBeenCalledTimes(0);
|
||||
expect(storage.setField).toHaveBeenCalledTimes(1);
|
||||
expect(storage.setField).toHaveBeenLastCalledWith(OWNER_TYPE, 'id1', 'visible', true);
|
||||
});
|
||||
|
||||
it('can remove an owner.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([{ id: 'id1', webId, visible: false },
|
||||
{ id: 'id2', webId: 'otherWebId', visible: false }]);
|
||||
await expect(store.removeOwner(id, webId)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id });
|
||||
expect(storage.delete).toHaveBeenCalledTimes(1);
|
||||
expect(storage.delete).toHaveBeenLastCalledWith(OWNER_TYPE, 'id1');
|
||||
});
|
||||
|
||||
it('does nothing if there is no matching owner.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([{ id: 'id2', webId: 'otherWebId', visible: false }]);
|
||||
await expect(store.removeOwner(id, webId)).resolves.toBeUndefined();
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id });
|
||||
expect(storage.delete).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('cannot remove the last owner.', async(): Promise<void> => {
|
||||
storage.find.mockResolvedValueOnce([{ id: 'id1', webId, visible: false }]);
|
||||
await expect(store.removeOwner(id, webId)).rejects.toThrow(BadRequestHttpError);
|
||||
expect(storage.find).toHaveBeenCalledTimes(1);
|
||||
expect(storage.find).toHaveBeenLastCalledWith(OWNER_TYPE, { podId: id });
|
||||
expect(storage.delete).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { ServerResponse } from 'http';
|
||||
import { createResponse } from 'node-mocks-http';
|
||||
import { RepresentationMetadata } from '../../../../../../src/http/representation/RepresentationMetadata';
|
||||
import { OwnerMetadataWriter } from '../../../../../../src/identity/interaction/pod/util/OwnerMetadataWriter';
|
||||
import { PodStore } from '../../../../../../src/identity/interaction/pod/util/PodStore';
|
||||
import type { StorageLocationStrategy } from '../../../../../../src/server/description/StorageLocationStrategy';
|
||||
import { joinUrl } from '../../../../../../src/util/PathUtil';
|
||||
|
||||
describe('An OwnerMetadataWriter', (): void => {
|
||||
const id = 'id';
|
||||
const accountId = 'accountId';
|
||||
const target = { path: 'http://example.com/pod/' };
|
||||
const webId = 'http://example.com/webId#me';
|
||||
let metadata: RepresentationMetadata;
|
||||
let response: ServerResponse;
|
||||
let podStore: jest.Mocked<PodStore>;
|
||||
let storageStrategy: jest.Mocked<StorageLocationStrategy>;
|
||||
let writer: OwnerMetadataWriter;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
metadata = new RepresentationMetadata(target);
|
||||
|
||||
response = createResponse();
|
||||
|
||||
podStore = {
|
||||
findByBaseUrl: jest.fn().mockResolvedValue({ id, accountId }),
|
||||
getOwners: jest.fn().mockResolvedValue([{ webId, visible: true }]),
|
||||
} satisfies Partial<PodStore> as any;
|
||||
|
||||
storageStrategy = {
|
||||
getStorageIdentifier: jest.fn().mockResolvedValue(target),
|
||||
};
|
||||
|
||||
writer = new OwnerMetadataWriter(podStore, storageStrategy);
|
||||
});
|
||||
|
||||
it('adds the correct link headers.', async(): Promise<void> => {
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({ link: `<${webId}>; rel="http://www.w3.org/ns/solid/terms#owner"` });
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith(target);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenLastCalledWith(target.path);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.getOwners).toHaveBeenLastCalledWith(id);
|
||||
});
|
||||
|
||||
it('adds no headers if the identifier is a blank node.', async(): Promise<void> => {
|
||||
metadata = new RepresentationMetadata();
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({});
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds no headers if no root storage could be found.', async(): Promise<void> => {
|
||||
storageStrategy.getStorageIdentifier.mockRejectedValueOnce(new Error('bad identifier'));
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({});
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith(target);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds no headers if the target is not a pod base URL.', async(): Promise<void> => {
|
||||
metadata = new RepresentationMetadata({ path: joinUrl(target.path, 'document') });
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({});
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith({ path: joinUrl(target.path, 'document') });
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds no headers if there is no matching pod object.', async(): Promise<void> => {
|
||||
podStore.findByBaseUrl.mockResolvedValueOnce(undefined);
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({});
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith(target);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenLastCalledWith(target.path);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('adds no headers if there are no matching owners.', async(): Promise<void> => {
|
||||
podStore.getOwners.mockResolvedValueOnce(undefined);
|
||||
await expect(writer.handle({ metadata, response })).resolves.toBeUndefined();
|
||||
expect(response.getHeaders()).toEqual({});
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith(target);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenLastCalledWith(target.path);
|
||||
expect(podStore.getOwners).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.getOwners).toHaveBeenLastCalledWith(id);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHt
|
||||
|
||||
describe('A LinkWebIdHandler', (): void => {
|
||||
const id = 'id';
|
||||
const podId = 'podId';
|
||||
const accountId = 'accountId';
|
||||
const webId = 'http://example.com/pod/profile/card#me';
|
||||
let json: unknown;
|
||||
@@ -29,7 +30,7 @@ describe('A LinkWebIdHandler', (): void => {
|
||||
} satisfies Partial<OwnershipValidator> as any;
|
||||
|
||||
podStore = {
|
||||
findAccount: jest.fn().mockResolvedValue(accountId),
|
||||
findByBaseUrl: jest.fn().mockResolvedValue({ accountId, id: podId }),
|
||||
} satisfies Partial<PodStore> as any;
|
||||
|
||||
webIdStore = {
|
||||
@@ -80,8 +81,8 @@ describe('A LinkWebIdHandler', (): void => {
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(webId, accountId);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith({ path: webId });
|
||||
expect(podStore.findAccount).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findAccount).toHaveBeenLastCalledWith(podUrl);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenLastCalledWith(podUrl);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.create).toHaveBeenLastCalledWith(webId, accountId);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(0);
|
||||
@@ -93,12 +94,12 @@ describe('A LinkWebIdHandler', (): void => {
|
||||
expect(webIdStore.isLinked).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(webId, accountId);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.findAccount).toHaveBeenCalledTimes(0);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(0);
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls the ownership validator if the account did not create the pod the WebID is in.', async(): Promise<void> => {
|
||||
podStore.findAccount.mockResolvedValueOnce(undefined);
|
||||
podStore.findByBaseUrl.mockResolvedValueOnce(undefined);
|
||||
await expect(handler.handle({ accountId, json } as any)).resolves.toEqual({
|
||||
json: { resource, webId, oidcIssuer: baseUrl },
|
||||
});
|
||||
@@ -106,8 +107,8 @@ describe('A LinkWebIdHandler', (): void => {
|
||||
expect(webIdStore.isLinked).toHaveBeenLastCalledWith(webId, accountId);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenCalledTimes(1);
|
||||
expect(storageStrategy.getStorageIdentifier).toHaveBeenLastCalledWith({ path: webId });
|
||||
expect(podStore.findAccount).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findAccount).toHaveBeenLastCalledWith(podUrl);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenCalledTimes(1);
|
||||
expect(podStore.findByBaseUrl).toHaveBeenLastCalledWith(podUrl);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
expect(webIdStore.create).toHaveBeenCalledTimes(1);
|
||||
|
||||
Reference in New Issue
Block a user