From 6e345bc4ccbb4bb96ca2d657585a5b8319b9e3ab Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Mon, 7 Mar 2022 19:21:23 +0100 Subject: [PATCH] Dashlane Csv-Importer (#708) * Move existing dashlane importer into dashlaneImporters * Add testData for Dashlane CSV importer * Add dashlane Csv importer and unit tests * Fixed linting issues * Moved dashlaneCsv types to own file * Register DashlaneCsv importer * Removed temp private method and use base impl * rename spec imports * Move scope of mapped columns * Migrate folders into collection if imported via org --- common/src/enums/importOptions.ts | 1 + .../dashlaneImporters/dashlaneCsvImporter.ts | 271 +++++++++++++ .../dashlaneJsonImporter.ts | 19 +- .../types/dashlaneCsvTypes.ts | 68 ++++ common/src/services/import.service.ts | 5 +- package-lock.json | 3 + .../importers/dashlaneCsvImporter.spec.ts | 367 ++++++++++++++++++ .../testData/dashlaneCsv/credentials.csv.ts | 2 + .../importers/testData/dashlaneCsv/id.csv.ts | 6 + .../dashlaneCsv/multiplePersonalInfo.csv.ts | 7 + .../testData/dashlaneCsv/payments.csv.ts | 3 + .../testData/dashlaneCsv/personalInfo.csv.ts | 6 + .../testData/dashlaneCsv/securenotes.csv.ts | 2 + 13 files changed, 749 insertions(+), 11 deletions(-) create mode 100644 common/src/importers/dashlaneImporters/dashlaneCsvImporter.ts rename common/src/importers/{ => dashlaneImporters}/dashlaneJsonImporter.ts (91%) create mode 100644 common/src/importers/dashlaneImporters/types/dashlaneCsvTypes.ts create mode 100644 spec/common/importers/dashlaneCsvImporter.spec.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/credentials.csv.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/id.csv.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/multiplePersonalInfo.csv.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/payments.csv.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/personalInfo.csv.ts create mode 100644 spec/common/importers/testData/dashlaneCsv/securenotes.csv.ts diff --git a/common/src/enums/importOptions.ts b/common/src/enums/importOptions.ts index 01773b60ac5..42f9619e122 100644 --- a/common/src/enums/importOptions.ts +++ b/common/src/enums/importOptions.ts @@ -7,6 +7,7 @@ export const featuredImportOptions = [ { id: "bitwardenjson", name: "Bitwarden (json)" }, { id: "bitwardencsv", name: "Bitwarden (csv)" }, { id: "chromecsv", name: "Chrome (csv)" }, + { id: "dashlanecsv", name: "Dashlane (csv)" }, { id: "dashlanejson", name: "Dashlane (json)" }, { id: "firefoxcsv", name: "Firefox (csv)" }, { id: "keepass2xml", name: "KeePass 2 (xml)" }, diff --git a/common/src/importers/dashlaneImporters/dashlaneCsvImporter.ts b/common/src/importers/dashlaneImporters/dashlaneCsvImporter.ts new file mode 100644 index 00000000000..2bb6ce14fe7 --- /dev/null +++ b/common/src/importers/dashlaneImporters/dashlaneCsvImporter.ts @@ -0,0 +1,271 @@ +import { CipherType } from "../../enums/cipherType"; +import { SecureNoteType } from "../../enums/secureNoteType"; +import { ImportResult } from "../../models/domain/importResult"; +import { CardView } from "../../models/view/cardView"; +import { CipherView } from "../../models/view/cipherView"; +import { IdentityView } from "../../models/view/identityView"; +import { LoginView } from "../../models/view/loginView"; +import { BaseImporter } from "../baseImporter"; +import { Importer } from "../importer"; + +import { + CredentialsRecord, + IdRecord, + PaymentsRecord, + PersonalInformationRecord, + SecureNoteRecord, +} from "./types/dashlaneCsvTypes"; + +const _mappedCredentialsColums = new Set([ + "title", + "note", + "username", + "password", + "url", + "otpSecret", + "category", +]); + +const _mappedPersonalInfoAsIdentiyColumns = new Set([ + "type", + "title", + "first_name", + "middle_name", + "last_name", + "login", + "email", + "phone_number", + "address", + "country", + "state", + "city", + "zip", + // Skip item_name as we already have set a combined name + "item_name", +]); + +const _mappedSecureNoteColumns = new Set(["title", "note"]); + +export class DashlaneCsvImporter extends BaseImporter implements Importer { + parse(data: string): Promise { + const result = new ImportResult(); + const results = this.parseCsv(data, true); + if (results == null) { + result.success = false; + return Promise.resolve(result); + } + + if (results[0].type != null && results[0].title != null) { + const personalRecords = results as PersonalInformationRecord[]; + + // If personalRecords has only one "name" then create an Identity-Cipher + if (personalRecords.filter((x) => x.type === "name").length === 1) { + const cipher = this.initLoginCipher(); + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + results.forEach((row) => { + this.parsePersonalInformationRecordAsIdentity(cipher, row); + }); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + result.success = true; + return Promise.resolve(result); + } + } + + results.forEach((row) => { + const cipher = this.initLoginCipher(); + + const rowKeys = Object.keys(row); + if (rowKeys[0] === "username") { + this.processFolder(result, row.category); + this.parseCredentialsRecord(cipher, row); + } + + if (rowKeys[0] === "type" && rowKeys[1] === "account_name") { + this.parsePaymentRecord(cipher, row); + } + + if (rowKeys[0] === "type" && rowKeys[1] === "number") { + this.parseIdRecord(cipher, row); + } + + if ((rowKeys[0] === "type") != null && rowKeys[1] === "title") { + this.parsePersonalInformationRecord(cipher, row); + } + + if (rowKeys[0] === "title" && rowKeys[1] === "note") { + this.parseSecureNoteRecords(cipher, row); + } + + this.convertToNoteIfNeeded(cipher); + this.cleanupCipher(cipher); + result.ciphers.push(cipher); + }); + + if (this.organization) { + this.moveFoldersToCollections(result); + } + + result.success = true; + return Promise.resolve(result); + } + + parseCredentialsRecord(cipher: CipherView, row: CredentialsRecord) { + cipher.type = CipherType.Login; + cipher.login = new LoginView(); + + cipher.name = row.title; + cipher.notes = row.note; + cipher.login.username = row.username; + cipher.login.password = row.password; + cipher.login.totp = row.otpSecret; + cipher.login.uris = this.makeUriArray(row.url); + + this.importUnmappedFields(cipher, row, _mappedCredentialsColums); + } + + parsePaymentRecord(cipher: CipherView, row: PaymentsRecord) { + cipher.type = CipherType.Card; + cipher.card = new CardView(); + + cipher.name = row.account_name; + let mappedValues: string[] = []; + switch (row.type) { + case "credit_card": + cipher.card.cardholderName = row.account_name; + cipher.card.number = row.cc_number; + cipher.card.brand = this.getCardBrand(cipher.card.number); + cipher.card.code = row.code; + cipher.card.expMonth = row.expiration_month; + cipher.card.expYear = row.expiration_year.substring(2, 4); + + // If you add more mapped fields please extend this + mappedValues = [ + "account_name", + "account_holder", + "cc_number", + "code", + "expiration_month", + "expiration_year", + ]; + break; + case "bank": + cipher.card.cardholderName = row.account_holder; + cipher.card.number = row.account_number; + + // If you add more mapped fields please extend this + mappedValues = ["account_name", "account_holder", "account_number"]; + break; + default: + break; + } + + this.importUnmappedFields(cipher, row, new Set(mappedValues)); + } + + parseIdRecord(cipher: CipherView, row: IdRecord) { + cipher.type = CipherType.Identity; + cipher.identity = new IdentityView(); + + const mappedValues: string[] = ["name", "number"]; + switch (row.type) { + case "card": + cipher.name = `${row.name} ${row.type}`; + this.processFullName(cipher, row.name); + cipher.identity.licenseNumber = row.number; + break; + case "passport": + cipher.name = `${row.name} ${row.type}`; + this.processFullName(cipher, row.name); + cipher.identity.passportNumber = row.number; + break; + case "license": + cipher.name = `${row.name} ${row.type}`; + this.processFullName(cipher, row.name); + cipher.identity.licenseNumber = row.number; + cipher.identity.state = row.state; + + mappedValues.push("state"); + break; + case "social_security": + cipher.name = `${row.name} ${row.type}`; + this.processFullName(cipher, row.name); + cipher.identity.ssn = row.number; + break; + case "tax_number": + cipher.name = row.type; + cipher.identity.licenseNumber = row.number; + break; + + default: + break; + } + + // If you add more mapped fields please extend this + this.importUnmappedFields(cipher, row, new Set(mappedValues)); + } + + parsePersonalInformationRecord(cipher: CipherView, row: PersonalInformationRecord) { + cipher.type = CipherType.SecureNote; + cipher.secureNote.type = SecureNoteType.Generic; + if (row.type === "name") { + cipher.name = `${row.title} ${row.first_name} ${row.middle_name} ${row.last_name}` + .replace(" ", " ") + .trim(); + } else { + cipher.name = row.item_name; + } + + const dataRow = row as any; + Object.keys(row).forEach((key) => { + this.processKvp(cipher, key, dataRow[key]); + }); + } + + parsePersonalInformationRecordAsIdentity(cipher: CipherView, row: PersonalInformationRecord) { + switch (row.type) { + case "name": + this.processFullName(cipher, `${row.first_name} ${row.middle_name} ${row.last_name}`); + cipher.identity.title = row.title; + cipher.name = cipher.identity.fullName; + + cipher.identity.username = row.login; + break; + case "email": + cipher.identity.email = row.email; + break; + case "number": + cipher.identity.phone = row.phone_number; + break; + case "address": + cipher.identity.address1 = row.address; + cipher.identity.city = row.city; + cipher.identity.postalCode = row.zip; + cipher.identity.state = row.state; + cipher.identity.country = row.country; + break; + default: + break; + } + + this.importUnmappedFields(cipher, row, _mappedPersonalInfoAsIdentiyColumns); + } + + parseSecureNoteRecords(cipher: CipherView, row: SecureNoteRecord) { + cipher.type = CipherType.SecureNote; + cipher.secureNote.type = SecureNoteType.Generic; + cipher.name = row.title; + cipher.notes = row.note; + + this.importUnmappedFields(cipher, row, _mappedSecureNoteColumns); + } + + importUnmappedFields(cipher: CipherView, row: any, mappedValues: Set) { + const unmappedFields = Object.keys(row).filter((x) => !mappedValues.has(x)); + unmappedFields.forEach((key) => { + const item = row as any; + this.processKvp(cipher, key, item[key]); + }); + } +} diff --git a/common/src/importers/dashlaneJsonImporter.ts b/common/src/importers/dashlaneImporters/dashlaneJsonImporter.ts similarity index 91% rename from common/src/importers/dashlaneJsonImporter.ts rename to common/src/importers/dashlaneImporters/dashlaneJsonImporter.ts index fea540ef91f..cb7db59dadc 100644 --- a/common/src/importers/dashlaneJsonImporter.ts +++ b/common/src/importers/dashlaneImporters/dashlaneJsonImporter.ts @@ -1,13 +1,12 @@ -import { CipherType } from "../enums/cipherType"; -import { SecureNoteType } from "../enums/secureNoteType"; -import { ImportResult } from "../models/domain/importResult"; -import { CardView } from "../models/view/cardView"; -import { CipherView } from "../models/view/cipherView"; -import { IdentityView } from "../models/view/identityView"; -import { SecureNoteView } from "../models/view/secureNoteView"; - -import { BaseImporter } from "./baseImporter"; -import { Importer } from "./importer"; +import { CipherType } from "../../enums/cipherType"; +import { SecureNoteType } from "../../enums/secureNoteType"; +import { ImportResult } from "../../models/domain/importResult"; +import { CardView } from "../../models/view/cardView"; +import { CipherView } from "../../models/view/cipherView"; +import { IdentityView } from "../../models/view/identityView"; +import { SecureNoteView } from "../../models/view/secureNoteView"; +import { BaseImporter } from "../baseImporter"; +import { Importer } from "../importer"; const HandledResults = new Set([ "ADDRESS", diff --git a/common/src/importers/dashlaneImporters/types/dashlaneCsvTypes.ts b/common/src/importers/dashlaneImporters/types/dashlaneCsvTypes.ts new file mode 100644 index 00000000000..cb321c56da8 --- /dev/null +++ b/common/src/importers/dashlaneImporters/types/dashlaneCsvTypes.ts @@ -0,0 +1,68 @@ +// tslint:disable +export class CredentialsRecord { + username: string; + username2: string; + username3: string; + title: string; + password: string; + note: string; + url: string; + category: string; + otpSecret: string; +} + +export class PaymentsRecord { + type: string; + account_name: string; + account_holder: string; + cc_number: string; + code: string; + expiration_month: string; + expiration_year: string; + routing_number: string; + account_number: string; + country: string; + issuing_bank: string; +} + +export class IdRecord { + type: string; + number: string; + name: string; + issue_date: string; + expiration_date: string; + place_of_issue: string; + state: string; +} + +export class PersonalInformationRecord { + type: string; + title: string; + first_name: string; + middle_name: string; + last_name: string; + login: string; + date_of_birth: string; + place_of_birth: string; + email: string; + email_type: string; + item_name: string; + phone_number: string; + address: string; + country: string; + state: string; + city: string; + zip: string; + address_recipient: string; + address_building: string; + address_apartment: string; + address_floor: string; + address_door_code: string; + job_title: string; + url: string; +} + +export class SecureNoteRecord { + title: string; + note: string; +} diff --git a/common/src/services/import.service.ts b/common/src/services/import.service.ts index 1a59b20acce..86226d7d6a7 100644 --- a/common/src/services/import.service.ts +++ b/common/src/services/import.service.ts @@ -26,7 +26,8 @@ import { ButtercupCsvImporter } from "../importers/buttercupCsvImporter"; import { ChromeCsvImporter } from "../importers/chromeCsvImporter"; import { ClipperzHtmlImporter } from "../importers/clipperzHtmlImporter"; import { CodebookCsvImporter } from "../importers/codebookCsvImporter"; -import { DashlaneJsonImporter } from "../importers/dashlaneJsonImporter"; +import { DashlaneCsvImporter } from "../importers/dashlaneImporters/dashlaneCsvImporter"; +import { DashlaneJsonImporter } from "../importers/dashlaneImporters/dashlaneJsonImporter"; import { EncryptrCsvImporter } from "../importers/encryptrCsvImporter"; import { EnpassCsvImporter } from "../importers/enpassCsvImporter"; import { EnpassJsonImporter } from "../importers/enpassJsonImporter"; @@ -218,6 +219,8 @@ export class ImportService implements ImportServiceAbstraction { return new EnpassJsonImporter(); case "pwsafexml": return new PasswordSafeXmlImporter(); + case "dashlanecsv": + return new DashlaneCsvImporter(); case "dashlanejson": return new DashlaneJsonImporter(); case "msecurecsv": diff --git a/package-lock.json b/package-lock.json index 1b7ad74f0be..8331f6e48d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ } }, "common": { + "name": "@bitwarden/jslib-common", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -114,6 +115,7 @@ } }, "electron": { + "name": "@bitwarden/jslib-electron", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -133,6 +135,7 @@ } }, "node": { + "name": "@bitwarden/jslib-node", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { diff --git a/spec/common/importers/dashlaneCsvImporter.spec.ts b/spec/common/importers/dashlaneCsvImporter.spec.ts new file mode 100644 index 00000000000..666e32323b8 --- /dev/null +++ b/spec/common/importers/dashlaneCsvImporter.spec.ts @@ -0,0 +1,367 @@ +import { CipherType } from "jslib-common/enums/cipherType"; +import { DashlaneCsvImporter as Importer } from "jslib-common/importers/dashlaneImporters/dashlaneCsvImporter"; + +import { credentialsData } from "./testData/dashlaneCsv/credentials.csv"; +import { identityData } from "./testData/dashlaneCsv/id.csv"; +import { multiplePersonalInfoData } from "./testData/dashlaneCsv/multiplePersonalInfo.csv"; +import { paymentsData } from "./testData/dashlaneCsv/payments.csv"; +import { personalInfoData } from "./testData/dashlaneCsv/personalInfo.csv"; +import { secureNoteData } from "./testData/dashlaneCsv/securenotes.csv"; + +describe("Dashlane CSV Importer", () => { + let importer: Importer; + beforeEach(() => { + importer = new Importer(); + }); + + it("should parse login records", async () => { + const result = await importer.parse(credentialsData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("example.com"); + expect(cipher.login.username).toEqual("jdoe"); + expect(cipher.login.password).toEqual("somePassword"); + expect(cipher.login.totp).toEqual("someTOTPSeed"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://www.example.com"); + expect(cipher.notes).toEqual("some note for example.com"); + }); + + it("should parse an item and create a folder", async () => { + const result = await importer.parse(credentialsData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.folders.length).toBe(1); + expect(result.folders[0].name).toBe("Entertainment"); + expect(result.folderRelationships[0]).toEqual([0, 0]); + }); + + it("should parse payment records", async () => { + const result = await importer.parse(paymentsData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(2); + + // Account + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.Card); + expect(cipher.name).toBe("John's savings account"); + expect(cipher.card.brand).toBeNull(); + expect(cipher.card.cardholderName).toBe("John Doe"); + expect(cipher.card.number).toBe("accountNumber"); + expect(cipher.card.code).toBeNull(); + expect(cipher.card.expMonth).toBeNull(); + expect(cipher.card.expYear).toBeNull(); + + expect(cipher.fields.length).toBe(4); + + expect(cipher.fields[0].name).toBe("type"); + expect(cipher.fields[0].value).toBe("bank"); + + expect(cipher.fields[1].name).toBe("routing_number"); + expect(cipher.fields[1].value).toBe("routingNumber"); + + expect(cipher.fields[2].name).toBe("country"); + expect(cipher.fields[2].value).toBe("US"); + + expect(cipher.fields[3].name).toBe("issuing_bank"); + expect(cipher.fields[3].value).toBe("US-ALLY"); + + // CreditCard + const cipher2 = result.ciphers.shift(); + expect(cipher2.type).toBe(CipherType.Card); + expect(cipher2.name).toBe("John Doe"); + expect(cipher2.card.brand).toBe("Visa"); + expect(cipher2.card.cardholderName).toBe("John Doe"); + expect(cipher2.card.number).toBe("41111111111111111"); + expect(cipher2.card.code).toBe("123"); + expect(cipher2.card.expMonth).toBe("01"); + expect(cipher2.card.expYear).toBe("23"); + + expect(cipher2.fields.length).toBe(2); + + expect(cipher2.fields[0].name).toBe("type"); + expect(cipher2.fields[0].value).toBe("credit_card"); + + expect(cipher2.fields[1].name).toBe("country"); + expect(cipher2.fields[1].value).toBe("US"); + }); + + it("should parse ids records", async () => { + const result = await importer.parse(identityData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + + // Type card + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.Identity); + expect(cipher.name).toBe("John Doe card"); + expect(cipher.identity.fullName).toBe("John Doe"); + expect(cipher.identity.firstName).toBe("John"); + expect(cipher.identity.middleName).toBeNull(); + expect(cipher.identity.lastName).toBe("Doe"); + expect(cipher.identity.licenseNumber).toBe("123123123"); + + expect(cipher.fields.length).toBe(3); + + expect(cipher.fields[0].name).toEqual("type"); + expect(cipher.fields[0].value).toEqual("card"); + + expect(cipher.fields[1].name).toEqual("issue_date"); + expect(cipher.fields[1].value).toEqual("2022-1-30"); + + expect(cipher.fields[2].name).toEqual("expiration_date"); + expect(cipher.fields[2].value).toEqual("2032-1-30"); + + // Type passport + const cipher2 = result.ciphers.shift(); + expect(cipher2.type).toBe(CipherType.Identity); + expect(cipher2.name).toBe("John Doe passport"); + expect(cipher2.identity.fullName).toBe("John Doe"); + expect(cipher2.identity.firstName).toBe("John"); + expect(cipher2.identity.middleName).toBeNull(); + expect(cipher2.identity.lastName).toBe("Doe"); + expect(cipher2.identity.passportNumber).toBe("123123123"); + + expect(cipher2.fields.length).toBe(4); + + expect(cipher2.fields[0].name).toEqual("type"); + expect(cipher2.fields[0].value).toEqual("passport"); + expect(cipher2.fields[1].name).toEqual("issue_date"); + expect(cipher2.fields[1].value).toEqual("2022-1-30"); + expect(cipher2.fields[2].name).toEqual("expiration_date"); + expect(cipher2.fields[2].value).toEqual("2032-1-30"); + expect(cipher2.fields[3].name).toEqual("place_of_issue"); + expect(cipher2.fields[3].value).toEqual("somewhere in Germany"); + + // Type license + const cipher3 = result.ciphers.shift(); + expect(cipher3.type).toBe(CipherType.Identity); + expect(cipher3.name).toBe("John Doe license"); + expect(cipher3.identity.fullName).toBe("John Doe"); + expect(cipher3.identity.firstName).toBe("John"); + expect(cipher3.identity.middleName).toBeNull(); + expect(cipher3.identity.lastName).toBe("Doe"); + expect(cipher3.identity.licenseNumber).toBe("1234556"); + expect(cipher3.identity.state).toBe("DC"); + + expect(cipher3.fields.length).toBe(3); + expect(cipher3.fields[0].name).toEqual("type"); + expect(cipher3.fields[0].value).toEqual("license"); + expect(cipher3.fields[1].name).toEqual("issue_date"); + expect(cipher3.fields[1].value).toEqual("2022-8-10"); + expect(cipher3.fields[2].name).toEqual("expiration_date"); + expect(cipher3.fields[2].value).toEqual("2022-10-10"); + + // Type social_security + const cipher4 = result.ciphers.shift(); + expect(cipher4.type).toBe(CipherType.Identity); + expect(cipher4.name).toBe("John Doe social_security"); + expect(cipher4.identity.fullName).toBe("John Doe"); + expect(cipher4.identity.firstName).toBe("John"); + expect(cipher4.identity.middleName).toBeNull(); + expect(cipher4.identity.lastName).toBe("Doe"); + expect(cipher4.identity.ssn).toBe("123123123"); + + expect(cipher4.fields.length).toBe(1); + expect(cipher4.fields[0].name).toEqual("type"); + expect(cipher4.fields[0].value).toEqual("social_security"); + + // Type tax_number + const cipher5 = result.ciphers.shift(); + expect(cipher5.type).toBe(CipherType.Identity); + expect(cipher5.name).toBe("tax_number"); + expect(cipher5.identity.licenseNumber).toBe("123123123"); + + expect(cipher5.fields.length).toBe(1); + expect(cipher5.fields[0].name).toEqual("type"); + expect(cipher5.fields[0].value).toEqual("tax_number"); + }); + + it("should parse secureNote records", async () => { + const result = await importer.parse(secureNoteData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.SecureNote); + expect(cipher.name).toBe("01"); + expect(cipher.notes).toBe("test"); + }); + + it("should parse personal information records (multiple identities)", async () => { + const result = await importer.parse(multiplePersonalInfoData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(6); + + // name + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.SecureNote); + expect(cipher.name).toBe("MR John Doe"); + + expect(cipher.fields.length).toBe(7); + expect(cipher.fields[0].name).toEqual("type"); + expect(cipher.fields[0].value).toEqual("name"); + expect(cipher.fields[1].name).toEqual("title"); + expect(cipher.fields[1].value).toEqual("MR"); + expect(cipher.fields[2].name).toEqual("first_name"); + expect(cipher.fields[2].value).toEqual("John"); + expect(cipher.fields[3].name).toEqual("last_name"); + expect(cipher.fields[3].value).toEqual("Doe"); + expect(cipher.fields[4].name).toEqual("login"); + expect(cipher.fields[4].value).toEqual("jdoe"); + expect(cipher.fields[5].name).toEqual("date_of_birth"); + expect(cipher.fields[5].value).toEqual("2022-01-30"); + expect(cipher.fields[6].name).toEqual("place_of_birth"); + expect(cipher.fields[6].value).toEqual("world"); + + // email + const cipher2 = result.ciphers.shift(); + expect(cipher2.type).toBe(CipherType.SecureNote); + expect(cipher2.name).toBe("Johns email"); + + expect(cipher2.fields.length).toBe(4); + expect(cipher2.fields[0].name).toEqual("type"); + expect(cipher2.fields[0].value).toEqual("email"); + expect(cipher2.fields[1].name).toEqual("email"); + expect(cipher2.fields[1].value).toEqual("jdoe@example.com"); + expect(cipher2.fields[2].name).toEqual("email_type"); + expect(cipher2.fields[2].value).toEqual("personal"); + expect(cipher2.fields[3].name).toEqual("item_name"); + expect(cipher2.fields[3].value).toEqual("Johns email"); + + // number + const cipher3 = result.ciphers.shift(); + expect(cipher3.type).toBe(CipherType.SecureNote); + expect(cipher3.name).toBe("John's number"); + + expect(cipher3.fields.length).toBe(3); + expect(cipher3.fields[0].name).toEqual("type"); + expect(cipher3.fields[0].value).toEqual("number"); + expect(cipher3.fields[1].name).toEqual("item_name"); + expect(cipher3.fields[1].value).toEqual("John's number"); + expect(cipher3.fields[2].name).toEqual("phone_number"); + expect(cipher3.fields[2].value).toEqual("+49123123123"); + + // address + const cipher4 = result.ciphers.shift(); + expect(cipher4.type).toBe(CipherType.SecureNote); + expect(cipher4.name).toBe("John's home address"); + + expect(cipher4.fields.length).toBe(12); + expect(cipher4.fields[0].name).toEqual("type"); + expect(cipher4.fields[0].value).toEqual("address"); + expect(cipher4.fields[1].name).toEqual("item_name"); + expect(cipher4.fields[1].value).toEqual("John's home address"); + expect(cipher4.fields[2].name).toEqual("address"); + expect(cipher4.fields[2].value).toEqual("1 some street"); + expect(cipher4.fields[3].name).toEqual("country"); + expect(cipher4.fields[3].value).toEqual("de"); + expect(cipher4.fields[4].name).toEqual("state"); + expect(cipher4.fields[4].value).toEqual("DE-0-NW"); + expect(cipher4.fields[5].name).toEqual("city"); + expect(cipher4.fields[5].value).toEqual("some city"); + expect(cipher4.fields[6].name).toEqual("zip"); + expect(cipher4.fields[6].value).toEqual("123123"); + expect(cipher4.fields[7].name).toEqual("address_recipient"); + expect(cipher4.fields[7].value).toEqual("John"); + expect(cipher4.fields[8].name).toEqual("address_building"); + expect(cipher4.fields[8].value).toEqual("1"); + expect(cipher4.fields[9].name).toEqual("address_apartment"); + expect(cipher4.fields[9].value).toEqual("1"); + expect(cipher4.fields[10].name).toEqual("address_floor"); + expect(cipher4.fields[10].value).toEqual("1"); + expect(cipher4.fields[11].name).toEqual("address_door_code"); + expect(cipher4.fields[11].value).toEqual("123"); + + // website + const cipher5 = result.ciphers.shift(); + expect(cipher5.type).toBe(CipherType.SecureNote); + expect(cipher5.name).toBe("Website"); + + expect(cipher5.fields.length).toBe(3); + expect(cipher5.fields[0].name).toEqual("type"); + expect(cipher5.fields[0].value).toEqual("website"); + expect(cipher5.fields[1].name).toEqual("item_name"); + expect(cipher5.fields[1].value).toEqual("Website"); + expect(cipher5.fields[2].name).toEqual("url"); + expect(cipher5.fields[2].value).toEqual("website.com"); + + // 2nd name/identity + const cipher6 = result.ciphers.shift(); + expect(cipher6.type).toBe(CipherType.SecureNote); + expect(cipher6.name).toBe("Mrs Jane Doe"); + + expect(cipher6.fields.length).toBe(7); + expect(cipher6.fields[0].name).toEqual("type"); + expect(cipher6.fields[0].value).toEqual("name"); + expect(cipher6.fields[1].name).toEqual("title"); + expect(cipher6.fields[1].value).toEqual("Mrs"); + expect(cipher6.fields[2].name).toEqual("first_name"); + expect(cipher6.fields[2].value).toEqual("Jane"); + expect(cipher6.fields[3].name).toEqual("last_name"); + expect(cipher6.fields[3].value).toEqual("Doe"); + expect(cipher6.fields[4].name).toEqual("login"); + expect(cipher6.fields[4].value).toEqual("jdoe"); + expect(cipher6.fields[5].name).toEqual("date_of_birth"); + expect(cipher6.fields[5].value).toEqual("2022-01-30"); + expect(cipher6.fields[6].name).toEqual("place_of_birth"); + expect(cipher6.fields[6].value).toEqual("earth"); + }); + + it("should combine personal information records to one identity if only one identity present", async () => { + const result = await importer.parse(personalInfoData); + + expect(result).not.toBeNull(); + expect(result.success).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.type).toBe(CipherType.Identity); + expect(cipher.name).toBe("MR John Doe"); + expect(cipher.identity.fullName).toBe("MR John Doe"); + expect(cipher.identity.title).toBe("MR"); + expect(cipher.identity.firstName).toBe("John"); + expect(cipher.identity.middleName).toBeNull(); + expect(cipher.identity.lastName).toBe("Doe"); + expect(cipher.identity.username).toBe("jdoe"); + expect(cipher.identity.email).toBe("jdoe@example.com"); + expect(cipher.identity.phone).toBe("+49123123123"); + + expect(cipher.fields.length).toBe(9); + expect(cipher.fields[0].name).toBe("date_of_birth"); + expect(cipher.fields[0].value).toBe("2022-01-30"); + + expect(cipher.fields[1].name).toBe("place_of_birth"); + expect(cipher.fields[1].value).toBe("world"); + + expect(cipher.fields[2].name).toBe("email_type"); + expect(cipher.fields[2].value).toBe("personal"); + + expect(cipher.fields[3].name).toBe("address_recipient"); + expect(cipher.fields[3].value).toBe("John"); + + expect(cipher.fields[4].name).toBe("address_building"); + expect(cipher.fields[4].value).toBe("1"); + + expect(cipher.fields[5].name).toBe("address_apartment"); + expect(cipher.fields[5].value).toBe("1"); + + expect(cipher.fields[6].name).toBe("address_floor"); + expect(cipher.fields[6].value).toBe("1"); + + expect(cipher.fields[7].name).toBe("address_door_code"); + expect(cipher.fields[7].value).toBe("123"); + + expect(cipher.fields[8].name).toBe("url"); + expect(cipher.fields[8].value).toBe("website.com"); + }); +}); diff --git a/spec/common/importers/testData/dashlaneCsv/credentials.csv.ts b/spec/common/importers/testData/dashlaneCsv/credentials.csv.ts new file mode 100644 index 00000000000..78e42cb1ae8 --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/credentials.csv.ts @@ -0,0 +1,2 @@ +export const credentialsData = `username,username2,username3,title,password,note,url,category,otpSecret +jdoe,,,example.com,somePassword,some note for example.com,https://www.example.com,Entertainment,someTOTPSeed`; diff --git a/spec/common/importers/testData/dashlaneCsv/id.csv.ts b/spec/common/importers/testData/dashlaneCsv/id.csv.ts new file mode 100644 index 00000000000..1c939557b48 --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/id.csv.ts @@ -0,0 +1,6 @@ +export const identityData = `type,number,name,issue_date,expiration_date,place_of_issue,state +card,123123123,John Doe,2022-1-30,2032-1-30,, +passport,123123123,John Doe,2022-1-30,2032-1-30,somewhere in Germany, +license,1234556,John Doe,2022-8-10,2022-10-10,,DC +social_security,123123123,John Doe,,,, +tax_number,123123123,,,,,`; diff --git a/spec/common/importers/testData/dashlaneCsv/multiplePersonalInfo.csv.ts b/spec/common/importers/testData/dashlaneCsv/multiplePersonalInfo.csv.ts new file mode 100644 index 00000000000..cca5748b45d --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/multiplePersonalInfo.csv.ts @@ -0,0 +1,7 @@ +export const multiplePersonalInfoData = `type,title,first_name,middle_name,last_name,login,date_of_birth,place_of_birth,email,email_type,item_name,phone_number,address,country,state,city,zip,address_recipient,address_building,address_apartment,address_floor,address_door_code,job_title,url +name,MR,John,,Doe,jdoe,2022-01-30,world,,,,,,,,,,,,,,,, +email,,,,,,,,jdoe@example.com,personal,Johns email,,,,,,,,,,,,, +number,,,,,,,,,,John's number,+49123123123,,,,,,,,,,,, +address,,,,,,,,,,John's home address,,1 some street,de,DE-0-NW,some city,123123,John,1,1,1,123,, +website,,,,,,,,,,Website,,,,,,,,,,,,,website.com +name,Mrs,Jane,,Doe,jdoe,2022-01-30,earth,,,,,,,,,,,,,,,,`; diff --git a/spec/common/importers/testData/dashlaneCsv/payments.csv.ts b/spec/common/importers/testData/dashlaneCsv/payments.csv.ts new file mode 100644 index 00000000000..336986c1c49 --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/payments.csv.ts @@ -0,0 +1,3 @@ +export const paymentsData = `type,account_name,account_holder,cc_number,code,expiration_month,expiration_year,routing_number,account_number,country,issuing_bank +bank,John's savings account,John Doe,,,,,routingNumber,accountNumber,US,US-ALLY +credit_card,John Doe,,41111111111111111,123,01,2023,,,US,`; diff --git a/spec/common/importers/testData/dashlaneCsv/personalInfo.csv.ts b/spec/common/importers/testData/dashlaneCsv/personalInfo.csv.ts new file mode 100644 index 00000000000..ec99f8006ac --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/personalInfo.csv.ts @@ -0,0 +1,6 @@ +export const personalInfoData = `type,title,first_name,middle_name,last_name,login,date_of_birth,place_of_birth,email,email_type,item_name,phone_number,address,country,state,city,zip,address_recipient,address_building,address_apartment,address_floor,address_door_code,job_title,url +name,MR,John,,Doe,jdoe,2022-01-30,world,,,,,,,,,,,,,,,, +email,,,,,,,,jdoe@example.com,personal,Johns email,,,,,,,,,,,,, +number,,,,,,,,,,John's number,+49123123123,,,,,,,,,,,, +address,,,,,,,,,,John's home address,,1 some street,de,DE-0-NW,some city,123123,John,1,1,1,123,, +website,,,,,,,,,,Website,,,,,,,,,,,,,website.com`; diff --git a/spec/common/importers/testData/dashlaneCsv/securenotes.csv.ts b/spec/common/importers/testData/dashlaneCsv/securenotes.csv.ts new file mode 100644 index 00000000000..22a3b904f9d --- /dev/null +++ b/spec/common/importers/testData/dashlaneCsv/securenotes.csv.ts @@ -0,0 +1,2 @@ +export const secureNoteData = `title,note +01,test`;