add csv export
This commit is contained in:
50
entries/bitwarden_csv.go
Normal file
50
entries/bitwarden_csv.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package entries
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"io"
|
||||
)
|
||||
|
||||
// WriteBitwardenCSV writes Bitwarden's native CSV interchange format.
|
||||
// CSV is intentionally offered alongside JSON because Bitwarden routes the two
|
||||
// formats through different importers.
|
||||
func WriteBitwardenCSV(w io.Writer, secrets []Secret) error {
|
||||
csvWriter := csv.NewWriter(w)
|
||||
|
||||
if err := csvWriter.Write([]string{
|
||||
"folder",
|
||||
"favorite",
|
||||
"type",
|
||||
"name",
|
||||
"notes",
|
||||
"fields",
|
||||
"reprompt",
|
||||
"login_uri",
|
||||
"login_username",
|
||||
"login_password",
|
||||
"login_totp",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if err := csvWriter.Write([]string{
|
||||
"",
|
||||
"0",
|
||||
"login",
|
||||
secret.Description,
|
||||
secret.Notes,
|
||||
"",
|
||||
"0",
|
||||
secret.URL,
|
||||
secret.Username,
|
||||
secret.Password,
|
||||
"",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
csvWriter.Flush()
|
||||
return csvWriter.Error()
|
||||
}
|
||||
61
entries/bitwarden_csv_test.go
Normal file
61
entries/bitwarden_csv_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package entries
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteBitwardenCSV(t *testing.T) {
|
||||
secrets := []Secret{{
|
||||
Description: `Example, "personal"`,
|
||||
URL: "https://example.com/login?a=1,b=2",
|
||||
Notes: "First line\nSecond line",
|
||||
Username: "person@example.com",
|
||||
Password: `correct,"quoted" password`,
|
||||
}}
|
||||
|
||||
var output strings.Builder
|
||||
if err := WriteBitwardenCSV(&output, secrets); err != nil {
|
||||
t.Fatalf("write export: %v", err)
|
||||
}
|
||||
|
||||
records, err := csv.NewReader(strings.NewReader(output.String())).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("read generated CSV: %v", err)
|
||||
}
|
||||
if len(records) != 2 {
|
||||
t.Fatalf("expected header and one item, got %d rows", len(records))
|
||||
}
|
||||
|
||||
wantHeader := []string{
|
||||
"folder", "favorite", "type", "name", "notes", "fields", "reprompt",
|
||||
"login_uri", "login_username", "login_password", "login_totp",
|
||||
}
|
||||
if strings.Join(records[0], "|") != strings.Join(wantHeader, "|") {
|
||||
t.Fatalf("unexpected header: %#v", records[0])
|
||||
}
|
||||
|
||||
row := records[1]
|
||||
if row[2] != "login" || row[3] != secrets[0].Description || row[4] != secrets[0].Notes {
|
||||
t.Fatalf("item metadata was not preserved: %#v", row)
|
||||
}
|
||||
if row[7] != secrets[0].URL || row[8] != secrets[0].Username || row[9] != secrets[0].Password {
|
||||
t.Fatalf("login fields were not preserved: %#v", row)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBitwardenCSVWithNoSecrets(t *testing.T) {
|
||||
var output strings.Builder
|
||||
if err := WriteBitwardenCSV(&output, nil); err != nil {
|
||||
t.Fatalf("write export: %v", err)
|
||||
}
|
||||
|
||||
records, err := csv.NewReader(strings.NewReader(output.String())).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("read generated CSV: %v", err)
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("expected only the header, got %d rows", len(records))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user