264 lines
11 KiB
Go
264 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The tests of ADR 0534 §1, §2 (the operator, 2026-10-23): the forge requires no status to merge. Main's rule keeps no
|
|
// push, no push whitelist, the tools' account alone in its merge whitelist and no administrator merging past it, and
|
|
// requires no status: the program writes it so on every repository it lists, and takes every required status off a
|
|
// rule covering main in every repository of the organisation it does not list. Nothing else of an unlisted
|
|
// repository's rule is sent.
|
|
|
|
func narrowedRule(contexts ...any) map[string]any {
|
|
return map[string]any{
|
|
"enable_push": false, "enable_push_whitelist": false, "push_whitelist_usernames": []any{},
|
|
"enable_merge_whitelist": true, "merge_whitelist_usernames": []any{tools},
|
|
"enable_status_check": len(contexts) > 0, "status_check_contexts": contexts,
|
|
"block_admin_merge_override": true,
|
|
}
|
|
}
|
|
|
|
// requiresNone is whether a rule as the fake forge keeps it requires no status.
|
|
func requiresNone(rule map[string]any) bool {
|
|
return rule["enable_status_check"] != true && len(strs(rule["status_check_contexts"])) == 0
|
|
}
|
|
|
|
// 0534-1: a listed repository whose main requires statuses has them all taken off, the rest of the rule kept; a
|
|
// second pass changes nothing.
|
|
func TestListedRepositoryRequiresNoStatus(t *testing.T) {
|
|
f := newFake()
|
|
repo := f.add("novox/mesh-delivery", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate", "mesh/repo-check", "other/x")}, "main")
|
|
checked := f.add("novox/checked", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main")
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{"mesh-delivery": {}, "checked": {}})
|
|
for name, r := range map[string]*fRepo{"mesh-delivery": repo, "checked": checked} {
|
|
rule := r.Rules[mainRule]
|
|
if !requiresNone(rule) {
|
|
t.Errorf("%s's main still requires %v", name, rule["status_check_contexts"])
|
|
}
|
|
if rule["enable_push"] != false || rule["enable_merge_whitelist"] != true ||
|
|
strings.Join(strs(rule["merge_whitelist_usernames"]), ",") != tools || rule["block_admin_merge_override"] != true {
|
|
t.Errorf("%s's main lost the rest of its rule: %v", name, rule)
|
|
}
|
|
if r := result(o, name); r.Did != DidPutRight {
|
|
t.Errorf("%s outcome %+v, want put right", name, r)
|
|
}
|
|
}
|
|
before := len(f.writes())
|
|
o = run(t, s, Value{"mesh-delivery": {}, "checked": {}})
|
|
if w := f.writes(); len(w) != before {
|
|
t.Errorf("a second pass wrote %v", w[before:])
|
|
}
|
|
if result(o, "checked").Did != DidUnchanged {
|
|
t.Errorf("second outcome %+v, want unchanged", result(o, "checked"))
|
|
}
|
|
}
|
|
|
|
// 0534-1: a repository the program makes requires no status from its first rule in the organisation on.
|
|
func TestNewRepositoryRequiresNoStatus(t *testing.T) {
|
|
f := newFake()
|
|
s, _ := syncer(t, f)
|
|
run(t, s, Value{"fresh": {Description: "d"}})
|
|
rule := f.repos["novox/fresh"].Rules[mainRule]
|
|
if !requiresNone(rule) {
|
|
t.Errorf("a new repository's main requires %v, want none", rule["status_check_contexts"])
|
|
}
|
|
if readme := f.repos["novox/fresh"].Files["README.md"]; strings.Contains(readme, "mesh/merge-gate") || strings.Contains(readme, "mesh/repo-check") {
|
|
t.Errorf("the README names the statuses of before: %q", readme)
|
|
}
|
|
}
|
|
|
|
// 0534-1: a listed repository whose main requires nothing stays so, and is not written when the rest is right.
|
|
func TestListedRuleRequiringNothingIsRight(t *testing.T) {
|
|
f := newFake()
|
|
none := f.add("novox/none", map[string]map[string]any{mainRule: narrowedRule()}, "main")
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{"none": {}})
|
|
if !requiresNone(none.Rules[mainRule]) {
|
|
t.Errorf("none requires %v", none.Rules[mainRule]["status_check_contexts"])
|
|
}
|
|
if result(o, "none").Did != DidUnchanged {
|
|
t.Errorf("outcome %+v, want unchanged", result(o, "none"))
|
|
}
|
|
for _, w := range f.writes() {
|
|
t.Errorf("a right rule was written: %s", w)
|
|
}
|
|
}
|
|
|
|
// 0534-1: hq, whose main required mesh/repo-check alone, requires no status either, listed or not.
|
|
func TestOwnCheckAloneGoesToo(t *testing.T) {
|
|
f := newFake()
|
|
hq := f.add("novox/hq", map[string]map[string]any{mainRule: narrowedRule("mesh/repo-check")}, "main")
|
|
lab := f.add("novox/mesh-lab", map[string]map[string]any{mainRule: narrowedRule("mesh/repo-check")}, "main")
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{"hq": {}})
|
|
for name, repo := range map[string]*fRepo{"hq": hq, "mesh-lab": lab} {
|
|
if !requiresNone(repo.Rules[mainRule]) {
|
|
t.Errorf("%s requires %v, want none", name, repo.Rules[mainRule]["status_check_contexts"])
|
|
}
|
|
if r := result(o, name); r.Did != DidPutRight {
|
|
t.Errorf("%s outcome %+v, want put right", name, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 0534-1: a repository the value does not list whose rule covering main requires any status has them taken off, and
|
|
// only its statuses: nothing else of its rule is sent. One that requires none is not written to.
|
|
func TestUnlistedRepositoryRequiringAStatusHasItTakenOff(t *testing.T) {
|
|
f := newFake()
|
|
catalog := f.add("novox/mesh-catalog", map[string]map[string]any{mainRule: {
|
|
"enable_push": true, "enable_status_check": true, "status_check_contexts": []any{"mesh/check"},
|
|
}}, "main")
|
|
toolsGo := f.add("novox/mesh-tools-go", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "main")
|
|
f.add("novox/plain", map[string]map[string]any{mainRule: {"enable_push": false}}, "main")
|
|
f.add("novox/unticked", map[string]map[string]any{mainRule: {"enable_push": false, "enable_status_check": false,
|
|
"status_check_contexts": []any{"mesh/check"}}}, "main")
|
|
f.add("novox/norule", nil, "main")
|
|
f.add("mesh-admin/elsewhere", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "main")
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{})
|
|
|
|
if !requiresNone(catalog.Rules[mainRule]) {
|
|
t.Errorf("mesh-catalog requires %v, want none", catalog.Rules[mainRule]["status_check_contexts"])
|
|
}
|
|
if catalog.Rules[mainRule]["enable_push"] != true {
|
|
t.Errorf("an unlisted repository's rule was changed beyond its statuses: %v", catalog.Rules[mainRule])
|
|
}
|
|
if !requiresNone(toolsGo.Rules[mainRule]) {
|
|
t.Errorf("mesh-tools-go requires %v, want none", toolsGo.Rules[mainRule]["status_check_contexts"])
|
|
}
|
|
want := []string{
|
|
"PATCH /api/v1/repos/novox/mesh-catalog/branch_protections/main",
|
|
"PATCH /api/v1/repos/novox/mesh-tools-go/branch_protections/main",
|
|
}
|
|
if got := f.writes(); strings.Join(got, "\n") != strings.Join(want, "\n") {
|
|
t.Errorf("writes:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
|
}
|
|
for _, n := range []string{"mesh-catalog", "mesh-tools-go"} {
|
|
if r := result(o, n); r.Did != DidPutRight {
|
|
t.Errorf("%s outcome %+v, want put right", n, r)
|
|
}
|
|
}
|
|
for _, n := range []string{"plain", "unticked", "norule", "elsewhere"} {
|
|
if r := result(o, n); r.Name != "" {
|
|
t.Errorf("%s is in the outcome though nothing was done: %+v", n, r)
|
|
}
|
|
}
|
|
before := len(f.writes())
|
|
run(t, s, Value{})
|
|
if w := f.writes(); len(w) != before {
|
|
t.Errorf("a second pass wrote %v", w[before:])
|
|
}
|
|
}
|
|
|
|
// 0534-1: the organisation's repositories are read page by page, so one past the first page is put right too.
|
|
func TestUnlistedRepositoriesAreReadPastTheFirstPage(t *testing.T) {
|
|
f := newFake()
|
|
f.pageSize = 2
|
|
for _, n := range []string{"a", "b", "c", "d", "e"} {
|
|
f.add("novox/"+n, map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main")
|
|
}
|
|
s, _ := syncer(t, f)
|
|
run(t, s, Value{})
|
|
for _, n := range []string{"a", "b", "c", "d", "e"} {
|
|
if rule := f.repos["novox/"+n].Rules[mainRule]; !requiresNone(rule) {
|
|
t.Errorf("%s requires %v", n, rule["status_check_contexts"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 0534-1: a refused removal is said with the repository and its step, and the pass goes on to the next.
|
|
func TestRefusedRemovalIsSaid(t *testing.T) {
|
|
f := newFake()
|
|
f.add("novox/a", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main")
|
|
f.add("novox/b", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main")
|
|
f.refuse = func(method, path string) (int, string) {
|
|
if method == "PATCH" && strings.Contains(path, "/novox/a/") {
|
|
return 403, "no"
|
|
}
|
|
return 0, ""
|
|
}
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{})
|
|
if r := result(o, "a"); r.Did != DidRefused || r.Step != StepNoStatus {
|
|
t.Errorf("a: %+v, want refused at %s", r, StepNoStatus)
|
|
}
|
|
if r := result(o, "b"); r.Did != DidPutRight {
|
|
t.Errorf("b: %+v, want put right", r)
|
|
}
|
|
}
|
|
|
|
// 1301-S3: the organisation's list refused is said, and the listed repositories are still done.
|
|
func TestOrganisationListRefusedIsSaid(t *testing.T) {
|
|
f := newFake()
|
|
f.refuse = func(method, path string) (int, string) {
|
|
if strings.HasPrefix(path, "/api/v1/orgs/") {
|
|
return 500, "down"
|
|
}
|
|
return 0, ""
|
|
}
|
|
s, _ := syncer(t, f)
|
|
o := run(t, s, Value{"x": {}})
|
|
if result(o, "x").Did != DidMade {
|
|
t.Errorf("x: %+v", result(o, "x"))
|
|
}
|
|
refusedList := false
|
|
for _, r := range o.Refused() {
|
|
refusedList = refusedList || r.Step == StepList
|
|
}
|
|
if !refusedList {
|
|
t.Errorf("the refused list is not in the outcome: %+v", o)
|
|
}
|
|
}
|
|
|
|
// 0534-2 (the operator, 2026-10-23 ~12:05): the operator's own forge login stays on main's merge whitelist beside the
|
|
// tools' account, so the operator can always merge by hand at the forge; a rule naming the tools' account alone is put
|
|
// right, and a second pass changes nothing.
|
|
func TestTheOperatorsLoginStaysBesideTheTools(t *testing.T) {
|
|
f := newFake()
|
|
repo := f.add("novox/mesh-delivery", map[string]map[string]any{mainRule: narrowedRule()}, "main")
|
|
s, _ := syncer(t, f)
|
|
s.Mergers = []string{"jschoubben", tools, "jschoubben"}
|
|
o := run(t, s, Value{"mesh-delivery": {}, "fresh": {}})
|
|
for name, rule := range map[string]map[string]any{"mesh-delivery": repo.Rules[mainRule], "fresh": f.repos["novox/fresh"].Rules[mainRule]} {
|
|
if got := strings.Join(strs(rule["merge_whitelist_usernames"]), ","); got != tools+",jschoubben" {
|
|
t.Errorf("%s's merge whitelist is %s, want %s and jschoubben", name, got, tools)
|
|
}
|
|
if !requiresNone(rule) || rule["enable_push"] != false {
|
|
t.Errorf("%s's rule: %v", name, rule)
|
|
}
|
|
}
|
|
if r := result(o, "mesh-delivery"); r.Did != DidPutRight {
|
|
t.Errorf("outcome %+v, want put right", r)
|
|
}
|
|
before := len(f.writes())
|
|
run(t, s, Value{"mesh-delivery": {}, "fresh": {}})
|
|
if w := f.writes(); len(w) != before {
|
|
t.Errorf("a second pass wrote %v", w[before:])
|
|
}
|
|
}
|
|
|
|
// 0534-2: the mergers are read from the module's runtime config file; a missing key is none; a name that is no forge login
|
|
// is refused.
|
|
func TestReadMergers(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "r.json")
|
|
_ = os.WriteFile(p, []byte(`{"url":"","main-mergers":["jschoubben"]}`), 0o600)
|
|
if m, err := ReadMergers(p); err != nil || strings.Join(m, ",") != "jschoubben" {
|
|
t.Fatalf("%v %v", m, err)
|
|
}
|
|
_ = os.WriteFile(p, []byte(`{"url":""}`), 0o600)
|
|
if m, err := ReadMergers(p); err != nil || len(m) != 0 {
|
|
t.Fatalf("%v %v", m, err)
|
|
}
|
|
for _, bad := range []string{`["a b"]`, `["../x"]`, `[""]`, `"jschoubben"`} {
|
|
_ = os.WriteFile(p, []byte(`{"main-mergers":`+bad+`}`), 0o600)
|
|
if _, err := ReadMergers(p); err == nil {
|
|
t.Errorf("%s was accepted", bad)
|
|
}
|
|
}
|
|
}
|