package ccsettings
import (
"os"
"path/filepath"
"testing"
)
func TestFingerprint(t *testing.T) {
base := Grant{
Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}, "b": {Command: "y"}},
Allow: []string{"Bash(make:*)", "Edit"},
}
same := Grant{
Servers: map[string]ServerDef{"b": {Command: "y"}, "a": {Command: "x", Env: map[string]string{"J": "2", "K": "1"}}},
Allow: []string{"Edit", "Bash(make:*)"},
}
if base.Fingerprint() != same.Fingerprint() {
t.Error("the same definitions in another order should be the same fingerprint")
}
changed := []Grant{
{Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}, "b": {Command: "y", Args: []string{"--evil"}}}, Allow: base.Allow},
{Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}}, Allow: base.Allow},
{Servers: base.Servers, Allow: []string{"Bash(make:*)", "Edit", "Bash"}},
{Servers: base.Servers, Allow: []string{"Bash(make:*)", "Edit"}, AllowFrom: []string{"elsewhere"}},
}
for i, g := range changed[:3] {
if g.Fingerprint() == base.Fingerprint() {
t.Errorf("change %d did not change the fingerprint", i)
}
}
// Which file a rule came from is not part of what is trusted.
if changed[3].Fingerprint() != base.Fingerprint() {
t.Error("AllowFrom should not affect the fingerprint")
}
if !(Grant{}).Empty() || base.Empty() {
t.Error("Empty is wrong")
}
}
func TestNewGrantTakesOnlyProjectScope(t *testing.T) {
mcp := []MCPFile{
{Scope: ScopeLocal, Servers: map[string]ServerDef{"l": {Command: "l"}}},
{Scope: ScopeProject, Servers: map[string]ServerDef{"p": {Command: "p"}}},
{Scope: ScopeUser, Servers: map[string]ServerDef{"u": {Command: "u"}}},
}
perms := []Permissions{
{Scope: ScopeUser, Allow: []string{"Bash"}},
{Scope: ScopeProject, Shown: ".claude/settings.json", Allow: []string{"Edit"}, Deny: []string{"Read(.env)"}},
{Scope: ScopeLocal, Allow: []string{"Write"}},
}
g := NewGrant(mcp, perms)
if len(g.Servers) != 1 || g.Servers["p"].Command != "p" {
t.Errorf("servers %v", g.Servers)
}
if len(g.Allow) != 1 || g.Allow[0] != "Edit" || g.AllowFrom[0] != ".claude/settings.json" {
t.Errorf("allow %v from %v", g.Allow, g.AllowFrom)
}
if len(g.Describe()) != 4 {
t.Errorf("describe: %q", g.Describe())
}
}
func TestTrustStore(t *testing.T) {
s := TrustStore{Path: filepath.Join(t.TempDir(), "codey", TrustFilename)}
root := "/src/proj"
if decided, _, err := s.Lookup(root, "fp1"); decided || err != nil {
t.Fatalf("a fresh store decided=%v err=%v", decided, err)
}
if err := s.Record(root, "fp1", true); err != nil {
t.Fatal(err)
}
if decided, trusted, _ := s.Lookup(root, "fp1"); !decided || !trusted {
t.Error("the recorded decision was not found")
}
// Changed definitions are a new question.
if decided, _, _ := s.Lookup(root, "fp2"); decided {
t.Error("a decision about other definitions counted")
}
// Another checkout is another question.
if decided, _, _ := s.Lookup("/src/other", "fp1"); decided {
t.Error("a decision about another root counted")
}
// A refusal is remembered as a refusal.
if err := s.Record(root, "fp2", false); err != nil {
t.Fatal(err)
}
if decided, trusted, _ := s.Lookup(root, "fp2"); !decided || trusted {
t.Error("the refusal was not remembered")
}
if decided, _, _ := s.Lookup(root, "fp1"); decided {
t.Error("the old decision should have been replaced")
}
// Other roots' decisions survive a write.
s.Record("/src/other", "x", true)
if decided, _, _ := s.Lookup(root, "fp2"); !decided {
t.Error("recording one root lost another")
}
}
// A store that cannot be read is an error, not "nobody decided": the caller
// treats it as untrusted, and must not overwrite it.
func TestTrustStoreCorrupt(t *testing.T) {
path := filepath.Join(t.TempDir(), TrustFilename)
os.WriteFile(path, []byte("{"), 0o644)
s := TrustStore{Path: path}
if _, _, err := s.Lookup("/r", "fp"); err == nil {
t.Error("want an error")
}
if err := s.Record("/r", "fp", true); err == nil {
t.Error("recording over a corrupt store should fail rather than replace it")
}
}