Skip to content
quarry Artifact [6f26dec17f]
Login

Artifact [6f26dec17f]

Artifact 6f26dec17fc8c2adc5b996e0bcfc2069b561824104cc7ae038ed2176797656f5:


package ci

import (
	"bytes"
	"strings"
	"testing"

	"gopkg.in/yaml.v3"
)

// PERMISSIONS: ARE READ, NARROWED PER JOB, AND STRIPPED (RFC 0021 §runs).
func TestPermissionsAreReadPerJobAndNarrowed(t *testing.T) {
	spec := []byte(`on:
  tag: {}
permissions:
  contents: write
  uv: write
jobs:
  build:
    runs-on: any
    steps:
      - run: make
  publish:
    environment: release
    timeout-minutes: 20
    permissions:
      uv: write
    steps:
      - run: ./publish
  wide:
    permissions: {contents: write, wiki: write}
    steps:
      - run: true
`)
	grants, err := permissionsOf(spec)
	if err != nil {
		t.Fatalf("FAIL: %v", err)
	}
	want := map[string]JobGrant{
		"build":   {Letters: "gioy"},
		"publish": {Environment: "release", Letters: "y", Timeout: 20},
		// A job narrows the workflow's, never widens: wiki is not there.
		"wide": {Letters: "gio"},
	}
	for job, w := range want {
		if g := grants[job]; g != w {
			t.Errorf("FAIL: %s = %+v; want %+v", job, g, w)
		}
	}
	if len(grants) != len(want) {
		t.Errorf("FAIL: grants for %d jobs; want %d", len(grants), len(want))
	}

	// No key anywhere is contents: read.
	grants, err = permissionsOf([]byte("jobs:\n  t:\n    steps:\n      - run: make\n"))
	if err != nil || grants["t"].Letters != "go" {
		t.Errorf("FAIL: undeclared = %+v, %v; want contents: read (go)", grants, err)
	}
	// A job's own key stands when the workflow declares none.
	grants, err = permissionsOf([]byte("jobs:\n  t:\n    permissions:\n      tickets: write\n    steps:\n      - run: make\n"))
	if err != nil || grants["t"].Letters != "crw" {
		t.Errorf("FAIL: job-only = %+v, %v; want crw", grants, err)
	}
	t.Log("ok: workflow and job permissions read, a job narrows, the default is contents: read")
}

func TestUnknownPermissionsAreNamedWithAPosition(t *testing.T) {
	for _, c := range []struct{ spec, line string }{
		{"permissions:\n  packages: write\njobs:\n  t:\n    steps:\n      - run: x\n", "line 2, column 3"},
		{"permissions:\n  contents: admin\njobs:\n  t:\n    steps:\n      - run: x\n", "line 2, column 13"},
		{"jobs:\n  t:\n    permissions:\n      uv: delete\n    steps:\n      - run: x\n", "line 4, column 11"},
		{"permissions: write-all\njobs:\n  t:\n    steps:\n      - run: x\n", "line 1, column 14"},
	} {
		_, err := permissionsOf([]byte(c.spec))
		if err == nil || !strings.Contains(err.Error(), c.line) || !strings.Contains(err.Error(), "not supported") {
			t.Errorf("FAIL: %q gave %v; want an unsupported error at %s", c.spec, err, c.line)
		}
	}
	t.Log("ok: unknown nouns, values and forms are refused with a line and a column")
}

func TestPermissionsAreStrippedKeepingLines(t *testing.T) {
	spec := []byte(`permissions:
  uv: write
inherit: false
jobs:
  publish:
    permissions: {uv: write}
    steps:
      - run: ./publish   # permissions: in a comment stays
`)
	out := withoutQuarryKeys(spec)
	if bytes.Count(out, []byte("\n")) != bytes.Count(spec, []byte("\n")) {
		t.Errorf("FAIL: line count changed:\n%s", out)
	}
	var doc map[string]any
	if err := yaml.Unmarshal(out, &doc); err != nil {
		t.Fatalf("FAIL: stripped spec does not parse: %v\n%s", err, out)
	}
	if _, ok := doc["permissions"]; ok {
		t.Errorf("FAIL: top-level permissions survived:\n%s", out)
	}
	if _, ok := doc["inherit"]; ok {
		t.Errorf("FAIL: inherit survived:\n%s", out)
	}
	job := doc["jobs"].(map[string]any)["publish"].(map[string]any)
	if _, ok := job["permissions"]; ok {
		t.Errorf("FAIL: the job's permissions survived:\n%s", out)
	}
	if _, ok := job["steps"]; !ok {
		t.Errorf("FAIL: the job's steps went with it:\n%s", out)
	}
	if !bytes.Contains(out, []byte("# permissions: in a comment stays")) {
		t.Errorf("FAIL: a step's line was touched:\n%s", out)
	}
	t.Log("ok: permissions: stripped at both levels, every other line where it was")
}