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:
467
test/integration/Accounts.test.ts
Normal file
467
test/integration/Accounts.test.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
import fetch from 'cross-fetch';
|
||||
import { parse, splitCookiesString } from 'set-cookie-parser';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
||||
import { joinUrl } from '../../src/util/PathUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
|
||||
|
||||
const port = getPort('Accounts');
|
||||
const baseUrl = `http://localhost:${port}/`;
|
||||
|
||||
// Don't send actual e-mails
|
||||
jest.mock('nodemailer');
|
||||
|
||||
describe('A server with account management', (): void => {
|
||||
let app: App;
|
||||
let sendMail: jest.Mock;
|
||||
|
||||
let cookie: string;
|
||||
const email = 'test@example.com';
|
||||
let password = 'secret';
|
||||
const indexUrl = joinUrl(baseUrl, '.account/');
|
||||
let controls: {
|
||||
main: Record<'index' | 'logins', string>;
|
||||
account: Record<'create' | 'account' | 'logout' | 'pod' | 'webId' | 'clientCredentials', string>;
|
||||
password: Record<'login' | 'forgot' | 'create', string>;
|
||||
};
|
||||
let passwordResource: string;
|
||||
let pod: string;
|
||||
let webId: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Needs to happen before Components.js instantiation
|
||||
sendMail = jest.fn();
|
||||
const nodemailer = jest.requireMock('nodemailer');
|
||||
Object.assign(nodemailer, { createTransport: (): any => ({ sendMail }) });
|
||||
|
||||
const instances = await instantiateFromConfig(
|
||||
'urn:solid-server:test:Instances',
|
||||
getTestConfigPath('server-memory.json'),
|
||||
getDefaultVariables(port, baseUrl),
|
||||
) as Record<string, any>;
|
||||
({ app } = instances);
|
||||
await app.start();
|
||||
|
||||
controls = { main: {}, account: {}, login: {}, password: {}} as any;
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
it('can get the general index.', async(): Promise<void> => {
|
||||
const res = await fetch(indexUrl);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
|
||||
expect(json.controls.main.index).toBe(indexUrl);
|
||||
expect(json.controls.main.logins).toBeDefined();
|
||||
controls.main = json.controls.main;
|
||||
|
||||
expect(json.controls.account.create).toBeDefined();
|
||||
controls.account = json.controls.account;
|
||||
|
||||
expect(json.controls.password.login).toBeDefined();
|
||||
expect(json.controls.password.forgot).toBeDefined();
|
||||
controls.password = json.controls.password;
|
||||
|
||||
expect(json.controls.html).toBeDefined();
|
||||
expect(json.controls.html.main).toBeDefined();
|
||||
expect(json.controls.html.password).toBeDefined();
|
||||
});
|
||||
|
||||
it('can create an account.', async(): Promise<void> => {
|
||||
const res = await fetch(controls.account.create, { method: 'POST' });
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.resource).toBeDefined();
|
||||
expect(res.headers.get('set-cookie')).toBeDefined();
|
||||
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
|
||||
expect(cookies).toHaveLength(1);
|
||||
|
||||
cookie = `${cookies[0].name}=${cookies[0].value}`;
|
||||
expect(json.cookie).toBe(cookies[0].value);
|
||||
|
||||
controls.account.account = json.resource;
|
||||
});
|
||||
|
||||
it('can access the account using the cookie.', async(): Promise<void> => {
|
||||
expect((await fetch(controls.account.account)).status).toBe(401);
|
||||
const res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.controls.account.account).toEqual(controls.account.account);
|
||||
expect(json.controls.account.logout).toBeDefined();
|
||||
expect(json.controls.account.pod).toBeDefined();
|
||||
expect(json.controls.account.webId).toBeDefined();
|
||||
expect(json.controls.account.clientCredentials).toBeDefined();
|
||||
|
||||
controls.account = json.controls.account;
|
||||
|
||||
expect(json.controls.password.create).toBeDefined();
|
||||
controls.password = json.controls.password;
|
||||
|
||||
expect(json.controls.html.account).toBeDefined();
|
||||
});
|
||||
|
||||
it('can also access the account using the custom authorization header.', async(): Promise<void> => {
|
||||
expect((await fetch(controls.account.account)).status).toBe(401);
|
||||
const res = await fetch(controls.account.account, { headers:
|
||||
{ authorization: `CSS-Account-Cookie ${cookie.split('=')[1]}` }});
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.controls.account.account).toEqual(controls.account.account);
|
||||
});
|
||||
|
||||
it('can not create a pod since the account has no login.', async(): Promise<void> => {
|
||||
const res = await fetch(controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'test' }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect((await res.json()).message).toBe('An account needs at least 1 login method.');
|
||||
});
|
||||
|
||||
it('can add a password login to the account.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.password.create, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.resource).toBeDefined();
|
||||
({ resource: passwordResource } = json);
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).logins.password).toEqual({ [email]: passwordResource });
|
||||
});
|
||||
|
||||
it('can not delete its last login method.', async(): Promise<void> => {
|
||||
const res = await fetch(passwordResource, { method: 'DELETE', headers: { cookie }});
|
||||
expect(res.status).toBe(400);
|
||||
expect((await res.json()).message).toBe('An account needs at least 1 login method.');
|
||||
});
|
||||
|
||||
it('can not use the same email for a different account.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.create, { method: 'POST' });
|
||||
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
|
||||
const newCookie = `${cookies[0].name}=${cookies[0].value}`;
|
||||
const { resource } = await res.json();
|
||||
|
||||
res = await fetch(resource, { headers: { cookie: newCookie }});
|
||||
const oldAccount: { controls: typeof controls } = await res.json();
|
||||
|
||||
// This will fail because the email address is already used by a different account
|
||||
res = await fetch(oldAccount.controls.password.create, {
|
||||
method: 'POST',
|
||||
headers: { cookie: newCookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
|
||||
// Make sure the account still has no login method
|
||||
res = await fetch(resource, { headers: { cookie: newCookie }});
|
||||
await expect(res.json()).resolves.toEqual(oldAccount);
|
||||
});
|
||||
|
||||
it('can log out.', async(): Promise<void> => {
|
||||
const res = await fetch(controls.account.logout, { method: 'POST', headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
// Cookie doesn't work anymore
|
||||
expect((await fetch(controls.account.account, { headers: { cookie }})).status).toBe(401);
|
||||
});
|
||||
|
||||
it('can login again with email/password.', async(): Promise<void> => {
|
||||
const res = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
|
||||
expect(cookies).toHaveLength(1);
|
||||
cookie = `${cookies[0].name}=${cookies[0].value}`;
|
||||
// Cookie is valid again
|
||||
expect((await fetch(controls.account.account, { headers: { cookie }})).status).toBe(200);
|
||||
});
|
||||
|
||||
it('can change the password.', async(): Promise<void> => {
|
||||
let res = await fetch(passwordResource, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
oldPassword: password,
|
||||
newPassword: 'secret2',
|
||||
}),
|
||||
});
|
||||
password = 'secret2';
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Check new password
|
||||
res = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('set-cookie')).toBeDefined();
|
||||
});
|
||||
|
||||
it('can create a pod.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'test' }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
let json = await res.json();
|
||||
expect(json.pod).toBeDefined();
|
||||
expect(json.podResource).toBeDefined();
|
||||
expect(json.webId).toBeDefined();
|
||||
expect(json.webIdResource).toBeDefined();
|
||||
({ pod, webId } = json);
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
json = await res.json();
|
||||
expect(json.pods[pod]).toBeDefined();
|
||||
expect(json.webIds[webId]).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not store any data if creating a pod fails on the same account.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
const oldAccount = await res.json();
|
||||
|
||||
res = await fetch(controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'test' }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
|
||||
// Verify nothing was added
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
await expect(res.json()).resolves.toEqual(oldAccount);
|
||||
});
|
||||
|
||||
it('does not store any data if creating a pod fails on a different account.', async(): Promise<void> => {
|
||||
// We have to create a new account here to try to create a pod with the same name.
|
||||
// Otherwise the server will never try to write data
|
||||
// since it would notice the account already has a pod with that name.
|
||||
let res = await fetch(controls.account.create, { method: 'POST' });
|
||||
const cookies = parse(splitCookiesString(res.headers.get('set-cookie')!));
|
||||
const newCookie = `${cookies[0].name}=${cookies[0].value}`;
|
||||
res = await fetch(indexUrl, { headers: { cookie: newCookie }});
|
||||
const json: { controls: typeof controls } = await res.json();
|
||||
res = await fetch(json.controls.password.create, {
|
||||
method: 'POST',
|
||||
headers: { cookie: newCookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: 'differentMail@example.com',
|
||||
password,
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
res = await fetch(json.controls.account.account, { headers: { cookie: newCookie }});
|
||||
const oldAccount = await res.json();
|
||||
|
||||
// This will fail because there already is a pod with this name
|
||||
res = await fetch(json.controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { cookie: newCookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'test' }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
expect((await res.json()).message).toContain('Pod creation failed');
|
||||
|
||||
// Make sure there is no reference in the account data
|
||||
res = await fetch(json.controls.account.account, { headers: { cookie: newCookie }});
|
||||
await expect(res.json()).resolves.toEqual(oldAccount);
|
||||
});
|
||||
|
||||
it('can remove the WebID link.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
const webIdResource = (await res.json()).webIds[webId];
|
||||
res = await fetch(webIdResource, { method: 'DELETE', headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect((await res.json()).webIds[webId]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can link the WebID again.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.webId, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
let json = await res.json();
|
||||
expect(json.resource).toBeDefined();
|
||||
expect(json.oidcIssuer).toBe(baseUrl);
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
json = await res.json();
|
||||
expect(json.webIds[webId]).toBeDefined();
|
||||
});
|
||||
|
||||
it('needs to prove ownership when linking a WebID outside of a pod.', async(): Promise<void> => {
|
||||
const otherWebId = joinUrl(baseUrl, 'other#me');
|
||||
// Create the WebID
|
||||
let res = await fetch(otherWebId, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: '',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
|
||||
// Try to link the WebID
|
||||
res = await fetch(controls.account.webId, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId: otherWebId }),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
let json = await res.json();
|
||||
expect(json.details?.quad).toBeDefined();
|
||||
const { quad } = json.details;
|
||||
|
||||
// Update the WebID with the identifying quad
|
||||
await fetch(otherWebId, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: quad,
|
||||
});
|
||||
|
||||
// Try to link the WebID again
|
||||
res = await fetch(controls.account.webId, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ webId: otherWebId }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
json = await res.json();
|
||||
// 2 linked WebIDs now
|
||||
expect(json.webIds[webId]).toBeDefined();
|
||||
expect(json.webIds[otherWebId]).toBeDefined();
|
||||
});
|
||||
|
||||
it('can create a client credentials token.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.clientCredentials, {
|
||||
method: 'POST',
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'token', webId }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.id).toMatch(/^token/u);
|
||||
expect(json.secret).toBeDefined();
|
||||
expect(json.resource).toBeDefined();
|
||||
const { id, resource, secret } = json;
|
||||
|
||||
// Verify if the content was added to the profile
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
const { clientCredentials } = await res.json();
|
||||
expect(clientCredentials[id]).toBe(resource);
|
||||
|
||||
// Request a token
|
||||
const authString = `${encodeURIComponent(id)}:${encodeURIComponent(secret)}`;
|
||||
res = await fetch(joinUrl(baseUrl, '.oidc/token'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Basic ${Buffer.from(authString).toString('base64')}`,
|
||||
'content-type': APPLICATION_X_WWW_FORM_URLENCODED,
|
||||
},
|
||||
body: 'grant_type=client_credentials&scope=webid',
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const { access_token: token } = await res.json();
|
||||
expect(token).toBeDefined();
|
||||
});
|
||||
|
||||
it('can remove registered WebIDs.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
let json = await res.json();
|
||||
|
||||
res = await fetch(json.webIds[webId], { method: 'DELETE', headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Make sure it's gone
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
json = await res.json();
|
||||
expect(json.webIds[webId]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('can remove credential tokens.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
let json = await res.json();
|
||||
|
||||
const tokenUrl = Object.values(json.clientCredentials)[0] as string;
|
||||
res = await fetch(tokenUrl, { method: 'DELETE', headers: { cookie }});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Make sure it's gone
|
||||
res = await fetch(controls.account.account, { headers: { cookie }});
|
||||
json = await res.json();
|
||||
expect(Object.keys(json.clientCredentials)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('can reset a password if forgotten.', async(): Promise<void> => {
|
||||
let res = await fetch(controls.password.forgot, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
expect(sendMail).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Parse reset URL out of mail
|
||||
const mail = sendMail.mock.calls[0][0];
|
||||
expect(mail.to).toBe(email);
|
||||
const match = /(http:.*)$/u.exec(mail.text);
|
||||
expect(match).toBeDefined();
|
||||
const resetUrl = match![1];
|
||||
res = await fetch(resetUrl);
|
||||
const url = new URL(resetUrl);
|
||||
const recordId = url.searchParams.get('rid');
|
||||
expect(recordId).toBeDefined();
|
||||
|
||||
// Reset the password
|
||||
password = 'resetSecret';
|
||||
res = await fetch(resetUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ recordId, password }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Verify logging in with the new password works
|
||||
res = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('set-cookie')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -54,7 +54,7 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
||||
'urn:solid-server:default:variable:socket': null,
|
||||
'urn:solid-server:default:variable:loggingLevel': 'off',
|
||||
'urn:solid-server:default:variable:showStackTrace': true,
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
||||
'urn:solid-server:default:variable:seedConfig': null,
|
||||
'urn:solid-server:default:variable:workers': 1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { mkdirSync } from 'fs';
|
||||
import fetch from 'cross-fetch';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { joinFilePath } from '../../src/util/PathUtil';
|
||||
import { register } from '../util/AccountUtil';
|
||||
import type { User } from '../util/AccountUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
|
||||
@@ -12,6 +14,7 @@ const podConfigJson = joinFilePath(rootFilePath, 'config-pod.json');
|
||||
|
||||
const configs: [string, any][] = [
|
||||
[ 'memory.json', {
|
||||
// Need to remove the config-pod.json
|
||||
teardown: async(): Promise<void> => removeFolder(rootFilePath),
|
||||
}],
|
||||
[ 'filesystem.json', {
|
||||
@@ -23,16 +26,18 @@ const configs: [string, any][] = [
|
||||
// Tests are very similar to subdomain/pod tests. Would be nice if they can be combined
|
||||
describe.each(configs)('A dynamic pod server with template config %s', (template, { teardown }): void => {
|
||||
let app: App;
|
||||
const settings = {
|
||||
const user: User = {
|
||||
podName: 'alice',
|
||||
webId: 'http://test.com/#alice',
|
||||
webId: 'http://example.com/#alice',
|
||||
email: 'alice@test.email',
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
template,
|
||||
createPod: true,
|
||||
settings: {
|
||||
template,
|
||||
},
|
||||
};
|
||||
const podUrl = `${baseUrl}${settings.podName}/`;
|
||||
const podUrl = `${baseUrl}${user.podName}/`;
|
||||
let controls: any;
|
||||
let authorization: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const variables: Record<string, any> = {
|
||||
@@ -61,13 +66,9 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
it('creates a pod with the given config.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(podUrl);
|
||||
const result = await register(baseUrl, user);
|
||||
({ controls, authorization } = result);
|
||||
expect(result.pod).toBe(podUrl);
|
||||
});
|
||||
|
||||
it('can fetch the created pod.', async(): Promise<void> => {
|
||||
@@ -83,7 +84,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
it('should be able to read acl file with the correct credentials.', async(): Promise<void> => {
|
||||
const res = await fetch(`${podUrl}.acl`, {
|
||||
headers: {
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@@ -92,7 +93,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
it('should be able to write to the pod now as the owner.', async(): Promise<void> => {
|
||||
let res = await fetch(`${podUrl}test`, {
|
||||
headers: {
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
@@ -100,7 +101,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
res = await fetch(`${podUrl}test`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
body: 'this is new data!',
|
||||
@@ -110,7 +111,7 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
|
||||
res = await fetch(`${podUrl}test`, {
|
||||
headers: {
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@@ -118,13 +119,12 @@ describe.each(configs)('A dynamic pod server with template config %s', (template
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
|
||||
const res = await fetch(`${baseUrl}idp/register/`, {
|
||||
const res = await fetch(controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(newSettings),
|
||||
headers: { authorization, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: user.podName, settings: { template }}),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.status).toBe(400);
|
||||
await expect(res.text()).resolves.toContain(`There already is a pod at ${podUrl}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { stringify } from 'querystring';
|
||||
import { URL } from 'url';
|
||||
import type { KeyPair } from '@inrupt/solid-client-authn-core';
|
||||
import {
|
||||
buildAuthenticatedFetch,
|
||||
createDpopHeader,
|
||||
generateDpopKeyPair,
|
||||
} from '@inrupt/solid-client-authn-core';
|
||||
import { load } from 'cheerio';
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import { parse, splitCookiesString } from 'set-cookie-parser';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { APPLICATION_JSON, APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
||||
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
||||
import { joinUrl } from '../../src/util/PathUtil';
|
||||
import { register } from '../util/AccountUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
import { IdentityTestState } from './IdentityTestState';
|
||||
@@ -31,20 +29,9 @@ const stores: [string, any][] = [
|
||||
}],
|
||||
];
|
||||
|
||||
// Don't send actual e-mails
|
||||
jest.mock('nodemailer');
|
||||
|
||||
// Prevent panva/node-openid-client from emitting DraftWarning
|
||||
jest.spyOn(process, 'emitWarning').mockImplementation();
|
||||
|
||||
async function postForm(url: string, formBody: string): Promise<Response> {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
|
||||
body: formBody,
|
||||
});
|
||||
}
|
||||
|
||||
// No way around the cookies https://github.com/panva/node-oidc-provider/issues/552 .
|
||||
// They will be simulated by storing the values and passing them along.
|
||||
// This is why the redirects are handled manually.
|
||||
@@ -54,23 +41,20 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
const redirectUrl = 'http://mockedredirect/';
|
||||
const container = new URL('secret/', baseUrl).href;
|
||||
const oidcIssuer = baseUrl;
|
||||
const card = joinUrl(baseUrl, 'profile/card');
|
||||
const webId = `${card}#me`;
|
||||
const webId2 = `${card}#someoneElse`;
|
||||
let webId3: string;
|
||||
const email = 'test@test.com';
|
||||
const email2 = 'bob@test.email';
|
||||
const email3 = 'alice@test.email';
|
||||
const indexUrl = joinUrl(baseUrl, '.account/');
|
||||
let webId: string;
|
||||
let webId2: string;
|
||||
const email = 'test@example.com';
|
||||
const email2 = 'otherMail@example.com';
|
||||
const password = 'password!';
|
||||
const password2 = 'password2!';
|
||||
let sendMail: jest.Mock;
|
||||
let controls: {
|
||||
oidc: { webId: string; consent: string; forgetWebId: string; prompt: string };
|
||||
main: { index: string };
|
||||
account: { create: string; pod: string; logout: string };
|
||||
password: { create: string; login: string };
|
||||
};
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Needs to happen before Components.js instantiation
|
||||
sendMail = jest.fn();
|
||||
const nodemailer = jest.requireMock('nodemailer');
|
||||
Object.assign(nodemailer, { createTransport: (): any => ({ sendMail }) });
|
||||
|
||||
const instances = await instantiateFromConfig(
|
||||
'urn:solid-server:test:Instances',
|
||||
getTestConfigPath(config),
|
||||
@@ -82,13 +66,9 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
({ app } = instances);
|
||||
await app.start();
|
||||
|
||||
// Create a simple webId
|
||||
const webIdTurtle = `<${webId}> <http://www.w3.org/ns/solid/terms#oidcIssuer> <${baseUrl}> .`;
|
||||
await fetch(card, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: webIdTurtle,
|
||||
});
|
||||
// Create accounts
|
||||
({ webId, controls } = await register(baseUrl, { email, password, podName: 'test' }));
|
||||
({ webId: webId2 } = await register(baseUrl, { email: email2, password, podName: 'otherTest' }));
|
||||
|
||||
// Create container where only webId can write
|
||||
const aclTurtle = `
|
||||
@@ -114,43 +94,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
describe('doing registration', (): void => {
|
||||
let formBody: string;
|
||||
let registrationTriple: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// We will need this twice
|
||||
formBody = stringify({ email, webId, password, confirmPassword: password, register: 'ok' });
|
||||
});
|
||||
|
||||
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register/`, formBody);
|
||||
expect(res.status).toBe(400);
|
||||
const json = await res.json();
|
||||
registrationTriple = json.details.quad;
|
||||
});
|
||||
|
||||
it('updates the webId with the registration token.', async(): Promise<void> => {
|
||||
const patchBody = `INSERT DATA { ${registrationTriple} }`;
|
||||
const res = await fetch(webId, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: patchBody,
|
||||
});
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('sends the form again to successfully register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register/`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.json()).resolves.toEqual(expect.objectContaining({
|
||||
webId,
|
||||
email,
|
||||
oidcIssuer: baseUrl,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('authenticating', (): void => {
|
||||
let state: IdentityTestState;
|
||||
|
||||
@@ -162,16 +105,64 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
await state.session.logout();
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
let url = await state.startSession();
|
||||
it('initializes the session.', async(): Promise<void> => {
|
||||
// This is the auth URL with all the relevant query parameters
|
||||
let url = await state.initSession();
|
||||
expect(url.startsWith(oidcIssuer)).toBeTruthy();
|
||||
|
||||
// Always redirect to our index page
|
||||
url = await state.handleRedirect(url);
|
||||
|
||||
// Compare received URL with login URL in our controls
|
||||
expect(controls.main.index).toBe(url);
|
||||
|
||||
// Add the OIDC controls to the object
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, email, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
controls = {
|
||||
...(await res.json()).controls,
|
||||
...controls,
|
||||
};
|
||||
});
|
||||
|
||||
it('can only access the container when using the logged in session.', async(): Promise<void> => {
|
||||
it('logs in.', async(): Promise<void> => {
|
||||
// Log in using email/password
|
||||
const res = await state.fetchIdp(controls.password.login, 'POST', JSON.stringify({ email, password }));
|
||||
|
||||
// Redirect to WebID picker
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
});
|
||||
|
||||
it('sends a token for the chosen WebID.', async(): Promise<void> => {
|
||||
// See the available WebIDs
|
||||
let res = await state.fetchIdp(controls.oidc.webId);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.webIds).toEqual([ webId ]);
|
||||
|
||||
// Pick the WebID
|
||||
// Errors if the WebID is not registered to the account
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId: 'http://example.com/wrong' });
|
||||
expect(res.status).toBe(400);
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId, remember: true });
|
||||
|
||||
// Redirect to the consent page
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
});
|
||||
|
||||
it('consents and redirects back to the client.', async(): Promise<void> => {
|
||||
let res = await state.fetchIdp(controls.oidc.consent);
|
||||
const json = await res.json();
|
||||
expect(json.webId).toBe(webId);
|
||||
expect(json.client.grant_types).toContain('authorization_code');
|
||||
expect(json.client.grant_types).toContain('refresh_token');
|
||||
|
||||
res = await state.fetchIdp(controls.oidc.consent, 'POST');
|
||||
|
||||
// Redirect back to the client and verify login success
|
||||
await state.handleIncomingRedirect(res, webId);
|
||||
});
|
||||
|
||||
it('can only access the profile container when using the logged in session.', async(): Promise<void> => {
|
||||
let res = await fetch(container);
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
@@ -185,16 +176,61 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('can log in again.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
it('immediately gets redirect to the consent page in the next session.', async(): Promise<void> => {
|
||||
const url = await state.initSession();
|
||||
await state.handleRedirect(url);
|
||||
|
||||
const res = await state.fetchIdp(url);
|
||||
const res = await state.fetchIdp(controls.oidc.prompt);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.json()).resolves.toEqual(expect.objectContaining({ prompt: 'consent' }));
|
||||
});
|
||||
|
||||
it('can forget the stored WebID.', async(): Promise<void> => {
|
||||
let res = await state.fetchIdp(controls.oidc.forgetWebId, 'POST');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Will receive confirm screen here instead of login screen
|
||||
await state.consent(url);
|
||||
// We have to pick a WebID again
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId });
|
||||
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
// Redirect back to the consent page
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
});
|
||||
|
||||
it('can consent again.', async(): Promise<void> => {
|
||||
let res = await state.fetchIdp(controls.oidc.consent, 'POST');
|
||||
|
||||
// Redirect back to the client and verify login success
|
||||
await state.handleIncomingRedirect(res, webId);
|
||||
|
||||
// Verify by accessing the private container
|
||||
res = await state.session.fetch(container);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can log out.', async(): Promise<void> => {
|
||||
// Log out
|
||||
let res = await state.fetchIdp(controls.account.logout, 'POST');
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Log out of the previous session and start a new one
|
||||
await state.session.logout();
|
||||
const url = await state.initSession();
|
||||
await state.handleRedirect(url);
|
||||
|
||||
// Log in
|
||||
res = await state.fetchIdp(controls.password.login, 'POST', { email: email2, password });
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
|
||||
// Pick the new WebID
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId: webId2 });
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
|
||||
// Consent again
|
||||
res = await state.fetchIdp(controls.oidc.consent, 'POST');
|
||||
|
||||
// Redirect back to the client and verify login success
|
||||
await state.handleIncomingRedirect(res, webId2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,20 +284,37 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
let url = await state.startSession(clientId);
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, email, password);
|
||||
const url = await state.initSession(clientId);
|
||||
|
||||
// Redirect to our login page
|
||||
await state.handleRedirect(url);
|
||||
|
||||
// Log in using email/password
|
||||
let res = await state.fetchIdp(controls.password.login, 'POST', { email, password });
|
||||
|
||||
// Redirect to WebID picker
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
|
||||
// Pick the WebID
|
||||
res = await state.fetchIdp(controls.oidc.webId, 'POST', { webId, remember: true });
|
||||
|
||||
// Redirect to the consent page
|
||||
await expect(state.handleLocationRedirect(res)).resolves.toBe(indexUrl);
|
||||
|
||||
// Verify the client information the server discovered
|
||||
const consentRes = await state.fetchIdp(url, 'GET');
|
||||
expect(consentRes.status).toBe(200);
|
||||
const { client } = await consentRes.json();
|
||||
expect(client.client_id).toBe(clientJson.client_id);
|
||||
expect(client.client_name).toBe(clientJson.client_name);
|
||||
res = await state.fetchIdp(controls.oidc.consent);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.webId).toBe(webId);
|
||||
expect(json.client.client_id).toBe(clientJson.client_id);
|
||||
expect(json.client.client_name).toBe(clientJson.client_name);
|
||||
expect(json.client.grant_types).toContain('authorization_code');
|
||||
expect(json.client.grant_types).toContain('refresh_token');
|
||||
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
res = await state.fetchIdp(controls.oidc.consent, 'POST');
|
||||
|
||||
// Redirect back to the client and verify login success
|
||||
await state.handleIncomingRedirect(res, webId);
|
||||
});
|
||||
|
||||
it('rejects requests in case the redirect URL is not accepted.', async(): Promise<void> => {
|
||||
@@ -288,7 +341,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
});
|
||||
|
||||
describe('using client_credentials', (): void => {
|
||||
const credentialsUrl = joinUrl(baseUrl, '/idp/credentials/');
|
||||
const tokenUrl = joinUrl(baseUrl, '.oidc/token');
|
||||
let dpopKey: KeyPair;
|
||||
let id: string | undefined;
|
||||
@@ -300,18 +352,26 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
});
|
||||
|
||||
it('can request a credentials token.', async(): Promise<void> => {
|
||||
// Login and save cookie
|
||||
const loginResponse = await fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
const cookies = parse(splitCookiesString(loginResponse.headers.get('set-cookie')!));
|
||||
const cookie = `${cookies[0].name}=${cookies[0].value}`;
|
||||
|
||||
// Request token
|
||||
const accountJson = await (await fetch(indexUrl, { headers: { cookie }})).json();
|
||||
const credentialsUrl = accountJson.controls.account.clientCredentials;
|
||||
const res = await fetch(credentialsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': APPLICATION_JSON,
|
||||
},
|
||||
body: JSON.stringify({ email, password, name: 'token' }),
|
||||
headers: { cookie, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: 'token', webId }),
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
({ id, secret } = await res.json());
|
||||
expect(typeof id).toBe('string');
|
||||
expect(typeof secret).toBe('string');
|
||||
expect(id).toMatch(/^token/u);
|
||||
});
|
||||
|
||||
it('can request an access token using the credentials.', async(): Promise<void> => {
|
||||
@@ -339,288 +399,6 @@ describe.each(stores)('A Solid server with IDP using %s', (name, { config, teard
|
||||
res = await authFetch(container);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can see all credentials.', async(): Promise<void> => {
|
||||
const res = await fetch(credentialsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': APPLICATION_JSON,
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.json()).resolves.toEqual([ id ]);
|
||||
});
|
||||
|
||||
it('can delete credentials.', async(): Promise<void> => {
|
||||
let res = await fetch(credentialsUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': APPLICATION_JSON,
|
||||
},
|
||||
body: JSON.stringify({ email, password, delete: id }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Client_credentials call should fail now
|
||||
const dpopHeader = await createDpopHeader(tokenUrl, 'POST', dpopKey);
|
||||
const authString = `${encodeURIComponent(id!)}:${encodeURIComponent(secret!)}`;
|
||||
res = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: `Basic ${Buffer.from(authString).toString('base64')}`,
|
||||
'content-type': APPLICATION_X_WWW_FORM_URLENCODED,
|
||||
dpop: dpopHeader,
|
||||
},
|
||||
body: 'grant_type=client_credentials&scope=webid',
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetting password', (): void => {
|
||||
let nextUrl: string;
|
||||
|
||||
it('sends the corresponding email address through the form to get a mail.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/forgotpassword/`, stringify({ email }));
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json.email).toBe(email);
|
||||
|
||||
const mail = sendMail.mock.calls[0][0];
|
||||
expect(mail.to).toBe(email);
|
||||
const match = /(http:.*)$/u.exec(mail.text);
|
||||
expect(match).toBeDefined();
|
||||
nextUrl = match![1];
|
||||
expect(nextUrl).toMatch(/\/resetpassword\/[^/]+$/u);
|
||||
});
|
||||
|
||||
it('resets the password through the given link.', async(): Promise<void> => {
|
||||
// Extract the submit URL from the reset password form
|
||||
let res = await fetch(nextUrl);
|
||||
expect(res.status).toBe(200);
|
||||
const text = await res.text();
|
||||
const relative = load(text)('form').attr('action');
|
||||
// Reset password form has no action causing the current URL to be used
|
||||
expect(relative).toBeUndefined();
|
||||
|
||||
// Extract recordId from URL since JS is used to add it
|
||||
const recordId = /\?rid=([^/]+)$/u.exec(nextUrl)?.[1];
|
||||
expect(typeof recordId).toBe('string');
|
||||
|
||||
// POST the new password to the same URL
|
||||
const formData = stringify({ password: password2, confirmPassword: password2, recordId });
|
||||
res = await fetch(nextUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': APPLICATION_X_WWW_FORM_URLENCODED },
|
||||
body: formData,
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging in after password reset', (): void => {
|
||||
let state: IdentityTestState;
|
||||
let nextUrl: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
await state.session.logout();
|
||||
});
|
||||
|
||||
it('can not log in with the old password anymore.', async(): Promise<void> => {
|
||||
const url = await state.startSession();
|
||||
nextUrl = url;
|
||||
let res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
const formData = stringify({ email, password });
|
||||
res = await state.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(500);
|
||||
expect(await res.text()).toContain('Incorrect password');
|
||||
});
|
||||
|
||||
it('can log in with the new password.', async(): Promise<void> => {
|
||||
const url = await state.login(nextUrl, email, password2);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating pods without registering with the IDP', (): void => {
|
||||
let formBody: string;
|
||||
let registrationTriple: string;
|
||||
const podName = 'myPod';
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// We will need this twice
|
||||
formBody = stringify({
|
||||
email: email2,
|
||||
webId: webId2,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
podName,
|
||||
createPod: 'ok',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends the form once to receive the registration triple.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register/`, formBody);
|
||||
expect(res.status).toBe(400);
|
||||
const json = await res.json();
|
||||
registrationTriple = json.details.quad;
|
||||
});
|
||||
|
||||
it('updates the webId with the registration token.', async(): Promise<void> => {
|
||||
const patchBody = `INSERT DATA { ${registrationTriple} }`;
|
||||
const res = await fetch(webId2, {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: patchBody,
|
||||
});
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('sends the form again to successfully register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register/`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.json()).resolves.toEqual(expect.objectContaining({
|
||||
email: email2,
|
||||
webId: webId2,
|
||||
podBaseUrl: `${baseUrl}${podName}/`,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('creating a new WebID', (): void => {
|
||||
const podName = 'alice';
|
||||
let state: IdentityTestState;
|
||||
|
||||
const formBody = stringify({
|
||||
email: email3, password, confirmPassword: password, podName, createWebId: 'ok', register: 'ok', createPod: 'ok',
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
await state.session.logout();
|
||||
});
|
||||
|
||||
it('sends the form to create the WebID and register.', async(): Promise<void> => {
|
||||
const res = await postForm(`${baseUrl}idp/register/`, formBody);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
expect(json).toEqual(expect.objectContaining({
|
||||
webId: expect.any(String),
|
||||
email: email3,
|
||||
oidcIssuer: baseUrl,
|
||||
podBaseUrl: `${baseUrl}${podName}/`,
|
||||
}));
|
||||
webId3 = json.webId;
|
||||
});
|
||||
|
||||
it('initializes the session and logs in.', async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
let url = await state.startSession();
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, email3, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId3);
|
||||
});
|
||||
|
||||
it('can only write to the new profile when using the logged in session.', async(): Promise<void> => {
|
||||
const patchOptions = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/sparql-update' },
|
||||
body: `INSERT DATA { <> <http://www.w3.org/2000/01/rdf-schema#label> "A cool WebID." }`,
|
||||
};
|
||||
|
||||
let res = await fetch(webId3, patchOptions);
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
res = await state.session.fetch(webId3, patchOptions);
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('always has control over data in the pod.', async(): Promise<void> => {
|
||||
const podBaseUrl = `${baseUrl}${podName}/`;
|
||||
const brokenAcl = '<#authorization> a <http://www.w3.org/ns/auth/acl#Authorization> .';
|
||||
|
||||
// Make the acl file unusable
|
||||
let res = await state.session.fetch(`${podBaseUrl}.acl`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: brokenAcl,
|
||||
});
|
||||
expect(res.status).toBe(205);
|
||||
|
||||
// The owner is locked out of their own pod due to a faulty acl file
|
||||
res = await state.session.fetch(podBaseUrl);
|
||||
expect(res.status).toBe(403);
|
||||
|
||||
const fixedAcl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
||||
|
||||
<#authorization>
|
||||
a acl:Authorization;
|
||||
acl:agentClass foaf:Agent;
|
||||
acl:mode acl:Read;
|
||||
acl:accessTo <./>.`;
|
||||
// Owner can still update the acl
|
||||
res = await state.session.fetch(`${podBaseUrl}.acl`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: fixedAcl,
|
||||
});
|
||||
expect(res.status).toBe(205);
|
||||
|
||||
// Access is possible again
|
||||
res = await state.session.fetch(podBaseUrl);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('having multiple accounts', (): void => {
|
||||
let state: IdentityTestState;
|
||||
let url: string;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
state = new IdentityTestState(baseUrl, redirectUrl, oidcIssuer);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
await state.session.logout();
|
||||
});
|
||||
|
||||
it('initializes the session and logs in with the first account.', async(): Promise<void> => {
|
||||
url = await state.startSession();
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, email, password2);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
});
|
||||
|
||||
it('can log out on the consent page.', async(): Promise<void> => {
|
||||
await state.session.logout();
|
||||
|
||||
url = await state.startSession();
|
||||
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Will receive confirm screen here instead of login screen
|
||||
url = await state.logout(url);
|
||||
});
|
||||
|
||||
it('can log in with a different account.', async(): Promise<void> => {
|
||||
const res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, email3, password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup', (): void => {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { stringify } from 'querystring';
|
||||
import { URL } from 'url';
|
||||
import { Session } from '@inrupt/solid-client-authn-node';
|
||||
import { load } from 'cheerio';
|
||||
import type { Response } from 'cross-fetch';
|
||||
import { fetch } from 'cross-fetch';
|
||||
import type { Cookie } from 'set-cookie-parser';
|
||||
import { parse, splitCookiesString } from 'set-cookie-parser';
|
||||
import { APPLICATION_X_WWW_FORM_URLENCODED } from '../../src/util/ContentTypes';
|
||||
|
||||
/* eslint-disable jest/no-standalone-expect */
|
||||
/**
|
||||
@@ -34,11 +30,17 @@ export class IdentityTestState {
|
||||
* Performs a fetch call while keeping track of the stored cookies and preventing redirects.
|
||||
* @param url - URL to call.
|
||||
* @param method - Method to use.
|
||||
* @param body - Body to send along.
|
||||
* @param contentType - Content-Type of the body.
|
||||
* @param body - Body to send along. If this is not a string it will be JSONified.
|
||||
* @param contentType - Content-Type of the body. If not defined but there is a body, this will be set to JSON.
|
||||
*/
|
||||
public async fetchIdp(url: string, method = 'GET', body?: string, contentType?: string): Promise<Response> {
|
||||
public async fetchIdp(url: string, method = 'GET', body?: string | unknown, contentType?: string): Promise<Response> {
|
||||
const options = { method, headers: { cookie: this.cookie }, body, redirect: 'manual' } as any;
|
||||
if (body && typeof body !== 'string') {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
if (body && !contentType) {
|
||||
contentType = 'application/json';
|
||||
}
|
||||
if (contentType) {
|
||||
options.headers['content-type'] = contentType;
|
||||
}
|
||||
@@ -58,24 +60,11 @@ export class IdentityTestState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the given jquery command to find a node in the given html body.
|
||||
* The value from the given attribute field then gets extracted and combined with the base url.
|
||||
* @param html - Body to parse.
|
||||
* @param jquery - Query to run on the body.
|
||||
* @param attr - Attribute to extract.
|
||||
* Initializes the OIDC session for the given clientId.
|
||||
* If undefined, dynamic registration will be used.
|
||||
*/
|
||||
public extractUrl(html: string, jquery: string, attr: string): string {
|
||||
const url = load(html)(jquery).attr(attr);
|
||||
expect(typeof url).toBe('string');
|
||||
return new URL(url!, this.baseUrl).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes an authentication session and stores the relevant cookies for later re-use.
|
||||
* All te relevant links from the login page get extracted.
|
||||
*/
|
||||
public async startSession(clientId?: string): Promise<string> {
|
||||
let nextUrl = '';
|
||||
public async initSession(clientId?: string): Promise<string> {
|
||||
let nextUrl: string;
|
||||
await this.session.login({
|
||||
redirectUrl: this.redirectUrl,
|
||||
oidcIssuer: this.oidcIssuer,
|
||||
@@ -84,67 +73,43 @@ export class IdentityTestState {
|
||||
nextUrl = data;
|
||||
},
|
||||
});
|
||||
expect(nextUrl.length > 0).toBeTruthy();
|
||||
expect(nextUrl.startsWith(this.oidcIssuer)).toBeTruthy();
|
||||
|
||||
// Need to catch the redirect so we can copy the cookies
|
||||
let res = await this.fetchIdp(nextUrl);
|
||||
expect(res.status).toBe(303);
|
||||
nextUrl = res.headers.get('location')!;
|
||||
|
||||
// Handle redirect
|
||||
res = await this.fetchIdp(nextUrl);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
// Need to send request to prompt API to get actual location
|
||||
let json = await res.json();
|
||||
res = await this.fetchIdp(json.controls.prompt);
|
||||
json = await res.json();
|
||||
nextUrl = json.location;
|
||||
|
||||
return nextUrl;
|
||||
return nextUrl!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in by sending the corresponding email and password to the given form action.
|
||||
* The URL should be extracted from the login page.
|
||||
* Handles a URL that is expected to redirect and returns the target it would redirect to.
|
||||
*/
|
||||
public async login(url: string, email: string, password: string): Promise<string> {
|
||||
const formData = stringify({ email, password });
|
||||
let res = await this.fetchIdp(url, 'POST', formData, APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
res = await this.fetchIdp(json.location);
|
||||
public async handleRedirect(url: string): Promise<string> {
|
||||
const res = await this.fetchIdp(url);
|
||||
expect(res.status).toBe(303);
|
||||
expect(res.headers.has('location')).toBe(true);
|
||||
return res.headers.get('location')!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the consent screen at the given URL and the followup redirect back to the client.
|
||||
* Handles a JSON redirect. That is a request that returns a 200,
|
||||
* but has a `location` field in the JSON to indicate what it should redirect to.
|
||||
* That URL is expected to be another redirect, and this returns what it would redirect to.
|
||||
*/
|
||||
public async consent(url: string): Promise<void> {
|
||||
let res = await this.fetchIdp(url, 'POST', '', APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
public async handleLocationRedirect(res: Response): Promise<string> {
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
// The OIDC redirect
|
||||
expect(json.location).toBeDefined();
|
||||
|
||||
res = await this.fetchIdp(json.location);
|
||||
expect(res.status).toBe(303);
|
||||
const mockUrl = res.headers.get('location')!;
|
||||
expect(mockUrl.startsWith(this.redirectUrl)).toBeTruthy();
|
||||
return this.handleRedirect(json.location);
|
||||
}
|
||||
|
||||
public async handleIncomingRedirect(res: Response, webId: string): Promise<void> {
|
||||
// Redirect back to the client
|
||||
const url = await this.handleLocationRedirect(res);
|
||||
expect(url.startsWith(this.redirectUrl)).toBe(true);
|
||||
|
||||
// Workaround for https://github.com/inrupt/solid-client-authn-js/issues/2985
|
||||
const strippedUrl = new URL(mockUrl);
|
||||
const strippedUrl = new URL(url);
|
||||
strippedUrl.searchParams.delete('iss');
|
||||
const info = await this.session.handleIncomingRedirect(strippedUrl.href);
|
||||
expect(info?.isLoggedIn).toBe(true);
|
||||
}
|
||||
|
||||
public async logout(url: string): Promise<string> {
|
||||
let res = await this.fetchIdp(url, 'POST', stringify({ logOut: true }), APPLICATION_X_WWW_FORM_URLENCODED);
|
||||
expect(res.status).toBe(200);
|
||||
const json = await res.json();
|
||||
res = await this.fetchIdp(json.location);
|
||||
expect(res.status).toBe(303);
|
||||
return res.headers.get('location')!;
|
||||
expect(info?.webId).toBe(webId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Response } from 'cross-fetch';
|
||||
import { ensureDir, pathExists, stat } from 'fs-extra';
|
||||
import { joinUrl } from '../../src';
|
||||
import type { App } from '../../src';
|
||||
import { register } from '../util/AccountUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, getTestFolder, instantiateFromConfig, removeFolder } from './Config';
|
||||
|
||||
@@ -24,23 +25,7 @@ async function performSimplePutWithLength(path: string, length: number): Promise
|
||||
/** Registers two test pods on the server matching the 'baseUrl' */
|
||||
async function registerTestPods(baseUrl: string, pods: string[]): Promise<void> {
|
||||
for (const pod of pods) {
|
||||
await fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
createWebId: 'on',
|
||||
webId: '',
|
||||
register: 'on',
|
||||
createPod: 'on',
|
||||
podName: pod,
|
||||
email: `${pod}@example.ai`,
|
||||
password: 't',
|
||||
confirmPassword: 't',
|
||||
submit: '',
|
||||
}),
|
||||
});
|
||||
await register(baseUrl, { podName: pod, email: `${pod}@example.ai`, password: 't' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { fetch } from 'cross-fetch';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { joinUrl } from '../../src/util/PathUtil';
|
||||
import { register } from '../util/AccountUtil';
|
||||
import type { User } from '../util/AccountUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
|
||||
import { IdentityTestState } from './IdentityTestState';
|
||||
|
||||
const port = getPort('RestrictedIdentity');
|
||||
const baseUrl = `http://localhost:${port}/`;
|
||||
@@ -16,16 +17,13 @@ jest.spyOn(process, 'emitWarning').mockImplementation();
|
||||
|
||||
describe('A server with restricted IDP access', (): void => {
|
||||
let app: App;
|
||||
const settings = {
|
||||
const user: User = {
|
||||
podName: 'alice',
|
||||
email: 'alice@test.email',
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
createWebId: true,
|
||||
register: true,
|
||||
createPod: true,
|
||||
};
|
||||
const webId = joinUrl(baseUrl, 'alice/profile/card#me');
|
||||
let controls: any;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const instances = await instantiateFromConfig(
|
||||
@@ -45,22 +43,17 @@ describe('A server with restricted IDP access', (): void => {
|
||||
let res = await fetch(joinUrl(baseUrl, '.well-known/.acl'));
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
res = await fetch(joinUrl(baseUrl, 'idp/.acl'));
|
||||
res = await fetch(joinUrl(baseUrl, '.account/.acl'));
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can create a pod.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.webId).toBe(webId);
|
||||
const result = await register(baseUrl, user);
|
||||
({ controls } = result);
|
||||
expect(result.webId).toBe(webId);
|
||||
});
|
||||
|
||||
it('can restrict registration access.', async(): Promise<void> => {
|
||||
it('can restrict account creation.', async(): Promise<void> => {
|
||||
// Only allow new WebID to register
|
||||
const restrictedAcl = `@prefix acl: <http://www.w3.org/ns/auth/acl#>.
|
||||
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
|
||||
@@ -71,49 +64,33 @@ describe('A server with restricted IDP access', (): void => {
|
||||
acl:mode acl:Read, acl:Write, acl:Control;
|
||||
acl:accessTo <./>.`;
|
||||
|
||||
let res = await fetch(`${baseUrl}idp/register/.acl`, {
|
||||
let res = await fetch(`${controls.account.create}.acl`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'text/turtle' },
|
||||
body: restrictedAcl,
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(`${baseUrl}idp/register/.acl`);
|
||||
expect(res.headers.get('location')).toBe(`${controls.account.create}.acl`);
|
||||
|
||||
// Registration is now disabled
|
||||
res = await fetch(`${baseUrl}idp/register/`);
|
||||
res = await fetch(controls.account.create);
|
||||
expect(res.status).toBe(401);
|
||||
|
||||
res = await fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }),
|
||||
});
|
||||
res = await fetch(controls.account.create, { method: 'POST' });
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it('can still access registration with the correct credentials.', async(): Promise<void> => {
|
||||
// Logging into session
|
||||
const state = new IdentityTestState(baseUrl, 'http://mockedredirect/', baseUrl);
|
||||
let url = await state.startSession();
|
||||
let res = await state.fetchIdp(url);
|
||||
expect(res.status).toBe(200);
|
||||
url = await state.login(url, settings.email, settings.password);
|
||||
await state.consent(url);
|
||||
expect(state.session.info?.webId).toBe(webId);
|
||||
|
||||
// Registration still works for this WebID
|
||||
res = await state.session.fetch(`${baseUrl}idp/register/`);
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
res = await state.session.fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify({ ...settings, email: 'bob@test.email', podName: 'bob' }),
|
||||
it('can still create accounts with the correct credentials.', async(): Promise<void> => {
|
||||
// Account creation still works for the WebID
|
||||
let res = await fetch(controls.account.create, {
|
||||
headers: { authorization: `WebID ${webId}` },
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.webId).toBe(joinUrl(baseUrl, 'bob/profile/card#me'));
|
||||
|
||||
await state.session.logout();
|
||||
res = await fetch(controls.account.create, {
|
||||
method: 'POST',
|
||||
headers: { authorization: `WebID ${webId}` },
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fetch from 'cross-fetch';
|
||||
import { outputJson } from 'fs-extra';
|
||||
import { ensureFile, writeJson } from 'fs-extra';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { joinFilePath, joinUrl } from '../../src/util/PathUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
@@ -11,34 +11,50 @@ const baseUrl = `http://localhost:${port}/`;
|
||||
const rootFilePath = getTestFolder('seeding-pods');
|
||||
|
||||
describe('A server with seeded pods', (): void => {
|
||||
const seedingJson = joinFilePath(rootFilePath, 'pods.json');
|
||||
const indexUrl = joinUrl(baseUrl, '.account/');
|
||||
let app: App;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Create seeding config
|
||||
await outputJson(seedingJson, [
|
||||
// Create the seed file
|
||||
const seed = [
|
||||
{
|
||||
podName: 'alice',
|
||||
email: 'alice@example.com',
|
||||
password: 'alice-password',
|
||||
email: 'test1@example.com',
|
||||
password: 'password1',
|
||||
pods: [
|
||||
{ name: 'pod1' },
|
||||
{ name: 'pod2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
podName: 'bob',
|
||||
email: 'bob@example.com',
|
||||
password: 'bob-password',
|
||||
register: false,
|
||||
email: 'test2@example.com',
|
||||
password: 'password2',
|
||||
pods: [
|
||||
{ name: 'pod3' },
|
||||
// This will fail
|
||||
{ name: 'pod2' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const variables = {
|
||||
...getDefaultVariables(port, baseUrl),
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': seedingJson,
|
||||
};
|
||||
{
|
||||
// This will all fail
|
||||
email: 'test1@example.com',
|
||||
password: 'password3',
|
||||
pods: [
|
||||
{ name: 'pod4' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const path = joinFilePath(rootFilePath, 'seed.json');
|
||||
await ensureFile(path);
|
||||
await writeJson(path, seed);
|
||||
|
||||
// Start server with the seed config
|
||||
const instances = await instantiateFromConfig(
|
||||
'urn:solid-server:test:Instances',
|
||||
getTestConfigPath('server-memory.json'),
|
||||
variables,
|
||||
{
|
||||
...getDefaultVariables(port, baseUrl),
|
||||
'urn:solid-server:default:variable:seedConfig': path,
|
||||
},
|
||||
) as Record<string, any>;
|
||||
({ app } = instances);
|
||||
await app.start();
|
||||
@@ -49,10 +65,33 @@ describe('A server with seeded pods', (): void => {
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
it('has created the requested pods.', async(): Promise<void> => {
|
||||
let response = await fetch(joinUrl(baseUrl, 'alice/profile/card#me'));
|
||||
expect(response.status).toBe(200);
|
||||
response = await fetch(joinUrl(baseUrl, 'bob/profile/card#me'));
|
||||
expect(response.status).toBe(200);
|
||||
it('can seed accounts and pods.', async(): Promise<void> => {
|
||||
// Get the controls
|
||||
const res = await fetch(indexUrl);
|
||||
expect(res.status).toBe(200);
|
||||
const { controls } = await res.json();
|
||||
|
||||
// Verify that the pods exists
|
||||
await expect(fetch(joinUrl(baseUrl, 'pod1/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
|
||||
await expect(fetch(joinUrl(baseUrl, 'pod2/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
|
||||
await expect(fetch(joinUrl(baseUrl, 'pod3/'))).resolves.toEqual(expect.objectContaining({ status: 200 }));
|
||||
await expect(fetch(joinUrl(baseUrl, 'pod4/'))).resolves.toEqual(expect.objectContaining({ status: 404 }));
|
||||
|
||||
// Verify that we can log in with the accounts
|
||||
await expect(fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test1@example.com', password: 'password1' }),
|
||||
})).resolves.toEqual(expect.objectContaining({ status: 200 }));
|
||||
await expect(fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test2@example.com', password: 'password2' }),
|
||||
})).resolves.toEqual(expect.objectContaining({ status: 200 }));
|
||||
await expect(fetch(controls.password.login, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ email: 'test1@example.com', password: 'password3' }),
|
||||
})).resolves.toEqual(expect.objectContaining({ status: 403 }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ const baseUrl = `http://localhost:${port}/`;
|
||||
|
||||
// Some tests with real Requests/Responses until the mocking library has been removed from the tests
|
||||
describe('A Solid server', (): void => {
|
||||
const document = `${baseUrl}document`;
|
||||
const container = `${baseUrl}container/`;
|
||||
let app: App;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
@@ -31,76 +33,70 @@ describe('A Solid server', (): void => {
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
it('can PUT to containers.', async(): Promise<void> => {
|
||||
const res = await fetch(container, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(container);
|
||||
});
|
||||
|
||||
it('can PUT to documents.', async(): Promise<void> => {
|
||||
const res = await fetch(document, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(document);
|
||||
});
|
||||
|
||||
it('can do a successful HEAD request to a container.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl, { method: 'HEAD' });
|
||||
const res = await fetch(container, { method: 'HEAD' });
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful HEAD request to a container without accept headers.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl, { method: 'HEAD', headers: { accept: '' }});
|
||||
const res = await fetch(container, { method: 'HEAD', headers: { accept: '' }});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful HEAD request to a document.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}.acl`;
|
||||
const res = await fetch(url, { method: 'HEAD' });
|
||||
const res = await fetch(document, { method: 'HEAD' });
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful HEAD request to a document without accept headers.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}.acl`;
|
||||
const res = await fetch(url, { method: 'HEAD', headers: { accept: '' }});
|
||||
const res = await fetch(document, { method: 'HEAD', headers: { accept: '' }});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful GET request to a container.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl);
|
||||
const res = await fetch(container);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful GET request to a container without accept headers.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl, { headers: { accept: '' }});
|
||||
const res = await fetch(container, { headers: { accept: '' }});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful GET request to a document.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}.acl`;
|
||||
const res = await fetch(url);
|
||||
const res = await fetch(document);
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can do a successful GET request to a document without accept headers.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}.acl`;
|
||||
const res = await fetch(url, { headers: { accept: '' }});
|
||||
const res = await fetch(document, { headers: { accept: '' }});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
it('can PUT to containers.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}containerPUT/`;
|
||||
const res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(url);
|
||||
});
|
||||
|
||||
it('can PUT to resources.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}resourcePUT`;
|
||||
const res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(url);
|
||||
});
|
||||
|
||||
it('can handle PUT errors.', async(): Promise<void> => {
|
||||
// There was a specific case where the following request caused the connection to close instead of error
|
||||
const res = await fetch(baseUrl, {
|
||||
@@ -116,7 +112,7 @@ describe('A Solid server', (): void => {
|
||||
});
|
||||
|
||||
it('can POST to create a container.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl, {
|
||||
const res = await fetch(container, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
@@ -126,58 +122,41 @@ describe('A Solid server', (): void => {
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(`${baseUrl}containerPOST/`);
|
||||
expect(res.headers.get('location')).toBe(`${container}containerPOST/`);
|
||||
});
|
||||
|
||||
it('can POST to create a document.', async(): Promise<void> => {
|
||||
const res = await fetch(baseUrl, {
|
||||
const res = await fetch(container, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
slug: 'resourcePOST',
|
||||
slug: 'documentPOST',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
expect(res.status).toBe(201);
|
||||
expect(res.headers.get('location')).toBe(`${baseUrl}resourcePOST`);
|
||||
expect(res.headers.get('location')).toBe(`${container}documentPOST`);
|
||||
});
|
||||
|
||||
it('can DELETE containers.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}containerDELETE/`;
|
||||
await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
const res = await fetch(url, { method: 'DELETE' });
|
||||
const res = await fetch(`${container}containerPOST/`, { method: 'DELETE' });
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('can DELETE documents.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}resourceDELETE`;
|
||||
await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
const res = await fetch(url, { method: 'DELETE' });
|
||||
const res = await fetch(`${container}documentPOST`, { method: 'DELETE' });
|
||||
expect(res.status).toBe(205);
|
||||
});
|
||||
|
||||
it('can PATCH documents.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}resourcePATCH`;
|
||||
await fetch(url, {
|
||||
await fetch(document, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
const res = await fetch(document, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'content-type': 'application/sparql-update',
|
||||
@@ -188,14 +167,13 @@ describe('A Solid server', (): void => {
|
||||
});
|
||||
|
||||
it('can not PATCH containers.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}containerPATCH/`;
|
||||
await fetch(url, {
|
||||
await fetch(container, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
const res = await fetch(container, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'content-type': 'application/sparql-update',
|
||||
@@ -206,15 +184,7 @@ describe('A Solid server', (): void => {
|
||||
});
|
||||
|
||||
it('can PATCH metadata resources.', async(): Promise<void> => {
|
||||
const url = `${baseUrl}resourcePATCH`;
|
||||
await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'content-type': 'text/turtle',
|
||||
},
|
||||
body: '<a:b> <a:b> <a:b>.',
|
||||
});
|
||||
const res = await fetch(`${url}.meta`, {
|
||||
const res = await fetch(`${document}.meta`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'content-type': 'application/sparql-update',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import fetch from 'cross-fetch';
|
||||
import type { App } from '../../src/init/App';
|
||||
import { register } from '../util/AccountUtil';
|
||||
import type { User } from '../util/AccountUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import {
|
||||
getDefaultVariables,
|
||||
@@ -28,16 +30,16 @@ const stores: [string, any][] = [
|
||||
// Simulating subdomains using the forwarded header so no DNS changes are required
|
||||
describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardown }): void => {
|
||||
let app: App;
|
||||
const settings = {
|
||||
podName: 'alice',
|
||||
webId: 'http://test.com/#alice',
|
||||
email: 'alice@test.email',
|
||||
const user: User = {
|
||||
email: 'alice@example.com',
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
createPod: true,
|
||||
webId: 'http://example.com/#alice',
|
||||
podName: 'alice',
|
||||
};
|
||||
const podHost = `alice.localhost:${port}`;
|
||||
const podUrl = `http://${podHost}/`;
|
||||
let authorization: string;
|
||||
let controls: any;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const variables: Record<string, any> = {
|
||||
@@ -74,7 +76,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
let res = await fetch(`${baseUrl}alice`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
body: 'this is new data!',
|
||||
@@ -90,13 +92,9 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
|
||||
describe('handling pods', (): void => {
|
||||
it('creates pods in a subdomain.', async(): Promise<void> => {
|
||||
const res = await fetch(`${baseUrl}idp/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
await expect(res.text()).resolves.toContain(podUrl);
|
||||
const result = await register(baseUrl, user);
|
||||
({ controls, authorization } = result);
|
||||
expect(result.pod).toBe(podUrl);
|
||||
});
|
||||
|
||||
it('can fetch the created pod in a subdomain.', async(): Promise<void> => {
|
||||
@@ -113,7 +111,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
const res = await fetch(`${baseUrl}.acl`, {
|
||||
headers: {
|
||||
forwarded: `host=${podHost}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@@ -123,7 +121,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
let res = await fetch(`${baseUrl}alice`, {
|
||||
headers: {
|
||||
forwarded: `host=${podHost}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
@@ -132,7 +130,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
forwarded: `host=${podHost}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
'content-type': 'text/plain',
|
||||
},
|
||||
body: 'this is new data!',
|
||||
@@ -143,7 +141,7 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
res = await fetch(`${baseUrl}alice`, {
|
||||
headers: {
|
||||
forwarded: `host=${podHost}`,
|
||||
authorization: `WebID ${settings.webId}`,
|
||||
authorization: `WebID ${user.webId}`,
|
||||
},
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
@@ -151,13 +149,12 @@ describe.each(stores)('A subdomain server with %s', (name, { storeConfig, teardo
|
||||
});
|
||||
|
||||
it('should not be able to create a pod with the same name.', async(): Promise<void> => {
|
||||
const newSettings = { ...settings, webId: 'http://test.com/#bob', email: 'bob@test.email' };
|
||||
const res = await fetch(`${baseUrl}idp/register/`, {
|
||||
const res = await fetch(controls.account.pod, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(newSettings),
|
||||
headers: { authorization, 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ name: user.podName }),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.status).toBe(400);
|
||||
await expect(res.text()).resolves.toContain(`There already is a resource at ${podUrl}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/acp.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
"css:config/identity/access/restricted.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
"css:config/identity/pod/dynamic.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
@@ -29,7 +29,7 @@
|
||||
"css:config/util/index/default.json",
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/memory.json",
|
||||
"css:config/util/resource-locker/debug-void.json",
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"css:config/http/notifications/disabled.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
|
||||
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
|
||||
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
@@ -30,10 +30,7 @@
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/redis.json",
|
||||
"css:config/util/variables/default.json",
|
||||
|
||||
"css:config/identity/handler/account-store/default.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json"
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/default.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"css:config/http/notifications/websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
|
||||
|
||||
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
@@ -30,9 +30,7 @@
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/memory.json",
|
||||
"css:config/util/variables/default.json",
|
||||
|
||||
"css:config/identity/handler/account-store/default.json"
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
]
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/interaction/no-accounts.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
|
||||
Reference in New Issue
Block a user