Do not export trashed items (#241)
* Do not export trashed items * Test Item exporting Does not test organization export. Export's use of apiService is not very testable. We will either need a testApiService or to refactor apiService to make mocking easier. * Linter fixes
This commit is contained in:
parent
48144a7eae
commit
1420082348
|
@ -24,6 +24,7 @@
|
||||||
"test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\""
|
"test:node:watch": "concurrently -k -n TSC,Node -c yellow,cyan \"npm run build:watch\" \"nodemon -w ./dist --delay 500ms --exec jasmine\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@fluffy-spoon/substitute": "^1.179.0",
|
||||||
"@types/commander": "^2.12.2",
|
"@types/commander": "^2.12.2",
|
||||||
"@types/form-data": "^2.2.1",
|
"@types/form-data": "^2.2.1",
|
||||||
"@types/inquirer": "^0.0.43",
|
"@types/inquirer": "^0.0.43",
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { Substitute, SubstituteOf } from '@fluffy-spoon/substitute';
|
||||||
|
|
||||||
|
import { ApiService } from '../../../src/abstractions/api.service';
|
||||||
|
import { CipherService } from '../../../src/abstractions/cipher.service';
|
||||||
|
import { FolderService } from '../../../src/abstractions/folder.service';
|
||||||
|
|
||||||
|
import { ExportService } from '../../../src/services/export.service';
|
||||||
|
|
||||||
|
import { Cipher } from '../../../src/models/domain/cipher';
|
||||||
|
import { CipherString } from '../../../src/models/domain/cipherString';
|
||||||
|
import { Login } from '../../../src/models/domain/login';
|
||||||
|
import { CipherWithIds as CipherExport } from '../../../src/models/export/cipherWithIds';
|
||||||
|
|
||||||
|
import { CipherType } from '../../../src/enums/cipherType';
|
||||||
|
import { CipherView } from '../../../src/models/view/cipherView';
|
||||||
|
import { LoginView } from '../../../src/models/view/loginView';
|
||||||
|
|
||||||
|
import { BuildTestObject, GetUniqueString } from '../../utils';
|
||||||
|
|
||||||
|
const UserCipherViews = [
|
||||||
|
generateCipherView(false),
|
||||||
|
generateCipherView(false),
|
||||||
|
generateCipherView(true)
|
||||||
|
];
|
||||||
|
|
||||||
|
const UserCipherDomains = [
|
||||||
|
generateCipherDomain(false),
|
||||||
|
generateCipherDomain(false),
|
||||||
|
generateCipherDomain(true)
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateCipherView(deleted: boolean) {
|
||||||
|
return BuildTestObject({
|
||||||
|
id: GetUniqueString('id'),
|
||||||
|
notes: GetUniqueString('notes'),
|
||||||
|
type: CipherType.Login,
|
||||||
|
login: BuildTestObject<LoginView>({
|
||||||
|
username: GetUniqueString('username'),
|
||||||
|
password: GetUniqueString('password'),
|
||||||
|
}, LoginView),
|
||||||
|
collectionIds: null,
|
||||||
|
deletedDate: deleted ? new Date() : null,
|
||||||
|
}, CipherView);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateCipherDomain(deleted: boolean) {
|
||||||
|
return BuildTestObject({
|
||||||
|
id: GetUniqueString('id'),
|
||||||
|
notes: new CipherString(GetUniqueString('notes')),
|
||||||
|
type: CipherType.Login,
|
||||||
|
login: BuildTestObject<Login>({
|
||||||
|
username: new CipherString(GetUniqueString('username')),
|
||||||
|
password: new CipherString(GetUniqueString('password')),
|
||||||
|
}, Login),
|
||||||
|
collectionIds: null,
|
||||||
|
deletedDate: deleted ? new Date() : null,
|
||||||
|
}, Cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) {
|
||||||
|
const actual = JSON.stringify(JSON.parse(jsonResult).items);
|
||||||
|
const items: CipherExport[] = [];
|
||||||
|
ciphers.forEach((c: CipherView | Cipher) => {
|
||||||
|
const item = new CipherExport();
|
||||||
|
item.build(c);
|
||||||
|
items.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(actual).toEqual(JSON.stringify(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ExportService', () => {
|
||||||
|
let exportService: ExportService;
|
||||||
|
let apiService: SubstituteOf<ApiService>;
|
||||||
|
let cipherService: SubstituteOf<CipherService>;
|
||||||
|
let folderService: SubstituteOf<FolderService>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
apiService = Substitute.for<ApiService>();
|
||||||
|
cipherService = Substitute.for<CipherService>();
|
||||||
|
folderService = Substitute.for<FolderService>();
|
||||||
|
|
||||||
|
folderService.getAllDecrypted().resolves([]);
|
||||||
|
folderService.getAll().resolves([]);
|
||||||
|
|
||||||
|
exportService = new ExportService(folderService, cipherService, apiService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports unecrypted user ciphers', async () => {
|
||||||
|
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
|
||||||
|
|
||||||
|
const actual = await exportService.getExport('json');
|
||||||
|
|
||||||
|
expectEqualCiphers(UserCipherViews.slice(0, 1), actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports encrypted json user ciphers', async () => {
|
||||||
|
cipherService.getAll().resolves(UserCipherDomains.slice(0, 1));
|
||||||
|
|
||||||
|
const actual = await exportService.getExport('encrypted_json');
|
||||||
|
|
||||||
|
expectEqualCiphers(UserCipherDomains.slice(0, 1), actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not unecrypted export trashed user items', async () => {
|
||||||
|
cipherService.getAllDecrypted().resolves(UserCipherViews);
|
||||||
|
|
||||||
|
const actual = await exportService.getExport('json');
|
||||||
|
|
||||||
|
expectEqualCiphers(UserCipherViews.slice(0, 2), actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not encrypted export trashed user items', async () => {
|
||||||
|
cipherService.getAll().resolves(UserCipherDomains);
|
||||||
|
|
||||||
|
const actual = await exportService.getExport('encrypted_json');
|
||||||
|
|
||||||
|
expectEqualCiphers(UserCipherDomains.slice(0, 2), actual);
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,6 +9,7 @@ module.exports = (config) => {
|
||||||
|
|
||||||
// list of files / patterns to load in the browser
|
// list of files / patterns to load in the browser
|
||||||
files: [
|
files: [
|
||||||
|
'spec/utils.ts',
|
||||||
'spec/common/**/*.ts',
|
'spec/common/**/*.ts',
|
||||||
'spec/web/**/*.ts',
|
'spec/web/**/*.ts',
|
||||||
'src/abstractions/**/*.ts',
|
'src/abstractions/**/*.ts',
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
function newGuid() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
// tslint:disable:no-bitwise
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetUniqueString(prefix: string = '') {
|
||||||
|
return prefix + '_' + newGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BuildTestObject<T, K extends keyof T = keyof T>(def: Partial<Pick<T, K>> | T, constructor?: (new () => T)): T {
|
||||||
|
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ export class ExportService implements ExportServiceAbstraction {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
promises.push(this.cipherService.getAllDecrypted().then((ciphers) => {
|
promises.push(this.cipherService.getAllDecrypted().then((ciphers) => {
|
||||||
decCiphers = ciphers;
|
decCiphers = ciphers.filter(f => f.deletedDate == null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -127,8 +127,19 @@ export class ExportService implements ExportServiceAbstraction {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getEncryptedExport(): Promise<string> {
|
private async getEncryptedExport(): Promise<string> {
|
||||||
const folders = await this.folderService.getAll();
|
let folders: Folder[] = [];
|
||||||
const ciphers = await this.cipherService.getAll();
|
let ciphers: Cipher[] = [];
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
promises.push(this.folderService.getAll().then((f) => {
|
||||||
|
folders = f;
|
||||||
|
}));
|
||||||
|
|
||||||
|
promises.push(this.cipherService.getAll().then((c) => {
|
||||||
|
ciphers = c.filter((f) => f.deletedDate == null);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
const jsonDoc: any = {
|
const jsonDoc: any = {
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
|
@ -179,7 +190,7 @@ export class ExportService implements ExportServiceAbstraction {
|
||||||
promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => {
|
promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => {
|
||||||
const cipherPromises: any = [];
|
const cipherPromises: any = [];
|
||||||
if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) {
|
if (ciphers != null && ciphers.data != null && ciphers.data.length > 0) {
|
||||||
ciphers.data.forEach((c) => {
|
ciphers.data.filter((c) => c.deletedDate === null).forEach((c) => {
|
||||||
const cipher = new Cipher(new CipherData(c));
|
const cipher = new Cipher(new CipherData(c));
|
||||||
cipherPromises.push(cipher.decrypt().then((decCipher) => {
|
cipherPromises.push(cipher.decrypt().then((decCipher) => {
|
||||||
decCiphers.push(decCipher);
|
decCiphers.push(decCipher);
|
||||||
|
@ -256,8 +267,8 @@ export class ExportService implements ExportServiceAbstraction {
|
||||||
promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => {
|
promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => {
|
||||||
const cipherPromises: any = [];
|
const cipherPromises: any = [];
|
||||||
if (c != null && c.data != null && c.data.length > 0) {
|
if (c != null && c.data != null && c.data.length > 0) {
|
||||||
c.data.forEach((r) => {
|
c.data.filter((item) => item.deletedDate === null).forEach((item) => {
|
||||||
const cipher = new Cipher(new CipherData(r));
|
const cipher = new Cipher(new CipherData(item));
|
||||||
ciphers.push(cipher);
|
ciphers.push(cipher);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue