diff --git a/libs/importer/spec/psono-json-importer.spec.ts b/libs/importer/spec/psono-json-importer.spec.ts index 32cba7e3c0..371a3829f2 100644 --- a/libs/importer/spec/psono-json-importer.spec.ts +++ b/libs/importer/spec/psono-json-importer.spec.ts @@ -11,6 +11,7 @@ import { FoldersTestData } from "./test-data/psono-json/folders"; import { GPGData } from "./test-data/psono-json/gpg"; import { NotesData } from "./test-data/psono-json/notes"; import { ReducedWebsiteLoginsData } from "./test-data/psono-json/reduced-website-logins"; +import { SubFoldersTestData } from "./test-data/psono-json/subfolders"; import { TOTPData } from "./test-data/psono-json/totp"; import { WebsiteLoginsData } from "./test-data/psono-json/website-logins"; @@ -37,6 +38,7 @@ describe("PSONO JSON Importer", () => { const TOTPDataJson = JSON.stringify(TOTPData); const EmptyTestFolderDataJson = JSON.stringify(EmptyTestFolderData); const FoldersTestDataJson = JSON.stringify(FoldersTestData); + const SubFoldersTestDataJson = JSON.stringify(SubFoldersTestData); const GPGDataJson = JSON.stringify(GPGData); const EnvVariablesDataJson = JSON.stringify(EnvVariablesData); const ReducedWebsiteLoginsDataJson = JSON.stringify(ReducedWebsiteLoginsData); @@ -245,4 +247,39 @@ describe("PSONO JSON Importer", () => { expect(collections[0].name).toBe("TestFolder"); expect(collections[1].name).toBe("TestFolder2"); }); + + it("should create sub folders on folders with no items", async () => { + const importer = new PsonoJsonImporter(); + const result = await importer.parse(SubFoldersTestDataJson); + expect(result != null).toBe(true); + + const folders = result.folders; + expect(folders.length).toBe(4); + expect(folders[0].name).toBe("TestFolder/SubFolder1/SubSubFolder1"); + expect(folders[1].name).toBe("TestFolder/SubFolder1"); + expect(folders[2].name).toBe("TestFolder"); + expect(folders[3].name).toBe("TestFolder2"); + }); + + it("should assign entries to subfolders", async () => { + const importer = new PsonoJsonImporter(); + const result = await importer.parse(SubFoldersTestDataJson); + expect(result != null).toBe(true); + + const folders = result.folders; + const relationship1 = result.folderRelationships[0]; + const relationship2 = result.folderRelationships[1]; + const relationship3 = result.folderRelationships[2]; + // // Check that ciphers have a folder assigned to them + expect(result.folderRelationships.length).toBe(result.ciphers.length); + + expect(result.ciphers[relationship1[0]] == result.ciphers[0]); + expect(result.ciphers[relationship1[1]] == folders[0]); + + expect(result.ciphers[relationship2[0]] == result.ciphers[1]); + expect(result.ciphers[relationship2[1]] == folders[2]); + + expect(result.ciphers[relationship3[0]] == result.ciphers[2]); + expect(result.ciphers[relationship3[1]] == folders[3]); + }); }); diff --git a/libs/importer/spec/test-data/psono-json/subfolders.ts b/libs/importer/spec/test-data/psono-json/subfolders.ts new file mode 100644 index 0000000000..eff55581da --- /dev/null +++ b/libs/importer/spec/test-data/psono-json/subfolders.ts @@ -0,0 +1,83 @@ +import { PsonoJsonExport } from "../../../src/importers/psono/psono-json-types"; + +export const SubFoldersTestData: PsonoJsonExport = { + folders: [ + { + name: "TestFolder", + folders: [ + { + name: "SubFolder1", + folders: [ + { + name: "SubSubFolder1", + items: [ + { + type: "website_password", + name: "TestEntry on SubSubfolder", + autosubmit: true, + urlfilter: "filter", + website_password_title: "TestEntry on SubSubfolder", + website_password_url: "bitwarden.com", + website_password_username: "testUser", + website_password_password: "testPassword", + website_password_notes: "some notes", + website_password_auto_submit: true, + website_password_url_filter: "filter", + create_date: "2022-12-13T19:24:09.810266Z", + write_date: "2022-12-13T19:24:09.810292Z", + callback_url: "callback", + callback_user: "callbackUser", + callback_pass: "callbackPassword", + }, + ], + }, + ], + items: [ + { + type: "website_password", + name: "TestEntry on Subfolder", + autosubmit: true, + urlfilter: "filter", + website_password_title: "TestEntry on Subfolder", + website_password_url: "bitwarden.com", + website_password_username: "testUser", + website_password_password: "testPassword", + website_password_notes: "some notes", + website_password_auto_submit: true, + website_password_url_filter: "filter", + create_date: "2022-12-13T19:24:09.810266Z", + write_date: "2022-12-13T19:24:09.810292Z", + callback_url: "callback", + callback_user: "callbackUser", + callback_pass: "callbackPassword", + }, + ], + }, + ], + }, + { + name: "TestFolder2", + items: [ + { + type: "website_password", + name: "TestEntry2", + autosubmit: true, + urlfilter: "filter", + website_password_title: "TestEntry2", + website_password_url: "bitwarden.com", + website_password_username: "testUser", + website_password_password: "testPassword", + website_password_notes: "some notes", + website_password_auto_submit: true, + website_password_url_filter: "filter", + create_date: "2022-12-13T19:24:09.810266Z", + write_date: "2022-12-13T19:24:09.810292Z", + callback_url: "callback", + callback_user: "callbackUser", + callback_pass: "callbackPassword", + }, + ], + }, + ], + items: [], +}; diff --git a/libs/importer/src/importers/base-importer.ts b/libs/importer/src/importers/base-importer.ts index 116a278e35..f50044c633 100644 --- a/libs/importer/src/importers/base-importer.ts +++ b/libs/importer/src/importers/base-importer.ts @@ -350,7 +350,11 @@ export abstract class BaseImporter { } } - protected processFolder(result: ImportResult, folderName: string) { + protected processFolder( + result: ImportResult, + folderName: string, + addRelationship: boolean = true, + ) { if (this.isNullOrWhitespace(folderName)) { return; } @@ -374,7 +378,10 @@ export abstract class BaseImporter { result.folders.push(f); } - result.folderRelationships.push([result.ciphers.length, folderIndex]); + //Some folders can have sub-folders but no ciphers directly, we should not add to the folderRelationships array + if (addRelationship) { + result.folderRelationships.push([result.ciphers.length, folderIndex]); + } } protected convertToNoteIfNeeded(cipher: CipherView) { diff --git a/libs/importer/src/importers/psono/psono-json-importer.ts b/libs/importer/src/importers/psono/psono-json-importer.ts index e79dd8ddf4..85fe646517 100644 --- a/libs/importer/src/importers/psono/psono-json-importer.ts +++ b/libs/importer/src/importers/psono/psono-json-importer.ts @@ -1,3 +1,4 @@ +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { FieldType, SecureNoteType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; @@ -39,17 +40,28 @@ export class PsonoJsonImporter extends BaseImporter implements Importer { return Promise.resolve(result); } - private parseFolders(result: ImportResult, folders: FoldersEntity[]) { + private parseFolders(result: ImportResult, folders: FoldersEntity[], parentName?: string) { if (folders == null || folders.length === 0) { return; } folders.forEach((folder) => { - if (folder.items == null || folder.items.length == 0) { + const folderHasItems = folder.items != null && folder.items.length > 0; + const folderHasSubfolders = folder.folders != null && folder.folders.length > 0; + + if (!folderHasItems && !folderHasSubfolders) { return; } - this.processFolder(result, folder.name); + if (!Utils.isNullOrWhitespace(parentName)) { + folder.name = parentName + "/" + folder.name; + } + + if (folderHasSubfolders) { + this.parseFolders(result, folder.folders, folder.name); + } + + this.processFolder(result, folder.name, folderHasItems); this.handleItemParsing(result, folder.items); }); diff --git a/libs/importer/src/importers/psono/psono-json-types.ts b/libs/importer/src/importers/psono/psono-json-types.ts index ce0f4c23bc..cbaed3704a 100644 --- a/libs/importer/src/importers/psono/psono-json-types.ts +++ b/libs/importer/src/importers/psono/psono-json-types.ts @@ -14,7 +14,8 @@ export interface PsonoJsonExport { export interface FoldersEntity { name: string; - items: PsonoItemTypes[] | null; + items?: PsonoItemTypes[] | null; + folders?: FoldersEntity[] | null; } export interface RecordBase {