package oltp
import (
"errors"
"slices"
"strings"
"testing"
)
// ENVIRONMENTS HOLD SECRETS UNDER A BRANCH POLICY, AND THE SETTINGS LOG
// SAYS WHO CHANGED WHAT, NEVER THE VALUE (RFC 0019 §environments).
func TestEnvironmentsAndSecrets(t *testing.T) {
d := testDB(t)
if err := d.SetSecret("alpha", "production", "TOKEN", []byte("sealed-1"), "dan"); !errors.Is(err, ErrNoEnvironment) {
t.Errorf("FAIL: a secret in an environment that does not exist answered %v; want ErrNoEnvironment", err)
}
if err := d.SetEnvironment("alpha", "production", []string{"trunk, release-*", "trunk"}, "dan"); err != nil {
t.Fatal(err)
}
for _, bad := range []string{"1TOKEN", "A-B", "", "a b"} {
if err := d.SetSecret("alpha", "production", bad, []byte("x"), "dan"); err == nil {
t.Errorf("FAIL: secret name %q was accepted", bad)
}
}
if err := d.SetEnvironment("alpha", "has space", nil, "dan"); err == nil {
t.Error("FAIL: an environment name with a space was accepted")
}
must := func(err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
must(d.SetSecret("alpha", "production", "TOKEN", []byte("sealed-1"), "dan"))
must(d.SetSecret("alpha", "production", "TOKEN", []byte("sealed-2"), "dan"))
must(d.SetSecret("alpha", "", "SHARED", []byte("sealed-3"), "dan"))
must(d.SetSecret("alpha", "", "TOKEN", []byte("sealed-4"), "dan"))
envs, err := d.Environments("alpha")
must(err)
if len(envs) != 2 || envs[0].Name != "" || envs[1].Name != "production" {
t.Fatalf("FAIL: environments %+v; want the repository's own, then production", envs)
}
if !slices.Equal(envs[1].Branches, []string{"release-*", "trunk"}) || len(envs[0].Branches) != 0 {
t.Errorf("FAIL: policies %v and %v; want production's patterns deduplicated and sorted, the repository's protected-only", envs[1].Branches, envs[0].Branches)
}
if len(envs[1].Secrets) != 1 || envs[1].Secrets[0].Name != "TOKEN" || envs[1].Secrets[0].UpdatedBy != "dan" {
t.Errorf("FAIL: production's secrets %+v", envs[1].Secrets)
}
got, err := d.SealedSecrets("alpha", "production", []string{"TOKEN", "SHARED", "MISSING"})
must(err)
if string(got["TOKEN"].Sealed) != "sealed-2" || got["TOKEN"].Environment != "production" {
t.Errorf("FAIL: TOKEN came from %q as %q; want production's latest, shadowing the repository's", got["TOKEN"].Environment, got["TOKEN"].Sealed)
}
if string(got["SHARED"].Sealed) != "sealed-3" || got["SHARED"].Environment != "" {
t.Errorf("FAIL: SHARED %+v; want the repository's own", got["SHARED"])
}
if _, ok := got["MISSING"]; ok {
t.Error("FAIL: a secret nobody set was returned")
}
if !slices.Equal(got["TOKEN"].Branches, []string{"release-*", "trunk"}) {
t.Errorf("FAIL: TOKEN carries policy %v; want production's", got["TOKEN"].Branches)
}
must(d.DeleteEnvironment("alpha", "production", "dan"))
if got, _ := d.SealedSecrets("alpha", "production", []string{"TOKEN"}); string(got["TOKEN"].Sealed) != "sealed-4" {
t.Errorf("FAIL: after deleting production, TOKEN is %q; want the repository's own", got["TOKEN"].Sealed)
}
log, err := d.SettingsLog("alpha", 50)
must(err)
var lines []string
for _, c := range log {
lines = append(lines, c.Field+" "+c.Was+" -> "+c.Now)
if strings.Contains(c.Was+c.Now, "sealed") {
t.Errorf("FAIL: the settings log carries a value: %+v", c)
}
}
joined := strings.Join(lines, "\n")
for _, want := range []string{
"environment:production (none) -> release-* trunk",
"secret:production/TOKEN (none) -> set",
"secret:production/TOKEN set -> replaced",
"secret:/SHARED (none) -> set",
"environment:production present -> deleted",
} {
if !strings.Contains(joined, want) {
t.Errorf("FAIL: the settings log lacks %q:\n%s", want, joined)
}
}
t.Log("ok: policies, shadowing, deletion and a log with no values")
}
// Deleting an environment logs each secret it took with it, and
// protecting or unprotecting a branch is logged with who did it.
func TestTheSettingsLogKeepsWhatWentAndWhoProtected(t *testing.T) {
d := testDB(t)
if err := d.SetEnvironment("alpha", "production", nil, "dan"); err != nil {
t.Fatal(err)
}
for _, n := range []string{"A_TOKEN", "B_TOKEN"} {
if err := d.SetSecret("alpha", "production", n, []byte("x"), "dan"); err != nil {
t.Fatal(err)
}
}
if err := d.DeleteEnvironment("alpha", "production", "dan"); err != nil {
t.Fatal(err)
}
if err := d.SetProtectionBy("alpha", "trunk", []string{"dan"}, "jan"); err != nil {
t.Fatal(err)
}
if err := d.SetProtectionBy("alpha", "trunk", []string{"dan"}, "jan"); err != nil { // no change: no line
t.Fatal(err)
}
if err := d.SetProtectionBy("alpha", "trunk", nil, "jan"); err != nil {
t.Fatal(err)
}
log, err := d.SettingsLog("alpha", 50)
if err != nil {
t.Fatal(err)
}
var lines []string
for _, c := range log {
lines = append(lines, c.Actor+" "+c.Field+" "+c.Was+" -> "+c.Now)
}
joined := strings.Join(lines, "\n")
for _, want := range []string{
"dan secret:production/A_TOKEN set -> deleted with its environment",
"dan secret:production/B_TOKEN set -> deleted with its environment",
"jan protect:trunk (not protected) -> dan",
"jan protect:trunk dan -> (not protected)",
} {
if !strings.Contains(joined, want) {
t.Errorf("FAIL: the settings log lacks %q:\n%s", want, joined)
}
}
if n := strings.Count(joined, "protect:trunk"); n != 2 {
t.Errorf("FAIL: %d protect lines; want 2, an unchanged save logs nothing", n)
}
}