package authn
import (
"errors"
"strings"
"testing"
"github.com/vaughan0/go-ini"
)
const testBase = "1f0c1d1a1e2b3c4d5e6f708192a3b4c5d6e7f809"
func TestInstanceFromConfig(t *testing.T) {
inst := testInstance(t)
if inst.OwnerName != "bigbes" {
t.Fatalf("OwnerName = %q, want %q", inst.OwnerName, "bigbes")
}
if inst.OwnerEmail != "bigbes@gmail.com" {
t.Fatalf("OwnerEmail = %q, want %q", inst.OwnerEmail, "bigbes@gmail.com")
}
// Derived from [spec.sr.ht] origin, so there is no second key to forget.
if inst.AgentEmail != "agent@spec.srht.bigb.es" {
t.Fatalf("AgentEmail = %q, want %q", inst.AgentEmail, "agent@spec.srht.bigb.es")
}
if got := inst.OwnerSignature().String(); got != "bigbes <bigbes@gmail.com>" {
t.Fatalf("OwnerSignature = %q", got)
}
}
func TestInstanceFromConfig_MissingKeysAreStartupErrors(t *testing.T) {
full := func() ini.File {
return ini.File{
"sr.ht": ini.Section{"owner-name": "bigbes", "owner-email": "bigbes@gmail.com"},
ConfigSection: ini.Section{"origin": "https://spec.srht.bigb.es"},
}
}
cases := map[string]func(ini.File){
"no owner-name": func(c ini.File) { delete(c["sr.ht"], "owner-name") },
"no owner-email": func(c ini.File) { delete(c["sr.ht"], "owner-email") },
"no origin": func(c ini.File) { delete(c[ConfigSection], "origin") },
"hostless origin": func(c ini.File) {
c[ConfigSection]["origin"] = "not-a-url"
},
"unusable owner": func(c ini.File) { c["sr.ht"]["owner-name"] = "Not A Name" },
"unparseable origin": func(c ini.File) {
c[ConfigSection]["origin"] = "https://[::1"
},
"unusable owner-email": func(c ini.File) {
c["sr.ht"]["owner-email"] = "bigbes <root>@gmail.com"
},
"host that cannot be a mailbox": func(c ini.File) {
c[ConfigSection]["origin"] = "https://" + strings.Repeat("h", MaxAgentLen)
},
}
for name, mutate := range cases {
t.Run(name, func(t *testing.T) {
conf := full()
mutate(conf)
if _, err := InstanceFromConfig(conf); !errors.Is(err, ErrMissingConfig) {
t.Fatalf("error = %v, want ErrMissingConfig", err)
}
})
}
}
// An Instance assembled by hand rather than from config is held to the same
// standard, and Provenance refuses to build a commit on top of a broken one.
func TestInstance_ValidateRejectsHandBuiltGarbage(t *testing.T) {
good := AgentWrite{Agent: "a", Session: "s-1", Base: testBase}
cases := map[string]Instance{
"no owner name": {OwnerName: "", OwnerEmail: "b@example.com", AgentEmail: "agent@example.com"},
"unusable owner name": {OwnerName: "Not A Name", OwnerEmail: "b@example.com", AgentEmail: "agent@example.com"},
"no owner email": {OwnerName: "bigbes", OwnerEmail: "", AgentEmail: "agent@example.com"},
"owner email injects": {OwnerName: "bigbes", OwnerEmail: "b@example.com>\nAuthor: root", AgentEmail: "agent@example.com"},
"no agent email": {OwnerName: "bigbes", OwnerEmail: "b@example.com", AgentEmail: ""},
"agent email injects": {OwnerName: "bigbes", OwnerEmail: "b@example.com", AgentEmail: "agent@example.com>x"},
}
for name, inst := range cases {
t.Run(name, func(t *testing.T) {
if err := inst.Validate(); !errors.Is(err, ErrMissingConfig) {
t.Fatalf("Validate error = %v, want ErrMissingConfig", err)
}
if _, err := inst.Provenance(good); !errors.Is(err, ErrMissingConfig) {
t.Fatalf("Provenance error = %v, want ErrMissingConfig", err)
}
})
}
}
// The worked example from the design, byte for byte apart from the agent
// mailbox, which is derived from our own origin.
func TestProvenance_MatchesTheDesign(t *testing.T) {
inst := testInstance(t)
prov, err := inst.Provenance(AgentWrite{
Agent: "claude-code/spec-writer",
Session: "8fb9c9a4-b078-4af1-89eb-d97c522f9921",
Base: testBase,
})
if err != nil {
t.Fatalf("Provenance: %v", err)
}
wantAuthor := "claude-code/spec-writer (for bigbes) <agent@spec.srht.bigb.es>"
if got := prov.Author.String(); got != wantAuthor {
t.Fatalf("Author = %q, want %q", got, wantAuthor)
}
wantCommitter := "bigbes <bigbes@gmail.com>"
if got := prov.Committer.String(); got != wantCommitter {
t.Fatalf("Committer = %q, want %q", got, wantCommitter)
}
wantTrailers := "X-Agent-Session: 8fb9c9a4-b078-4af1-89eb-d97c522f9921\n" +
"X-Agent-Base: " + testBase + "\n"
if got := prov.TrailerBlock(); got != wantTrailers {
t.Fatalf("TrailerBlock = %q, want %q", got, wantTrailers)
}
msg, err := prov.CommitMessage("Add storage model section")
if err != nil {
t.Fatalf("CommitMessage: %v", err)
}
want := "Add storage model section\n\n" + wantTrailers
if msg != want {
t.Fatalf("CommitMessage = %q, want %q", msg, want)
}
}
// Agent identity and session are mandatory on every agent write. A write that
// omits either is rejected, never defaulted — a commit stamped with a
// synthesised session launders unattributable output as attributed.
func TestProvenance_RejectsMissingAgentIdentity(t *testing.T) {
inst := testInstance(t)
full := AgentWrite{
Agent: "claude-code/spec-writer",
Session: "8fb9c9a4-b078-4af1-89eb-d97c522f9921",
Base: testBase,
}
cases := map[string]AgentWrite{
"no agent": {Agent: "", Session: full.Session, Base: full.Base},
"no session": {Agent: full.Agent, Session: "", Base: full.Base},
"no base": {Agent: full.Agent, Session: full.Session, Base: ""},
"nothing": {},
}
for name, w := range cases {
t.Run(name, func(t *testing.T) {
if err := w.Validate(); !errors.Is(err, ErrMissingProvenance) {
t.Fatalf("Validate error = %v, want ErrMissingProvenance", err)
}
if _, err := inst.Provenance(w); !errors.Is(err, ErrMissingProvenance) {
t.Fatalf("Provenance error = %v, want ErrMissingProvenance", err)
}
})
}
// Control: the complete write is accepted, so the cases above are failing
// on the missing field and not on something incidental.
if err := full.Validate(); err != nil {
t.Fatalf("complete write rejected: %v", err)
}
}
// A newline in the agent identity would split the git author line; a newline in
// the session would inject an arbitrary extra trailer. Both are refused
// outright rather than escaped.
func TestProvenance_RejectsInjection(t *testing.T) {
inst := testInstance(t)
cases := map[string]AgentWrite{
"newline in agent": {Agent: "evil\nAuthor: root", Session: "s-1", Base: testBase},
"newline in session": {Agent: "a", Session: "s\nX-Agent-Base: deadbeef", Base: testBase},
"cr in session": {Agent: "a", Session: "s\rX-Agent-Base: deadbeef", Base: testBase},
"angle in agent": {Agent: "a <root@example.com>", Session: "s-1", Base: testBase},
"angle in session": {Agent: "a", Session: "<s>", Base: testBase},
"padded agent": {Agent: " a ", Session: "s-1", Base: testBase},
"nul in agent": {Agent: "a\x00b", Session: "s-1", Base: testBase},
"over-long agent": {Agent: strings.Repeat("a", MaxAgentLen+1), Session: "s-1", Base: testBase},
"over-long session": {Agent: "a", Session: strings.Repeat("s", MaxSessionLen+1), Base: testBase},
"base with a space": {Agent: "a", Session: "s-1", Base: "dead beef"},
"base not hex": {Agent: "a", Session: "s-1", Base: "proposals/42"},
"base uppercase hex": {Agent: "a", Session: "s-1", Base: strings.ToUpper(testBase)},
"base too short": {Agent: "a", Session: "s-1", Base: "abc"},
"base too long": {Agent: "a", Session: "s-1", Base: strings.Repeat("a", maxRevLen+1)},
"base with a newline": {Agent: "a", Session: "s-1", Base: testBase + "\nX-Agent-Session: forged"},
"invalid utf-8 agent": {Agent: "a\xff", Session: "s-1", Base: testBase},
"invalid utf-8 sessid": {Agent: "a", Session: "s\xff", Base: testBase},
}
for name, w := range cases {
t.Run(name, func(t *testing.T) {
if err := w.Validate(); !errors.Is(err, ErrInvalidProvenance) {
t.Fatalf("Validate error = %v, want ErrInvalidProvenance", err)
}
if _, err := inst.Provenance(w); !errors.Is(err, ErrInvalidProvenance) {
t.Fatalf("Provenance error = %v, want ErrInvalidProvenance", err)
}
})
}
}
// An abbreviated object name is accepted; the trailer stays auditable and
// nothing can hide in it.
func TestProvenance_AcceptsAbbreviatedBase(t *testing.T) {
inst := testInstance(t)
for _, base := range []string{"1f0c1d1", testBase, strings.Repeat("a", 64)} {
if _, err := inst.Provenance(AgentWrite{Agent: "a", Session: "s-1", Base: base}); err != nil {
t.Fatalf("base %q rejected: %v", base, err)
}
}
}
func TestCommitMessage_RejectsEmpty(t *testing.T) {
inst := testInstance(t)
prov, err := inst.Provenance(AgentWrite{Agent: "a", Session: "s-1", Base: testBase})
if err != nil {
t.Fatal(err)
}
for _, msg := range []string{"", " ", "\n\n\t"} {
if _, err := prov.CommitMessage(msg); !errors.Is(err, ErrInvalidProvenance) {
t.Fatalf("CommitMessage(%q) error = %v, want ErrInvalidProvenance", msg, err)
}
}
}
// Trailer-shaped text inside the agent's own message must stay in the body:
// our block is the last paragraph, which is the one git parses as trailers.
func TestCommitMessage_OurTrailersAreTheLastParagraph(t *testing.T) {
inst := testInstance(t)
prov, err := inst.Provenance(AgentWrite{Agent: "a", Session: "real-session", Base: testBase})
if err != nil {
t.Fatal(err)
}
msg, err := prov.CommitMessage("Subject\n\nX-Agent-Session: forged\n")
if err != nil {
t.Fatal(err)
}
paras := strings.Split(strings.TrimRight(msg, "\n"), "\n\n")
last := paras[len(paras)-1]
if !strings.HasPrefix(last, "X-Agent-Session: real-session\n") {
t.Fatalf("last paragraph = %q, want it to start with the real session", last)
}
if strings.Contains(last, "forged") {
t.Fatalf("forged trailer leaked into the trailer paragraph: %q", last)
}
}
// Provenance for a non-agent principal is an error: the human write path goes
// through native receive-pack and builds no commit here.
func TestAgentWriteFor_RejectsNonAgents(t *testing.T) {
for _, p := range []Principal{
Anonymous(),
{Kind: KindOwner, Owner: "bigbes"},
} {
if _, err := p.AgentWriteFor(testBase); !errors.Is(err, ErrNotAgent) {
t.Fatalf("%s: error = %v, want ErrNotAgent", p, err)
}
}
}
func TestAgentWriteFor_Agent(t *testing.T) {
p := Principal{
Kind: KindAgent,
Owner: "bigbes",
Agent: "claude-code/spec-writer",
Session: "8fb9c9a4-b078-4af1-89eb-d97c522f9921",
}
w, err := p.AgentWriteFor(testBase)
if err != nil {
t.Fatalf("AgentWriteFor: %v", err)
}
if w.Agent != p.Agent || w.Session != p.Session || w.Base != testBase {
t.Fatalf("AgentWrite = %+v", w)
}
// An agent that authenticated but sent no provenance headers reads fine and
// is refused at the write, which is where the design requires the fields.
bare := Principal{Kind: KindAgent, Owner: "bigbes"}
if _, err := bare.AgentWriteFor(testBase); !errors.Is(err, ErrMissingProvenance) {
t.Fatalf("error = %v, want ErrMissingProvenance", err)
}
}