mirror of
https://github.com/balzack/databag.git
synced 2025-04-24 10:35:23 +00:00
testing account module
This commit is contained in:
parent
eea9583968
commit
3d9396029a
@ -49,13 +49,16 @@ export class MockAccountModule implements Account {
|
||||
public async confirmMFA(code: string): Promise<void> {
|
||||
}
|
||||
|
||||
public async setAccountSeal(password: string): Promise<void> {
|
||||
public async setSeal(password: string): Promise<void> {
|
||||
}
|
||||
|
||||
public async clearAccountSeal(): Promise<void> {
|
||||
public async clearSeal(): Promise<void> {
|
||||
}
|
||||
|
||||
public async unlockAccountSeal(password: string): Promise<void> {
|
||||
public async unlockSeal(password: string): Promise<void> {
|
||||
}
|
||||
|
||||
public async forgetSeal(): Promise<void> {
|
||||
}
|
||||
|
||||
public async setLogin(username: string, password: string): Promise<void> {
|
||||
|
104
app/sdk/__tests__/account.tests.ts
Normal file
104
app/sdk/__tests__/account.tests.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { AccountModule } from '../src/account';
|
||||
import { NoStore } from '../src/store';
|
||||
import { Crypto } from '../src/crypto';
|
||||
import { ConsoleLogging } from '../src/logging';
|
||||
import { defaultAccountEntity } from '../src/entities';
|
||||
import { AccountStatus } from '../src/types';
|
||||
import { waitFor } from '../__mocks__/waitFor';
|
||||
import axios from 'redaxios';
|
||||
|
||||
const testStatus = JSON.parse(JSON.stringify(defaultAccountEntity));
|
||||
|
||||
jest.mock('redaxios', () => {
|
||||
return {
|
||||
get: jest.fn().mockImplementation(() => {
|
||||
testStatus.storageUsed = 2;
|
||||
return Promise.resolve({ status: 200, data: testStatus });
|
||||
}),
|
||||
put: jest.fn().mockImplementation((url, body) => {
|
||||
if (url == 'http://test_url/account/notification?agent=test_token') {
|
||||
testStatus.pushEnabled = body;
|
||||
}
|
||||
if (url == 'http://test_url/account/searchable?agent=test_token') {
|
||||
testStatus.searchable = body;
|
||||
}
|
||||
if (url == 'http://test_url/account/seal?agent=test_token') {
|
||||
testStatus.seal = body;
|
||||
}
|
||||
return Promise.resolve({ status: 200 });
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
class TestCrypto implements Crypto {
|
||||
|
||||
public pbkdfSalt() {
|
||||
return { saltHex: 'SALT_HEX' }
|
||||
}
|
||||
|
||||
public pbkdfKey(saltHex: string, password: string) {
|
||||
return { aesKeyHex: 'AES_KEY_HEX' }
|
||||
}
|
||||
|
||||
public aesKey() {
|
||||
return { aesKeyHex: 'AES_KEY_HEX' };
|
||||
}
|
||||
|
||||
public aesIv() {
|
||||
return { ivHex: 'IV_HEX' };
|
||||
}
|
||||
|
||||
public aesEncrypt(data: string, ivHex: string, aesKeyHex: string) {
|
||||
return { encryptedDataB64: 'ENCRYPTED_DATA_B64' };
|
||||
}
|
||||
|
||||
public aesDecrypt(encryptedDataB64: string, ivHex: string, aesKeyHex: string) {
|
||||
return { data: 'DATA' }
|
||||
}
|
||||
|
||||
public rsaKey() {
|
||||
return { publicKeyB64: 'PUBLIC_KEY_B64', privateKeyB64: 'PRIVATE_KEY_B64' };
|
||||
}
|
||||
|
||||
public rsaEncrypt(data: string, publicKeyB64: string) {
|
||||
return { encryptedDataB64: 'ENCRYPTED_DATA_B64' }
|
||||
}
|
||||
|
||||
public rsaDecrypt(encryptedDataB64: string, privateKeyB64: string) {
|
||||
return { data: 'DATA' }
|
||||
}
|
||||
}
|
||||
|
||||
class TestStore extends NoStore {
|
||||
public async getProfileRevision(): Promise<number> {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
test('allocates session correctly', async () => {
|
||||
let status: AccountStatus | null = null;
|
||||
const log = new ConsoleLogging();
|
||||
const store = new TestStore();
|
||||
const crypto = new TestCrypto();
|
||||
const account = new AccountModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false);
|
||||
account.addStatusListener((ev: AccountStatus) => { status = ev });
|
||||
account.setRevision(5);
|
||||
await waitFor(() => (status?.storageUsed == 2));
|
||||
account.enableRegistry();
|
||||
account.setRevision(6);
|
||||
await waitFor(() => Boolean(status?.searchable));
|
||||
account.disableRegistry();
|
||||
account.setRevision(7);
|
||||
await waitFor(() => !Boolean(status?.searchable));
|
||||
|
||||
account.enableNotifications();
|
||||
account.setRevision(8);
|
||||
await waitFor(() => Boolean(status?.pushEnabled));
|
||||
account.disableNotifications();
|
||||
account.setRevision(9);
|
||||
await waitFor(() => !Boolean(status?.pushEnabled));
|
||||
|
||||
account.setSeal('password');
|
||||
account.setRevision(10);
|
||||
await waitFor(() => Boolean(status?.sealSet));
|
||||
});
|
@ -1,7 +1,9 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import type { Account, Logging } from './api';
|
||||
import type { Account } from './api';
|
||||
import type { AccountStatus } from './types';
|
||||
import { Store } from './store';
|
||||
import { Crypto } from './crypto';
|
||||
import { Logging } from './logging';
|
||||
import { defaultAccountEntity, AccountEntity } from './entities';
|
||||
import { getAccountStatus } from './net/getAccountStatus';
|
||||
import { addAccountMFAuth } from './net/addAccountMFAuth';
|
||||
@ -12,7 +14,6 @@ import { setAccountNotifications } from './net/setAccountNotifications';
|
||||
import { setAccountSearchable } from './net/setAccountSearchable';
|
||||
import { setAccountSeal } from './net/setAccountSeal';
|
||||
import { clearAccountSeal } from './net/clearAccountSeal';
|
||||
import { Crypto } from './crypto';
|
||||
|
||||
const CLOSE_POLL_MS = 100;
|
||||
const RETRY_POLL_MS = 2000;
|
||||
@ -25,6 +26,7 @@ export class AccountModule implements Account {
|
||||
private node: string;
|
||||
private secure: boolean;
|
||||
private log: Logging;
|
||||
private store: Store;
|
||||
private crypto: Crypto | null;
|
||||
private syncing: boolean;
|
||||
private closing: boolean;
|
||||
@ -35,11 +37,13 @@ export class AccountModule implements Account {
|
||||
|
||||
constructor(log: Logging, store: Store, crypto: Crypto | null, guid: string, token: string, node: string, secure: boolean) {
|
||||
this.log = log;
|
||||
this.store = store;
|
||||
this.crypto = crypto;
|
||||
this.emitter = new EventEmitter();
|
||||
this.guid = guid;
|
||||
this.token = token;
|
||||
this.node = node;
|
||||
this.seal = null;
|
||||
this.sealKey = null;
|
||||
this.secure = secure;
|
||||
this.revision = 0;
|
||||
this.entity = defaultAccountEntity;
|
||||
@ -52,7 +56,7 @@ export class AccountModule implements Account {
|
||||
private async init() {
|
||||
this.revision = await this.store.getAccountRevision(this.guid);
|
||||
this.entity = await this.store.getAccountData(this.guid);
|
||||
this.seal = await this.store.getSeal(this.guid);
|
||||
this.sealKey = await this.store.getSeal(this.guid);
|
||||
this.syncing = false;
|
||||
await this.sync();
|
||||
}
|
||||
@ -69,7 +73,7 @@ export class AccountModule implements Account {
|
||||
try {
|
||||
const { guid, node, secure, token } = this;
|
||||
const status = await getAccountStatus(node, secure, token);
|
||||
await this.store.setAccountStatus(guid, status);
|
||||
await this.store.setAccountData(guid, status);
|
||||
await this.store.setAccountRevision(guid, nextRev);
|
||||
this.entity = status;
|
||||
this.emitter.emit('status', this.getStatus());
|
||||
@ -90,9 +94,11 @@ export class AccountModule implements Account {
|
||||
}
|
||||
|
||||
public getStatus() {
|
||||
const { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnseaed, pushEnabled, sealable, seal, enableIce, multiFactorAuth, webPushKey } = this.entity;
|
||||
const sealSet = this.seal && seal && this.seal.publicKey == seal.publicKey && this.seal.privateKey
|
||||
return { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, sealSet, enableIce, multiFactorAuth, webPushKey };
|
||||
const { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, seal, enableIce, multiFactorAuth, webPushKey } = this.entity;
|
||||
const { passwordSalt, privateKeyIv, privateKeyEncrypted, publicKey } = seal || {};
|
||||
const sealSet = Boolean(passwordSalt && privateKeyIv && privateKeyEncrypted && publicKey);
|
||||
const sealUnlocked = Boolean(sealSet && this.sealKey?.privateKey && this.sealKey?.publicKey == publicKey)
|
||||
return { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, sealSet, sealUnlocked, enableIce, multiFactorAuth, webPushKey };
|
||||
}
|
||||
|
||||
public addStatusListener(ev: (status: AccountStatus) => void): void {
|
||||
@ -104,7 +110,7 @@ export class AccountModule implements Account {
|
||||
this.emitter.off('status', ev);
|
||||
}
|
||||
|
||||
public async close(): void {
|
||||
public async close(): Promise<void> {
|
||||
this.closing = true;
|
||||
while(this.syncing) {
|
||||
await new Promise(r => setTimeout(r, CLOSE_POLL_MS));
|
||||
@ -152,7 +158,7 @@ export class AccountModule implements Account {
|
||||
await setAccountMFAuth(node, secure, token, code);
|
||||
}
|
||||
|
||||
public async setAccountSeal(password: string): Promise<void> {
|
||||
public async setSeal(password: string): Promise<void> {
|
||||
const { crypto, guid, node, secure, token } = this;
|
||||
if (!crypto) {
|
||||
throw new Error('crypto not enabled');
|
||||
@ -168,19 +174,20 @@ export class AccountModule implements Account {
|
||||
|
||||
const seal = { publicKey: publicKeyB64, privateKey: privateKeyB64 };
|
||||
this.store.setSeal(guid, seal);
|
||||
this.seal = seal;
|
||||
this.sealKey = seal;
|
||||
|
||||
this.emitter.emit('status', this.getStatus());
|
||||
}
|
||||
|
||||
public async clearAccountSeal(): Promise<void> {
|
||||
public async clearSeal(): Promise<void> {
|
||||
const { guid, node, secure, token } = this;
|
||||
await this.store.clearAccountSeal(guid, node, secure, token);
|
||||
this.seal = null;
|
||||
await clearAccountSeal(node, secure, token);
|
||||
await this.store.clearSeal(guid);
|
||||
this.sealKey = null;
|
||||
this.emitter.emit('status', this.getStatus());
|
||||
}
|
||||
|
||||
public async unlockAccountSeal(password: string): Promise<void> {
|
||||
public async unlockSeal(password: string): Promise<void> {
|
||||
const { guid, entity, crypto } = this;
|
||||
const { passwordSalt, privateKeyIv, privateKeyEncrypted, publicKey } = entity.seal;
|
||||
if (!passwordSalt || !privateKeyIv || !privateKeyEncrypted || !publicKey) {
|
||||
@ -194,11 +201,18 @@ export class AccountModule implements Account {
|
||||
|
||||
const seal = { publicKey: publicKey, privateKey: data };
|
||||
this.store.setSeal(guid, seal);
|
||||
this.seal = seal;
|
||||
this.sealKey = seal;
|
||||
|
||||
this.emitter.emit('status', this.getStatus());
|
||||
}
|
||||
|
||||
public async forgetSeal(): Promise<void> {
|
||||
const { guid } = this;
|
||||
await this.store.clearSeal(guid);
|
||||
this.sealKey = null;
|
||||
this.emitter.emit('status', this.getStatus());
|
||||
}
|
||||
|
||||
public async setLogin(username: string, password: string): Promise<void> {
|
||||
const { node, secure, token } = this;
|
||||
await setAccountLogin(node, secure, token, username, password);
|
||||
|
@ -91,9 +91,10 @@ export interface Account {
|
||||
enableMFA(): Promise<{ secretImage: string, secretText: string }>;
|
||||
disableMFA(): Promise<void>;
|
||||
confirmMFA(code: string): Promise<void>;
|
||||
setAccountSeal(password: string): Promise<void>;
|
||||
clearAccountSeal(): Promise<void>;
|
||||
unlockAccountSeal(password: string): Promise<void>;
|
||||
setSeal(password: string): Promise<void>;
|
||||
clearSeal(): Promise<void>;
|
||||
unlockSeal(password: string): Promise<void>;
|
||||
forgetSeal(): Promise<void>;
|
||||
|
||||
addStatusListener(ev: (status: AccountStatus) => void): void;
|
||||
removeStatusListener(ev: (status: AccountStatus) => void): void;
|
||||
|
@ -1,10 +1,11 @@
|
||||
import axios from 'redaxios';
|
||||
|
||||
export async function addAccountMFAuth(node: string, secure: boolean, token: string): { text: string, image: string } {
|
||||
export async function addAccountMFAuth(node: string, secure: boolean, token: string): Promise<{ text: string, image: string }> {
|
||||
const endpoint = `http${secure ? 's' : ''}://${node}/account/mfauth=${token}`;
|
||||
const response = await axios.post(endpoint);
|
||||
if (response.status >= 400 && response.status < 600) {
|
||||
throw new Error('setAccountMFAuth failed');
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from 'redaxios';
|
||||
|
||||
export async function setAccountSeal(node: string, secure: boolean, token: string) {
|
||||
export async function clearAccountSeal(node: string, secure: boolean, token: string) {
|
||||
const endpoint = `http${secure ? 's' : ''}://${node}/account/seal?agent=${token}`;
|
||||
const response = await axios.delete(endpoint);
|
||||
if (response.status >= 400 && response.status < 600) {
|
||||
|
@ -145,6 +145,7 @@ export type AccountStatus = {
|
||||
pushEnabled: boolean,
|
||||
sealable: boolean,
|
||||
sealSet: boolean,
|
||||
sealUnlocked: boolean,
|
||||
enableIce: boolean,
|
||||
multiFactorAuth: boolean,
|
||||
webPushKey: string,
|
||||
|
Loading…
x
Reference in New Issue
Block a user