package api
import (
"os"
"reflect"
"regexp"
"strings"
"testing"
)
// repo.update is a PATCH: a field the body did not mention is left
// alone.
//
// Every field was a plain string or bool, so "absent" and "the zero
// value" were indistinguishable and every call wrote every field. The
// expensive one was mirrorable: false makes the sync endpoint answer
// 404, so editing a description silently stopped the repository
// syncing. jva hit exactly that at 22:37:21Z, one minute after the caps
// gate stopped refusing repository admins — the partial-update footgun
// had been reachable only by site admins until then, and the gate fix
// unmasked it (dc748de1).
//
// Asserted on the type rather than through a live registry because the
// property is about what the DECODER can distinguish: a plain bool
// cannot say "not sent", and no amount of Run-side care recovers that.
func TestRepoUpdateFieldsAreATriState(t *testing.T) {
for _, field := range []string{"Description", "Private", "Homepage", "Topics", "Chat", "Mirrorable"} {
f, ok := fieldOf(repoUpdateIn{}, field)
if !ok {
t.Errorf("repoUpdateIn has no %s — if it was renamed, move this guard with it", field)
continue
}
if !strings.HasPrefix(f, "*") {
t.Errorf("repoUpdateIn.%s is %s, not a pointer: absent and the zero value are the "+
"same value, so a partial update overwrites this field with nothing", field, f)
}
}
// Name is the one field a body must always carry.
if f, _ := fieldOf(repoUpdateIn{}, "Name"); f != "string" {
t.Errorf("repoUpdateIn.Name is %s, want a plain string: it identifies the repository "+
"and is never optional", f)
}
}
// The lengths must still be enforced. ops.Validate's applyRules falls
// through to `default: return nil` for a pointer, so the `max=` tags
// these fields used to carry became decoration the moment they became
// pointers. Moving a field to a tri-state and keeping its tag would
// silently drop its validation, which is exactly the class of mistake
// this ticket is about.
func TestRepoUpdateStillBoundsItsStrings(t *testing.T) {
body := opBody(t, "repo.update", "repo.delete")
for _, want := range []string{
"description is at most 300",
"homepage is at most 300",
"topics are at most 600",
} {
if !strings.Contains(body, want) {
t.Errorf("repo.update no longer checks %q by hand, and a validate tag on a pointer "+
"field does nothing: the limit is unenforced", want)
}
}
}
// The merge reads what the repository holds now. Without this the
// pointers change nothing: an unmentioned field would still be written
// back as the zero value, just more deliberately.
func TestRepoUpdateWritesBackWhatItWasNotAsked(t *testing.T) {
src := readSource(t, "register.go")
if !strings.Contains(src, "func (d Deps) repoSettings(repo string) repoSettingsNow") {
t.Fatal("repoSettings is gone: repo.update has nothing to merge an absent field against")
}
body := opBody(t, "repo.update", "repo.delete")
if !strings.Contains(body, "cur := d.repoSettings(in.Name)") {
t.Error("repo.update does not read the repository's current settings, so an absent " +
"field is still written as the zero value")
}
// The one that broke sync: mirrorable must come from the merge.
if !strings.Contains(body, "cur.mirrorable") {
t.Error("repo.update no longer writes mirrorable from the merged settings — " +
"a description-only update can turn sync off again")
}
if strings.Contains(body, "in.Mirrorable)") {
t.Error("repo.update passes in.Mirrorable straight through: nil would write false")
}
}
// fieldOf reports a struct field's type as written, for a guard that is
// about the TYPE rather than about any particular value.
func fieldOf(v any, name string) (string, bool) {
t := reflect.TypeOf(v)
f, ok := t.FieldByName(name)
if !ok {
return "", false
}
return f.Type.String(), true
}
func readSource(t *testing.T, name string) string {
t.Helper()
b, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
return string(b)
}
// opBody returns the source of one op's registration, from its Name up
// to the next op's. Matched with a regexp rather than an exact string
// because gofmt realigns `Name:` when a neighbouring field changes
// width — which broke these guards the first time the Doc string grew,
// and a guard that fails on formatting is a guard nobody trusts.
func opBody(t *testing.T, name, next string) string {
t.Helper()
src := readSource(t, "register.go")
at := func(n string) int {
loc := regexp.MustCompile(`Name:\s*"` + regexp.QuoteMeta(n) + `"`).FindStringIndex(src)
if loc == nil {
return -1
}
return loc[0]
}
i := at(name)
if i < 0 {
t.Fatalf("%s is gone from register.go", name)
}
body := src[i:]
if j := at(next); j > i {
body = src[i:j]
}
return body
}
// A private detour must not cost a repository its mirrorable flag.
//
// Deps.Mirrorable answers the EFFECTIVE question — MayMirror, false for
// every private repository whatever the stored flag says — and the
// partial-update merge read it. So taking a repository private and then
// restoring it to public wrote "false because private" back as "false
// because asked": the repository returned reading Public everywhere,
// with /xfer answering 404 and no one able to clone it. Found by jva on
// dantest-alias within an hour of the merge landing (G-115).
//
// The merge must read the STORED flag, which is what RepoMeta now
// carries.
func TestAPrivateDetourKeepsTheMirrorableFlag(t *testing.T) {
src := readSource(t, "register.go")
i := strings.Index(src, "func (d Deps) repoSettings(repo string) repoSettingsNow")
if i < 0 {
t.Fatal("repoSettings is gone")
}
body := src[i:]
if end := strings.Index(body, "\ntype repoUpdateOut"); end > 0 {
body = body[:end]
}
if strings.Contains(body, "d.Mirrorable(repo)") {
t.Error("repoSettings reads the EFFECTIVE mirrorable and writes it back: " +
"a repository taken private and restored to public comes out unclonable")
}
if !strings.Contains(body, "meta.Mirrorable") {
t.Error("repoSettings does not read the stored mirrorable flag from RepoMeta")
}
}