diff options
Diffstat (limited to 'internal/migrate/migrator_test.go')
| -rw-r--r-- | internal/migrate/migrator_test.go | 202 |
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) + } +} |
