mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Full rework of account management
Complete rewrite of the account management and related systems. Makes the architecture more modular, allowing for easier extensions and configurations.
This commit is contained in:
107
test/unit/identity/interaction/webid/LinkWebIdHandler.test.ts
Normal file
107
test/unit/identity/interaction/webid/LinkWebIdHandler.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { Account } from '../../../../../src/identity/interaction/account/util/Account';
|
||||
import type { AccountStore } from '../../../../../src/identity/interaction/account/util/AccountStore';
|
||||
import { LinkWebIdHandler } from '../../../../../src/identity/interaction/webid/LinkWebIdHandler';
|
||||
import type { WebIdStore } from '../../../../../src/identity/interaction/webid/util/WebIdStore';
|
||||
import type { WebIdLinkRoute } from '../../../../../src/identity/interaction/webid/WebIdLinkRoute';
|
||||
import type { OwnershipValidator } from '../../../../../src/identity/ownership/OwnershipValidator';
|
||||
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
|
||||
import type { IdentifierStrategy } from '../../../../../src/util/identifiers/IdentifierStrategy';
|
||||
import { createAccount, mockAccountStore } from '../../../../util/AccountUtil';
|
||||
|
||||
describe('A LinkWebIdHandler', (): void => {
|
||||
let account: Account;
|
||||
const accountId = 'accountId';
|
||||
const webId = 'http://example.com/profile/card#me';
|
||||
let json: unknown;
|
||||
const resource = 'http://example.com/.account/link';
|
||||
const baseUrl = 'http://example.com/';
|
||||
let ownershipValidator: jest.Mocked<OwnershipValidator>;
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let webIdStore: jest.Mocked<WebIdStore>;
|
||||
let webIdRoute: jest.Mocked<WebIdLinkRoute>;
|
||||
let identifierStrategy: jest.Mocked<IdentifierStrategy>;
|
||||
let handler: LinkWebIdHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
json = { webId };
|
||||
|
||||
ownershipValidator = {
|
||||
handleSafe: jest.fn(),
|
||||
} as any;
|
||||
|
||||
account = createAccount();
|
||||
accountStore = mockAccountStore(account);
|
||||
|
||||
webIdStore = {
|
||||
add: jest.fn().mockResolvedValue(resource),
|
||||
} as any;
|
||||
|
||||
identifierStrategy = {
|
||||
contains: jest.fn().mockReturnValue(true),
|
||||
} as any;
|
||||
|
||||
handler = new LinkWebIdHandler({
|
||||
accountStore,
|
||||
identifierStrategy,
|
||||
webIdRoute,
|
||||
webIdStore,
|
||||
ownershipValidator,
|
||||
baseUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('requires a WebID as input.', async(): Promise<void> => {
|
||||
await expect(handler.getView()).resolves.toEqual({
|
||||
json: {
|
||||
fields: {
|
||||
webId: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('links the WebID.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ accountId, json } as any)).resolves.toEqual({
|
||||
json: { resource, webId, oidcIssuer: baseUrl },
|
||||
});
|
||||
expect(webIdStore.add).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.add).toHaveBeenLastCalledWith(webId, account);
|
||||
});
|
||||
|
||||
it('throws an error if the WebID is already registered.', async(): Promise<void> => {
|
||||
account.webIds[webId] = resource;
|
||||
await expect(handler.handle({ accountId, json } as any)).rejects.toThrow(BadRequestHttpError);
|
||||
expect(webIdStore.add).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('checks if the WebID is in a pod owned by the account.', async(): Promise<void> => {
|
||||
account.pods['http://example.com/.account/pod/'] = resource;
|
||||
await expect(handler.handle({ accountId, json } as any)).resolves.toEqual({
|
||||
json: { resource, webId, oidcIssuer: baseUrl },
|
||||
});
|
||||
expect(identifierStrategy.contains).toHaveBeenCalledTimes(1);
|
||||
expect(identifierStrategy.contains)
|
||||
.toHaveBeenCalledWith({ path: 'http://example.com/.account/pod/' }, { path: webId }, true);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('calls the ownership validator if none of the pods contain the WebId.', async(): Promise<void> => {
|
||||
identifierStrategy.contains.mockReturnValue(false);
|
||||
account.pods['http://example.com/.account/pod/'] = resource;
|
||||
account.pods['http://example.com/.account/pod2/'] = resource;
|
||||
|
||||
await expect(handler.handle({ accountId, json } as any)).resolves.toEqual({
|
||||
json: { resource, webId, oidcIssuer: baseUrl },
|
||||
});
|
||||
expect(identifierStrategy.contains).toHaveBeenCalledTimes(2);
|
||||
expect(identifierStrategy.contains)
|
||||
.toHaveBeenCalledWith({ path: 'http://example.com/.account/pod/' }, { path: webId }, true);
|
||||
expect(identifierStrategy.contains)
|
||||
.toHaveBeenCalledWith({ path: 'http://example.com/.account/pod2/' }, { path: webId }, true);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Account } from '../../../../../src/identity/interaction/account/util/Account';
|
||||
import type { AccountStore } from '../../../../../src/identity/interaction/account/util/AccountStore';
|
||||
import { UnlinkWebIdHandler } from '../../../../../src/identity/interaction/webid/UnlinkWebIdHandler';
|
||||
import type { WebIdStore } from '../../../../../src/identity/interaction/webid/util/WebIdStore';
|
||||
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
|
||||
import { createAccount, mockAccountStore } from '../../../../util/AccountUtil';
|
||||
|
||||
describe('A UnlinkWebIdHandler', (): void => {
|
||||
const resource = 'http://example.com/.account/link';
|
||||
const webId = 'http://example.com/.account/card#me';
|
||||
const accountId = 'accountId';
|
||||
let account: Account;
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let webIdStore: jest.Mocked<WebIdStore>;
|
||||
let handler: UnlinkWebIdHandler;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
account = createAccount(accountId);
|
||||
account.webIds[webId] = resource;
|
||||
|
||||
accountStore = mockAccountStore(account);
|
||||
|
||||
webIdStore = {
|
||||
get: jest.fn(),
|
||||
add: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
handler = new UnlinkWebIdHandler(accountStore, webIdStore);
|
||||
});
|
||||
|
||||
it('removes the WebID link.', async(): Promise<void> => {
|
||||
await expect(handler.handle({ target: { path: resource }, accountId } as any)).resolves.toEqual({ json: {}});
|
||||
expect(webIdStore.delete).toHaveBeenCalledTimes(1);
|
||||
expect(webIdStore.delete).toHaveBeenLastCalledWith(webId, account);
|
||||
});
|
||||
|
||||
it('errors if there is no matching link resource.', async(): Promise<void> => {
|
||||
delete account.webIds[webId];
|
||||
await expect(handler.handle({ target: { path: resource }, accountId } as any)).rejects.toThrow(NotFoundHttpError);
|
||||
});
|
||||
});
|
||||
14
test/unit/identity/interaction/webid/WebIdLinkRoute.test.ts
Normal file
14
test/unit/identity/interaction/webid/WebIdLinkRoute.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseAccountIdRoute } from '../../../../../src/identity/interaction/account/AccountIdRoute';
|
||||
import {
|
||||
AbsolutePathInteractionRoute,
|
||||
} from '../../../../../src/identity/interaction/routing/AbsolutePathInteractionRoute';
|
||||
import { BaseWebIdLinkRoute } from '../../../../../src/identity/interaction/webid/WebIdLinkRoute';
|
||||
|
||||
describe('A WebIdLinkRoute', (): void => {
|
||||
it('uses the WebID link key.', async(): Promise<void> => {
|
||||
const webIdLinkRoute = new BaseWebIdLinkRoute(new BaseAccountIdRoute(
|
||||
new AbsolutePathInteractionRoute('http://example.com/'),
|
||||
));
|
||||
expect(webIdLinkRoute.matchPath('http://example.com/123/456/')).toEqual({ accountId: '123', webIdLink: '456' });
|
||||
});
|
||||
});
|
||||
112
test/unit/identity/interaction/webid/util/BaseWebIdStore.test.ts
Normal file
112
test/unit/identity/interaction/webid/util/BaseWebIdStore.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { Account } from '../../../../../../src/identity/interaction/account/util/Account';
|
||||
import type { AccountStore } from '../../../../../../src/identity/interaction/account/util/AccountStore';
|
||||
import { BaseWebIdStore } from '../../../../../../src/identity/interaction/webid/util/BaseWebIdStore';
|
||||
import type { WebIdLinkRoute } from '../../../../../../src/identity/interaction/webid/WebIdLinkRoute';
|
||||
import type { KeyValueStorage } from '../../../../../../src/storage/keyvalue/KeyValueStorage';
|
||||
import { BadRequestHttpError } from '../../../../../../src/util/errors/BadRequestHttpError';
|
||||
import { createAccount, mockAccountStore } from '../../../../../util/AccountUtil';
|
||||
|
||||
describe('A BaseWebIdStore', (): void => {
|
||||
const webId = 'http://example.com/card#me';
|
||||
let account: Account;
|
||||
const route: WebIdLinkRoute = {
|
||||
getPath: (): string => 'http://example.com/.account/resource',
|
||||
matchPath: (): any => ({}),
|
||||
};
|
||||
let accountStore: jest.Mocked<AccountStore>;
|
||||
let storage: jest.Mocked<KeyValueStorage<string, string[]>>;
|
||||
let store: BaseWebIdStore;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
account = createAccount();
|
||||
|
||||
accountStore = mockAccountStore(createAccount());
|
||||
|
||||
storage = {
|
||||
get: jest.fn().mockResolvedValue([ account.id ]),
|
||||
set: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
} as any;
|
||||
|
||||
store = new BaseWebIdStore(route, accountStore, storage);
|
||||
});
|
||||
|
||||
it('returns the stored account identifiers.', async(): Promise<void> => {
|
||||
await expect(store.get(webId)).resolves.toEqual([ account.id ]);
|
||||
});
|
||||
|
||||
it('returns an empty list if there are no matching idenfitiers.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValueOnce(undefined);
|
||||
await expect(store.get(webId)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('can add an account to the linked list.', async(): Promise<void> => {
|
||||
await expect(store.add(webId, account)).resolves.toBe('http://example.com/.account/resource');
|
||||
expect(storage.set).toHaveBeenCalledTimes(1);
|
||||
expect(storage.set).toHaveBeenLastCalledWith(webId, [ account.id ]);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.update).toHaveBeenLastCalledWith(account);
|
||||
expect(account.webIds[webId]).toBe('http://example.com/.account/resource');
|
||||
});
|
||||
|
||||
it('creates a new list if one did not exist yet.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValueOnce(undefined);
|
||||
await expect(store.add(webId, account)).resolves.toBe('http://example.com/.account/resource');
|
||||
expect(storage.set).toHaveBeenCalledTimes(1);
|
||||
expect(storage.set).toHaveBeenLastCalledWith(webId, [ account.id ]);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.update).toHaveBeenLastCalledWith(account);
|
||||
expect(account.webIds[webId]).toBe('http://example.com/.account/resource');
|
||||
});
|
||||
|
||||
it('can not create a link if the WebID is already linked.', async(): Promise<void> => {
|
||||
account.webIds[webId] = 'resource';
|
||||
await expect(store.add(webId, account)).rejects.toThrow(BadRequestHttpError);
|
||||
expect(storage.set).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not update the account if something goes wrong.', async(): Promise<void> => {
|
||||
storage.set.mockRejectedValueOnce(new Error('bad data'));
|
||||
await expect(store.add(webId, account)).rejects.toThrow('bad data');
|
||||
expect(storage.set).toHaveBeenCalledTimes(1);
|
||||
expect(storage.set).toHaveBeenLastCalledWith(webId, [ account.id ]);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(2);
|
||||
expect(accountStore.update).toHaveBeenLastCalledWith(account);
|
||||
expect(account.webIds).toEqual({});
|
||||
});
|
||||
|
||||
it('can delete a link.', async(): Promise<void> => {
|
||||
await expect(store.delete(webId, account)).resolves.toBeUndefined();
|
||||
expect(storage.delete).toHaveBeenCalledTimes(1);
|
||||
expect(storage.delete).toHaveBeenLastCalledWith(webId);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.update).toHaveBeenLastCalledWith(account);
|
||||
expect(account.webIds).toEqual({});
|
||||
});
|
||||
|
||||
it('does not remove the entire list if there are still other entries.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValueOnce([ account.id, 'other-id' ]);
|
||||
await expect(store.delete(webId, account)).resolves.toBeUndefined();
|
||||
expect(storage.set).toHaveBeenCalledTimes(1);
|
||||
expect(storage.set).toHaveBeenLastCalledWith(webId, [ 'other-id' ]);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(1);
|
||||
expect(accountStore.update).toHaveBeenLastCalledWith(account);
|
||||
expect(account.webIds).toEqual({});
|
||||
});
|
||||
|
||||
it('does not do anything if the the delete WebID target does not exist.', async(): Promise<void> => {
|
||||
storage.get.mockResolvedValueOnce(undefined);
|
||||
await expect(store.delete('random-webId', account)).resolves.toBeUndefined();
|
||||
expect(storage.set).toHaveBeenCalledTimes(0);
|
||||
expect(storage.delete).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not do anything if the the delete account target is not linked.', async(): Promise<void> => {
|
||||
await expect(store.delete(webId, { ...account, id: 'random-id' })).resolves.toBeUndefined();
|
||||
expect(storage.set).toHaveBeenCalledTimes(0);
|
||||
expect(storage.delete).toHaveBeenCalledTimes(0);
|
||||
expect(accountStore.update).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user