padloc/packages/core/src/spec/app.ts

296 lines
14 KiB
TypeScript

import { App, AppState } from "../app";
import { Server, ServerConfig } from "../server";
import { StubMessenger } from "../messenger";
import { MFAMessage, InviteCreatedMessage, MemberAddedMessage } from "../messages";
import { DirectSender } from "../transport";
import { MemoryStorage } from "../storage";
import { MemoryAttachmentStorage } from "../attachment";
import { ErrorCode } from "../error";
import { OrgType } from "../org";
import { Logger } from "../log";
import { MFAPurpose } from "../mfa";
import { Spec, assertResolve, assertReject } from "./spec";
export function appSpec(): Spec {
console.log("testing app");
const clientUrl = "https://padloc.app";
const messenger = new StubMessenger();
const server = new Server(
new ServerConfig({ clientUrl, reportErrors: "support@padloc.app" }),
new MemoryStorage(),
messenger,
new Logger(new MemoryStorage()),
new MemoryAttachmentStorage()
);
const app = new App(new DirectSender(server));
const otherApp = new App(new DirectSender(server));
const user = {
email: "lengden@olga.com",
name: "Lengden Olga",
password: "correct battery horse staple",
};
const otherUser = {
email: "max@mustermann.com",
name: "Max Mustermann",
password: "password",
};
let sharedVaultID = "";
// let otherVaultID = "";
return (test, assert) => {
test("App initializes successfully", async () => {
await app.loaded;
});
test("Signup", async () => {
await app.requestMFACode(user.email, MFAPurpose.Signup);
const message = messenger.lastMessage(user.email);
assert.instanceOf(message, MFAMessage);
const code = (message! as MFAMessage).request.code;
const { token } = await app.retrieveMFAToken(user.email, code, MFAPurpose.Signup);
await app.signup({ ...user, verify: token });
assert.isFalse(app.state.locked, "App should be in unlocked state after signup.");
assert.isNotNull(app.account, "Account object should be populated after signup.");
const account = app.account!;
assert.ownInclude(account, { email: user.email, name: user.name }, "Account info should be set correctly.");
assert.isNotNull(app.mainVault, "Main vault should be created.");
});
test("Create Personal Vault Item", async () => {
const item = await app.createItem("My First Item", app.mainVault!);
assert.equal(app.mainVault!.items.size, 1, "Item count should be 1.");
assert.ok(app.getItem(item.id), "Item should be accessible by ID.");
assert.equal(app.getItem(item.id)!.item, item);
assert.equal(app.getItem(item.id)!.vault, app.mainVault);
});
test("Create Org", async () => {
const org = await app.createOrg("My Org", OrgType.Business);
assert.equal(org.name, "My Org", "Organization name should be correct.");
assert.ok(org.id, "Organization ID should be set.");
assert.isTrue(org.isOwner(app.account!), "Account should be organization owner.");
assert.equal(app.state.orgs.length, 1);
await assertResolve(
assert,
() => app.account!.verifyOrg(app.state.orgs[0]),
"Organization should be verified successfully."
);
});
test("Create Vault", async () => {
const name = "My Shared Vault";
const vault = await app.createVault(name, app.state.orgs[0], [{ id: app.account!.id, readonly: false }]);
assert.equal(vault.name, name);
await app.synchronize();
assert.equal(app.state.vaults.length, 2);
});
test("Invite Member", async () => {
let org = app.state.orgs[0];
let [invite] = await app.createInvites(org, [otherUser.email]);
// Remember secret - in practice this will be communicated
// directly between the invitor and invitee
const { secret } = invite;
assert.equal(invite.email, otherUser.email);
const inviteMessage = messenger.lastMessage(otherUser.email) as InviteCreatedMessage;
assert.instanceOf(inviteMessage, InviteCreatedMessage);
const linkPattern = new RegExp(`${clientUrl}/invite/${org.id}/${invite.id}\\?email=(.*)&verify=(.*)`);
assert.match(inviteMessage.link, linkPattern);
const [, email, verify] = inviteMessage.link.match(linkPattern)!;
assert.equal(email, invite.email);
await otherApp.signup({ ...otherUser, verify });
invite = (await otherApp.getInvite(org.id, invite.id))!;
assert.isTrue(await otherApp.acceptInvite(invite, secret));
invite = (await app.getInvite(org.id, invite.id))!;
invite.secret = secret;
assert.isTrue(await invite.verifyInvitee());
await app.confirmInvite(invite);
assert.isTrue(app.state.orgs[0].isMember(otherApp.account!));
await otherApp.synchronize();
assert.equal(otherApp.state.orgs.length, 1);
assert.isTrue(otherApp.state.orgs[0].isMember(otherApp.account!));
const addedMessage = messenger.lastMessage(otherUser.email) as MemberAddedMessage;
assert.instanceOf(addedMessage, MemberAddedMessage);
assert.equal(addedMessage.org.id, org.id);
});
test("Create Group", async () => {
const org = app.state.orgs[0];
await app.createGroup(org, "Everyone", org.members)!;
const group = app.state.orgs[0].getGroup("Everyone")!;
assert.ok(group);
const vault = await app.createVault(
"Another Vault",
app.state.orgs[0],
[],
[{ name: group.name, readonly: false }]
);
sharedVaultID = vault.id;
await otherApp.synchronize();
assert.equal(otherApp.vaults.length, 2);
});
// test("Make Admin", async () => {
// const vault = app.getVault(sharedVaultID)!;
// const member = vault.members.get(otherApp.account!.id)!;
// vault.members.update({ ...member, permissions: { ...member.permissions, manage: true } });
// await app.syncVault(vault);
// await otherApp.syncVault(vault);
// assert.isTrue(app.getVault(sharedVaultID)!.isAdmin(otherApp.account));
// assert.isTrue(otherApp.getVault(sharedVaultID)!.isAdmin(otherApp.account));
// // @ts-ignore
// assert.isOk(otherApp.getVault(sharedVaultID)!._privateKey);
// await otherApp.syncVault(vault);
// await app.syncVault(vault);
// });
//
// test("Remove Admin", async () => {
// const vault = app.getVault(sharedVaultID)!;
// const member = vault.members.get(otherApp.account!.id)!;
// vault.members.update({ ...member, permissions: { ...member.permissions, manage: false } });
// await app.reinitializeVault(vault);
// await otherApp.synchronize();
//
// assert.isFalse(
// otherApp.getVault(sharedVaultID)!.isAdmin(otherApp.account),
// "Other member should no longer be admin"
// );
//
// assert.isTrue(
// otherApp.getVault(sharedVaultID)!.getMember(otherApp.account!)!.suspended,
// "Other member should be suspended"
// );
//
// const message = messenger.lastMessage(otherApp.account!.email);
// assert.instanceOf(message, InviteCreatedMessage);
// const { id: inviteID } = (message as InviteCreatedMessage).invite;
// const { secret } = vault.invites.get(inviteID)!;
// let invite = (await otherApp.getInvite(vault.id, inviteID))!;
// await otherApp.acceptInvite(invite, secret);
// invite = (await app.getInvite(vault.id, invite.id))!;
// assert.isTrue(await invite.verify());
// await app.confirmInvite(invite);
// await otherApp.syncVault(vault);
// assert.isFalse(otherApp.getVault(sharedVaultID)!.isAdmin());
// assert.isFalse(otherApp.getVault(sharedVaultID)!.isSuspended());
// assert.isTrue(otherApp.getVault(sharedVaultID)!.hasItemsAccess());
// assert.equal(app.items.length, 1);
// });
//
test("Simulataneous Edit", async () => {
const [item1, item2] = await Promise.all([
app.createItem("Added Item 1", { id: sharedVaultID }),
otherApp.createItem("Added Item 2", { id: sharedVaultID }),
]);
await Promise.all([app.syncVault({ id: sharedVaultID }), otherApp.syncVault({ id: sharedVaultID })]);
await Promise.all([app.syncVault({ id: sharedVaultID }), otherApp.syncVault({ id: sharedVaultID })]);
assert.ok(app.getItem(item2.id), "Created item from second app should should show up in first app");
assert.ok(otherApp.getItem(item1.id), "Created item from first app should should show up in second app");
await app.updateItem(item1, { name: "Edited Item" });
const item3 = await app.createItem("Added Item 3", app.getVault(sharedVaultID)!);
await otherApp.deleteItems([item2]);
await Promise.all([app.syncVault({ id: sharedVaultID }), otherApp.syncVault({ id: sharedVaultID })]);
await Promise.all([app.syncVault({ id: sharedVaultID }), otherApp.syncVault({ id: sharedVaultID })]);
assert.isNull(app.getItem(item2.id));
assert.ok(otherApp.getItem(item3.id), "Created Item show up other instance");
assert.equal(otherApp.getItem(item1.id)!.item.name, "Edited Item");
});
//
// test("Archive Vault", async () => {
// let vault = await app.createVault("Test");
// otherVaultID = vault.id;
// // const invite = await app.createInvite(vault, otherApp.account!.email);
// // await otherApp.acceptInvite(invite, invite.secret);
// // await app.confirmInvite(invite);
// // await app.syncVault(vault);
// assert.isTrue(vault.isMember(app.account!));
// await app.archiveVault(vault);
// vault = app.getVault(vault.id)!;
// assert.isTrue(vault.archived);
// });
//
// test("Unarchive Vault", async () => {
// await app.unarchiveVault(app.getVault(otherVaultID)!);
// assert.isFalse(app.getVault(otherVaultID)!.archived);
// });
//
// test("Delete Vault", async () => {
// await app.deleteVault(app.getVault(otherVaultID)!);
// assert.isNull(app.getVault(otherVaultID));
// });
//
// test("Remove Member", async () => {
// await app.removeMember(app.state.vaults[1], app.state.vaults[1].members.get(otherApp.account!.id)!);
// await otherApp.synchronize();
// assert.isNull(app.state.vaults[1].members.get(otherApp.account!.id));
// assert.isNull(app.state.vaults[2].members.get(otherApp.account!.id));
// assert.equal(otherApp.vaults.length, 1);
// assert.isNull(otherApp.getVault(app.state.vaults[1].id));
// assert.isNull(otherApp.getVault(app.state.vaults[2].id));
// });
//
test("Lock", async () => {
await app.lock();
assert.isTrue(app.state.locked, "App should be in 'locked' state.");
assert.isNotOk(app.account!.privateKey, "Private key should be inaccessible after locking.");
assert.equal(app.mainVault!.items.size, 0, "Main vault should be inacessible after locking.");
});
test("Unlock", async () => {
await app.unlock(user.password);
assert.isFalse(app.state.locked, "App should be in 'unlocked' state.");
assert.instanceOf(app.account!.privateKey, Uint8Array, "Private key should be loaded.");
assert.isNotNull(app.mainVault, "Main vault should be loaded.");
assert.equal(app.mainVault!.items.size, 1, "Items should be loaded.");
});
test("Logout", async () => {
await app.logout();
const state = await app.storage.get(AppState, app.state.id);
assert.isNotOk(state.account, "Account should be unloaded.");
assert.isNotOk(state.session, "Session should be unloaded.");
assert.equal(state.orgs.length, 0, "Orgs should be unloaded.");
assert.equal(state.vaults.length, 0, "Vaults should be unloaded.");
});
test("Login", async () => {
const app = new App(new DirectSender(server));
await assertReject(
assert,
() => app.login(user.email, user.password),
ErrorCode.MFA_REQUIRED,
"Logging in from a new device should require email verification."
);
await app.requestMFACode(user.email, MFAPurpose.Login);
const message = messenger.lastMessage(user.email);
const code = (message! as MFAMessage).request.code;
const { token } = await app.retrieveMFAToken(user.email, code, MFAPurpose.Login);
await app.login(user.email, user.password, token);
assert.isNotNull(app.account, "Account should be loaded.");
const account = app.account!;
assert.ownInclude(account, { email: user.email, name: user.name }, "Account info should be correct.");
assert.equal(app.mainVault!.items.size, 1, "Vault Items should be loaded");
});
};
}