package api
import (
"context"
"errors"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// The attack this gate exists for: a repository's CI spec lives in that
// repository, so its author chooses the worker_group its steps publish
// to. Naming ANOTHER repository's group puts this author's steps on that
// repository's machines. Every spelling YAML allows has to be caught,
// because one that is not is the whole hole.
func TestAForeignWorkerGroupIsRefusedHoweverItIsSpelled(t *testing.T) {
for _, tc := range []struct{ name, spec string }{
{"plain", "checks:\n - name: test\n worker_group: victim\n"},
{"double quoted value", "checks:\n - worker_group: \"victim\"\n"},
{"single quoted value", "checks:\n - worker_group: 'victim'\n"},
{"quoted key", "checks:\n - \"worker_group\": victim\n"},
{"flow mapping", "checks: [{name: test, worker_group: victim}]\n"},
{"extra spaces", "checks:\n - worker_group : victim\n"},
{"deep nesting", "a:\n b:\n c:\n worker_group: victim\n"},
{"deploy block", "deploy:\n worker_group: victim\n steps: []\n"},
// dagnats honours YAML merge keys, and the anchor block carries
// the literal key, so the declaration is visible there too.
{"anchor and merge key", "x-def: &d\n worker_group: victim\nchecks:\n - <<: *d\n"},
{"trailing comment", "checks:\n - worker_group: victim # ours, honest\n"},
{"one of several", "checks:\n - worker_group: orchard\n - worker_group: victim\n"},
} {
err := refuseForeignWorkerGroup("orchard", tc.spec)
if err == nil {
t.Errorf("FAIL %s: a foreign group was allowed", tc.name)
} else if !strings.Contains(err.Error(), "victim") {
t.Errorf("FAIL %s: refusal does not name the group: %v", tc.name, err)
} else {
t.Logf("ok %s refused", tc.name)
}
}
}
// Absent is the ungrouped queue and the forge's own runners — the
// default every repository that does not run its own machines relies on.
// Its own group is the point of the feature. Neither may be refused.
func TestOwnGroupAndNoGroupAreAllowed(t *testing.T) {
for _, tc := range []struct{ name, spec string }{
{"no group at all", "checks:\n - name: test\n run: go test ./...\n"},
{"its own group", "checks:\n - worker_group: orchard\n"},
{"its own, quoted", "checks:\n - worker_group: \"orchard\"\n"},
{"its own, twice", "checks:\n - worker_group: orchard\n - worker_group: orchard\n"},
{"empty spec", ""},
} {
if err := refuseForeignWorkerGroup("orchard", tc.spec); err != nil {
t.Errorf("FAIL %s was refused: %v", tc.name, err)
} else {
t.Logf("ok %s allowed", tc.name)
}
}
}
// A repository whose name had to be folded uses the FOLDED group, not
// its raw name — so a spec naming the raw name is foreign, and one
// naming the fold is its own. Getting this backwards would either lock
// such a repository out of its own runners or let it reach another's.
func TestAFoldedRepositoryNameUsesItsFoldedGroup(t *testing.T) {
const repo = "my.repo"
own := "" // derived below, through the same function the gate uses
if err := refuseForeignWorkerGroup(repo, "checks:\n - worker_group: my.repo\n"); err == nil {
t.Error("FAIL the RAW name was accepted as this repository's group")
} else {
t.Log("ok the raw dotted name is not the group")
own = strings.Split(strings.SplitN(err.Error(), "own worker group \"", 2)[1], "\"")[0]
}
if strings.ContainsAny(own, ".=") {
t.Fatalf("FAIL derived group %q is not one subject token", own)
}
if err := refuseForeignWorkerGroup(repo, "checks:\n - worker_group: "+own+"\n"); err != nil {
t.Errorf("FAIL a repository was locked out of its own group %q: %v", own, err)
} else {
t.Logf("ok the folded group %q is its own", own)
}
}
// `worker_group:` with nothing after it is not "ungrouped" — it is a
// declaration whose meaning the author did not state. Guessing which
// they meant is how a step lands on a queue nobody expected, so it is
// refused and the author says which they meant.
func TestAnEmptyWorkerGroupIsRefusedRatherThanGuessed(t *testing.T) {
for _, spec := range []string{
"checks:\n - worker_group:\n",
"checks:\n - worker_group: \"\"\n",
"checks:\n - worker_group: ''\n",
} {
err := refuseForeignWorkerGroup("orchard", spec)
if err == nil {
t.Errorf("FAIL an empty group was taken as ungrouped: %q", spec)
} else if !strings.Contains(err.Error(), "empty group") {
t.Errorf("FAIL refusal does not explain the empty group: %v", err)
} else {
t.Log("ok an empty group is refused, not guessed")
}
}
}
// One mistake repeated is one diagnostic, not one per step: a spec with
// six steps naming the same wrong group should say so once.
func TestRepeatedForeignGroupsAreReportedOnce(t *testing.T) {
spec := strings.Repeat(" - worker_group: victim\n", 6)
err := refuseForeignWorkerGroup("orchard", "checks:\n"+spec)
if err == nil {
t.Fatal("FAIL not refused")
}
if n := strings.Count(err.Error(), "victim"); n != 1 {
t.Errorf("FAIL group named %d times, want 1: %v", n, err)
} else {
t.Log("ok one diagnostic for one mistake")
}
}
// Through the OP, not the helper. A gate that is correct and unwired
// protects nothing, and a passing unit test on the checker says nothing
// about whether ci.pipeline.save calls it — the same way a template
// condition can be right in isolation and absent from the page.
//
// Both paths: validate (the editor, while the author is still typing)
// and save (the commit). A gate on only the second lets the editor tell
// someone their spec is fine right up until it is refused.
func TestPipelineSaveRefusesAForeignWorkerGroupThroughTheOp(t *testing.T) {
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t))}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
const foreign = "checks:\n - name: t\n worker_group: victim\n run: go test ./...\n"
for _, validate := range []bool{true, false} {
_, err := r.Invoke(ctx, "ci.pipeline.save", ciPipelineSaveIn{
Repo: "test", Path: "ci/quarry.yml", Content: foreign, Validate: validate,
})
switch {
case err == nil:
t.Errorf("FAIL validate=%v: a foreign worker group was accepted", validate)
case !errors.Is(err, ops.ErrInvalid):
t.Errorf("FAIL validate=%v: refusal does not wrap ErrInvalid: %v", validate, err)
case !strings.Contains(err.Error(), "victim"):
t.Errorf("FAIL validate=%v: refusal does not name the group: %v", validate, err)
default:
t.Logf("ok validate=%v refused, naming the group", validate)
}
}
// And a spec with no group at all must still reach the ordinary
// path — the gate must not become a wall in front of every save.
if _, err := r.Invoke(ctx, "ci.pipeline.save", ciPipelineSaveIn{
Repo: "test", Path: "ci/quarry.yml", Validate: true,
Content: "checks:\n - name: t\n run: go test ./...\n",
}); errors.Is(err, ops.ErrInvalid) {
t.Errorf("FAIL an ungrouped spec was refused: %v", err)
} else {
t.Log("ok an ungrouped spec is not refused by this gate")
}
}