summaryrefslogtreecommitdiff
path: root/internal/store/data_test.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-17 08:45:05 +0300
committerPaul Buetow <paul@buetow.org>2026-04-17 08:45:05 +0300
commitb53e348d89046ea8de5b82c283fb56980b53cdd8 (patch)
treef2115ccf556dddfc99ad129522069845c53ebd21 /internal/store/data_test.go
parente4671dc15d224d944fe5c3715c95793c21167443 (diff)
feat: add WriteBack hook on store.Data for backend-specific edit round-trip (tasks i4+l4)
Add optional WriteBack func([]byte) error field to Data struct. ReimportAfterExport calls WriteBack(newContent) when set, otherwise falls back to Commit. applyAction now calls s.LoadData (exported) so the geheim and KeePass edit paths are symmetric. Tests cover nil, non-nil, and error-propagation paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/store/data_test.go')
-rw-r--r--internal/store/data_test.go140
1 files changed, 140 insertions, 0 deletions
diff --git a/internal/store/data_test.go b/internal/store/data_test.go
index 1c05a42..351618d 100644
--- a/internal/store/data_test.go
+++ b/internal/store/data_test.go
@@ -4,6 +4,7 @@ package store
import (
"context"
+ "errors"
"os"
"path/filepath"
"strings"
@@ -275,3 +276,142 @@ func TestDataCommitMissingCommitter(t *testing.T) {
t.Fatalf("Commit error = %q; want missing committer", err.Error())
}
}
+
+// --- TestReimportAfterExportWriteBack ----------------------------------------
+
+// writeBackMode selects which sub-case of TestReimportAfterExportWriteBack runs.
+type writeBackMode int
+
+const (
+ modeNoWriteBack writeBackMode = iota // nil WriteBack → falls back to Commit
+ modeWriteBack // non-nil WriteBack → hook is called
+ modeWriteBackError // non-nil WriteBack that returns an error
+)
+
+// TestReimportAfterExportWriteBack exercises three paths through ReimportAfterExport:
+// - nil WriteBack: falls back to Commit (verified via "missing committer" error)
+// - non-nil WriteBack: hook is called with the new content
+// - non-nil WriteBack that errors: error is propagated to the caller
+//
+// Table-driven so new cases can be added without duplicating setup logic.
+func TestReimportAfterExportWriteBack(t *testing.T) {
+ cases := []struct {
+ name string
+ editedContent string
+ mode writeBackMode
+ }{
+ {
+ name: "nil WriteBack uses Commit path",
+ editedContent: "updated via commit path\n",
+ mode: modeNoWriteBack,
+ },
+ {
+ name: "non-nil WriteBack is called with new content",
+ editedContent: "updated via WriteBack hook\n",
+ mode: modeWriteBack,
+ },
+ {
+ name: "WriteBack error is propagated",
+ editedContent: "updated content that triggers hook error\n",
+ mode: modeWriteBackError,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx := context.Background()
+ dir := t.TempDir()
+
+ // Write the "edited" file that ReimportAfterExport will read.
+ exportedPath := filepath.Join(dir, "secret.txt")
+ if err := os.WriteFile(exportedPath, []byte(tc.editedContent), 0o600); err != nil {
+ t.Fatalf("writing exported file: %v", err)
+ }
+
+ switch tc.mode {
+ case modeWriteBack:
+ testReimportWithWriteBack(t, ctx, exportedPath, tc.editedContent)
+ case modeWriteBackError:
+ testReimportWithWriteBackError(t, ctx, exportedPath)
+ case modeNoWriteBack:
+ testReimportWithoutWriteBack(t, ctx, dir, exportedPath, tc.editedContent)
+ }
+ })
+ }
+}
+
+// testReimportWithWriteBack verifies that ReimportAfterExport calls WriteBack
+// with the file's content when WriteBack is non-nil.
+func testReimportWithWriteBack(t *testing.T, ctx context.Context, exportedPath, wantContent string) {
+ t.Helper()
+
+ var capturedContent []byte
+ d := &Data{
+ ExportedPath: exportedPath,
+ WriteBack: func(newContent []byte) error {
+ capturedContent = newContent
+ return nil
+ },
+ }
+
+ if err := d.ReimportAfterExport(ctx); err != nil {
+ t.Fatalf("ReimportAfterExport with WriteBack: %v", err)
+ }
+ if string(capturedContent) != wantContent {
+ t.Errorf("WriteBack received %q; want %q", capturedContent, wantContent)
+ }
+ // Content field should also be updated.
+ if string(d.Content) != wantContent {
+ t.Errorf("d.Content = %q; want %q", d.Content, wantContent)
+ }
+}
+
+// testReimportWithWriteBackError verifies that when WriteBack returns a non-nil
+// error, ReimportAfterExport propagates that error to the caller unchanged.
+func testReimportWithWriteBackError(t *testing.T, ctx context.Context, exportedPath string) {
+ t.Helper()
+
+ sentinelErr := errors.New("backend write failed")
+ d := &Data{
+ ExportedPath: exportedPath,
+ WriteBack: func(newContent []byte) error {
+ return sentinelErr
+ },
+ }
+
+ err := d.ReimportAfterExport(ctx)
+ if err == nil {
+ t.Fatal("ReimportAfterExport with failing WriteBack: expected error, got nil")
+ }
+ if !errors.Is(err, sentinelErr) {
+ t.Errorf("error = %v; want sentinel error %v", err, sentinelErr)
+ }
+}
+
+// testReimportWithoutWriteBack verifies that ReimportAfterExport falls back to
+// Commit (encrypt+git-stage) when WriteBack is nil. We stub out git by leaving
+// committer nil and expect the "missing committer" error, which confirms the
+// Commit path was reached (not a hook).
+func testReimportWithoutWriteBack(t *testing.T, ctx context.Context, dir, exportedPath, editedContent string) {
+ t.Helper()
+
+ c := newTestCipher(t)
+ dataPath := filepath.Join(dir, "entry.data")
+
+ d := &Data{
+ ExportedPath: exportedPath,
+ DataPath: dataPath,
+ encryptor: c,
+ // WriteBack intentionally left nil — must use Commit path.
+ // committer left nil so Commit returns "missing committer" error,
+ // confirming we reached Commit rather than any WriteBack hook.
+ }
+
+ err := d.ReimportAfterExport(ctx)
+ if err == nil {
+ t.Fatal("expected missing committer error from nil-WriteBack path, got nil")
+ }
+ if !strings.Contains(err.Error(), "missing committer") {
+ t.Errorf("error = %q; want missing committer (confirms Commit path was taken)", err.Error())
+ }
+}