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) } } }