package runner
import (
"context"
"encoding/json"
"net/http/httptest"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
"time"
)
// A JOB'S RUN TOKEN REACHES ITS STEPS AS QUARRY_USER AND QUARRY_TOKEN,
// MASKED IN THE LOG; A DECLINE IS A LINE IN THE LOG, NOT A FAILURE
// (RFC 0021 §runs). A forge without the route hands nothing.
func TestARunTokenReachesTheJobMasked(t *testing.T) {
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
g, err := user.LookupGroupId(u.Gid)
if err != nil {
t.Skipf("no group name for gid %s: %v", u.Gid, err)
}
bin := t.TempDir()
fake := "#!/bin/sh\n# -n -u SLOT -- CMD...\n[ \"$6\" = job-reclaim ] && exit 0\nshift 4\nexec env QUARRY_TEST_JOBENV=1 \"$@\"\n"
if err := os.WriteFile(filepath.Join(bin, "sudo"), []byte(fake), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
step := Job{Steps: []JobStep{{Run: "echo user=${QUARRY_USER:-none} token=${QUARRY_TOKEN:-none}"}}}
raw, _ := json.Marshal(step)
with := func(id string) polled {
p := task(id, "c1")
p.Metadata = map[string]string{JobKey: string(raw)}
return p
}
run := func(jobUsers []string, token func(string) (map[string]string, string), ids ...string) *fakeBridge {
var q []polled
for _, id := range ids {
q = append(q, with(id))
}
b := &fakeBridge{queue: map[string][]polled{"t-job": q}, resolved: map[string]map[string]any{}, logs: map[string]string{}, token: token}
srv := httptest.NewServer(b.handler())
defer srv.Close()
r, err := New(Config{
Bridge: srv.URL, Token: "WT", Tasks: []string{"t-job"}, MaxTasks: 1,
WorkDir: t.TempDir(), Checkout: appCheckout{}, Heartbeat: 50 * time.Millisecond, Logf: t.Logf,
JobUsers: jobUsers, Self: self,
})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { r.Run(ctx); close(done) }()
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
b.mu.Lock()
n := len(b.resolved)
b.mu.Unlock()
if n == len(ids) {
break
}
}
cancel()
<-done
return b
}
forge := func(id string) (map[string]string, string) {
if id == "declined" {
return nil, "no run token: environment release requires the person who starts a run to hold a on alpha, and dev does not"
}
return map[string]string{"QUARRY_USER": "run-abc", "QUARRY_TOKEN": "qrt_abc_t0ken-value"}, ""
}
b := run([]string{g.Name}, forge, "publish", "declined")
if got := b.resolved["publish"]; got["action"] != "complete" {
t.Fatalf("FAIL: publish resolved %v, log %q", got, b.logs["publish"])
}
if log := b.logs["publish"]; strings.Contains(log, "t0ken-value") || !strings.Contains(log, "user=run-abc token=***") {
t.Errorf("FAIL: the job's log %q; want the login shown and the token masked", log)
}
if got, log := b.resolved["declined"], b.logs["declined"]; got["action"] != "complete" || !strings.Contains(log, "user=none token=none") || !strings.Contains(log, "requires the person who starts a run to hold a") {
t.Errorf("FAIL: a declined job resolved %v with log %q; want it run without a token and the reason logged", got, log)
}
b = run([]string{g.Name}, nil, "old")
if got, log := b.resolved["old"], b.logs["old"]; got["action"] != "complete" || !strings.Contains(log, "user=none") {
t.Errorf("FAIL: against a forge without the route the job resolved %v, log %q; want it run without a token", got, log)
}
// A runner running jobs as itself cannot keep a token to the job,
// so it never asks for one: the job runs without, and its log says
// why.
asked := false
b = run(nil, func(id string) (map[string]string, string) { asked = true; return forge(id) }, "publish")
if got, log := b.resolved["publish"], b.logs["publish"]; got["action"] != "complete" || asked || !strings.Contains(log, "user=none") || !strings.Contains(log, "--job-users") {
t.Errorf("FAIL: a non-isolating runner resolved %v (asked %v), log %q; want the job run without a token, unasked, and the reason logged", got, asked, log)
}
t.Log("ok: token in the job's env and masked; a decline is logged and the job runs; an old forge hands nothing")
}