feat(secrets): file-backed SecretStore for Stage-0

Implements ports.SecretStore over a 0600 JSON file as a stand-in for op/ESO so
the demo runs without live op. Put persists atomically (temp + rename) and
merges; Get returns ErrNotFound for unknown refs so a missing token fails loud.
Behind the port, so swapping to op/ESO later is wiring, not code (ADR-002).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 20:57:44 +02:00
co-authored by Claude Opus 4.8
parent c424d88c95
commit c93b433aaf
2 changed files with 179 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
package secrets_test
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets"
)
func TestPutThenGet(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "secrets.json")
s := secrets.NewFileStore(path)
if err := s.Put("youtube/refresh_token", "rt-123"); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := s.Get(context.Background(), "youtube/refresh_token")
if err != nil {
t.Fatalf("Get: %v", err)
}
if got != "rt-123" {
t.Errorf("Get = %q, want %q", got, "rt-123")
}
}
func TestGetUnknownRef(t *testing.T) {
s := secrets.NewFileStore(filepath.Join(t.TempDir(), "secrets.json"))
_, err := s.Get(context.Background(), "missing")
if !errors.Is(err, secrets.ErrNotFound) {
t.Errorf("err = %v, want ErrNotFound", err)
}
}
func TestPutIsOwnerOnly(t *testing.T) {
path := filepath.Join(t.TempDir(), "secrets.json")
s := secrets.NewFileStore(path)
if err := s.Put("k", "v"); err != nil {
t.Fatalf("Put: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatalf("Stat: %v", err)
}
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("file perm = %o, want 0600 (token must not be world-readable)", perm)
}
}
func TestPutMergesEntries(t *testing.T) {
path := filepath.Join(t.TempDir(), "secrets.json")
s := secrets.NewFileStore(path)
if err := s.Put("a", "1"); err != nil {
t.Fatalf("Put a: %v", err)
}
if err := s.Put("b", "2"); err != nil {
t.Fatalf("Put b: %v", err)
}
// Re-open from disk to prove persistence, not in-memory state.
s2 := secrets.NewFileStore(path)
for k, want := range map[string]string{"a": "1", "b": "2"} {
got, err := s2.Get(context.Background(), k)
if err != nil {
t.Fatalf("Get %q: %v", k, err)
}
if got != want {
t.Errorf("Get %q = %q, want %q", k, got, want)
}
}
}