From f1b69ad65d4051428be05e9176d67d1ad6c611b6 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Fri, 7 Mar 2025 16:58:43 -0600 Subject: [PATCH] [PM-16690] Bitwarden CSV Import - collections not created (#13636) --- .../bitwarden/bitwarden-csv-importer.spec.ts | 94 +++++++++++++++++++ .../bitwarden/bitwarden-csv-importer.ts | 11 +++ 2 files changed, 105 insertions(+) create mode 100644 libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts new file mode 100644 index 0000000000..e66779f037 --- /dev/null +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.spec.ts @@ -0,0 +1,94 @@ +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { BitwardenCsvImporter } from "./bitwarden-csv-importer"; + +describe("BitwardenCsvImporter", () => { + let importer: BitwardenCsvImporter; + + beforeEach(() => { + importer = new BitwardenCsvImporter(); + importer.organizationId = "orgId" as OrganizationId; + }); + + it("should return an empty result if data is null", async () => { + const result = await importer.parse(""); + expect(result.success).toBe(false); + expect(result.ciphers.length).toBe(0); + }); + + it("should parse CSV data correctly", async () => { + const data = + `collections,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp` + + `\ncollection1/collection2,login,testlogin,testnotes,,0,https://example.com,testusername,testpassword,`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("testlogin"); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.notes).toBe("testnotes"); + expect(cipher.reprompt).toBe(CipherRepromptType.None); + + expect(cipher.login).toBeDefined(); + expect(cipher.login.username).toBe("testusername"); + expect(cipher.login.password).toBe("testpassword"); + expect(cipher.login.uris[0].uri).toBe("https://example.com"); + + expect(result.collections.length).toBe(2); + expect(result.collections[0].name).toBe("collection1/collection2"); + expect(result.collections[1].name).toBe("collection1"); + }); + + it("should handle secure notes correctly", async () => { + const data = `name,type,notes` + `\nTest Note,note,Some secure notes`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("Test Note"); + expect(cipher.type).toBe(CipherType.SecureNote); + expect(cipher.notes).toBe("Some secure notes"); + + expect(cipher.secureNote).toBeDefined(); + expect(cipher.secureNote.type).toBe(SecureNoteType.Generic); + }); + + it("should handle missing fields gracefully", async () => { + const data = + `name,login_username,login_password,login_uri` + + `\nTest Login,username,password,http://example.com`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + + const cipher = result.ciphers[0]; + expect(cipher.name).toBe("Test Login"); + expect(cipher.type).toBe(CipherType.Login); + expect(cipher.login.username).toBe("username"); + expect(cipher.login.password).toBe("password"); + expect(cipher.login.uris[0].uri).toBe("http://example.com"); + }); + + it("should handle collections correctly", async () => { + const data = `name,collections` + `\nTest Login,collection1/collection2`; + + const result = await importer.parse(data); + + expect(result.success).toBe(true); + expect(result.ciphers.length).toBe(1); + + expect(result.collections.length).toBe(2); + expect(result.collections[0].name).toBe("collection1/collection2"); + expect(result.collections[1].name).toBe("collection1"); + }); +}); diff --git a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts index 026c055b21..fab47b30b1 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-csv-importer.ts @@ -43,6 +43,17 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer { } result.collectionRelationships.push([result.ciphers.length, collectionIndex]); + + // if the collection name is a/b/c/d, we need to create a/b/c and a/b and a + const parts = col.split("/"); + for (let i = parts.length - 1; i > 0; i--) { + const parentCollectionName = parts.slice(0, i).join("/") as string; + if (result.collections.find((c) => c.name === parentCollectionName) == null) { + const parentCollection = new CollectionView(); + parentCollection.name = parentCollectionName; + result.collections.push(parentCollection); + } + } }); } else if (!this.organization) { this.processFolder(result, value.folder);