Files
gnoma/internal/plugin/pins_test.go
T
vikingowl dc438ea181 feat(plugin): trust-on-first-use manifest pinning
Plugins are now verified against ~/.config/gnoma/plugins.pins.toml at
load time. Each plugin's plugin.json bytes are hashed (SHA-256) and:

- recorded automatically on first load (TOFU) with a prominent warning
- compared on subsequent loads
- refused with a clear error if the hash drifted, without overwriting
  the pin so the user can review and re-enrol deliberately

Pin-store I/O failures degrade to load-without-pinning rather than
locking the user out of previously-trusted plugins.

Closes audit finding C2. See ADR-003 for the decision rationale and
docs/plugins-trust.md for the end-user trust model.
2026-05-19 16:44:09 +02:00

91 lines
2.0 KiB
Go

package plugin
import (
"os"
"path/filepath"
"testing"
)
func TestFilePinStore_MissingFileIsEmpty(t *testing.T) {
path := filepath.Join(t.TempDir(), "pins.toml")
s, err := NewFilePinStore(path)
if err != nil {
t.Fatalf("NewFilePinStore on missing file: %v", err)
}
if _, ok := s.Get("anything"); ok {
t.Error("empty store should not return any pins")
}
}
func TestFilePinStore_RoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "pins.toml")
s, err := NewFilePinStore(path)
if err != nil {
t.Fatal(err)
}
if err := s.Set("git-tools", "abc123"); err != nil {
t.Fatalf("Set: %v", err)
}
// Re-open from disk: pin must persist.
s2, err := NewFilePinStore(path)
if err != nil {
t.Fatal(err)
}
got, ok := s2.Get("git-tools")
if !ok {
t.Fatal("pin missing after reload")
}
if got != "abc123" {
t.Errorf("Get = %q, want abc123", got)
}
}
func TestFilePinStore_OverwriteExisting(t *testing.T) {
path := filepath.Join(t.TempDir(), "pins.toml")
s, _ := NewFilePinStore(path)
if err := s.Set("p", "v1"); err != nil {
t.Fatal(err)
}
if err := s.Set("p", "v2"); err != nil {
t.Fatal(err)
}
got, _ := s.Get("p")
if got != "v2" {
t.Errorf("Get = %q, want v2 after overwrite", got)
}
}
func TestFilePinStore_PreservesOtherPinsOnWrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "pins.toml")
s, _ := NewFilePinStore(path)
if err := s.Set("a", "h1"); err != nil {
t.Fatal(err)
}
if err := s.Set("b", "h2"); err != nil {
t.Fatal(err)
}
s2, _ := NewFilePinStore(path)
if h, _ := s2.Get("a"); h != "h1" {
t.Errorf("Get(a) = %q, want h1", h)
}
if h, _ := s2.Get("b"); h != "h2" {
t.Errorf("Get(b) = %q, want h2", h)
}
}
func TestFilePinStore_CreatesParentDir(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "deeper", "pins.toml")
s, err := NewFilePinStore(path)
if err != nil {
t.Fatal(err)
}
if err := s.Set("x", "h"); err != nil {
t.Fatalf("Set into nested dir: %v", err)
}
if _, err := os.Stat(path); err != nil {
t.Errorf("pin file missing: %v", err)
}
}