gitmote / internal / meta / refs_test.go · history · branches
3671 bytes · raw
package meta
import (
"context"
"errors"
"testing"
)
const (
shaA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
shaB = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
shaC = "cccccccccccccccccccccccccccccccccccccccc"
)
// refSHA returns the current sha of a ref, or "" if absent.
func refSHA(t *testing.T, m *Metadata, repoID int64, name string) string {
t.Helper()
refs, err := m.ListRefs(context.Background(), repoID)
if err != nil {
t.Fatalf("ListRefs: %v", err)
}
for _, r := range refs {
if r.Name == name {
return r.SHA
}
}
return ""
}
func TestCASRefCreateUpdateDelete(t *testing.T) {
ctx := context.Background()
m := open(t)
r := seedRepo(t, m, "repo")
const main = "refs/heads/main"
// Create: old is the zero id (ref must be absent).
if err := m.CASRef(ctx, r.ID, main, ZeroSHA, shaA); err != nil {
t.Fatalf("CASRef create: %v", err)
}
if got := refSHA(t, m, r.ID, main); got != shaA {
t.Fatalf("after create sha = %q, want %q", got, shaA)
}
// Update: old must match the current value.
if err := m.CASRef(ctx, r.ID, main, shaA, shaB); err != nil {
t.Fatalf("CASRef update: %v", err)
}
if got := refSHA(t, m, r.ID, main); got != shaB {
t.Fatalf("after update sha = %q, want %q", got, shaB)
}
// Delete: new is the zero id.
if err := m.CASRef(ctx, r.ID, main, shaB, ZeroSHA); err != nil {
t.Fatalf("CASRef delete: %v", err)
}
if got := refSHA(t, m, r.ID, main); got != "" {
t.Fatalf("after delete sha = %q, want absent", got)
}
}
func TestCASRefRejectsAndRollsBack(t *testing.T) {
ctx := context.Background()
m := open(t)
r := seedRepo(t, m, "repo")
const main = "refs/heads/main"
if err := m.CASRef(ctx, r.ID, main, ZeroSHA, shaA); err != nil {
t.Fatalf("seed create: %v", err)
}
// Stale expected value: a non-fast-forward the server must refuse.
var mm *CASMismatchError
err := m.CASRef(ctx, r.ID, main, shaB, shaC)
if !errors.As(err, &mm) {
t.Fatalf("CASRef mismatch = %v, want *CASMismatchError", err)
}
if mm.Got != shaA || mm.Want != shaB {
t.Errorf("mismatch = %+v, want Want=%s Got=%s", mm, shaB, shaA)
}
// The ref is unchanged — the rejected update rolled back.
if got := refSHA(t, m, r.ID, main); got != shaA {
t.Errorf("after rejected update sha = %q, want %q (unchanged)", got, shaA)
}
// Create against an already-present ref is also a mismatch.
if err := m.CASRef(ctx, r.ID, main, ZeroSHA, shaB); !errors.As(err, &mm) {
t.Errorf("CASRef create-over-existing = %v, want *CASMismatchError", err)
}
}
func TestCASRefsAtomicMultiRef(t *testing.T) {
ctx := context.Background()
m := open(t)
r := seedRepo(t, m, "repo")
const (
main = "refs/heads/main"
dev = "refs/heads/dev"
)
// One bad update in the batch rolls back the whole thing — the good
// create for main must not survive.
err := m.CASRefs(ctx, r.ID, []RefUpdate{
{Name: main, Old: ZeroSHA, New: shaA},
{Name: dev, Old: shaB, New: shaC}, // wrong: dev is absent, not shaB
})
var mm *CASMismatchError
if !errors.As(err, &mm) || mm.Name != dev {
t.Fatalf("CASRefs = %v, want *CASMismatchError for %q", err, dev)
}
if got := refSHA(t, m, r.ID, main); got != "" {
t.Errorf("main sha = %q after failed batch, want absent (rolled back)", got)
}
// An all-valid batch commits both.
if err := m.CASRefs(ctx, r.ID, []RefUpdate{
{Name: main, Old: ZeroSHA, New: shaA},
{Name: dev, Old: ZeroSHA, New: shaB},
}); err != nil {
t.Fatalf("CASRefs valid batch: %v", err)
}
if got := refSHA(t, m, r.ID, main); got != shaA {
t.Errorf("main sha = %q, want %q", got, shaA)
}
if got := refSHA(t, m, r.ID, dev); got != shaB {
t.Errorf("dev sha = %q, want %q", got, shaB)
}
}