1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-22 12:24:01 +00:00

Merge branch 'main' of github.com:bitwarden/clients into feature/PM-30737-Migrate-DeleteAccount

This commit is contained in:
Isaac Ivins
2026-02-03 11:21:24 -05:00
41 changed files with 637 additions and 94 deletions

View File

@@ -91,4 +91,33 @@ describe("BitwardenCsvImporter", () => {
expect(result.collections[0].name).toBe("collection1/collection2");
expect(result.collections[1].name).toBe("collection1");
});
it("should parse archived items correctly", async () => {
const archivedDate = "2025-01-15T10:30:00.000Z";
const data =
`name,type,archivedDate,login_uri,login_username,login_password` +
`\nArchived Login,login,${archivedDate},https://example.com,user,pass`;
importer.organizationId = null;
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("Archived Login");
expect(cipher.archivedDate).toBeDefined();
expect(cipher.archivedDate.toISOString()).toBe(archivedDate);
});
it("should handle missing archivedDate gracefully", async () => {
const data = `name,type,login_uri` + `\nTest Login,login,https://example.com`;
importer.organizationId = null;
const result = await importer.parse(data);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(1);
expect(result.ciphers[0].archivedDate).toBeUndefined();
});
});

View File

@@ -51,6 +51,15 @@ export class BitwardenCsvImporter extends BaseImporter implements Importer {
cipher.reprompt = CipherRepromptType.None;
}
if (!this.isNullOrWhitespace(value.archivedDate)) {
try {
cipher.archivedDate = new Date(value.archivedDate);
} catch (e) {
// eslint-disable-next-line
console.error("Unable to parse archivedDate value", e);
}
}
if (!this.isNullOrWhitespace(value.fields)) {
const fields = this.splitNewLine(value.fields);
for (let i = 0; i < fields.length; i++) {

View File

@@ -0,0 +1,87 @@
import { ButtercupCsvImporter } from "./buttercup-csv-importer";
import {
buttercupCsvTestData,
buttercupCsvWithCustomFieldsTestData,
buttercupCsvWithNoteTestData,
buttercupCsvWithSubfoldersTestData,
buttercupCsvWithUrlFieldTestData,
} from "./spec-data/buttercup-csv/testdata.csv";
describe("Buttercup CSV Importer", () => {
let importer: ButtercupCsvImporter;
beforeEach(() => {
importer = new ButtercupCsvImporter();
});
describe("given basic login data", () => {
it("should parse login data when provided valid CSV", async () => {
const result = await importer.parse(buttercupCsvTestData);
expect(result.success).toBe(true);
expect(result.ciphers.length).toBe(2);
const cipher = result.ciphers[0];
expect(cipher.name).toEqual("Test Entry");
expect(cipher.login.username).toEqual("testuser");
expect(cipher.login.password).toEqual("testpass123");
expect(cipher.login.uris.length).toEqual(1);
expect(cipher.login.uris[0].uri).toEqual("https://example.com");
});
it("should assign entries to folders based on group_name", async () => {
const result = await importer.parse(buttercupCsvTestData);
expect(result.success).toBe(true);
expect(result.folders.length).toBe(1);
expect(result.folders[0].name).toEqual("General");
expect(result.folderRelationships.length).toBe(2);
});
});
describe("given URL field variations", () => {
it("should handle lowercase url field", async () => {
const result = await importer.parse(buttercupCsvWithUrlFieldTestData);
expect(result.success).toBe(true);
const cipher = result.ciphers[0];
expect(cipher.login.uris.length).toEqual(1);
expect(cipher.login.uris[0].uri).toEqual("https://lowercase-url.com");
});
});
describe("given note field", () => {
it("should map note field to notes", async () => {
const result = await importer.parse(buttercupCsvWithNoteTestData);
expect(result.success).toBe(true);
const cipher = result.ciphers[0];
expect(cipher.notes).toEqual("This is a note");
});
});
describe("given custom fields", () => {
it("should import custom fields and exclude official props", async () => {
const result = await importer.parse(buttercupCsvWithCustomFieldsTestData);
expect(result.success).toBe(true);
const cipher = result.ciphers[0];
expect(cipher.fields.length).toBe(2);
expect(cipher.fields[0].name).toEqual("custom_field");
expect(cipher.fields[0].value).toEqual("custom value");
expect(cipher.fields[1].name).toEqual("another_field");
expect(cipher.fields[1].value).toEqual("another value");
});
});
describe("given subfolders", () => {
it("should create nested folder structure", async () => {
const result = await importer.parse(buttercupCsvWithSubfoldersTestData);
expect(result.success).toBe(true);
const folderNames = result.folders.map((f) => f.name);
expect(folderNames).toContain("Work/Projects");
expect(folderNames).toContain("Work");
expect(folderNames).toContain("Personal/Finance");
expect(folderNames).toContain("Personal");
});
});
});

View File

@@ -3,7 +3,18 @@ import { ImportResult } from "../models/import-result";
import { BaseImporter } from "./base-importer";
import { Importer } from "./importer";
const OfficialProps = ["!group_id", "!group_name", "title", "username", "password", "URL", "id"];
const OfficialProps = [
"!group_id",
"!group_name",
"!type",
"title",
"username",
"password",
"URL",
"url",
"note",
"id",
];
export class ButtercupCsvImporter extends BaseImporter implements Importer {
parse(data: string): Promise<ImportResult> {
@@ -21,16 +32,24 @@ export class ButtercupCsvImporter extends BaseImporter implements Importer {
cipher.name = this.getValueOrDefault(value.title, "--");
cipher.login.username = this.getValueOrDefault(value.username);
cipher.login.password = this.getValueOrDefault(value.password);
cipher.login.uris = this.makeUriArray(value.URL);
let processingCustomFields = false;
// Handle URL field (case-insensitive)
const urlValue = value.URL || value.url || value.Url;
cipher.login.uris = this.makeUriArray(urlValue);
// Handle note field (case-insensitive)
const noteValue = value.note || value.Note || value.notes || value.Notes;
if (noteValue) {
cipher.notes = noteValue;
}
// Process custom fields, excluding official props (case-insensitive)
for (const prop in value) {
// eslint-disable-next-line
if (value.hasOwnProperty(prop)) {
if (!processingCustomFields && OfficialProps.indexOf(prop) === -1) {
processingCustomFields = true;
}
if (processingCustomFields) {
const lowerProp = prop.toLowerCase();
const isOfficialProp = OfficialProps.some((p) => p.toLowerCase() === lowerProp);
if (!isOfficialProp && value[prop]) {
this.processKvp(cipher, prop, value[prop]);
}
}

View File

@@ -0,0 +1,16 @@
export const buttercupCsvTestData = `!group_id,!group_name,title,username,password,URL,id
1,General,Test Entry,testuser,testpass123,https://example.com,entry1
1,General,Another Entry,anotheruser,anotherpass,https://another.com,entry2`;
export const buttercupCsvWithUrlFieldTestData = `!group_id,!group_name,title,username,password,url,id
1,General,Entry With Lowercase URL,user1,pass1,https://lowercase-url.com,entry1`;
export const buttercupCsvWithNoteTestData = `!group_id,!group_name,title,username,password,URL,note,id
1,General,Entry With Note,user1,pass1,https://example.com,This is a note,entry1`;
export const buttercupCsvWithCustomFieldsTestData = `!group_id,!group_name,title,username,password,URL,custom_field,another_field,id
1,General,Entry With Custom Fields,user1,pass1,https://example.com,custom value,another value,entry1`;
export const buttercupCsvWithSubfoldersTestData = `!group_id,!group_name,title,username,password,URL,id
1,Work/Projects,Project Entry,projectuser,projectpass,https://project.com,entry1
2,Personal/Finance,Finance Entry,financeuser,financepass,https://finance.com,entry2`;

View File

@@ -59,6 +59,7 @@ export class BaseVaultExportService {
cipher.notes = c.notes;
cipher.fields = null;
cipher.reprompt = c.reprompt;
cipher.archivedDate = c.archivedDate ? c.archivedDate.toISOString() : null;
// Login props
cipher.login_uri = null;
cipher.login_username = null;

View File

@@ -12,6 +12,7 @@ export type BitwardenCsvExportType = {
login_password: string;
login_totp: string;
favorite: number | null;
archivedDate: string | null;
};
export type BitwardenCsvIndividualExportType = BitwardenCsvExportType & {