summaryrefslogtreecommitdiff
path: root/internal/migrate/migrator_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/migrate/migrator_test.go')
-rw-r--r--internal/migrate/migrator_test.go202
1 files changed, 202 insertions, 0 deletions
diff --git a/internal/migrate/migrator_test.go b/internal/migrate/migrator_test.go
new file mode 100644
index 0000000..b8a11a2
--- /dev/null
+++ b/internal/migrate/migrator_test.go
@@ -0,0 +1,202 @@
+package migrate_test
+
+import (
+ "context"
+ "strings"
+ "testing"
+
+ "codeberg.org/snonux/foostore/internal/migrate"
+ "codeberg.org/snonux/foostore/internal/store"
+)
+
+// ---- ExtractPasswordFromContent tests ----------------------------------------
+
+func TestExtractPasswordFromContent(t *testing.T) {
+ cases := []struct {
+ name string
+ input string
+ wantPassword string
+ wantNotesHas string
+ wantNotesMiss string
+ }{
+ {
+ name: "password line extracted",
+ input: "user: alice\npassword: s3cr3t\nurl: example.com\n",
+ wantPassword: "s3cr3t",
+ wantNotesHas: "user: alice",
+ wantNotesMiss: "password: s3cr3t",
+ },
+ {
+ name: "pass shorthand extracted",
+ input: "pass: abc123\nhost: db.local",
+ wantPassword: "abc123",
+ wantNotesHas: "host: db.local",
+ wantNotesMiss: "pass: abc123",
+ },
+ {
+ name: "case insensitive",
+ input: "PASSWORD: hidden\nother: line",
+ wantPassword: "hidden",
+ wantNotesHas: "other: line",
+ wantNotesMiss: "PASSWORD",
+ },
+ {
+ name: "no password line",
+ input: "just notes\nno password here",
+ wantPassword: "",
+ wantNotesHas: "just notes",
+ wantNotesMiss: "",
+ },
+ {
+ name: "only first password line taken",
+ input: "password: first\npassword: second",
+ wantPassword: "first",
+ wantNotesMiss: "first",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ pw, notes := migrate.ExtractPasswordFromContent(tc.input)
+ if pw != tc.wantPassword {
+ t.Errorf("password = %q; want %q", pw, tc.wantPassword)
+ }
+ if tc.wantNotesHas != "" && !strings.Contains(notes, tc.wantNotesHas) {
+ t.Errorf("notes %q should contain %q", notes, tc.wantNotesHas)
+ }
+ if tc.wantNotesMiss != "" && strings.Contains(notes, tc.wantNotesMiss) {
+ t.Errorf("notes %q should NOT contain %q", notes, tc.wantNotesMiss)
+ }
+ })
+ }
+}
+
+// ---- Run tests with fakes ----------------------------------------------------
+
+// fakeWalker implements StoreWalker using an in-memory list of entries.
+type fakeWalker struct {
+ indexes []*store.Index
+ // dataByDesc maps description to raw content bytes.
+ dataByDesc map[string][]byte
+}
+
+func (w *fakeWalker) WalkIndexes(_ context.Context, _ string, fn func(*store.Index) error) error {
+ for _, idx := range w.indexes {
+ if err := fn(idx); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (w *fakeWalker) LoadData(_ context.Context, idx *store.Index) (*store.Data, error) {
+ content := w.dataByDesc[idx.Description]
+ return &store.Data{Content: content}, nil
+}
+
+// fakeKDBX records calls to UpsertTextEntry and UpsertBinaryEntry.
+type fakeKDBX struct {
+ texts []string
+ binaries []string
+ saved bool
+}
+
+func (k *fakeKDBX) UpsertTextEntry(groupPath []string, title, password, notes string) (bool, error) {
+ k.texts = append(k.texts, title+"|"+password)
+ return false, nil
+}
+
+func (k *fakeKDBX) UpsertBinaryEntry(groupPath []string, title, filename string, content []byte) (bool, error) {
+ k.binaries = append(k.binaries, title)
+ return false, nil
+}
+
+func (k *fakeKDBX) Save() error {
+ k.saved = true
+ return nil
+}
+
+func makeIndex(description string) *store.Index {
+ return &store.Index{Description: description}
+}
+
+func TestRun_dryRun(t *testing.T) {
+ walker := &fakeWalker{
+ indexes: []*store.Index{
+ makeIndex("work/notes"),
+ makeIndex("images/logo.png"),
+ },
+ dataByDesc: map[string][]byte{
+ "work/notes": []byte("password: secret\nsome notes"),
+ "images/logo.png": {0, 1, 2},
+ },
+ }
+
+ var logged []string
+ logFn := func(msg string) { logged = append(logged, msg) }
+ warnFn := func(string) {}
+
+ opts := migrate.Options{DryRun: true}
+ stats, err := migrate.Run(context.Background(), walker, nil, opts, logFn, warnFn)
+ if err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+ if stats.Total != 2 {
+ t.Errorf("Total = %d; want 2", stats.Total)
+ }
+ if stats.TextMigrated != 1 {
+ t.Errorf("TextMigrated = %d; want 1", stats.TextMigrated)
+ }
+ if stats.BinaryMigrated != 1 {
+ t.Errorf("BinaryMigrated = %d; want 1", stats.BinaryMigrated)
+ }
+ if stats.Errors != 0 {
+ t.Errorf("Errors = %d; want 0", stats.Errors)
+ }
+ // Verify dry-run log messages are emitted for both entry types.
+ foundText := false
+ foundBinary := false
+ for _, msg := range logged {
+ if strings.Contains(msg, "DRY-RUN text migrate") {
+ foundText = true
+ }
+ if strings.Contains(msg, "DRY-RUN binary migrate") {
+ foundBinary = true
+ }
+ }
+ if !foundText {
+ t.Errorf("no 'DRY-RUN text migrate' message in logged: %v", logged)
+ }
+ if !foundBinary {
+ t.Errorf("no 'DRY-RUN binary migrate' message in logged: %v", logged)
+ }
+}
+
+func TestRun_liveWrites(t *testing.T) {
+ walker := &fakeWalker{
+ indexes: []*store.Index{
+ makeIndex("finance/budget"),
+ makeIndex("attachments/report.pdf"),
+ },
+ dataByDesc: map[string][]byte{
+ "finance/budget": []byte("password: money\nnotes line"),
+ "attachments/report.pdf": {5, 6, 7},
+ },
+ }
+
+ kdbx := &fakeKDBX{}
+ opts := migrate.Options{DryRun: false}
+ stats, err := migrate.Run(context.Background(), walker, kdbx, opts, func(string) {}, func(string) {})
+ if err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+ if stats.TextMigrated != 1 || stats.BinaryMigrated != 1 {
+ t.Errorf("stats = %+v; want text=1 binary=1", stats)
+ }
+ if len(kdbx.texts) != 1 || !strings.Contains(kdbx.texts[0], "money") {
+ t.Errorf("texts = %v; want password field 'money'", kdbx.texts)
+ }
+ if len(kdbx.binaries) != 1 {
+ t.Errorf("binaries = %v; want 1 entry", kdbx.binaries)
+ }
+}