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