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:
Matt Gibson 2020-12-30 15:08:02 -06:00 committed by GitHub
parent 48144a7eae
commit 1420082348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 6 deletions

View File

@ -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",

View File

@ -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);
});
});

View File

@ -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',

16
spec/utils.ts Normal file
View File

@ -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;
}

View File

@ -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<string> {
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);
});
}