quorate/services/stalwart/cmd/controller/main_test.go

1381 lines
47 KiB
Go
Raw Permalink Normal View History

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
"time"
"gopkg.in/yaml.v3"
)
func servicePath(parts ...string) string {
return filepath.Join(append([]string{"..", ".."}, parts...)...)
}
func serviceFiles(t *testing.T, paths ...string) []byte {
t.Helper()
var content []byte
for _, path := range paths {
value, err := os.ReadFile(servicePath(strings.Split(path, "/")...))
if err != nil {
t.Fatal(err)
}
content = append(content, value...)
}
return content
}
type probeHTTP struct {
Path string `yaml:"path"`
Port any `yaml:"port"`
Scheme string `yaml:"scheme"`
HTTPHeaders []struct {
Name string `yaml:"name"`
Value string `yaml:"value"`
} `yaml:"httpHeaders"`
}
type deploymentManifest struct {
Spec struct {
Replicas int `yaml:"replicas"`
Strategy struct {
Type string `yaml:"type"`
RollingUpdate struct {
MaxUnavailable int `yaml:"maxUnavailable"`
MaxSurge int `yaml:"maxSurge"`
} `yaml:"rollingUpdate"`
} `yaml:"strategy"`
Template struct {
Metadata struct {
Annotations map[string]string `yaml:"annotations"`
} `yaml:"metadata"`
Spec struct {
Volumes []struct {
Name string `yaml:"name"`
Projected *struct {
Sources []struct {
ServiceAccountToken *struct {
Audience string `yaml:"audience"`
ExpirationSeconds *int64 `yaml:"expirationSeconds"`
} `yaml:"serviceAccountToken"`
} `yaml:"sources"`
} `yaml:"projected"`
} `yaml:"volumes"`
TopologySpreadConstraints []struct {
MaxSkew int `yaml:"maxSkew"`
MinDomains int `yaml:"minDomains"`
TopologyKey string `yaml:"topologyKey"`
WhenUnsatisfiable string `yaml:"whenUnsatisfiable"`
} `yaml:"topologySpreadConstraints"`
Containers []struct {
Name string `yaml:"name"`
Image string `yaml:"image"`
Command []string `yaml:"command"`
Args []string `yaml:"args"`
VolumeMounts []struct {
MountPath string `yaml:"mountPath"`
} `yaml:"volumeMounts"`
ReadinessProbe struct {
HTTPGet *probeHTTP `yaml:"httpGet"`
TCPSocket *struct {
Port any `yaml:"port"`
} `yaml:"tcpSocket"`
} `yaml:"readinessProbe"`
} `yaml:"containers"`
} `yaml:"spec"`
} `yaml:"template"`
} `yaml:"spec"`
}
func serviceDeployment(t *testing.T, path string) deploymentManifest {
t.Helper()
var deployment deploymentManifest
if err := yaml.Unmarshal(serviceFiles(t, path), &deployment); err != nil {
t.Fatal(err)
}
return deployment
}
func hasArgument(arguments []string, value string) bool {
return slices.Contains(arguments, value)
}
func argumentValue(arguments []string, name string) string {
for index := 0; index+1 < len(arguments); index++ {
if arguments[index] == name {
return arguments[index+1]
}
}
return ""
}
func hexValue(value string, length int) bool {
return len(value) == length && strings.Trim(value, "0123456789abcdef") == ""
}
func qualifiedImage(image, repository string) bool {
prefix, digest, found := strings.Cut(image, "@sha256:")
return found && strings.HasPrefix(prefix, repository+":") && hexValue(digest, 64)
}
func TestParseOptionsUsesProvidedArguments(t *testing.T) {
value, err := parseOptions([]string{
"--plan", "desired.json",
"--stalwart-url", "https://mail.example.invalid",
"--once",
})
if err != nil {
t.Fatal(err)
}
if value.plan != "desired.json" || value.stalwartURL != "https://mail.example.invalid" || !value.once {
t.Fatalf("options = %#v", value)
}
if err := validateOptions(value); err != nil {
t.Fatal(err)
}
}
func TestRunCLIReturnsRedactedDiagnostic(t *testing.T) {
path := filepath.Join(t.TempDir(), "stalwart-cli")
if err := os.WriteFile(path, []byte("#!/bin/sh\nprintf 'rejected token %s\\n' \"$STALWART_TOKEN\" >&2\nexit 1\n"), 0o700); err != nil {
t.Fatal(err)
}
_, err := runCLI(t.Context(), path, []string{"STALWART_TOKEN=synthetic-sensitive-value"}, false, "apply", "--file")
if err == nil {
t.Fatal("failed command returned no error")
}
value := err.Error()
if strings.Contains(value, "synthetic-sensitive-value") || !strings.Contains(value, "rejected token [redacted]") {
t.Fatalf("diagnostic = %q", value)
}
}
func TestNextBackoffIsBounded(t *testing.T) {
maximum := 5 * time.Minute
value := 5 * time.Second
for range 20 {
value = nextBackoff(value, maximum)
}
if value != maximum {
t.Fatalf("backoff = %s, want %s", value, maximum)
}
}
func TestPersistentLeaderFailureRelinquishesOwnership(t *testing.T) {
store := &statusStore{status: controllerStatus{LastSuccess: "2026-01-01T00:00:00Z", Ready: true}}
for range 2 {
if recordLeaderFailure(store, 3) {
t.Fatal("leader relinquished ownership before the failure threshold")
}
}
if !recordLeaderFailure(store, 3) {
t.Fatal("leader did not relinquish ownership at the failure threshold")
}
status := store.snapshot()
if status.Ready || !status.PersistentFailure || status.ConsecutiveFailures != 3 || status.LastResult != "failure" {
t.Fatalf("status = %#v", status)
}
}
func TestOwnershipSessionUsesBoundedKeepalives(t *testing.T) {
var statements []string
if err := configureOwnershipSession(context.Background(), func(_ context.Context, statement string) error {
statements = append(statements, statement)
return nil
}); err != nil {
t.Fatal(err)
}
if !slices.Equal(statements, ownershipSessionSettings) {
t.Fatalf("statements = %q, want %q", statements, ownershipSessionSettings)
}
}
func TestOwnershipSessionStopsAtSettingFailure(t *testing.T) {
failed := errors.New("setting failed")
var statements []string
err := configureOwnershipSession(context.Background(), func(_ context.Context, statement string) error {
statements = append(statements, statement)
if len(statements) == 2 {
return failed
}
return nil
})
if !errors.Is(err, failed) {
t.Fatalf("error = %v, want %v", err, failed)
}
if !slices.Equal(statements, ownershipSessionSettings[:2]) {
t.Fatalf("statements = %q, want %q", statements, ownershipSessionSettings[:2])
}
}
func TestRunCLIErrorNamesAction(t *testing.T) {
path := filepath.Join(t.TempDir(), "stalwart-cli")
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 17\n"), 0o700); err != nil {
t.Fatal(err)
}
_, err := runCLI(t.Context(), path, nil, false, "apply", "--file", "synthetic-plan")
if err == nil || !strings.Contains(err.Error(), "stalwart CLI apply --file") {
t.Fatalf("error = %v", err)
}
}
type recordedOwnership struct {
steps *[]string
}
func (value recordedOwnership) Close(context.Context) error {
*value.steps = append(*value.steps, "ownership")
return nil
}
func (recordedOwnership) Ping(context.Context) error {
return nil
}
type recordedServer struct {
steps *[]string
}
type testOwnershipManager struct {
mu sync.Mutex
held bool
}
type testOwnership struct {
manager *testOwnershipManager
}
func (value testOwnership) Close(context.Context) error {
value.manager.mu.Lock()
defer value.manager.mu.Unlock()
value.manager.held = false
return nil
}
func (testOwnership) Ping(context.Context) error {
return nil
}
func (manager *testOwnershipManager) acquire(context.Context, string) (ownershipConnection, error) {
manager.mu.Lock()
defer manager.mu.Unlock()
if manager.held {
return nil, errOwnerExists
}
manager.held = true
return testOwnership{manager: manager}, nil
}
func TestControllersHandOffOwnership(t *testing.T) {
manager := &testOwnershipManager{}
type runningController struct {
cancel context.CancelFunc
done chan error
reconciled chan struct{}
}
controllers := make([]runningController, 2)
for index := range controllers {
ctx, cancel := context.WithCancel(t.Context())
done := make(chan error, 1)
reconciled := make(chan struct{}, 1)
runtime := controllerRuntime{
acquireOwnership: manager.acquire,
planRevision: func(string) (string, error) { return "synthetic-revision", nil },
reconcile: func(context.Context, options) error {
reconciled <- struct{}{}
return nil
},
}
value := options{
dsnFile: "synthetic-dsn",
plan: "synthetic-plan",
stalwartURL: "http://stalwart.invalid",
bootstrapUser: "recovery",
bootstrapPasswordFile: "synthetic-password",
statusAddress: "127.0.0.1:0",
interval: time.Hour,
initialBackoff: 10 * time.Millisecond,
maxBackoff: 20 * time.Millisecond,
reconcileTimeout: time.Second,
persistentFailureAfter: 3,
}
go func() { done <- runControllerWithRuntime(ctx, value, runtime) }()
controllers[index] = runningController{cancel: cancel, done: done, reconciled: reconciled}
}
leader := -1
select {
case <-controllers[0].reconciled:
leader = 0
case <-controllers[1].reconciled:
leader = 1
case <-time.After(2 * time.Second):
t.Fatal("neither controller reconciled")
}
standby := 1 - leader
select {
case <-controllers[standby].reconciled:
t.Fatal("standby reconciled while the leader held ownership")
case <-time.After(50 * time.Millisecond):
}
controllers[leader].cancel()
if err := <-controllers[leader].done; err != nil {
t.Fatalf("leader shutdown: %v", err)
}
select {
case <-controllers[standby].reconciled:
case <-time.After(2 * time.Second):
t.Fatal("standby did not reconcile after leader shutdown")
}
controllers[standby].cancel()
if err := <-controllers[standby].done; err != nil {
t.Fatalf("standby shutdown: %v", err)
}
}
func TestUnhealthyLeaderCoolsDownWhileStandbyTakesOwnership(t *testing.T) {
manager := &testOwnershipManager{}
baseOptions := options{
dsnFile: "synthetic-dsn",
plan: "synthetic-plan",
stalwartURL: "http://stalwart.invalid",
bootstrapUser: "recovery",
bootstrapPasswordFile: "synthetic-password",
statusAddress: "127.0.0.1:0",
interval: time.Hour,
initialBackoff: 10 * time.Millisecond,
maxBackoff: 200 * time.Millisecond,
reconcileTimeout: time.Second,
persistentFailureAfter: 1,
}
unhealthyContext, cancelUnhealthy := context.WithCancel(t.Context())
unhealthyDone := make(chan error, 1)
unhealthyReconciled := make(chan struct{}, 1)
unhealthyRuntime := controllerRuntime{
acquireOwnership: manager.acquire,
planRevision: func(string) (string, error) { return "synthetic-revision", nil },
reconcile: func(context.Context, options) error {
unhealthyReconciled <- struct{}{}
return errors.New("synthetic reconciliation failure")
},
}
go func() {
unhealthyDone <- runControllerWithRuntime(unhealthyContext, baseOptions, unhealthyRuntime)
}()
select {
case <-unhealthyReconciled:
case <-time.After(2 * time.Second):
t.Fatal("unhealthy controller did not reconcile")
}
healthyContext, cancelHealthy := context.WithCancel(t.Context())
healthyDone := make(chan error, 1)
healthyReconciled := make(chan struct{}, 1)
healthyRuntime := controllerRuntime{
acquireOwnership: manager.acquire,
planRevision: func(string) (string, error) { return "synthetic-revision", nil },
reconcile: func(context.Context, options) error {
healthyReconciled <- struct{}{}
return nil
},
}
go func() {
healthyDone <- runControllerWithRuntime(healthyContext, baseOptions, healthyRuntime)
}()
select {
case <-healthyReconciled:
case <-time.After(2 * time.Second):
t.Fatal("healthy standby did not take ownership")
}
select {
case err := <-unhealthyDone:
t.Fatalf("unhealthy controller exited during cooldown: %v", err)
default:
}
cancelHealthy()
if err := <-healthyDone; err != nil {
t.Fatalf("healthy controller shutdown: %v", err)
}
cancelUnhealthy()
if err := <-unhealthyDone; err != nil {
t.Fatalf("unhealthy controller shutdown: %v", err)
}
}
func (value recordedServer) Shutdown(context.Context) error {
*value.steps = append(*value.steps, "server")
return nil
}
func TestControllerShutdownReleasesOwnershipBeforeStatusServer(t *testing.T) {
var steps []string
closeOwnership(recordedOwnership{steps: &steps})
shutdownStatusServer(recordedServer{steps: &steps})
if !slices.Equal(steps, []string{"ownership", "server"}) {
t.Fatalf("shutdown order = %v", steps)
}
}
func TestStandbyStatusIsLiveButNotReady(t *testing.T) {
store := &statusStore{status: controllerStatus{LastResult: "standby"}}
handler := statusHandler(store)
for _, check := range []struct {
path string
want int
}{
{path: "/healthz", want: http.StatusOK},
{path: "/readyz", want: http.StatusServiceUnavailable},
} {
response := httptest.NewRecorder()
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, check.path, nil))
if response.Code != check.want {
t.Fatalf("%s status = %d, want %d", check.path, response.Code, check.want)
}
}
}
func TestPlanRevisionChangesWithReadableSource(t *testing.T) {
path := filepath.Join(t.TempDir(), "plan.json")
if err := os.WriteFile(path, []byte(`[{"revision":1}]`), 0o600); err != nil {
t.Fatal(err)
}
first, err := planRevision(path)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(`[{"revision":2}]`), 0o600); err != nil {
t.Fatal(err)
}
second, err := planRevision(path)
if err != nil {
t.Fatal(err)
}
if first == second {
t.Fatal("plan revision did not change")
}
}
func TestRenderPlanInjectsSecretWithoutChangingSource(t *testing.T) {
root := t.TempDir()
source := filepath.Join(root, "plan.json")
target := filepath.Join(root, "plan.ndjson")
secretRoot := filepath.Join(root, "secrets")
if err := os.Mkdir(secretRoot, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(secretRoot, "password"), []byte("projected-value"), 0o400); err != nil {
t.Fatal(err)
}
original := `[{"credential":{"$secretFile":"password"},"name":"account"}]`
if err := os.WriteFile(source, []byte(original), 0o600); err != nil {
t.Fatal(err)
}
if err := renderPlan(source, target, secretRoot); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
var operation map[string]any
if err := json.Unmarshal(content, &operation); err != nil {
t.Fatal(err)
}
if operation["credential"] != "projected-value" {
t.Fatalf("credential = %v", operation["credential"])
}
unchanged, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
if string(unchanged) != original {
t.Fatal("readable source changed")
}
info, err := os.Stat(target)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("rendered mode = %o", info.Mode().Perm())
}
}
func TestRenderSecretSet(t *testing.T) {
root := t.TempDir()
source := filepath.Join(root, "plan.json")
target := filepath.Join(root, "plan.ndjson")
const audience = `synthetic/oidc"audience\client`
if err := os.WriteFile(filepath.Join(root, "audience"), []byte(audience+"\n"), 0o600); err != nil {
t.Fatal(err)
}
original := `[{"@type":"reconcile","object":"Directory","value":{"zitadel":{"@type":"Oidc","acceptedAudiences":{"$secretSet":"audience"}}}}]`
if err := os.WriteFile(source, []byte(original), 0o600); err != nil {
t.Fatal(err)
}
if err := renderPlan(source, target, root); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
var operation struct {
Value map[string]struct {
Audiences map[string]bool `json:"acceptedAudiences"`
} `json:"value"`
}
if err := json.Unmarshal(content, &operation); err != nil {
t.Fatal(err)
}
audiences := operation.Value["zitadel"].Audiences
if len(audiences) != 1 || !audiences[audience] {
t.Fatalf("accepted audiences = %v", audiences)
}
unchanged, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
if string(unchanged) != original {
t.Fatal("readable source changed")
}
}
func TestSecretSetMissing(t *testing.T) {
_, err := injectSecrets(map[string]any{"$secretSet": "missing"}, t.TempDir())
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("missing secret error = %v", err)
}
}
func TestProductionPlanRendersFromProjectedSecrets(t *testing.T) {
secretRoot := t.TempDir()
secrets := map[string]string{
"dragonfly_url": "redis://default:[email protected]:6379",
"oidc_audience": "synthetic-oidc-audience",
"oidc_controller_subject": "synthetic-controller-subject",
"oidc_human_admin_subject": "synthetic-human-subject",
"oidc_issuer_url": "https://auth.example.invalid",
"oidc_notifier_subject": "synthetic-notifier-subject",
"oidc_sender_subject": "synthetic-sender-subject",
"oidc_test_subject": "synthetic-test-subject",
"relay_endpoint": "smtp.email.ap-osaka-1.oci.oraclecloud.com",
"relay_username": "synthetic-relay-user",
}
for name, value := range secrets {
if err := os.WriteFile(filepath.Join(secretRoot, name), []byte(value+"\n"), 0o600); err != nil {
t.Fatal(err)
}
}
target := filepath.Join(t.TempDir(), "plan.ndjson")
if err := renderPlan(servicePath("k8s", "base", "plan.json"), target, secretRoot); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(target)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(content, []byte(`"$secretFile"`)) {
t.Fatal("rendered production plan contains an unresolved secret file")
}
for _, value := range secrets {
if !bytes.Contains(content, []byte(value)) {
t.Fatalf("rendered production plan does not contain projected value %q", value)
}
}
decoder := json.NewDecoder(bytes.NewReader(content))
operations := 0
for {
var operation map[string]any
err := decoder.Decode(&operation)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatal(err)
}
if operation["@type"] == nil || operation["object"] == nil {
t.Fatalf("operation %d is incomplete", operations)
}
if operation["object"] == "Certificate" {
t.Fatal("production plan owns certificate lifecycle outside the controller")
}
operations++
}
if operations != 34 {
t.Fatalf("rendered production plan has %d operations, want 34", operations)
}
}
func TestProductionPlanOwnsOutboundSchedules(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "base", "plan.json"))
if err != nil {
t.Fatal(err)
}
var operations []struct {
Type string `json:"@type"`
Object string `json:"object"`
Value json.RawMessage `json:"value"`
}
if err := json.Unmarshal(content, &operations); err != nil {
t.Fatal(err)
}
queues := map[string]string{}
schedules := map[string]string{}
for _, operation := range operations {
if operation.Object != "MtaVirtualQueue" && operation.Object != "MtaDeliverySchedule" {
continue
}
if operation.Type != "reconcile" {
t.Fatalf("%s operation is %q, want reconcile", operation.Object, operation.Type)
}
var values map[string]struct {
Name string `json:"name"`
QueueID string `json:"queueId"`
}
if err := json.Unmarshal(operation.Value, &values); err != nil {
t.Fatal(err)
}
for reference, value := range values {
if operation.Object == "MtaVirtualQueue" {
queues[value.Name] = "#" + reference
} else {
schedules[value.Name] = value.QueueID
}
}
}
for _, name := range []string{"local", "remote", "dsn", "report"} {
queueReference, queueExists := queues[name]
scheduleReference, scheduleExists := schedules[name]
if !queueExists || !scheduleExists {
t.Fatalf("outbound inventory is missing %q", name)
}
if scheduleReference != queueReference {
t.Fatalf("schedule %q references %q, want %q", name, scheduleReference, queueReference)
}
}
if len(queues) != 4 || len(schedules) != 4 {
t.Fatalf("outbound inventory has %d queues and %d schedules, want 4 each", len(queues), len(schedules))
}
}
func TestProductionPlanBindsZitadelSubjectsWithoutNativeCredentials(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "base", "plan.json"))
if err != nil {
t.Fatal(err)
}
value := string(content)
for _, required := range []string{
`"claimUsername": "sub"`,
`"usernameDomain": "mail-auth.quorate.internal"`,
`"$secretFile": "oidc_human_admin_subject"`,
`"$secretFile": "oidc_controller_subject"`,
`"$secretFile": "oidc_notifier_subject"`,
`"$secretFile": "oidc_sender_subject"`,
`"$secretFile": "oidc_test_subject"`,
} {
if !strings.Contains(value, required) {
t.Fatalf("production plan lacks %s", required)
}
}
for _, forbidden := range []string{
`"$secretFile": "operator_password"`,
`"$secretFile": "controller_password"`,
`"$secretFile": "alerts_password"`,
`"$secretFile": "test_password"`,
`"claimUsername": "preferred_username"`,
} {
if strings.Contains(value, forbidden) {
t.Fatalf("production plan contains %s", forbidden)
}
}
}
func TestProductionPlanExposesOnlyAccountAPIEndpointsOnUserHTTP(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "base", "plan.json"))
if err != nil {
t.Fatal(err)
}
value := string(content)
required := `listener == \"user-http\" && starts_with(path, \"/api\") && path != \"/api/schema\" && !starts_with(path, \"/api/schema/\") && path != \"/api/account\" && !starts_with(path, \"/api/discover/\")`
if !strings.Contains(value, required) {
t.Fatal("production plan lacks the account API boundary")
}
}
func TestProductionPlanReconcilesZitadelOwnedAccounts(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "base", "plan.json"))
if err != nil {
t.Fatal(err)
}
var operations []struct {
Type string `json:"@type"`
Object string `json:"object"`
Value json.RawMessage `json:"value"`
}
if err := json.Unmarshal(content, &operations); err != nil {
t.Fatal(err)
}
for _, operation := range operations {
if operation.Object != "Account" {
continue
}
if operation.Type != "upsert" {
t.Fatalf("Account operation is %q", operation.Type)
}
var accounts map[string]struct {
Credentials map[string]any `json:"credentials"`
Name string `json:"name"`
DomainID string `json:"domainId"`
Aliases map[string]struct {
Name any `json:"name"`
DomainID string `json:"domainId"`
} `json:"aliases"`
Permissions struct {
Type string `json:"@type"`
Enabled map[string]bool `json:"enabledPermissions"`
} `json:"permissions"`
}
if err := json.Unmarshal(operation.Value, &accounts); err != nil {
t.Fatal(err)
}
for _, name := range []string{"human-admin", "test-client", "controller", "alert-sender", "notification-sender"} {
if len(accounts[name].Credentials) != 0 {
t.Fatalf("%s has native credentials", name)
}
}
operatorAccount := accounts["operator"]
if len(operatorAccount.Aliases) != 0 {
t.Fatal("operator mailbox retains the test address alias")
}
operator := operatorAccount.Permissions
if operator.Type != "Replace" || len(operator.Enabled) != 1 || !operator.Enabled["emailReceive"] {
t.Fatal("operator mailbox cannot explicitly receive mail")
}
human := accounts["human-admin"].Permissions
if human.Type != "Replace" || len(human.Enabled) != 625 || !human.Enabled["authenticate"] || !human.Enabled["emailReceive"] || !human.Enabled["emailSend"] || !human.Enabled["sysDomainQuery"] {
t.Fatal("human administrator permissions are incomplete")
}
controller := accounts["controller"].Permissions
if controller.Type != "Replace" || len(controller.Enabled) != len(human.Enabled) || !maps.Equal(controller.Enabled, human.Enabled) {
t.Fatal("controller permissions are not explicit for full reconciliation")
}
sender := accounts["alert-sender"].Permissions
if sender.Type != "Replace" || len(sender.Enabled) != 7 || !sender.Enabled["authenticate"] || !sender.Enabled["emailSend"] {
t.Fatal("sender permissions are not restricted")
}
notifier := accounts["notification-sender"]
notifierSubject, ok := notifier.Aliases["0"].Name.(map[string]any)
if notifier.Name != "noreply" || notifier.DomainID != "#domain" || len(notifier.Aliases) != 1 || !ok || notifierSubject["$secretFile"] != "oidc_notifier_subject" || notifier.Aliases["0"].DomainID != "#authentication-domain" {
t.Fatal("notification sender lacks its Zitadel subject alias")
}
notifierPermissions := notifier.Permissions
if notifierPermissions.Type != "Replace" || len(notifierPermissions.Enabled) != 7 || !notifierPermissions.Enabled["authenticate"] || !notifierPermissions.Enabled["emailSend"] {
t.Fatal("notification sender permissions are not restricted")
}
testClient := accounts["test-client"]
testSubject, ok := testClient.Aliases["0"].Name.(map[string]any)
if len(testClient.Aliases) != 1 || !ok || testSubject["$secretFile"] != "oidc_test_subject" || testClient.Aliases["0"].DomainID != "#authentication-domain" {
t.Fatal("test client lacks its Zitadel subject alias")
}
testPermissions := testClient.Permissions
if testPermissions.Type != "Replace" || !testPermissions.Enabled["authenticate"] || !testPermissions.Enabled["emailReceive"] || !testPermissions.Enabled["emailSend"] || !testPermissions.Enabled["imapIdle"] || !testPermissions.Enabled["imapSearch"] {
t.Fatal("test client permissions are incomplete")
}
return
}
t.Fatal("Account operation is absent")
}
func TestBootstrapJobOwnsNoDesiredState(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "bootstrap", "server", "job.yaml"))
if err != nil {
t.Fatal(err)
}
value := string(content)
for _, forbidden := range []string{"--plan", "/desired/", "$secretFile"} {
if strings.Contains(value, forbidden) {
t.Fatalf("bootstrap Job contains %q", forbidden)
}
}
if !strings.Contains(value, "http://stalwart-controller:9090/readyz") {
t.Fatal("bootstrap Job does not wait for controller readiness")
}
}
func TestNormalControllerPrefersAStandbyOnAnotherNode(t *testing.T) {
deployment := serviceDeployment(t, "k8s/normal/controller/deploy.yaml")
if deployment.Spec.Replicas != 2 || deployment.Spec.Strategy.Type != "RollingUpdate" ||
deployment.Spec.Strategy.RollingUpdate.MaxUnavailable != 1 || deployment.Spec.Strategy.RollingUpdate.MaxSurge != 0 {
t.Fatalf("normal controller rollout = %#v", deployment.Spec)
}
if len(deployment.Spec.Template.Spec.TopologySpreadConstraints) != 1 {
t.Fatalf("topology spread constraints = %#v", deployment.Spec.Template.Spec.TopologySpreadConstraints)
}
spread := deployment.Spec.Template.Spec.TopologySpreadConstraints[0]
if spread.MaxSkew != 1 || spread.MinDomains != 2 || spread.TopologyKey != "kubernetes.io/hostname" || spread.WhenUnsatisfiable != "DoNotSchedule" {
t.Fatalf("topology spread constraint = %#v", spread)
}
if len(deployment.Spec.Template.Spec.Containers) != 1 || !hasArgument(deployment.Spec.Template.Spec.Containers[0].Args, "--standby-ready") {
t.Fatal("normal controller does not configure a ready standby")
}
content := string(serviceFiles(t, "k8s/normal/controller/pdb.yaml", "k8s/normal/kustomization.yaml"))
for _, required := range []string{"minAvailable: 1", "- controller/pdb.yaml", "- ../controller"} {
if !strings.Contains(content, required) {
t.Fatalf("normal controller HA manifests lack %q", required)
}
}
}
func TestControllerModesPatchOneSharedWorkload(t *testing.T) {
for _, path := range []string{"k8s/bootstrap/kustomization.yaml", "k8s/normal/kustomization.yaml"} {
var kustomization struct {
Resources []string `yaml:"resources"`
Patches []struct {
Path string `yaml:"path"`
} `yaml:"patches"`
}
if err := yaml.Unmarshal(serviceFiles(t, path), &kustomization); err != nil {
t.Fatal(err)
}
if !slices.Contains(kustomization.Resources, "../controller") {
t.Fatalf("%s does not use the shared controller", path)
}
controllerPatches := 0
for _, patch := range kustomization.Patches {
if patch.Path == "controller/deploy.yaml" {
controllerPatches++
}
}
if controllerPatches != 1 {
t.Fatalf("%s has %d controller deployment patches", path, controllerPatches)
}
}
}
func TestReplicatedStalwartWorkloadsUseHardTopologySpreading(t *testing.T) {
for _, path := range []string{"k8s/normal/server/deploy.yaml"} {
deployment := serviceDeployment(t, path)
if len(deployment.Spec.Template.Spec.TopologySpreadConstraints) != 1 {
t.Fatalf("%s does not have one topology spread constraint", path)
}
spread := deployment.Spec.Template.Spec.TopologySpreadConstraints[0]
if spread.MaxSkew != 1 || spread.MinDomains != 2 || spread.TopologyKey != "kubernetes.io/hostname" || spread.WhenUnsatisfiable != "DoNotSchedule" {
t.Fatalf("%s topology spread constraint = %#v", path, spread)
}
}
}
func TestControllerManifestsPinQualifiedImage(t *testing.T) {
const repository = "registry.quorate.internal/quorate/stalwart-controller"
controller := serviceDeployment(t, "k8s/controller/deploy.yaml")
if len(controller.Spec.Template.Spec.Containers) != 1 {
t.Fatal("shared controller deployment does not contain one controller")
}
image := controller.Spec.Template.Spec.Containers[0].Image
if !qualifiedImage(image, repository) {
t.Fatal("shared controller deployment does not pin the qualified controller image")
}
revision := controller.Spec.Template.Metadata.Annotations["quorate.internal/source-revision"]
if !hexValue(revision, 40) {
t.Fatal("shared controller deployment does not declare the embedded source revision")
}
if !strings.Contains(string(serviceFiles(t, "k8s/normal/server/deploy.yaml")), image) {
t.Fatal("normal server deployment does not pin the qualified controller image")
}
}
func TestPublicAccountLoginRoutes(t *testing.T) {
route := string(serviceFiles(t, "k8s/relay/gateway/route.yaml"))
frontend, err := os.ReadFile(servicePath("..", "mail-keys", "k8s", "delivery", "nginx.conf"))
if err != nil {
t.Fatal(err)
}
for _, content := range []string{route, string(frontend)} {
if !strings.Contains(content, "/login") || !strings.Contains(content, "/account/login") {
t.Fatalf("account login redirect is absent from %q", content)
}
}
}
func TestRecoveryCredentialIsLimitedToBootstrapController(t *testing.T) {
bootstrap := serviceDeployment(t, "k8s/bootstrap/controller/deploy.yaml").Spec.Template.Spec.Containers[0].Args
normal := serviceDeployment(t, "k8s/normal/controller/deploy.yaml").Spec.Template.Spec.Containers[0].Args
for _, required := range []string{"--bootstrap-user", "--bootstrap-password-file"} {
if !hasArgument(bootstrap, required) {
t.Fatalf("bootstrap controller lacks %q", required)
}
if hasArgument(normal, required) {
t.Fatalf("normal controller retains %q", required)
}
}
}
func TestNormalControllerUsesOIDCWithoutNativeSteadyCredentials(t *testing.T) {
normal := serviceDeployment(t, "k8s/normal/controller/deploy.yaml").Spec.Template.Spec.Containers[0].Args
for _, required := range []string{
"--oidc-issuer-file",
"--oidc-audience-file",
"--oidc-client-id-file",
"--oidc-client-secret-file",
} {
if !hasArgument(normal, required) {
t.Fatalf("normal controller lacks %s", required)
}
}
for _, forbidden := range []string{"--bootstrap-user", "--bootstrap-password-file"} {
if hasArgument(normal, forbidden) {
t.Fatalf("normal controller contains %s", forbidden)
}
}
server := serviceFiles(t, "k8s/normal/server/deploy.yaml")
if !strings.Contains(string(server), "wait-oidc") {
t.Fatal("normal Stalwart deployment lacks the OIDC startup gate")
}
bootstrap := serviceDeployment(t, "k8s/bootstrap/controller/deploy.yaml").Spec.Template.Spec.Containers[0].Args
if argumentValue(bootstrap, "--stalwart-url") != "http://stalwart-bootstrap.stalwart.svc.cluster.local:8080" {
t.Fatal("bootstrap controller does not target the bootstrap Stalwart service")
}
for _, forbidden := range []string{"--oidc-client-id-file", "--oidc-client-secret-file", "--app-passwords", "--vault-address"} {
if hasArgument(bootstrap, forbidden) {
t.Fatalf("bootstrap controller contains %s", forbidden)
}
}
}
func TestServerReadiness(t *testing.T) {
deployment := serviceDeployment(t, "k8s/normal/server/deploy.yaml")
probes := make(map[string]*probeHTTP)
for _, container := range deployment.Spec.Template.Spec.Containers {
probes[container.Name] = container.ReadinessProbe.HTTPGet
if (container.Name == "stalwart" || container.Name == "account-web") && container.ReadinessProbe.TCPSocket != nil {
t.Fatalf("%s readiness checks only a socket", container.Name)
}
}
backend := probes["stalwart"]
if backend == nil || backend.Path != "/healthz/live" || backend.Port != "management" || backend.Scheme != "" && backend.Scheme != "HTTP" {
t.Fatalf("Stalwart readiness = %#v", backend)
}
frontend := probes["account-web"]
if frontend == nil || frontend.Path != "/account/oauth/callback" || frontend.Port != 8443 || frontend.Scheme != "HTTPS" {
t.Fatalf("account frontend readiness = %#v", frontend)
}
if len(frontend.HTTPHeaders) != 1 || frontend.HTTPHeaders[0].Name != "Host" || frontend.HTTPHeaders[0].Value != "mail.quorate.dev" {
t.Fatalf("account frontend probe headers = %#v", frontend.HTTPHeaders)
}
}
func TestNormalControllerUsesKubernetesDefaultVaultTokenAudience(t *testing.T) {
deployment := serviceDeployment(t, "k8s/normal/controller/deploy.yaml")
for _, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Name != "vault-token" || volume.Projected == nil {
continue
}
for _, source := range volume.Projected.Sources {
if source.ServiceAccountToken == nil {
continue
}
if source.ServiceAccountToken.Audience != "" {
t.Fatalf("Vault token audience = %q", source.ServiceAccountToken.Audience)
}
if source.ServiceAccountToken.ExpirationSeconds == nil || *source.ServiceAccountToken.ExpirationSeconds != 600 {
t.Fatal("Vault token does not use the short-lived projection")
}
return
}
}
t.Fatal("Vault service-account token projection is absent")
}
func TestStalwartNetworkPoliciesAllowDependencyResolution(t *testing.T) {
for _, paths := range [][]string{
{"k8s/bootstrap/server/policy.yaml", "k8s/controller/policy.yaml"},
{"k8s/normal/server/policy.yaml", "k8s/controller/policy.yaml"},
} {
content := serviceFiles(t, paths...)
if strings.Count(string(content), `- matchPattern: "*"`) != 2 {
t.Fatalf("%v does not allow DNS queries for both workloads", paths)
}
}
}
func TestPrivateSubmissionListenerUsesCiliumTransportEncryption(t *testing.T) {
content, err := os.ReadFile(servicePath("k8s", "base", "plan.json"))
if err != nil {
t.Fatal(err)
}
var operations []struct {
Object string `json:"object"`
Value json.RawMessage `json:"value"`
}
if err := json.Unmarshal(content, &operations); err != nil {
t.Fatal(err)
}
listenersFound := false
authFound := false
for _, operation := range operations {
switch operation.Object {
case "NetworkListener":
listenersFound = true
var listeners map[string]struct {
UseTLS bool `json:"useTls"`
TLSImplicit bool `json:"implicitTls"`
}
if err := json.Unmarshal(operation.Value, &listeners); err != nil {
t.Fatal(err)
}
if listeners["submission"].UseTLS || listeners["submission"].TLSImplicit {
t.Fatal("cluster-local submission listener requires TLS")
}
if !listeners["smtp"].UseTLS || !listeners["submissions"].UseTLS || !listeners["submissions"].TLSImplicit || !listeners["imaps"].UseTLS || !listeners["imaps"].TLSImplicit {
t.Fatal("public mail listeners do not retain their TLS settings")
}
case "MtaStageAuth":
authFound = true
if !bytes.Contains(operation.Value, []byte(`"if": "local_port == 587 || (local_port != 25 && is_tls)"`)) {
t.Fatal("cluster-local submission does not offer password authentication")
}
}
}
if !listenersFound || !authFound {
t.Fatal("submission listener or authentication policy is absent")
}
}
func TestStalwartUsesCombinedTrustBundle(t *testing.T) {
for _, path := range []string{
"k8s/bootstrap/server/job.yaml",
"k8s/normal/server/deploy.yaml",
} {
content := serviceFiles(t, path)
for _, required := range []string{
"cat /etc/ssl/certs/ca-certificates.crt /run/runtime/internal_ca_cert > /run/trust/ca-certificates.crt",
"value: /run/trust/ca-certificates.crt",
"mountPath: /run/trust",
} {
if !bytes.Contains(content, []byte(required)) {
t.Fatalf("%s does not contain %q", path, required)
}
}
}
}
func TestRenderPlanRejectsSecretTraversal(t *testing.T) {
root := t.TempDir()
source := filepath.Join(root, "plan.json")
if err := os.WriteFile(source, []byte(`[{"credential":{"$secretFile":"../password"}}]`), 0o600); err != nil {
t.Fatal(err)
}
err := renderPlan(source, filepath.Join(root, "plan.ndjson"), root)
if err == nil {
t.Fatal("secret traversal was accepted")
}
}
func TestCertificateInventoryPreservesDuplicateSANsForCleanup(t *testing.T) {
output := strings.Join([]string{
`{"id":"other","subjectAlternativeNames":{"other.example.invalid":true}}`,
`{"id":"declared","subjectAlternativeNames":{"*.example.invalid":true}}`,
`{"id":"duplicate","subjectAlternativeNames":{"mail.example.invalid":true}}`,
}, "\n")
inventory, err := certificateInventory([]byte(output), "mail.example.invalid")
if err != nil {
t.Fatal(err)
}
if !slices.Equal(inventory.ids, []string{"other", "declared", "duplicate"}) {
t.Fatalf("ids = %q", inventory.ids)
}
if !slices.Equal(inventory.matches, []string{"declared", "duplicate"}) {
t.Fatalf("matches = %q", inventory.matches)
}
}
func TestReconcileUpdatesDefaultCertificateBeforePruningDuplicates(t *testing.T) {
root := t.TempDir()
plan := filepath.Join(root, "plan.json")
renderedPlan := filepath.Join(root, "plan.ndjson")
cli := filepath.Join(root, "stalwart-cli")
calls := filepath.Join(root, "calls")
if err := os.WriteFile(plan, []byte(`[{"@type":"update","object":"DataStore"}]`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(cli, []byte(`#!/bin/sh
printf '%s\n' "$*" >> "$STALWART_TEST_CALLS"
if [ "$1 $2" = "query Certificate" ]; then
printf '%s\n' '{"id":"obsolete","subjectAlternativeNames":{"mail.example.invalid":true}}'
printf '%s\n' '{"id":"mail","subjectAlternativeNames":{"mail.example.invalid":true}}'
fi
`), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("STALWART_TEST_CALLS", calls)
if err := reconcileOnce(context.Background(), options{
plan: plan,
renderedPlan: renderedPlan,
secretRoot: root,
cli: cli,
stalwartURL: "http://stalwart.invalid",
certificateSAN: "mail.example.invalid",
updateApps: true,
}); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(calls)
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
want := []string{
"apply --file " + renderedPlan + " --no-color",
"create Action/ReloadSettings --json {} --no-color",
"create Action/UpdateApps --json {} --no-color",
"query Certificate --fields id,subjectAlternativeNames --json --no-color",
`update Certificate mail --field certificate={"@type":"File","filePath":"/run/tls/./tls.crt"} --field privateKey={"@type":"File","filePath":"/run/tls/tls.key"} --no-color`,
`update Certificate mail --field certificate={"@type":"File","filePath":"/run/tls/tls.crt"} --field privateKey={"@type":"File","filePath":"/run/tls/tls.key"} --no-color`,
"create Action/ReloadTlsCertificates --json {} --no-color",
"query Certificate --fields id,subjectAlternativeNames --json --no-color",
"update SystemSettings --field defaultCertificateId=mail --no-color",
"delete Certificate --ids obsolete --no-color",
"create Action/ReloadTlsCertificates --json {} --no-color",
}
if strings.Join(lines, "\n") != strings.Join(want, "\n") {
t.Fatalf("calls = %q, want %q", lines, want)
}
}
func TestReconcileCertificateCreatesMissingCertificate(t *testing.T) {
root := t.TempDir()
cli := filepath.Join(root, "stalwart-cli")
state := filepath.Join(root, "created")
reloaded := filepath.Join(root, "reloaded")
calls := filepath.Join(root, "calls")
script := fmt.Sprintf(`#!/bin/sh
printf '%%s\n' "$*" >> "$STALWART_TEST_CALLS"
if [ "$1 $2" = "query Certificate" ] && [ -f %q ]; then
if [ -f %q ]; then
printf '%%s\n' '{"id":"mail","subjectAlternativeNames":{"mail.example.invalid":true}}'
else
printf '%%s\n' '{"id":"mail","subjectAlternativeNames":{}}'
fi
elif [ "$1 $2" = "create Certificate" ]; then
touch %q
elif [ "$1 $2" = "create Action/ReloadTlsCertificates" ]; then
touch %q
fi
`, state, reloaded, state, reloaded)
if err := os.WriteFile(cli, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("STALWART_TEST_CALLS", calls)
if err := reconcileCertificate(t.Context(), options{
cli: cli,
certificateSAN: "mail.example.invalid",
}, nil); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(calls)
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
if len(lines) != 8 || lines[0] != "query Certificate --fields id,subjectAlternativeNames --json --no-color" ||
!strings.HasPrefix(lines[1], "create Certificate ") || lines[2] != lines[0] ||
!strings.HasPrefix(lines[3], "update Certificate mail ") ||
!strings.HasPrefix(lines[4], "update Certificate mail ") ||
lines[5] != "create Action/ReloadTlsCertificates --json {} --no-color" || lines[6] != lines[0] ||
lines[7] != "update SystemSettings --field defaultCertificateId=mail --no-color" {
t.Fatalf("calls = %q", lines)
}
}
func TestBootstrapEnvironmentReadsProjectedCredential(t *testing.T) {
path := filepath.Join(t.TempDir(), "password")
if err := os.WriteFile(path, []byte("bootstrap-value\n"), 0o400); err != nil {
t.Fatal(err)
}
environment, err := bootstrapEnvironment(options{
bootstrapUser: "recovery",
bootstrapPasswordFile: path,
})
if err != nil {
t.Fatal(err)
}
if strings.Join(environment, "\n") != "STALWART_USER=recovery\nSTALWART_PASSWORD=bootstrap-value" {
t.Fatal("bootstrap environment does not use the projected credential")
}
}
func TestOIDCEnvironmentUsesDiscoveryAndProjectAudience(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/.well-known/openid-configuration":
_ = json.NewEncoder(response).Encode(map[string]string{
"issuer": server.URL,
"jwks_uri": server.URL + "/keys",
"token_endpoint": server.URL + "/token",
})
case "/token":
clientID, clientSecret, ok := request.BasicAuth()
if !ok || clientID != "controller-id" || clientSecret != "controller-secret" {
response.WriteHeader(http.StatusUnauthorized)
return
}
if err := request.ParseForm(); err != nil {
t.Fatal(err)
}
if request.Form.Get("grant_type") != "client_credentials" ||
request.Form.Get("scope") != "openid urn:zitadel:iam:org:project:id:project-id:aud" {
t.Fatalf("token form = %v", request.Form)
}
_ = json.NewEncoder(response).Encode(map[string]string{"access_token": "synthetic-token"})
default:
response.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
root := t.TempDir()
files := map[string]string{
"issuer": server.URL,
"audience": "project-id",
"client-id": "controller-id",
"client-secret": "controller-secret",
}
for name, content := range files {
if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0o400); err != nil {
t.Fatal(err)
}
}
environment, err := oidcEnvironment(context.Background(), options{
oidcIssuerFile: filepath.Join(root, "issuer"),
oidcAudienceFile: filepath.Join(root, "audience"),
oidcClientIDFile: filepath.Join(root, "client-id"),
oidcClientSecretFile: filepath.Join(root, "client-secret"),
})
if err != nil {
t.Fatal(err)
}
if strings.Join(environment, "\n") != "STALWART_TOKEN=synthetic-token" {
t.Fatalf("environment = %q", environment)
}
}
func TestWaitForOIDCRequiresAdvertisedKeys(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
switch request.URL.Path {
case "/.well-known/openid-configuration":
_ = json.NewEncoder(response).Encode(map[string]string{
"issuer": server.URL,
"jwks_uri": server.URL + "/keys",
"token_endpoint": server.URL + "/token",
})
case "/keys":
_, _ = io.WriteString(response, `{"keys":[{"kid":"synthetic"}]}`)
default:
response.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
issuerFile := filepath.Join(t.TempDir(), "issuer")
if err := os.WriteFile(issuerFile, []byte(server.URL), 0o400); err != nil {
t.Fatal(err)
}
if err := waitForOIDC(context.Background(), issuerFile, time.Millisecond); err != nil {
t.Fatal(err)
}
}
func TestReconcileUsesOnlyBootstrapCredentials(t *testing.T) {
root := t.TempDir()
plan := filepath.Join(root, "plan.json")
renderedPlan := filepath.Join(root, "plan.ndjson")
cli := filepath.Join(root, "stalwart-cli")
calls := filepath.Join(root, "calls")
password := filepath.Join(root, "bootstrap-password")
if err := os.WriteFile(plan, []byte(`[{"@type":"update","object":"DataStore"}]`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(password, []byte("bootstrap-value"), 0o400); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(cli, []byte(`#!/bin/sh
printf '%s|%s|%s|%s\n' "$STALWART_URL" "$STALWART_USER" "$STALWART_PASSWORD" "$*" >> "$STALWART_TEST_CALLS"
if [ "$STALWART_URL" != "http://stalwart-bootstrap:8080" ]; then
exit 1
fi
`), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("STALWART_TEST_CALLS", calls)
if err := reconcileOnce(context.Background(), options{
plan: plan,
renderedPlan: renderedPlan,
secretRoot: root,
cli: cli,
stalwartURL: "http://stalwart-bootstrap:8080",
bootstrapUser: "recovery",
bootstrapPasswordFile: password,
}); err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(calls)
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(content)), "\n")
if len(lines) != 2 {
t.Fatalf("CLI calls = %q", lines)
}
for _, line := range lines {
if !strings.HasPrefix(line, "http://stalwart-bootstrap:8080|recovery|bootstrap-value|") {
t.Fatalf("bootstrap CLI call = %q", line)
}
}
}
func TestMergedEnvironmentReplacesCredential(t *testing.T) {
environment := mergedEnvironment(
[]string{"PATH=/bin", "STALWART_PASSWORD=old"},
[]string{"STALWART_USER=operator", "STALWART_PASSWORD=new"},
)
value := strings.Join(environment, "\n")
if strings.Contains(value, "STALWART_PASSWORD=old") {
t.Fatal("old credential remains in child environment")
}
if !strings.Contains(value, "STALWART_PASSWORD=new") {
t.Fatal("new credential is absent from child environment")
}
}
func TestStatusDoesNotContainCredentialFields(t *testing.T) {
encoded, err := json.Marshal(controllerStatus{
Leader: true,
Ready: true,
PlanRevision: strings.Repeat("a", 64),
BuildRevision: "test-revision",
})
if err != nil {
t.Fatal(err)
}
value := strings.ToLower(string(encoded))
if strings.Contains(value, "password") || strings.Contains(value, "secret") {
t.Fatal("status exposes a credential field")
}
}