package main import ( "strings" "testing" ) // The tests of issue 1301 (ADR 0447 §1, §5; ADR 0503 §3, §5; lead decisions L4 and L11): main requires `mesh/check` // alone in place of `mesh/merge-gate` and `mesh/repo-check`; a main that requires `mesh/repo-check` without // `mesh/merge-gate` (hq) is swapped too (ADR 0527 §2); every repository of the organisation whose main requires either is // switched, listed in the value or not; no other status is dropped. 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": true, "status_check_contexts": contexts, "block_admin_merge_override": true, } } // 1301-S1: a listed repository whose main requires the two old statuses is put right to `mesh/check` in their place, // every other status kept; a second pass changes nothing. func TestListedRepositorySwapsTheTwoStatusesForCheck(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") s, _ := syncer(t, f) o := run(t, s, Value{"mesh-delivery": {}}) got := strs(repo.Rules[mainRule]["status_check_contexts"]) if strings.Join(got, ",") != "other/x,mesh/check" { t.Errorf("main requires %v, want other/x and mesh/check", got) } 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": {}}) if w := f.writes(); len(w) != before { t.Errorf("a second pass wrote %v", w[before:]) } } // 1301-S1: a repository the program makes requires `mesh/check` alone from its first rule in the organisation on. func TestNewRepositoryRequiresCheckAlone(t *testing.T) { f := newFake() s, _ := syncer(t, f) run(t, s, Value{"fresh": {Description: "d"}}) rule := f.repos["novox/fresh"].Rules[mainRule] if got := strs(rule["status_check_contexts"]); rule["enable_status_check"] != true || strings.Join(got, ",") != CheckStatus { t.Errorf("a new repository's main requires %v, want %s alone", got, CheckStatus) } if readme := f.repos["novox/fresh"].Files["README.md"]; !strings.Contains(readme, CheckStatus) || strings.Contains(readme, "mesh/merge-gate") || strings.Contains(readme, "mesh/repo-check") { t.Errorf("the README names the statuses of before: %q", readme) } } // 1301-S1: mesh/check is never dropped, and a rule that requires it beside a retired status loses the retired one. func TestCheckIsKeptAndARetiredStatusBesideItGoes(t *testing.T) { f := newFake() both := f.add("novox/both", map[string]map[string]any{mainRule: narrowedRule("mesh/check", "mesh/repo-check")}, "main") right := f.add("novox/right", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main") s, _ := syncer(t, f) o := run(t, s, Value{"both": {}, "right": {}}) if got := strs(both.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("both requires %v, want %s alone", got, CheckStatus) } if got := strs(right.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("right requires %v", got) } if result(o, "right").Did != DidUnchanged || result(o, "both").Did != DidPutRight { t.Errorf("outcome %+v", o) } } // 1301-S1: a listed repository whose main requires nothing is given mesh/check, not the old two. func TestListedRuleRequiringNothingGetsCheck(t *testing.T) { f := newFake() none := f.add("novox/none", map[string]map[string]any{mainRule: {"enable_push": false}}, "main") s, _ := syncer(t, f) run(t, s, Value{"none": {}}) if got := strs(none.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("none requires %v, want %s", got, CheckStatus) } } // 1296/E1 (ADR 0527 §2): a main that requires mesh/repo-check alone (hq) is given mesh/check in its place, listed or // not, since nothing writes mesh/repo-check once build-agent is gone. func TestOwnCheckAloneIsSwapped(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 got := strs(repo.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("%s requires %v, want %s alone", name, got, CheckStatus) } } if result(o, "hq").Did == DidUnchanged { t.Errorf("hq outcome %+v, want a change", result(o, "hq")) } } // 1301-S3 (L4, L11): a repository the value does not list whose main requires mesh/merge-gate has the two old // statuses swapped for mesh/check, and only its statuses: nothing else of its rule is sent. One that requires // neither is not written to. func TestUnlistedRepositoryRequiringTheGateIsSwitched(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/merge-gate", "mesh/repo-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") hq := f.add("novox/hq", map[string]map[string]any{mainRule: narrowedRule("mesh/repo-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 got := strs(catalog.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("mesh-catalog requires %v, want %s", got, CheckStatus) } if catalog.Rules[mainRule]["enable_push"] != true { t.Errorf("an unlisted repository's rule was narrowed beyond its statuses: %v", catalog.Rules[mainRule]) } if got := strs(toolsGo.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("mesh-tools-go requires %v, want %s", got, CheckStatus) } if got := strs(hq.Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("hq requires %v, want %s", got, CheckStatus) } want := []string{ "PATCH /api/v1/repos/novox/hq/branch_protections/main", "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{"hq", "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", "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:]) } } // 1301-S3: the organisation's repositories are read page by page, so one past the first page is switched 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/merge-gate", "mesh/repo-check")}, "main") } s, _ := syncer(t, f) run(t, s, Value{}) for _, n := range []string{"a", "b", "c", "d", "e"} { if got := strs(f.repos["novox/"+n].Rules[mainRule]["status_check_contexts"]); strings.Join(got, ",") != CheckStatus { t.Errorf("%s requires %v", n, got) } } } // 1301-S3: a refused swap is said with the repository and its step, and the pass goes on to the next. func TestRefusedSwapIsSaid(t *testing.T) { f := newFake() f.add("novox/a", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "main") f.add("novox/b", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "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 != StepSwap { t.Errorf("a: %+v, want refused at %s", r, StepSwap) } 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) } }