Skip to content
quarry Artifact [09df5d1ffd]
Login

Artifact [09df5d1ffd]

Artifact 09df5d1ffd1c7b820be73cc49156b5cf643f55a65d556293a0019de33f7e4cf7:


package ci

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"
	"time"

	"gopkg.in/yaml.v3"
)

// TWO LAUNCHES OF ONE WORKFLOW AT ONCE EACH START THE DEFINITION THEY
// REGISTERED. The engine starts a run by name, so a Register from one
// launch between another's Register and Start would run the wrong file
// under the right record.
func TestConcurrentLaunchesStartWhatTheyRegistered(t *testing.T) {
	var mu sync.Mutex
	current := map[string]string{} // name -> spec registered last
	ran := map[string]string{}     // run id -> spec current at its Start
	n := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			time.Sleep(30 * time.Millisecond) // widen the window between register and start
			mu.Lock()
			current[body.Name] = body.Spec
			mu.Unlock()
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true}`, body.Name)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct{ Workflow string }
			json.NewDecoder(r.Body).Decode(&body)
			mu.Lock()
			n++
			id := fmt.Sprintf("r%d", n)
			ran[id] = current[body.Workflow]
			mu.Unlock()
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintf(w, `{"run_id":%q}`, id)
		case r.URL.Path == "/runs":
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	def := func(tag string) []byte {
		return []byte("jobs:\n  t:\n    steps:\n      - run: echo " + tag + "\n")
	}
	repo := &fakeRepo{files: []string{"ci/test.yml"}, blobs: map[string][]byte{"ci/test.yml": def("base")},
		versioned: map[string]map[string][]byte{}}
	for i := 0; i < 6; i++ {
		repo.versioned[fmt.Sprintf("c%d", i)] = map[string][]byte{"ci/test.yml": def(fmt.Sprintf("v%d", i))}
	}
	st := &fakeStore{mark: map[string]string{}}
	e := New(srv.URL)
	var wg sync.WaitGroup
	for i := 0; i < 6; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			if err := RunAt(e, st, "alpha", repo, "u", fmt.Sprintf("c%d", i), ""); err != nil {
				t.Error(err)
			}
		}(i)
	}
	wg.Wait()
	st.mu.Lock()
	defer st.mu.Unlock()
	if len(st.runs) != 6 {
		t.Fatalf("FAIL: %d runs recorded; want 6", len(st.runs))
	}
	for _, line := range st.runs {
		id, checkin, _ := strings.Cut(line, " ")
		checkin, _, _ = strings.Cut(checkin, " ")
		want := "echo v" + strings.TrimPrefix(checkin, "c")
		if !strings.Contains(ran[id], want) {
			t.Errorf("FAIL: run %s of %s started %q; want its own definition (%s)", id, checkin, ran[id], want)
		}
	}
	t.Log("ok: register and start of one workflow are one step for each launch")
}

// THE STARTER IS WHOEVER AUTHENTICATED TO CARRY THE ARTIFACT, NEVER A
// U CARD: a check-in's for a push, the tag's control artifact's for a
// tag. Nobody recorded is no starter.
func TestTheStarterIsTheAuthenticatedPusher(t *testing.T) {
	n := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name string }
			json.NewDecoder(r.Body).Decode(&body)
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":{"push":{},"tag":{}}}`, body.Name)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			n++
			w.WriteHeader(http.StatusCreated)
			fmt.Fprintf(w, `{"run_id":"r%d"}`, n)
		case r.URL.Path == "/runs":
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	e := New(srv.URL)
	st := &fakeStore{mark: map[string]string{"alpha": "t0"}, pushedBy: map[string]string{"t1": "dev", "ctl": "dev"}}
	repo := &fakeRepo{files: []string{"ci/w.yml"},
		blobs: map[string][]byte{"ci/w.yml": []byte("on:\n  push: {}\n  tag: {}\njobs:\n  t:\n    steps:\n      - run: x\n")},
		news:  []Checkin{{Hash: "t1", Branch: "trunk", User: "admin"}, {Hash: "t2", Branch: "trunk", User: "admin"}}}
	if err := Trigger(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := st.starts["r1"].Starter; got != "dev" {
		t.Errorf("FAIL: a push carried by dev with U admin started by %q; want dev", got)
	}
	if got := st.starts["r2"].Starter; got != "" {
		t.Errorf("FAIL: a check-in nobody authenticated to carry started by %q; want nobody", got)
	}
	TriggerTags(e, st, "alpha", repo, "u") // first sight
	repo.tags = []Tagged{{TagKey: TagKey{Src: "ctl", Target: "t1", Name: "version-1"}, At: Checkin{Hash: "t1", Branch: "trunk", User: "admin", Starter: "admin"}}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := st.starts["r3"]; got.Event != "tag" || got.Starter != "dev" {
		t.Errorf("FAIL: a tag whose control artifact dev carried, signed U admin, recorded %+v; want started by dev", got)
	}
	t.Log("ok: the starter is the carrier the front authenticated, never the U card")
}

// PERMISSIONS IN A FLOW MAPPING, OR WITH A BLOCK SCALAR VALUE, ARE
// REFUSED WITH A POSITION, AND STRIPPING TAKES EVERY LINE A VALUE SPANS.
func TestPermissionsOddShapesAreRefusedAndStrippedWhole(t *testing.T) {
	flow := []byte("jobs:\n  t: {permissions: {uv: write}, steps: [{run: x}]}\n")
	if _, err := permissionsOf(flow); err == nil || !strings.Contains(err.Error(), "line 2") {
		t.Errorf("FAIL: permissions in a flow mapping gave %v; want a refusal with its line", err)
	}
	block := []byte("permissions: |\n  uv: write\n  contents: write\njobs:\n  t:\n    steps:\n      - run: x\n")
	if _, err := permissionsOf(block); err == nil || !strings.Contains(err.Error(), "line 1") {
		t.Errorf("FAIL: a block scalar permissions gave %v; want a refusal", err)
	}
	out := withoutQuarryKeys(block)
	if bytes.Count(out, []byte("\n")) != bytes.Count(block, []byte("\n")) || bytes.Contains(out, []byte("uv: write")) || bytes.Contains(out, []byte("contents: write")) {
		t.Errorf("FAIL: the block scalar was not stripped whole:\n%s", out)
	}
	var doc map[string]any
	if err := yaml.Unmarshal(out, &doc); err != nil || doc["jobs"] == nil {
		t.Errorf("FAIL: the stripped spec %v:\n%s", err, out)
	}
	t.Log("ok: odd shapes refused with a position; a value is stripped to its last line")
}