Account disconnect/delete needs to remove the per-user YouTube refresh token from the SecretStore. Add Delete(ref) on the file-backed store, mirroring Put: atomic temp-file+rename, 0600, no-op on an absent ref. Kept off the read-only ports.SecretStore (Get) — write/delete follow the existing auth.TokenWriter convention of narrow capability interfaces, so the youtube adapter's read-only dependency is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
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 TestDeleteRemovesRefAndLeavesOthers(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "secrets.json")
|
|
s := secrets.NewFileStore(path)
|
|
if err := s.Put("youtube/u1/refresh_token", "rt-1"); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
if err := s.Put("youtube/u2/refresh_token", "rt-2"); err != nil {
|
|
t.Fatalf("Put: %v", err)
|
|
}
|
|
|
|
if err := s.Delete("youtube/u1/refresh_token"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
|
|
// The deleted ref is gone (persisted: re-open from disk)...
|
|
s2 := secrets.NewFileStore(path)
|
|
if _, err := s2.Get(context.Background(), "youtube/u1/refresh_token"); !errors.Is(err, secrets.ErrNotFound) {
|
|
t.Errorf("Get deleted ref: err = %v, want ErrNotFound", err)
|
|
}
|
|
// ...and the other user's secret survives.
|
|
if got, err := s2.Get(context.Background(), "youtube/u2/refresh_token"); err != nil || got != "rt-2" {
|
|
t.Errorf("Get surviving ref = (%q, %v), want (%q, nil)", got, err, "rt-2")
|
|
}
|
|
}
|
|
|
|
func TestDeleteAbsentRefIsNoop(t *testing.T) {
|
|
// Deleting an unknown ref — or from a file that does not exist yet — is a
|
|
// no-op, not an error (mirrors store.DeleteConnection semantics).
|
|
s := secrets.NewFileStore(filepath.Join(t.TempDir(), "secrets.json"))
|
|
if err := s.Delete("missing"); err != nil {
|
|
t.Errorf("Delete absent ref: %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|