Skip to content
quarry Artifact [1bf310d5d5]
Login

Artifact [1bf310d5d5]

Artifact 1bf310d5d5bae0539e7375c4c23af1819c7c98a76f81727252d0d907aae2c5d5:


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")
	}
}