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