Skip to content
quarry Artifact [4ebf7b252b]
Login

Artifact [4ebf7b252b]

Artifact 4ebf7b252b70d7d1381fe7b913bd9e2ce3ddc1a61499fb5394cf2c5a5bcd089d:


package api

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"errors"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/cisecret"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// ONLY REVIEWED CODE RECEIVES A SECRET (RFC 0019 §secrets).
//
// The rule, from the forge's own records: the run must be quarry's, not
// a landing's merge, and its check-in's branch must satisfy the policy
// of each secret's environment: protected branches by default, or the
// environment's own patterns. Values are set through the ops and never
// come back from them.
func TestOnlyReviewedCodeReceivesASecret(t *testing.T) {
	ix, trunkTip, devTip := closeIndex(t) // trunk is protected below, dev is not
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	key := make([]byte, 32)
	rand.Read(key)
	box, err := cisecret.New(base64.StdEncoding.EncodeToString(key))
	if err != nil {
		t.Fatal(err)
	}
	d := Deps{Version: "v",
		Repos:     NewRepos(map[string]*Repo{"test": {Index: ix, Zit: &zitcli.Client{}}}),
		Protect:   stubProtect{rules: []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}}},
		Secrets:   db,
		SecretBox: box,
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, d); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "alice")
	call := func(op string, in any) (any, error) { return r.Invoke(ctx, op, in) }
	if _, err := call("ci.environment.set", ciEnvironmentSetIn{Repo: "test", Name: "production"}); err != nil {
		t.Fatal(err)
	}
	if _, err := call("ci.environment.set", ciEnvironmentSetIn{Repo: "test", Name: "preview", Branches: []string{"dev"}}); err != nil {
		t.Fatal(err)
	}
	for _, s := range []ciSecretSetIn{
		{Repo: "test", Environment: "production", Name: "CF_TOKEN", Value: "prod-value"},
		{Repo: "test", Environment: "preview", Name: "PREVIEW_TOKEN", Value: "preview-value"},
		{Repo: "test", Name: "SHARED", Value: "shared-value"},
	} {
		out, err := call("ci.secret.set", s)
		if err != nil {
			t.Fatalf("FAIL: setting %s: %v", s.Name, err)
		}
		if strings.Contains(strings.ToLower(asJSON(t, out)), "value\"") || strings.Contains(asJSON(t, out), s.Value) {
			t.Errorf("FAIL: ci.secret.set answered with the value: %s", asJSON(t, out))
		}
	}
	if _, err := call("ci.secret.set", ciSecretSetIn{Repo: "test", Environment: "nope", Name: "X", Value: "v"}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: a secret in a missing environment answered %v; want ErrNotFound", err)
	}

	run := func(id, checkin, pair string) {
		if err := db.AddCIRun(oltp.CIRun{RunID: id, Repo: "test", Checkin: checkin, Pair: pair, Workflow: "w", Pipeline: "ci/deploy.yml", Status: "running"}); err != nil {
			t.Fatal(err)
		}
	}
	run("trunkrun", trunkTip, "")
	run("devrun", devTip, "")
	run("pairrun", "pair:"+devTip+":"+trunkTip, "pair:"+devTip+":"+trunkTip)

	got, err := d.DeliverSecrets("test", "trunkrun.deploy", "production", []string{"CF_TOKEN", "SHARED"})
	if err != nil || got["CF_TOKEN"] != "prod-value" || got["SHARED"] != "shared-value" {
		t.Errorf("FAIL: a trunk run got %v, %v; want production's and the repository's secrets", got, err)
	}
	refused := func(what string, taskID, env string, names []string, why string) {
		t.Helper()
		got, err := d.DeliverSecrets("test", taskID, env, names)
		if !errors.Is(err, ErrSecretRefused) || !strings.Contains(err.Error(), why) || len(got) != 0 {
			t.Errorf("FAIL: %s got %v, %v; want a refusal saying %q", what, got, err, why)
		}
		if err != nil && strings.Contains(err.Error(), "-value") {
			t.Errorf("FAIL: the refusal carries a value: %v", err)
		}
	}
	refused("an unprotected branch asking for production", "devrun.deploy", "production", []string{"CF_TOKEN"}, "protected branches only")
	refused("an unprotected branch asking for the repository's own", "devrun.deploy", "", []string{"SHARED"}, "protected branches only")
	refused("a landing's merge", "pairrun.deploy", "production", []string{"CF_TOKEN"}, "landing's merge")
	refused("a run the forge never launched", "nosuch.deploy", "production", []string{"CF_TOKEN"}, "no run")
	refused("a missing secret", "trunkrun.deploy", "production", []string{"CF_TOKEN", "NOPE"}, "no secret NOPE")
	refused("an environment the repository lacks", "trunkrun.deploy", "staging", []string{"CF_TOKEN"}, "does not have")
	refused("trunk asking for preview's secret", "trunkrun.deploy", "preview", []string{"PREVIEW_TOKEN"}, "branches dev")
	if got, err := d.DeliverSecrets("test", "devrun.deploy", "preview", []string{"PREVIEW_TOKEN"}); err != nil || got["PREVIEW_TOKEN"] != "preview-value" {
		t.Errorf("FAIL: dev got preview's secret as %v, %v; want it, preview names dev", got, err)
	}

	list, err := call("ci.environments", ciEnvironmentsIn{Repo: "test"})
	if err != nil {
		t.Fatal(err)
	}
	if j := asJSON(t, list); strings.Contains(j, "-value") || !strings.Contains(j, "CF_TOKEN") || !strings.Contains(j, `"ready":true`) {
		t.Errorf("FAIL: ci.environments = %s; want names and readiness, no values", j)
	}
	if _, err := r.Invoke(ops.WithUser(context.Background(), "alice"), "ci.secret.set", ciSecretSetIn{Repo: "test", Name: "BIG", Value: strings.Repeat("x", maxSecretValue+1)}); !errors.Is(err, ops.ErrInvalid) {
		t.Errorf("FAIL: an oversized value answered %v; want ErrInvalid", err)
	}
	nokey := d
	nokey.SecretBox = cisecret.Box{}
	if _, err := nokey.DeliverSecrets("test", "trunkrun.deploy", "production", []string{"CF_TOKEN"}); !errors.Is(err, cisecret.ErrNoKey) {
		t.Errorf("FAIL: a forge with no key delivered: %v", err)
	}
	t.Log("ok: trunk gets production, dev gets preview, merges and strangers get nothing, and no answer carries a value")
}

func asJSON(t *testing.T, v any) string {
	t.Helper()
	b, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	return string(b)
}