From 14200823488a8b7063396ae23816124ad30a8371 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 30 Dec 2020 15:08:02 -0600 Subject: [PATCH] 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 --- package.json | 1 + spec/common/services/export.service.spec.ts | 120 ++++++++++++++++++++ spec/support/karma.conf.js | 1 + spec/utils.ts | 16 +++ src/services/export.service.ts | 23 +++- 5 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 spec/common/services/export.service.spec.ts create mode 100644 spec/utils.ts diff --git a/package.json b/package.json index b42d22374a..ca426d8e23 100644 --- a/package.json +++ b/package.json @@ -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\"" }, "devDependencies": { + "@fluffy-spoon/substitute": "^1.179.0", "@types/commander": "^2.12.2", "@types/form-data": "^2.2.1", "@types/inquirer": "^0.0.43", diff --git a/spec/common/services/export.service.spec.ts b/spec/common/services/export.service.spec.ts new file mode 100644 index 0000000000..4cbcf2831a --- /dev/null +++ b/spec/common/services/export.service.spec.ts @@ -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({ + 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({ + 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; + let cipherService: SubstituteOf; + let folderService: SubstituteOf; + + beforeEach(() => { + apiService = Substitute.for(); + cipherService = Substitute.for(); + folderService = Substitute.for(); + + 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); + }); +}); diff --git a/spec/support/karma.conf.js b/spec/support/karma.conf.js index 4ec04a07df..1196bafb65 100644 --- a/spec/support/karma.conf.js +++ b/spec/support/karma.conf.js @@ -9,6 +9,7 @@ module.exports = (config) => { // list of files / patterns to load in the browser files: [ + 'spec/utils.ts', 'spec/common/**/*.ts', 'spec/web/**/*.ts', 'src/abstractions/**/*.ts', diff --git a/spec/utils.ts b/spec/utils.ts new file mode 100644 index 0000000000..94535d9b82 --- /dev/null +++ b/spec/utils.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(def: Partial> | T, constructor?: (new () => T)): T { + return Object.assign(constructor === null ? {} : new constructor(), def) as T; +} diff --git a/src/services/export.service.ts b/src/services/export.service.ts index e0c4188e87..77a020d864 100644 --- a/src/services/export.service.ts +++ b/src/services/export.service.ts @@ -64,7 +64,7 @@ export class ExportService implements ExportServiceAbstraction { })); promises.push(this.cipherService.getAllDecrypted().then((ciphers) => { - decCiphers = ciphers; + decCiphers = ciphers.filter(f => f.deletedDate == null); })); await Promise.all(promises); @@ -127,8 +127,19 @@ export class ExportService implements ExportServiceAbstraction { } private async getEncryptedExport(): Promise { - const folders = await this.folderService.getAll(); - const ciphers = await this.cipherService.getAll(); + let folders: Folder[] = []; + 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 = { encrypted: true, @@ -179,7 +190,7 @@ export class ExportService implements ExportServiceAbstraction { promises.push(this.apiService.getCiphersOrganization(organizationId).then((ciphers) => { const cipherPromises: any = []; 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)); cipherPromises.push(cipher.decrypt().then((decCipher) => { decCiphers.push(decCipher); @@ -256,8 +267,8 @@ export class ExportService implements ExportServiceAbstraction { promises.push(this.apiService.getCiphersOrganization(organizationId).then((c) => { const cipherPromises: any = []; if (c != null && c.data != null && c.data.length > 0) { - c.data.forEach((r) => { - const cipher = new Cipher(new CipherData(r)); + c.data.filter((item) => item.deletedDate === null).forEach((item) => { + const cipher = new Cipher(new CipherData(item)); ciphers.push(cipher); }); }