package gitx
import (
"errors"
"strings"
"testing"
"github.com/go-git/go-git/v5/plumbing"
)
var (
hashA = plumbing.NewHash("1111111111111111111111111111111111111111")
hashB = plumbing.NewHash("2222222222222222222222222222222222222222")
zero = plumbing.ZeroHash
)
// TestCheckRefUpdate is the table for the one rule the whole write model rests
// on: the human pushes to the approved branch, agents may only write
// proposals/*.
func TestCheckRefUpdate(t *testing.T) {
cases := []struct {
name string
principal PrincipalKind
update RefUpdate
allow bool
// want, when set, must appear in the rejection message: a hook error
// that does not say why is a support ticket.
want string
}{
// --- the human and the approved branch -------------------------------
{
name: "human fast-forwards the approved branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true},
allow: true,
},
{
name: "human force-updates the approved branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: false},
allow: false,
want: "fast-forwards only",
},
{
name: "human deletes the approved branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/main", Old: hashA, New: zero},
allow: false,
want: "may not be deleted",
},
{
name: "human creates the approved branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/main", Old: zero, New: hashB, FastForward: true},
allow: true,
},
// --- the agent and the approved branch -------------------------------
{
name: "agent fast-forwards the approved branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true},
allow: false,
want: "not the approved branch",
},
{
name: "agent deletes the approved branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/main", Old: hashA, New: zero},
allow: false,
want: "not the approved branch",
},
// --- proposal branches ------------------------------------------------
{
name: "agent creates a proposal branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals/42", Old: zero, New: hashB, FastForward: true},
allow: true,
},
{
name: "agent force-updates its own proposal branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: hashB, FastForward: false},
allow: true,
},
{
name: "agent deletes a proposal branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: zero},
allow: true,
},
{
name: "human updates a proposal branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/proposals/42", Old: hashA, New: hashB, FastForward: true},
allow: true,
},
{
name: "a nested proposal branch is still a proposal branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals/agent/7", Old: zero, New: hashB, FastForward: true},
allow: true,
},
{
name: "the bare proposals namespace is not a proposal branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "and nothing else",
},
{
name: "a branch that merely starts with the word proposals",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals-evil", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "and nothing else",
},
// --- everything else --------------------------------------------------
{
name: "agent writes some other branch",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/scratch", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "and nothing else",
},
{
name: "human writes some other branch",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/scratch", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "and nothing else",
},
{
name: "human pushes a tag",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/tags/v1.0.0", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "only branches",
},
{
name: "notes ref",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/notes/commits", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "only branches",
},
{
name: "a bare name is not a ref",
principal: PrincipalHuman,
update: RefUpdate{Ref: "main", Old: hashA, New: hashB, FastForward: true},
allow: false,
want: "must start with",
},
{
name: "a traversing ref",
principal: PrincipalAgent,
update: RefUpdate{Ref: "refs/heads/proposals/../../main", Old: zero, New: hashB, FastForward: true},
allow: false,
want: "'..'",
},
{
name: "an update that moves nothing",
principal: PrincipalHuman,
update: RefUpdate{Ref: "refs/heads/main", Old: zero, New: zero},
allow: false,
want: "moves nothing",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := CheckRefUpdate(tc.principal, "main", tc.update)
if tc.allow {
if err != nil {
t.Fatalf("CheckRefUpdate = %v, want allowed", err)
}
return
}
if err == nil {
t.Fatal("CheckRefUpdate allowed an update it should have rejected")
}
if !errors.Is(err, ErrRefRejected) && !errors.Is(err, ErrBadRev) {
t.Fatalf("rejection %v is neither ErrRefRejected nor ErrBadRev", err)
}
if tc.want != "" && !strings.Contains(err.Error(), tc.want) {
t.Fatalf("rejection %q does not mention %q", err, tc.want)
}
})
}
}
func TestCheckRefUpdateHonoursANonDefaultApprovedBranch(t *testing.T) {
u := RefUpdate{Ref: "refs/heads/approved", Old: hashA, New: hashB, FastForward: true}
if err := CheckRefUpdate(PrincipalHuman, "approved", u); err != nil {
t.Fatalf("human on the configured approved branch = %v, want allowed", err)
}
if err := CheckRefUpdate(PrincipalAgent, "approved", u); !errors.Is(err, ErrRefRejected) {
t.Fatalf("agent on the configured approved branch = %v, want rejected", err)
}
// "main" is nothing special once the space says otherwise.
main := RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true}
if err := CheckRefUpdate(PrincipalHuman, "approved", main); !errors.Is(err, ErrRefRejected) {
t.Fatalf("human on a non-approved branch = %v, want rejected", err)
}
}
func TestCheckRefUpdateRejectsUnknownPrincipals(t *testing.T) {
u := RefUpdate{Ref: "refs/heads/proposals/1", Old: zero, New: hashB, FastForward: true}
for _, p := range []PrincipalKind{"", "root", "Human", "HUMAN"} {
if err := CheckRefUpdate(p, "main", u); !errors.Is(err, ErrRefRejected) {
t.Fatalf("CheckRefUpdate with principal %q = %v, want rejected", p, err)
}
}
if _, err := ParsePrincipalKind("agent"); err != nil {
t.Fatalf("ParsePrincipalKind(agent): %v", err)
}
}
func TestCheckRefUpdateRejectsAnUnusableApprovedBranch(t *testing.T) {
u := RefUpdate{Ref: "refs/heads/main", Old: hashA, New: hashB, FastForward: true}
for _, branch := range []string{"", "refs/heads/main", "ma in", "-main", "a..b"} {
if err := CheckRefUpdate(PrincipalHuman, branch, u); !errors.Is(err, ErrRefRejected) {
t.Fatalf("approved branch %q = %v, want rejected", branch, err)
}
}
}
func TestValidateBranch(t *testing.T) {
good := []string{"main", "approved", "proposals/42", "proposals/agent/7", "release-1.0"}
for _, b := range good {
if err := ValidateBranch(b); err != nil {
t.Fatalf("ValidateBranch(%q) = %v, want nil", b, err)
}
}
bad := []string{
"", "refs/heads/main", "-main", "main/", "/main", "a//b", "a..b",
"a b", "a~b", "a^b", "a:b", "a?b", "a*b", "a[b", `a\b`, "a@{b", "@",
"main.lock", ".hidden", "trailing.", "a\tb", "a\x00b",
strings.Repeat("a", maxRefLen+1),
}
for _, b := range bad {
if err := ValidateBranch(b); err == nil {
t.Fatalf("ValidateBranch(%q) = nil, want an error", b)
}
}
}
func TestValidateRev(t *testing.T) {
good := []string{
"main", "proposals/42", "HEAD",
"1111111111111111111111111111111111111111", "1111111",
}
for _, rev := range good {
if err := ValidateRev(rev); err != nil {
t.Fatalf("ValidateRev(%q) = %v, want nil", rev, err)
}
}
bad := []string{"", "main^", "main~1", "main@{0}", "a b", "-main", "a..b", ".hidden"}
for _, rev := range bad {
if err := ValidateRev(rev); !errors.Is(err, ErrBadRev) {
t.Fatalf("ValidateRev(%q) = %v, want ErrBadRev", rev, err)
}
}
}