import { describe, expect, it } from "vitest"; import { base64urlEncode, generatePasskeyKeypair, } from "../background/passkey-crypto"; import { detectImportFormat, parse1PasswordJSON, parseImportSource } from "./import"; describe("shared import parsing", () => { it("parses Bitwarden JSON login exports", async () => { const result = await parseImportSource( JSON.stringify({ encrypted: false, items: [ { type: 1, name: "Bitwarden Login", notes: "imported", login: { username: "june@peach.test", password: "hunter2", uris: [{ uri: "https://vault.bitwarden.com" }], totp: "otpauth://totp/Bitwarden?secret=JBSWY3DPEHPK3PXP&issuer=Bitwarden", }, }, ], }) ); expect(result.format).toBe("bitwarden-json"); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ title: "Bitwarden Login", username: "june@peach.test", password: "hunter2", url: "https://vault.bitwarden.com", notes: "imported", totp: "JBSWY3DPEHPK3PXP", }); }); it("parses Proton Pass CSV exports", async () => { const result = await parseImportSource( [ "name,url,email,password,note,totp", '"Peach","https://peachpasswords.com","hello@peach.test","pass123","daily driver","otpauth://totp/Peach?secret=JBSWY3DPEHPK3PXP&issuer=Peach"', ].join("\n") ); expect(result.format).toBe("csv"); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ title: "Peach", username: "hello@peach.test", password: "pass123", url: "https://peachpasswords.com", notes: "daily driver", totp: "JBSWY3DPEHPK3PXP", }); // PCH-01-019: a default-param URI carries no sibling fields (stays legacy). expect(result.entries[0].totpAlgorithm).toBeUndefined(); expect(result.entries[0].totpDigits).toBeUndefined(); expect(result.entries[0].totpPeriod).toBeUndefined(); }); it("parse_csv_returns_total_items_matching_rows (PCH-01-026 #5)", async () => { // 5 data rows; one has no password and is skipped. const result = await parseImportSource( [ "name,url,email,password,note,totp", '"A","https://a.test","a@a.test","pw","",""', '"B","https://b.test","b@b.test","pw","",""', '"C","https://c.test","c@c.test","pw","",""', '"D","https://d.test","d@d.test","pw","",""', '"E","https://e.test","e@e.test","","",""', ].join("\n") ); expect(result.format).toBe("csv"); expect(result.entries).toHaveLength(4); expect(result.totalItems).toBe(5); expect(result.skippedItems).toBe(1); }); it("preserves non-default TOTP params from an otpauth URI (PCH-01-019)", async () => { const result = await parseImportSource( [ "name,url,email,password,note,totp", '"Acct","https://e.test","u@e.test","pw","","otpauth://totp/Acct?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=8&period=60&issuer=Acct"', ].join("\n") ); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ totp: "JBSWY3DPEHPK3PXP", totpAlgorithm: "SHA256", totpDigits: 8, totpPeriod: 60, }); }); it("parses Proton Pass JSON exports", async () => { const result = await parseImportSource( JSON.stringify({ version: "1.8.0", encrypted: false, vaults: { primary: { name: "Personal", items: [ { data: { type: "login", metadata: { name: "Proton Login", note: "switching in", }, content: { username: "peach-user", password: "pass123", urls: ["https://proton.me"], totpUri: "otpauth://totp/Proton?secret=JBSWY3DPEHPK3PXP&issuer=Proton", }, }, state: 1, }, ], }, }, }) ); expect(result.format).toBe("protonpass-json"); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ title: "Proton Login", username: "peach-user", password: "pass123", url: "https://proton.me", notes: "switching in", totp: "JBSWY3DPEHPK3PXP", }); }); it("parses credential exchange passkey imports", async () => { const generated = await generatePasskeyKeypair(-7); const privateKey = await crypto.subtle.importKey( "jwk", generated.privateKeyJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"] ); const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey)); const result = await parseImportSource( JSON.stringify({ version: { major: 1, minor: 0 }, exporterRpId: "bitwarden.example", exporterDisplayName: "Bitwarden", timestamp: 1705228800, accounts: [ { id: "account-1", username: "june", items: [ { id: "item-1", title: "WebAuthn.io", credentials: [ { type: "passkey", credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))), rpId: "webauthn.io", username: "june", userDisplayName: "June", userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))), key: base64urlEncode(pkcs8), }, ], }, ], }, ], }) ); expect(result.format).toBe("credential-exchange-json"); expect(result.entries).toHaveLength(1); expect(result.entries[0]).toMatchObject({ type: "login", title: "WebAuthn.io", username: "june", passkey: { rpId: "webauthn.io", userName: "june", userDisplayName: "June", signCount: 0, }, }); }); it("falls back gracefully on malformed JSON", async () => { const result = await parseImportSource("not valid json at all"); expect(result.format).toBe("unknown"); }); it("detectImportFormat identifies the supported JSON and CSV shapes", () => { expect(detectImportFormat('{"items":[{"type":1}]}')).toBe("bitwarden-json"); expect( detectImportFormat( JSON.stringify({ accounts: [{ vaults: [{ items: [{ overview: { title: "GitHub" } }] }] }], }) ) ).toBe("1password-json"); expect( detectImportFormat( JSON.stringify({ vaults: { primary: { items: [{ data: { type: "login" } }] } }, }) ) ).toBe("protonpass-json"); expect(detectImportFormat("name,url,username,password\nGitHub,https://github.com,june,secret")).toBe("csv"); expect(detectImportFormat("totally unrecognized")).toBe("unknown"); }); it("parseCSV maps LastPass-style grouping columns to tags", async () => { const result = await parseImportSource( [ "name,url,username,password,grouping", '"GitHub","https://github.com","june","secret123","work/team"', ].join("\n") ); expect(result.entries).toMatchObject([ { title: "GitHub", tags: ["work/team"], }, ]); }); it("parse1PasswordJSON handles 1Password 8 data.items shape", () => { const entries = parse1PasswordJSON( JSON.stringify({ items: [ { title: "GitHub", url: "https://github.com", fields: [ { id: "username", value: "june" }, { id: "password", value: "secret123" }, ], notes: "from 1password", }, ], }) ); expect(entries).toMatchObject([ { title: "GitHub", username: "june", password: "secret123", url: "https://github.com", notes: "from 1password", }, ]); }); it("MV-5: parseImportSource rejects an over-limit raw string with IMPORT_TOO_LARGE", async () => { const huge = "x".repeat(11 * 1024 * 1024); // 11 MiB > 10 MiB cap await expect(parseImportSource(huge)).rejects.toThrow(/IMPORT_TOO_LARGE/); }); });