Skip to content
quarry Artifact [087b952761]
Login

Artifact [087b952761]

Artifact 087b952761345235e489261ccac3da58c79c96aa1ea0c4f0452c21d4daf3b620:


package web

import (
	"crypto/rand"
	"encoding/base64"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

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

// THE ENVIRONMENTS SECTION MANAGES SECRETS WITHOUT EVER SHOWING ONE
// (RFC 0019): create an environment, save a secret into it and into the
// repository's own, see the names and never a value, delete them.
func TestEnvironmentsSectionNeverShowsAValue(t *testing.T) {
	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, _ := cisecret.New(base64.StdEncoding.EncodeToString(key))
	plane := &memCaps{stored: map[string]string{"dan": "a"}}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos:   api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}}),
		Roster:  capsRoster{plane},
		Secrets: db, SecretBox: box}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin}}),
		WithCaps(plane)}))
	t.Cleanup(srv.Close)

	body := asDan(t, srv, "/r/alpha/settings")
	if !strings.Contains(body, `id="environments"`) || !strings.Contains(body, "Repository secrets") || !strings.Contains(body, "${{ secrets.NAME }}") {
		t.Fatalf("FAIL: the settings page has no Environments section with the repository's own secrets")
	}
	post := func(form url.Values) {
		t.Helper()
		resp := postAsDan(t, srv, "/r/alpha/settings/environments", form)
		if resp.StatusCode != http.StatusSeeOther || !strings.HasSuffix(resp.Header.Get("Location"), "#environments") {
			t.Fatalf("FAIL: %v answered %d -> %s", form["action"], resp.StatusCode, resp.Header.Get("Location"))
		}
	}
	post(url.Values{"action": {"set-environment"}, "name": {"production"}, "branches": {""}})
	post(url.Values{"action": {"set-secret"}, "environment": {"production"}, "name": {"CF_TOKEN"}, "value": {"very-secret-value-1"}})
	post(url.Values{"action": {"set-secret"}, "environment": {""}, "name": {"SHARED"}, "value": {"very-secret-value-2"}})
	post(url.Values{"action": {"set-environment"}, "name": {"preview"}, "branches": {"dev, feat-*"}})

	body = asDan(t, srv, "/r/alpha/settings")
	if strings.Contains(body, "very-secret-value") {
		t.Fatal("FAIL: the settings page shows a secret's value")
	}
	for _, want := range []string{`data-hook="secret-name">CF_TOKEN<`, `data-hook="secret-name">SHARED<`, "protected branches only", "branches dev feat-*", `value="dev feat-*"`} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: the settings page lacks %q", want)
		}
	}
	post(url.Values{"action": {"delete-secret"}, "environment": {"production"}, "name": {"CF_TOKEN"}})
	post(url.Values{"action": {"delete-environment"}, "name": {"preview"}})
	body = asDan(t, srv, "/r/alpha/settings")
	if strings.Contains(body, `>CF_TOKEN<`) || strings.Contains(body, "feat-*") {
		t.Error("FAIL: a deleted secret or environment is still listed")
	}
	log, _ := db.SettingsLog("alpha", 20)
	if len(log) < 5 || log[0].Actor != "dan" {
		t.Errorf("FAIL: the settings log has %d rows, latest by %q; want each change, by dan", len(log), log[0].Actor)
	}
	t.Log("ok: created, saved, listed by name only, deleted, and logged")
}

