import.test.ts

8.4 KB · raw

1import { describe, expect, it } from "vitest";23import {4  base64urlEncode,5  generatePasskeyKeypair,6} from "../background/passkey-crypto";7import { detectImportFormat, parse1PasswordJSON, parseImportSource } from "./import";89describe("shared import parsing", () => {10  it("parses Bitwarden JSON login exports", async () => {11    const result = await parseImportSource(12      JSON.stringify({13        encrypted: false,14        items: [15          {16            type: 1,17            name: "Bitwarden Login",18            notes: "imported",19            login: {20              username: "[email protected]",21              password: "hunter2",22              uris: [{ uri: "https://vault.bitwarden.com" }],23              totp:24                "otpauth://totp/Bitwarden?secret=JBSWY3DPEHPK3PXP&issuer=Bitwarden",25            },26          },27        ],28      })29    );3031    expect(result.format).toBe("bitwarden-json");32    expect(result.entries).toHaveLength(1);33    expect(result.entries[0]).toMatchObject({34      title: "Bitwarden Login",35      username: "[email protected]",36      password: "hunter2",37      url: "https://vault.bitwarden.com",38      notes: "imported",39      totp: "JBSWY3DPEHPK3PXP",40    });41  });4243  it("parses Proton Pass CSV exports", async () => {44    const result = await parseImportSource(45      [46        "name,url,email,password,note,totp",47        '"Peach","https://peachpasswords.com","[email protected]","pass123","daily driver","otpauth://totp/Peach?secret=JBSWY3DPEHPK3PXP&issuer=Peach"',48      ].join("\n")49    );5051    expect(result.format).toBe("csv");52    expect(result.entries).toHaveLength(1);53    expect(result.entries[0]).toMatchObject({54      title: "Peach",55      username: "[email protected]",56      password: "pass123",57      url: "https://peachpasswords.com",58      notes: "daily driver",59      totp: "JBSWY3DPEHPK3PXP",60    });61    // PCH-01-019: a default-param URI carries no sibling fields (stays legacy).62    expect(result.entries[0].totpAlgorithm).toBeUndefined();63    expect(result.entries[0].totpDigits).toBeUndefined();64    expect(result.entries[0].totpPeriod).toBeUndefined();65  });6667  it("parse_csv_returns_total_items_matching_rows (PCH-01-026 #5)", async () => {68    // 5 data rows; one has no password and is skipped.69    const result = await parseImportSource(70      [71        "name,url,email,password,note,totp",72        '"A","https://a.test","[email protected]","pw","",""',73        '"B","https://b.test","[email protected]","pw","",""',74        '"C","https://c.test","[email protected]","pw","",""',75        '"D","https://d.test","[email protected]","pw","",""',76        '"E","https://e.test","[email protected]","","",""',77      ].join("\n")78    );79    expect(result.format).toBe("csv");80    expect(result.entries).toHaveLength(4);81    expect(result.totalItems).toBe(5);82    expect(result.skippedItems).toBe(1);83  });8485  it("preserves non-default TOTP params from an otpauth URI (PCH-01-019)", async () => {86    const result = await parseImportSource(87      [88        "name,url,email,password,note,totp",89        '"Acct","https://e.test","[email protected]","pw","","otpauth://totp/Acct?secret=JBSWY3DPEHPK3PXP&algorithm=SHA256&digits=8&period=60&issuer=Acct"',90      ].join("\n")91    );9293    expect(result.entries).toHaveLength(1);94    expect(result.entries[0]).toMatchObject({95      totp: "JBSWY3DPEHPK3PXP",96      totpAlgorithm: "SHA256",97      totpDigits: 8,98      totpPeriod: 60,99    });100  });101102  it("parses Proton Pass JSON exports", async () => {103    const result = await parseImportSource(104      JSON.stringify({105        version: "1.8.0",106        encrypted: false,107        vaults: {108          primary: {109            name: "Personal",110            items: [111              {112                data: {113                  type: "login",114                  metadata: {115                    name: "Proton Login",116                    note: "switching in",117                  },118                  content: {119                    username: "peach-user",120                    password: "pass123",121                    urls: ["https://proton.me"],122                    totpUri:123                      "otpauth://totp/Proton?secret=JBSWY3DPEHPK3PXP&issuer=Proton",124                  },125                },126                state: 1,127              },128            ],129          },130        },131      })132    );133134    expect(result.format).toBe("protonpass-json");135    expect(result.entries).toHaveLength(1);136    expect(result.entries[0]).toMatchObject({137      title: "Proton Login",138      username: "peach-user",139      password: "pass123",140      url: "https://proton.me",141      notes: "switching in",142      totp: "JBSWY3DPEHPK3PXP",143    });144  });145146  it("parses credential exchange passkey imports", async () => {147    const generated = await generatePasskeyKeypair(-7);148    const privateKey = await crypto.subtle.importKey(149      "jwk",150      generated.privateKeyJwk,151      { name: "ECDSA", namedCurve: "P-256" },152      true,153      ["sign"]154    );155    const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));156157    const result = await parseImportSource(158      JSON.stringify({159        version: { major: 1, minor: 0 },160        exporterRpId: "bitwarden.example",161        exporterDisplayName: "Bitwarden",162        timestamp: 1705228800,163        accounts: [164          {165            id: "account-1",166            username: "june",167            items: [168              {169                id: "item-1",170                title: "WebAuthn.io",171                credentials: [172                  {173                    type: "passkey",174                    credentialId: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),175                    rpId: "webauthn.io",176                    username: "june",177                    userDisplayName: "June",178                    userHandle: base64urlEncode(crypto.getRandomValues(new Uint8Array(32))),179                    key: base64urlEncode(pkcs8),180                  },181                ],182              },183            ],184          },185        ],186      })187    );188189    expect(result.format).toBe("credential-exchange-json");190    expect(result.entries).toHaveLength(1);191    expect(result.entries[0]).toMatchObject({192      type: "login",193      title: "WebAuthn.io",194      username: "june",195      passkey: {196        rpId: "webauthn.io",197        userName: "june",198        userDisplayName: "June",199        signCount: 0,200      },201    });202  });203204  it("falls back gracefully on malformed JSON", async () => {205    const result = await parseImportSource("not valid json at all");206    expect(result.format).toBe("unknown");207  });208209  it("detectImportFormat identifies the supported JSON and CSV shapes", () => {210    expect(detectImportFormat('{"items":[{"type":1}]}')).toBe("bitwarden-json");211    expect(212      detectImportFormat(213        JSON.stringify({214          accounts: [{ vaults: [{ items: [{ overview: { title: "GitHub" } }] }] }],215        })216      )217    ).toBe("1password-json");218    expect(219      detectImportFormat(220        JSON.stringify({221          vaults: { primary: { items: [{ data: { type: "login" } }] } },222        })223      )224    ).toBe("protonpass-json");225    expect(detectImportFormat("name,url,username,password\nGitHub,https://github.com,june,secret")).toBe("csv");226    expect(detectImportFormat("totally unrecognized")).toBe("unknown");227  });228229  it("parseCSV maps LastPass-style grouping columns to tags", async () => {230    const result = await parseImportSource(231      [232        "name,url,username,password,grouping",233        '"GitHub","https://github.com","june","secret123","work/team"',234      ].join("\n")235    );236237    expect(result.entries).toMatchObject([238      {239        title: "GitHub",240        tags: ["work/team"],241      },242    ]);243  });244245  it("parse1PasswordJSON handles 1Password 8 data.items shape", () => {246    const entries = parse1PasswordJSON(247      JSON.stringify({248        items: [249          {250            title: "GitHub",251            url: "https://github.com",252            fields: [253              { id: "username", value: "june" },254              { id: "password", value: "secret123" },255            ],256            notes: "from 1password",257          },258        ],259      })260    );261262    expect(entries).toMatchObject([263      {264        title: "GitHub",265        username: "june",266        password: "secret123",267        url: "https://github.com",268        notes: "from 1password",269      },270    ]);271  });272273  it("MV-5: parseImportSource rejects an over-limit raw string with IMPORT_TOO_LARGE", async () => {274    const huge = "x".repeat(11 * 1024 * 1024); // 11 MiB > 10 MiB cap275    await expect(parseImportSource(huge)).rejects.toThrow(/IMPORT_TOO_LARGE/);276  });277});