feat: Store account settings separately

Account settings are stored using the WebID as key.
Reason for using the WebID is that this allows faster access to the settings
in authenticated requests.
A consequence of this is that passwords are now always required during registration,
and that there can only be 1 account per WebID.
This commit is contained in:
Joachim Van Herwegen
2021-08-30 16:47:34 +02:00
parent f40e2c768f
commit 6c4ccb334d
11 changed files with 246 additions and 166 deletions

View File

@@ -23,7 +23,15 @@ 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 = { podName: 'alice', webId: 'http://test.com/#alice', email: 'alice@test.email', template, createPod: true };
const settings = {
podName: 'alice',
webId: 'http://test.com/#alice',
email: 'alice@test.email',
password: 'password',
confirmPassword: 'password',
template,
createPod: true,
};
const podUrl = `${baseUrl}${settings.podName}/`;
beforeAll(async(): Promise<void> => {
@@ -109,10 +117,11 @@ 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`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings),
body: JSON.stringify(newSettings),
});
expect(res.status).toBe(409);
await expect(res.text()).resolves.toContain(`There already is a pod at ${podUrl}`);

View File

@@ -53,6 +53,7 @@ describe('A Solid server with IDP', (): void => {
const oidcIssuer = baseUrl;
const card = joinUrl(baseUrl, 'profile/card');
const webId = `${card}#me`;
const webId2 = `${card}#someoneElse`;
const email = 'test@test.com';
const password = 'password!';
const password2 = 'password2!';
@@ -241,25 +242,32 @@ describe('A Solid server with IDP', (): void => {
});
});
describe('creating pods without registering', (): void => {
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, webId, podName, createPod: 'ok' });
formBody = stringify({
email: 'bob@test.email',
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);
registrationTriple = extractRegistrationTriple(await res.text(), webId);
registrationTriple = extractRegistrationTriple(await res.text(), webId2);
});
it('updates the webId with the registration token.', async(): Promise<void> => {
const patchBody = `INSERT DATA { ${registrationTriple} }`;
const res = await fetch(webId, {
const res = await fetch(webId2, {
method: 'PATCH',
headers: { 'content-type': 'application/sparql-update' },
body: patchBody,

View File

@@ -28,7 +28,14 @@ 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', createPod: true };
const settings = {
podName: 'alice',
webId: 'http://test.com/#alice',
email: 'alice@test.email',
password: 'password',
confirmPassword: 'password',
createPod: true,
};
const podHost = `alice.localhost:${port}`;
const podUrl = `http://${podHost}/`;
@@ -142,10 +149,11 @@ 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`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings),
body: JSON.stringify(newSettings),
});
expect(res.status).toBe(409);
await expect(res.text()).resolves.toContain(`There already is a resource at ${podUrl}`);

View File

@@ -9,42 +9,44 @@ describe('A LoginHandler', (): void => {
const webId = 'http://alice.test.com/card#me';
const email = 'alice@test.email';
let input: InteractionHandlerInput;
let storageAdapter: AccountStore;
let accountStore: jest.Mocked<AccountStore>;
let handler: LoginHandler;
beforeEach(async(): Promise<void> => {
input = {} as any;
storageAdapter = {
accountStore = {
authenticate: jest.fn().mockResolvedValue(webId),
getSettings: jest.fn().mockResolvedValue({ useIdp: true }),
} as any;
handler = new LoginHandler(storageAdapter);
handler = new LoginHandler(accountStore);
});
it('errors on invalid emails.', async(): Promise<void> => {
input.operation = createPostJsonOperation({});
let prom = handler.handle(input);
await expect(prom).rejects.toThrow('Email required');
await expect(handler.handle(input)).rejects.toThrow('Email required');
input.operation = createPostJsonOperation({ email: [ 'a', 'b' ]});
prom = handler.handle(input);
await expect(prom).rejects.toThrow('Email required');
await expect(handler.handle(input)).rejects.toThrow('Email required');
});
it('errors on invalid passwords.', async(): Promise<void> => {
input.operation = createPostJsonOperation({ email });
let prom = handler.handle(input);
await expect(prom).rejects.toThrow('Password required');
await expect(handler.handle(input)).rejects.toThrow('Password required');
input.operation = createPostJsonOperation({ email, password: [ 'a', 'b' ]});
prom = handler.handle(input);
await expect(prom).rejects.toThrow('Password required');
await expect(handler.handle(input)).rejects.toThrow('Password required');
});
it('throws an error if there is a problem.', async(): Promise<void> => {
input.operation = createPostJsonOperation({ email, password: 'password!' });
(storageAdapter.authenticate as jest.Mock).mockRejectedValueOnce(new Error('auth failed!'));
const prom = handler.handle(input);
await expect(prom).rejects.toThrow('auth failed!');
accountStore.authenticate.mockRejectedValueOnce(new Error('auth failed!'));
await expect(handler.handle(input)).rejects.toThrow('auth failed!');
});
it('throws an error if the account does not have the correct settings.', async(): Promise<void> => {
input.operation = createPostJsonOperation({ email, password: 'password!' });
accountStore.getSettings.mockResolvedValueOnce({ useIdp: false });
await expect(handler.handle(input)).rejects.toThrow('This server is not an identity provider for this account.');
});
it('returns an InteractionCompleteResult when done.', async(): Promise<void> => {
@@ -53,7 +55,7 @@ describe('A LoginHandler', (): void => {
type: 'complete',
details: { webId, shouldRemember: false },
});
expect(storageAdapter.authenticate).toHaveBeenCalledTimes(1);
expect(storageAdapter.authenticate).toHaveBeenLastCalledWith(email, 'password!');
expect(accountStore.authenticate).toHaveBeenCalledTimes(1);
expect(accountStore.authenticate).toHaveBeenLastCalledWith(email, 'password!');
});
});

View File

@@ -1,3 +1,4 @@
import type { AccountSettings } from '../../../../../../src/identity/interaction/email-password/storage/AccountStore';
import type {
EmailPasswordData,
} from '../../../../../../src/identity/interaction/email-password/storage/BaseAccountStore';
@@ -11,6 +12,7 @@ describe('A BaseAccountStore', (): void => {
const email = 'test@test.com';
const webId = 'http://test.com/#webId';
const password = 'password!';
const settings: AccountSettings = { useIdp: true };
beforeEach(async(): Promise<void> => {
const map = new Map();
@@ -24,20 +26,26 @@ describe('A BaseAccountStore', (): void => {
});
it('can create accounts.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
});
it('errors when creating a second account for an email.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, 'diffId', 'diffPass')).rejects.toThrow('Account already exists');
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.create(email, webId, 'diffPass', settings)).rejects.toThrow('Account already exists');
});
it('errors when creating a second account for a WebID.', async(): Promise<void> => {
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.create('bob@test.email', webId, 'diffPass', settings))
.rejects.toThrow('There already is an account for this WebID');
});
it('errors when authenticating a non-existent account.', async(): Promise<void> => {
await expect(store.authenticate(email, password)).rejects.toThrow('No account by that email');
await expect(store.authenticate(email, password)).rejects.toThrow('Account does not exist');
});
it('errors when authenticating an unverified account.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.authenticate(email, 'wrongPassword')).rejects.toThrow('Account still needs to be verified');
});
@@ -46,13 +54,13 @@ describe('A BaseAccountStore', (): void => {
});
it('errors when authenticating with the wrong password.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.verify(email)).resolves.toBeUndefined();
await expect(store.authenticate(email, 'wrongPassword')).rejects.toThrow('Incorrect password');
});
it('can authenticate.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.verify(email)).resolves.toBeUndefined();
await expect(store.authenticate(email, password)).resolves.toBe(webId);
});
@@ -63,16 +71,35 @@ describe('A BaseAccountStore', (): void => {
it('can change the password.', async(): Promise<void> => {
const newPassword = 'newPassword!';
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.verify(email)).resolves.toBeUndefined();
await expect(store.changePassword(email, newPassword)).resolves.toBeUndefined();
await expect(store.authenticate(email, newPassword)).resolves.toBe(webId);
});
it('can get the settings.', async(): Promise<void> => {
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.verify(email)).resolves.toBeUndefined();
await expect(store.getSettings(webId)).resolves.toBe(settings);
});
it('can update the settings.', async(): Promise<void> => {
const newSettings = { webId, useIdp: false };
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.verify(email)).resolves.toBeUndefined();
await expect(store.updateSettings(webId, newSettings)).resolves.toBeUndefined();
await expect(store.getSettings(webId)).resolves.toBe(newSettings);
});
it('can delete an account.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
await expect(store.deleteAccount(email)).resolves.toBeUndefined();
await expect(store.authenticate(email, password)).rejects.toThrow('Account does not exist');
await expect(store.getSettings(webId)).rejects.toThrow('Account does not exist');
});
it('does nothing when deleting non-existent accounts.', async(): Promise<void> => {
await expect(store.deleteAccount(email)).resolves.toBeUndefined();
await expect(store.authenticate(email, password)).rejects.toThrow('No account by that email');
});
it('errors when forgetting the password of an account that does not exist.', async(): Promise<void> => {
@@ -80,7 +107,7 @@ describe('A BaseAccountStore', (): void => {
});
it('generates a recordId when a password was forgotten.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
const recordId = await store.generateForgotPasswordRecord(email);
expect(typeof recordId).toBe('string');
});
@@ -90,13 +117,13 @@ describe('A BaseAccountStore', (): void => {
});
it('returns the email matching the forgotten password record.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
const recordId = await store.generateForgotPasswordRecord(email);
await expect(store.getForgotPasswordRecord(recordId)).resolves.toBe(email);
});
it('can delete stored forgotten password records.', async(): Promise<void> => {
await expect(store.create(email, webId, password)).resolves.toBeUndefined();
await expect(store.create(email, webId, password, settings)).resolves.toBeUndefined();
const recordId = await store.generateForgotPasswordRecord(email);
await expect(store.deleteForgotPasswordRecord(recordId)).resolves.toBeUndefined();
await expect(store.getForgotPasswordRecord('unknownRecord')).resolves.toBeUndefined();

View File

@@ -74,70 +74,72 @@ describe('A RegistrationManager', (): void => {
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid e-mail address.');
});
it('errors on invalid passwords.', async(): Promise<void> => {
const input: any = { email, webId, password, confirmPassword: 'bad' };
expect((): any => manager.validateInput(input)).toThrow('Your password and confirmation did not match.');
});
it('errors on missing passwords.', async(): Promise<void> => {
const input: any = { email, webId };
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
});
it('errors when setting rootPod to true when not allowed.', async(): Promise<void> => {
const input = { email, createWebId, rootPod };
const input = { email, password, confirmPassword, createWebId, rootPod };
expect((): any => manager.validateInput(input)).toThrow('Creating a root pod is not supported.');
});
it('errors when a required WebID is not valid.', async(): Promise<void> => {
let input: any = { email, register, webId: undefined };
let input: any = { email, password, confirmPassword, register, webId: undefined };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
input = { email, register, webId: '' };
input = { email, password, confirmPassword, register, webId: '' };
expect((): any => manager.validateInput(input)).toThrow('Please enter a valid WebID.');
});
it('errors on invalid passwords when registering.', async(): Promise<void> => {
const input: any = { email, webId, password, confirmPassword: 'bad', register };
expect((): any => manager.validateInput(input)).toThrow('Your password and confirmation did not match.');
});
it('errors on invalid pod names when required.', async(): Promise<void> => {
let input: any = { email, webId, createPod, podName: undefined };
let input: any = { email, webId, password, confirmPassword, createPod, podName: undefined };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
input = { email, webId, createPod, podName: ' ' };
input = { email, webId, password, confirmPassword, createPod, podName: ' ' };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
input = { email, webId, createWebId };
input = { email, webId, password, confirmPassword, createWebId };
expect((): any => manager.validateInput(input)).toThrow('Please specify a Pod name.');
});
it('errors when trying to create a WebID without registering or creating a pod.', async(): Promise<void> => {
let input: any = { email, podName, createWebId };
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
input = { email, podName, createWebId, createPod };
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
input = { email, podName, createWebId, createPod, register };
expect((): any => manager.validateInput(input)).toThrow('Please enter a password.');
});
it('errors when no option is chosen.', async(): Promise<void> => {
const input = { email, webId };
const input = { email, webId, password, confirmPassword };
expect((): any => manager.validateInput(input)).toThrow('Please register for a WebID or create a Pod.');
});
it('adds the template parameter if there is one.', async(): Promise<void> => {
const input = { email, webId, podName, template: 'template', createPod };
const input = { email, webId, password, confirmPassword, podName, template: 'template', createPod };
expect(manager.validateInput(input)).toEqual({
email, webId, podName, template: 'template', createWebId: false, register: false, createPod, rootPod: false,
email,
webId,
password,
podName,
template: 'template',
createWebId: false,
register: false,
createPod,
rootPod: false,
});
});
it('does not require a pod name when creating a root pod.', async(): Promise<void> => {
const input = { email, webId, createPod, rootPod };
const input = { email, password, confirmPassword, webId, createPod, rootPod };
expect(manager.validateInput(input, true)).toEqual({
email, webId, createWebId: false, register: false, createPod, rootPod,
email, password, webId, createWebId: false, register: false, createPod, rootPod,
});
});
it('trims input parameters.', async(): Promise<void> => {
it('trims non-password input parameters.', async(): Promise<void> => {
let input: any = {
email: ` ${email} `,
password: ` ${password} `,
confirmPassword: ` ${password} `,
password: ' a ',
confirmPassword: ' a ',
podName: ` ${podName} `,
template: ' template ',
createWebId,
@@ -145,19 +147,19 @@ describe('A RegistrationManager', (): void => {
createPod,
};
expect(manager.validateInput(input)).toEqual({
email, password, podName, template: 'template', createWebId, register, createPod, rootPod: false,
email, password: ' a ', podName, template: 'template', createWebId, register, createPod, rootPod: false,
});
input = { email, webId: ` ${webId} `, password, confirmPassword, register: true };
input = { email, webId: ` ${webId} `, password: ' a ', confirmPassword: ' a ', register: true };
expect(manager.validateInput(input)).toEqual({
email, webId, password, createWebId: false, register, createPod: false, rootPod: false,
email, webId, password: ' a ', createWebId: false, register, createPod: false, rootPod: false,
});
});
});
describe('handling data', (): void => {
it('can register a user.', async(): Promise<void> => {
const params: any = { email, webId, password, confirmPassword, register, createPod: false, createWebId: false };
const params: any = { email, webId, password, register, createPod: false, createWebId: false };
await expect(manager.register(params)).resolves.toEqual({
email,
webId,
@@ -170,7 +172,7 @@ describe('A RegistrationManager', (): void => {
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
expect(accountStore.verify).toHaveBeenCalledTimes(1);
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
@@ -180,7 +182,7 @@ describe('A RegistrationManager', (): void => {
});
it('can create a pod.', async(): Promise<void> => {
const params: any = { email, webId, podName, createPod, createWebId: false, register: false };
const params: any = { email, webId, password, podName, createPod, createWebId: false, register: false };
await expect(manager.register(params)).resolves.toEqual({
email,
webId,
@@ -197,9 +199,10 @@ describe('A RegistrationManager', (): void => {
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
expect(podManager.createPod).toHaveBeenCalledTimes(1);
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, podSettings, false);
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: false });
expect(accountStore.verify).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenCalledTimes(0);
expect(accountStore.verify).toHaveBeenCalledTimes(0);
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
});
@@ -219,7 +222,7 @@ describe('A RegistrationManager', (): void => {
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
expect(podManager.createPod).toHaveBeenCalledTimes(1);
@@ -239,7 +242,7 @@ describe('A RegistrationManager', (): void => {
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: true });
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
expect(podManager.createPod).toHaveBeenCalledTimes(1);
@@ -250,23 +253,6 @@ describe('A RegistrationManager', (): void => {
expect(accountStore.verify).toHaveBeenCalledTimes(0);
});
it('does not try to delete an account on failure if there was no registration.', async(): Promise<void> => {
const params: any = { email, webId, podName, createPod };
(podManager.createPod as jest.Mock).mockRejectedValueOnce(new Error('pod error'));
await expect(manager.register(params)).rejects.toThrow('pod error');
expect(ownershipValidator.handleSafe).toHaveBeenCalledTimes(1);
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
expect(podManager.createPod).toHaveBeenCalledTimes(1);
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: `${baseUrl}${podName}/` }, podSettings, false);
expect(accountStore.create).toHaveBeenCalledTimes(0);
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
expect(accountStore.verify).toHaveBeenCalledTimes(0);
});
it('can create a WebID with an account and pod.', async(): Promise<void> => {
const params: any = { email, password, confirmPassword, podName, createWebId, register, createPod };
const generatedWebID = joinUrl(baseUrl, podName, webIdSuffix);
@@ -286,7 +272,7 @@ describe('A RegistrationManager', (): void => {
expect(identifierGenerator.generate).toHaveBeenCalledTimes(1);
expect(identifierGenerator.generate).toHaveBeenLastCalledWith(podName);
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, generatedWebID, password);
expect(accountStore.create).toHaveBeenLastCalledWith(email, generatedWebID, password, { useIdp: true });
expect(accountStore.verify).toHaveBeenCalledTimes(1);
expect(accountStore.verify).toHaveBeenLastCalledWith(email);
expect(podManager.createPod).toHaveBeenCalledTimes(1);
@@ -297,7 +283,7 @@ describe('A RegistrationManager', (): void => {
});
it('can create a root pod.', async(): Promise<void> => {
const params: any = { email, webId, createPod, rootPod, createWebId: false, register: false };
const params: any = { email, webId, password, createPod, rootPod, createWebId: false, register: false };
podSettings.podBaseUrl = baseUrl;
await expect(manager.register(params, true)).resolves.toEqual({
email,
@@ -313,10 +299,11 @@ describe('A RegistrationManager', (): void => {
expect(ownershipValidator.handleSafe).toHaveBeenLastCalledWith({ webId });
expect(podManager.createPod).toHaveBeenCalledTimes(1);
expect(podManager.createPod).toHaveBeenLastCalledWith({ path: baseUrl }, podSettings, true);
expect(accountStore.create).toHaveBeenCalledTimes(1);
expect(accountStore.create).toHaveBeenLastCalledWith(email, webId, password, { useIdp: false });
expect(accountStore.verify).toHaveBeenCalledTimes(1);
expect(identifierGenerator.generate).toHaveBeenCalledTimes(0);
expect(accountStore.create).toHaveBeenCalledTimes(0);
expect(accountStore.verify).toHaveBeenCalledTimes(0);
expect(accountStore.deleteAccount).toHaveBeenCalledTimes(0);
});
});