summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/store/data.go33
-rw-r--r--internal/store/data_test.go36
-rw-r--r--internal/store/dependencies.go15
-rw-r--r--internal/store/index.go29
-rw-r--r--internal/store/index_test.go17
-rw-r--r--internal/store/store.go8
6 files changed, 111 insertions, 27 deletions
diff --git a/internal/store/data.go b/internal/store/data.go
index fb33bf0..25e64b1 100644
--- a/internal/store/data.go
+++ b/internal/store/data.go
@@ -8,9 +8,6 @@ import (
"os"
"path/filepath"
"strings"
-
- "codeberg.org/snonux/foostore/internal/crypto"
- "codeberg.org/snonux/foostore/internal/git"
)
// Data holds a decrypted secret blob and the paths used to persist it.
@@ -24,13 +21,16 @@ type Data struct {
// loadData decrypts a .data file and returns a Data struct with Content populated.
// absoluteDataPath must be the full filesystem path to the encrypted .data file.
-func loadData(ctx context.Context, absoluteDataPath string, c *crypto.Cipher) (*Data, error) {
+func loadData(ctx context.Context, absoluteDataPath string, encryptor Encryptor) (*Data, error) {
ciphertext, err := os.ReadFile(absoluteDataPath)
if err != nil {
return nil, fmt.Errorf("reading data file %q: %w", absoluteDataPath, err)
}
- plain, err := c.Decrypt(ciphertext)
+ if encryptor == nil {
+ return nil, fmt.Errorf("decrypting data file %q: missing encryptor", absoluteDataPath)
+ }
+ plain, err := encryptor.Decrypt(ciphertext)
if err != nil {
return nil, fmt.Errorf("decrypting data file %q: %w", absoluteDataPath, err)
}
@@ -69,21 +69,21 @@ func (d *Data) Export(ctx context.Context, exportDir, destinationFile string) er
// ReimportAfterExport reads the (possibly edited) file from ExportedPath back
// into Content and then commits it. This is used by the edit workflow: export →
// user edits in external editor → reimport.
-func (d *Data) ReimportAfterExport(ctx context.Context, c *crypto.Cipher, g *git.Git) error {
+func (d *Data) ReimportAfterExport(ctx context.Context, encryptor Encryptor, committer Committer) error {
content, err := os.ReadFile(d.ExportedPath)
if err != nil {
return fmt.Errorf("reading exported file %q: %w", d.ExportedPath, err)
}
d.Content = content
- return d.Commit(ctx, c, g, true)
+ return d.Commit(ctx, encryptor, committer, true)
}
// Commit encrypts Content and writes it to DataPath, then stages the file with git.
// If force is false and the file already exists, the commit is silently skipped
// (matching the Ruby CommitFile#commit_content behaviour that avoids overwrites
// without explicit force).
-func (d *Data) Commit(ctx context.Context, c *crypto.Cipher, g *git.Git, force bool) error {
+func (d *Data) Commit(ctx context.Context, encryptor Encryptor, committer Committer, force bool) error {
if !force {
if _, err := os.Stat(d.DataPath); err == nil {
// File already exists; skip without error to preserve existing data.
@@ -96,7 +96,10 @@ func (d *Data) Commit(ctx context.Context, c *crypto.Cipher, g *git.Git, force b
return fmt.Errorf("creating data directory for %q: %w", d.DataPath, err)
}
- ciphertext, err := c.Encrypt(d.Content)
+ if encryptor == nil {
+ return fmt.Errorf("encrypting data for %q: missing encryptor", d.DataPath)
+ }
+ ciphertext, err := encryptor.Encrypt(d.Content)
if err != nil {
return fmt.Errorf("encrypting data for %q: %w", d.DataPath, err)
}
@@ -105,7 +108,10 @@ func (d *Data) Commit(ctx context.Context, c *crypto.Cipher, g *git.Git, force b
return fmt.Errorf("writing data file %q: %w", d.DataPath, err)
}
- if err := g.Add(ctx, d.DataPath); err != nil {
+ if committer == nil {
+ return fmt.Errorf("git add data %q: missing committer", d.DataPath)
+ }
+ if err := committer.Add(ctx, d.DataPath); err != nil {
return fmt.Errorf("git add data %q: %w", d.DataPath, err)
}
@@ -113,6 +119,9 @@ func (d *Data) Commit(ctx context.Context, c *crypto.Cipher, g *git.Git, force b
}
// Remove stages the .data file for deletion via git rm.
-func (d *Data) Remove(ctx context.Context, g *git.Git) error {
- return g.Remove(ctx, d.DataPath)
+func (d *Data) Remove(ctx context.Context, committer Committer) error {
+ if committer == nil {
+ return fmt.Errorf("git remove data %q: missing committer", d.DataPath)
+ }
+ return committer.Remove(ctx, d.DataPath)
}
diff --git a/internal/store/data_test.go b/internal/store/data_test.go
index 42721af..7bffd71 100644
--- a/internal/store/data_test.go
+++ b/internal/store/data_test.go
@@ -6,6 +6,7 @@ import (
"context"
"os"
"path/filepath"
+ "strings"
"testing"
"codeberg.org/snonux/foostore/internal/crypto"
@@ -240,3 +241,38 @@ func TestDataCommitSkipsExisting(t *testing.T) {
t.Errorf("file was overwritten: got %q; want %q", got, sentinel)
}
}
+
+func TestDataCommitMissingEncryptor(t *testing.T) {
+ ctx := context.Background()
+ dir := t.TempDir()
+ d := &Data{
+ Content: []byte("content"),
+ DataPath: filepath.Join(dir, "entry.data"),
+ }
+
+ err := d.Commit(ctx, nil, nil, true)
+ if err == nil {
+ t.Fatal("Commit with nil encryptor: expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "missing encryptor") {
+ t.Fatalf("Commit error = %q; want missing encryptor", err.Error())
+ }
+}
+
+func TestDataCommitMissingCommitter(t *testing.T) {
+ ctx := context.Background()
+ c := newTestCipher(t)
+ dir := t.TempDir()
+ d := &Data{
+ Content: []byte("content"),
+ DataPath: filepath.Join(dir, "entry.data"),
+ }
+
+ err := d.Commit(ctx, c, nil, true)
+ if err == nil {
+ t.Fatal("Commit with nil committer: expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "missing committer") {
+ t.Fatalf("Commit error = %q; want missing committer", err.Error())
+ }
+}
diff --git a/internal/store/dependencies.go b/internal/store/dependencies.go
new file mode 100644
index 0000000..c8935ce
--- /dev/null
+++ b/internal/store/dependencies.go
@@ -0,0 +1,15 @@
+package store
+
+import "context"
+
+// Encryptor is the minimal crypto dependency needed by store operations.
+type Encryptor interface {
+ Encrypt([]byte) ([]byte, error)
+ Decrypt([]byte) ([]byte, error)
+}
+
+// Committer is the minimal git dependency needed by store operations.
+type Committer interface {
+ Add(context.Context, string) error
+ Remove(context.Context, string) error
+}
diff --git a/internal/store/index.go b/internal/store/index.go
index 923082f..896c5da 100644
--- a/internal/store/index.go
+++ b/internal/store/index.go
@@ -10,9 +10,6 @@ import (
"os"
"path/filepath"
"strings"
-
- "codeberg.org/snonux/foostore/internal/crypto"
- "codeberg.org/snonux/foostore/internal/git"
)
// Index represents a decrypted .index file and its associated .data path.
@@ -28,13 +25,16 @@ type Index struct {
// loadIndex decrypts an .index file and builds an Index struct.
// absoluteIndexPath is the full path to the .index file on disk;
// dataDir is the root of the secret store (used to compute the relative DataFile).
-func loadIndex(ctx context.Context, absoluteIndexPath, dataDir string, c *crypto.Cipher) (*Index, error) {
+func loadIndex(ctx context.Context, absoluteIndexPath, dataDir string, encryptor Encryptor) (*Index, error) {
ciphertext, err := os.ReadFile(absoluteIndexPath)
if err != nil {
return nil, fmt.Errorf("reading index file %q: %w", absoluteIndexPath, err)
}
- plain, err := c.Decrypt(ciphertext)
+ if encryptor == nil {
+ return nil, fmt.Errorf("decrypting index file %q: missing encryptor", absoluteIndexPath)
+ }
+ plain, err := encryptor.Decrypt(ciphertext)
if err != nil {
return nil, fmt.Errorf("decrypting index file %q: %w", absoluteIndexPath, err)
}
@@ -98,7 +98,7 @@ func (idx *Index) String() string {
// the file with git. When force is false and IndexPath already exists the write
// is silently skipped, matching the Ruby CommitFile#commit_content behaviour and
// keeping the .index in sync with a skipped .data Commit.
-func (idx *Index) CommitIndex(ctx context.Context, c *crypto.Cipher, g *git.Git, force bool) error {
+func (idx *Index) CommitIndex(ctx context.Context, encryptor Encryptor, committer Committer, force bool) error {
if !force {
if _, err := os.Stat(idx.IndexPath); err == nil {
// File already exists; skip without error to keep the index/data pair consistent
@@ -108,7 +108,10 @@ func (idx *Index) CommitIndex(ctx context.Context, c *crypto.Cipher, g *git.Git,
}
}
- ciphertext, err := c.Encrypt([]byte(idx.Description))
+ if encryptor == nil {
+ return fmt.Errorf("encrypting index %q: missing encryptor", idx.IndexPath)
+ }
+ ciphertext, err := encryptor.Encrypt([]byte(idx.Description))
if err != nil {
return fmt.Errorf("encrypting index %q: %w", idx.IndexPath, err)
}
@@ -117,7 +120,10 @@ func (idx *Index) CommitIndex(ctx context.Context, c *crypto.Cipher, g *git.Git,
return fmt.Errorf("writing index file %q: %w", idx.IndexPath, err)
}
- if err := g.Add(ctx, idx.IndexPath); err != nil {
+ if committer == nil {
+ return fmt.Errorf("git add index %q: missing committer", idx.IndexPath)
+ }
+ if err := committer.Add(ctx, idx.IndexPath); err != nil {
return fmt.Errorf("git add index %q: %w", idx.IndexPath, err)
}
@@ -125,8 +131,11 @@ func (idx *Index) CommitIndex(ctx context.Context, c *crypto.Cipher, g *git.Git,
}
// Remove stages the .index file for deletion via git rm.
-func (idx *Index) Remove(ctx context.Context, g *git.Git) error {
- return g.Remove(ctx, idx.IndexPath)
+func (idx *Index) Remove(ctx context.Context, committer Committer) error {
+ if committer == nil {
+ return fmt.Errorf("git remove index %q: missing committer", idx.IndexPath)
+ }
+ return committer.Remove(ctx, idx.IndexPath)
}
// ---- sort.Interface for []*Index --------------------------------------------
diff --git a/internal/store/index_test.go b/internal/store/index_test.go
index 57aea8c..5624b3e 100644
--- a/internal/store/index_test.go
+++ b/internal/store/index_test.go
@@ -136,6 +136,23 @@ func TestLoadIndexCorrupted(t *testing.T) {
}
}
+func TestLoadIndexMissingEncryptor(t *testing.T) {
+ ctx := context.Background()
+ dir := t.TempDir()
+ indexPath := filepath.Join(dir, "entry.index")
+ if err := os.WriteFile(indexPath, []byte("ciphertext"), 0o600); err != nil {
+ t.Fatalf("writing index file: %v", err)
+ }
+
+ _, err := loadIndex(ctx, indexPath, dir, nil)
+ if err == nil {
+ t.Fatal("loadIndex with nil encryptor: expected error, got nil")
+ }
+ if !strings.Contains(err.Error(), "missing encryptor") {
+ t.Fatalf("loadIndex error = %q; want missing encryptor", err.Error())
+ }
+}
+
// --- TestIndexSort -----------------------------------------------------------
// TestIndexSort verifies that IndexSlice sorts by Description alphabetically
diff --git a/internal/store/store.go b/internal/store/store.go
index 44b22c3..9acb52d 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -21,8 +21,6 @@ import (
"strings"
"codeberg.org/snonux/foostore/internal/config"
- "codeberg.org/snonux/foostore/internal/crypto"
- "codeberg.org/snonux/foostore/internal/git"
)
// Action describes what to do with each matching secret during a Search call.
@@ -60,13 +58,13 @@ type PickerResult struct {
// regexCache avoids recompiling the same search-term regexp on every WalkIndexes call.
type Store struct {
cfg *config.Config
- cipher *crypto.Cipher
- git *git.Git
+ cipher Encryptor
+ git Committer
regexCache map[string]*regexp.Regexp
}
// New creates a Store, ensuring cfg.DataDir exists on disk.
-func New(cfg *config.Config, cipher *crypto.Cipher, g *git.Git) (*Store, error) {
+func New(cfg *config.Config, cipher Encryptor, g Committer) (*Store, error) {
if err := os.MkdirAll(cfg.DataDir, 0o700); err != nil {
return nil, fmt.Errorf("creating data directory %q: %w", cfg.DataDir, err)
}