// AN ENVIRONMENT, AND THE DEFAULT POLICY, SAY WHAT A RUN TOKEN MAY CARRY
// THERE AND WHICH LETTER ITS STARTER MUST HOLD (RFC 0021 §runs), edited
// where the branches are, without disturbing them.
func TestEnvironmentsCarryRunTokenRules(t *testing.T) {
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	plane := &memCaps{stored: map[string]string{"dan": "a"}}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos:   api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}}),
		Roster:  capsRoster{plane},
		Secrets: db}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin}}),
		WithCaps(plane)}))
	t.Cleanup(srv.Close)
	rules := func() map[string]map[string]string {
		out := map[string]map[string]string{}
		for _, h := range hookAttrs(asDan(t, srv, "/r/alpha/settings"), "env-tokens") {
			out[h["data-env"]] = h
		}
		return out
	}
	if got := rules()[""]; got == nil || got["data-allowed"] != "go8" || got["data-starter"] != "" {
		t.Fatalf("FAIL: the default policy's run token rule %v; want allowed go8, no starter letter", got)
	}
	post := func(form url.Values) {
		t.Helper()
		resp := postAsDan(t, srv, "/r/alpha/settings/environments", form)
		if resp.StatusCode != http.StatusSeeOther {
			t.Fatalf("FAIL: %v answered %d", form, resp.StatusCode)
		}
	}
	post(url.Values{"action": {"set-environment"}, "name": {"release"}, "branches": {"trunk"}})
	post(url.Values{"action": {"set-environment"}, "name": {"release"}, "branches": {"trunk"}, "allowed": {"y"}, "starter": {"a"}})
	post(url.Values{"action": {"set-environment"}, "name": {""}, "branches": {""}, "allowed": {"g o"}, "starter": {""}})
	got := rules()
	if got["release"]["data-allowed"] != "y" || got["release"]["data-starter"] != "a" {
		t.Errorf("FAIL: release shows %v; want allowed y, starter a", got["release"])
	}
	if got[""]["data-allowed"] != "go" {
		t.Errorf("FAIL: the default policy shows %v; want allowed go", got[""])
	}
	if p, _ := db.EnvironmentPolicy("alpha", "release"); len(p.Branches) != 1 || p.Branches[0] != "trunk" || p.Allowed != "y" || p.Starter != "a" {
		t.Errorf("FAIL: release stored %+v; want trunk kept, allowed y, starter a", p)
	}
	t.Log("ok: allowed and the starter letter shown and edited per environment and on the default policy")
}

// An environment's run token ceiling is set and shown in a workflow's
// own words, one choice per kind of access, never as letters: the
// release recipe (publish only, admins start) is three choices.
func TestEnvironmentsAreSetInWordsNotLetters(t *testing.T) {
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	plane := &memCaps{stored: map[string]string{"dan": "a"}}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos:   api.NewRepos(map[string]*api.Repo{"alpha": {Index: landingIndex(t, false)}}),
		Roster:  capsRoster{plane},
		Secrets: db}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan", roles: map[string]ops.Auth{"alpha": ops.Admin}}),
		WithCaps(plane)}))
	t.Cleanup(srv.Close)

	// The create form carries the choices, so one submit makes the
	// release environment whole.
	form := url.Values{"action": {"set-environment"}, "name": {"release"}, "branches": {"trunk"}, "starter": {"a"}}
	for _, n := range []string{"contents", "tickets", "wiki", "forum", "checks"} {
		form.Set("perm_"+n, "none")
	}
	form.Set("perm_uv", "write")
	if resp := postAsDan(t, srv, "/r/alpha/settings/environments", form); resp.StatusCode != http.StatusSeeOther {
		t.Fatalf("FAIL: creating release answered %d", resp.StatusCode)
	}
	p, _ := db.EnvironmentPolicy("alpha", "release")
	if p.Allowed != "y" || p.Starter != "a" || len(p.Branches) != 1 || p.Branches[0] != "trunk" {
		t.Fatalf("FAIL: release stored %+v; want publish only (y), admins start (a), trunk", p)
	}

	page := asDan(t, srv, "/r/alpha/settings")
	levels := map[string]string{}
	for _, h := range hookAttrs(page, "env-perm") {
		if h["data-noun"] == "uv" || h["data-noun"] == "contents" {
			levels[h["data-noun"]] = levels[h["data-noun"]] + h["data-level"] + ";"
		}
	}
	if !strings.Contains(levels["uv"], "write;") || !strings.Contains(levels["contents"], "none;") {
		t.Errorf("FAIL: the page's choices %v; want release shown as uv write and contents none", levels)
	}
	if strings.Contains(page, `placeholder="letters`) {
		t.Error("FAIL: the page still asks for letters")
	}
	t.Log("ok: an environment is created whole in words, and shown back in them")
}

// The settings trail words an environment's run token changes as the
// panel does, never as stored keys and letters.
func TestTrailWordsEnvironmentChanges(t *testing.T) {
	l := trailLineOf(SettingsChange{Field: "environment:release:allowed", Was: "go8", Now: "y"})
	if !l.Words || l.Now != permWordsOf("y") || l.Was != permWordsOf("go8") || strings.Contains(l.Label, "allowed") {
		t.Fatalf("FAIL: %+v; want the change worded, not keyed", l)
	}
	l = trailLineOf(SettingsChange{Field: "environment:release:starter", Was: "", Now: "a"})
	if !l.Words || l.Now != starterWordsOf("a") || l.Was == "" {
		t.Fatalf("FAIL: %+v; want who starts, in words, before and after", l)
	}
	if l = trailLineOf(SettingsChange{Field: "visibility", Was: "public", Now: "private"}); l.Words {
		t.Fatalf("FAIL: an unrelated field %+v was reworded", l)
	}
	t.Log("ok: environment changes read in words in the trail")
}