380 lines
14 KiB
Go
380 lines
14 KiB
Go
package settings
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFixtureIsConsumedByGo(t *testing.T) {
|
|
data, err := os.ReadFile("../../../contracts/settings/runtime-v1.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fixture struct {
|
|
Version int `json:"version"`
|
|
FileName string `json:"fileName"`
|
|
AllowedKeys []string `json:"allowedKeys"`
|
|
SecretKeys []string `json:"secretKeys"`
|
|
DatabaseBackedKeys []string `json:"databaseBackedKeys"`
|
|
}
|
|
if err := json.Unmarshal(data, &fixture); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if fixture.Version != 1 || fixture.FileName != ".env.local" || len(fixture.AllowedKeys) != 25 || len(fixture.SecretKeys) != 8 {
|
|
t.Fatalf("fixture = %#v", fixture)
|
|
}
|
|
wantDatabaseKeys := RuntimeSettingKeys()
|
|
gotDatabaseKeys := append([]string(nil), fixture.DatabaseBackedKeys...)
|
|
sort.Strings(gotDatabaseKeys)
|
|
if !reflect.DeepEqual(gotDatabaseKeys, wantDatabaseKeys) {
|
|
t.Fatalf("databaseBackedKeys=%#v want %#v", gotDatabaseKeys, wantDatabaseKeys)
|
|
}
|
|
}
|
|
|
|
func TestServiceSavePreservesSecretsAndUnrelatedEnvLines(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".env.local")
|
|
original := "# operator note\nDATABASE_URL=postgres://untouched\nZHINIAN_AUTH_SESSION_SECRET=keep-me\nZHINIAN_BILLING_CONTACT=old # prior\nCUSTOM_QUOTED=\"a b\"\n"
|
|
if err := os.WriteFile(path, []byte(original), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
environment := map[string]string{"EVOLINK_BASE_URL": "https://runtime.example"}
|
|
var applied map[string]string
|
|
service := New(path, environment, func(_ context.Context, updates map[string]string) error { applied = clone(updates); return nil })
|
|
|
|
value, err := service.Save(context.Background(), map[string]any{
|
|
"ZHINIAN_AUTH_SESSION_SECRET": " ",
|
|
"ZHINIAN_BILLING_CONTACT": " ",
|
|
"EVOLINK_BASE_URL": " https://new.example/v1 ",
|
|
"DATABASE_URL": "postgres://attack",
|
|
"ALI_OSS_BUCKET": 42,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(text)
|
|
for _, expected := range []string{"# operator note", "DATABASE_URL=postgres://untouched", "ZHINIAN_AUTH_SESSION_SECRET=keep-me", "ZHINIAN_BILLING_CONTACT=", "CUSTOM_QUOTED=\"a b\"", "EVOLINK_BASE_URL=https://new.example/v1"} {
|
|
if !strings.Contains(got, expected) {
|
|
t.Fatalf("file missing %q:\n%s", expected, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "postgres://attack") || strings.Contains(got, "ALI_OSS_BUCKET=42") {
|
|
t.Fatalf("unexpected unapproved update:\n%s", got)
|
|
}
|
|
if !reflect.DeepEqual(applied, map[string]string{"ZHINIAN_BILLING_CONTACT": "", "EVOLINK_BASE_URL": "https://new.example/v1"}) {
|
|
t.Fatalf("applied = %#v", applied)
|
|
}
|
|
if payload := value.(Payload); payload.RestartRequired {
|
|
t.Fatal("provider setting update must be applied without restart")
|
|
}
|
|
assertSecretProjection(t, value, "ZHINIAN_AUTH_SESSION_SECRET", true)
|
|
}
|
|
|
|
func TestRuntimeRepositoryOverridesEnvironmentAndProjectsSecrets(t *testing.T) {
|
|
repository := &runtimeSettingsRepositoryStub{values: map[string]string{
|
|
"EVOLINK_API_KEY": "database-secret",
|
|
"EVOLINK_BASE_URL": "https://database.example",
|
|
"IMAGE_GENERATE_ENGINE": "evolink",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": "database-session-secret",
|
|
"ZHINIAN_BILLING_CONTACT": "database-billing-contact",
|
|
"ALI_OSS_BUCKET": "database-bucket",
|
|
}}
|
|
service := NewWithRuntimeRepository(
|
|
filepath.Join(t.TempDir(), ".env.local"),
|
|
map[string]string{
|
|
"EVOLINK_API_KEY": "environment-secret",
|
|
"EVOLINK_BASE_URL": "https://environment.example",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": "environment-session-secret",
|
|
"ZHINIAN_BILLING_CONTACT": "environment-billing-contact",
|
|
"ALI_OSS_BUCKET": "environment-bucket",
|
|
},
|
|
repository,
|
|
nil,
|
|
)
|
|
value, err := service.Get(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := fieldValue(t, value, "EVOLINK_BASE_URL"); got != "https://database.example" {
|
|
t.Fatalf("base URL = %q", got)
|
|
}
|
|
assertSecretProjection(t, value, "EVOLINK_API_KEY", true)
|
|
runtime, err := service.RuntimeValues(context.Background())
|
|
if err != nil || runtime["EVOLINK_API_KEY"] != "database-secret" || runtime["ZHINIAN_AUTH_SESSION_SECRET"] != "database-session-secret" || runtime["ZHINIAN_BILLING_CONTACT"] != "database-billing-contact" || runtime["ALI_OSS_BUCKET"] != "database-bucket" {
|
|
t.Fatalf("runtime=%#v err=%v", runtime, err)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeRepositoryPersistsEveryEditableSettingWithoutCreatingLocalFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
|
repository := &runtimeSettingsRepositoryStub{}
|
|
service := NewWithRuntimeRepository(path, nil, repository, nil)
|
|
updates := map[string]any{}
|
|
want := map[string]string{}
|
|
for _, key := range RuntimeSettingKeys() {
|
|
value := "database-" + strings.ToLower(key)
|
|
updates[key] = value
|
|
want[key] = value
|
|
}
|
|
value, err := service.Save(context.Background(), updates)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(repository.saved, want) {
|
|
t.Fatalf("saved=%#v want %#v", repository.saved, want)
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("database-backed save unexpectedly created local settings file: %v", err)
|
|
}
|
|
if !value.(Payload).RestartRequired {
|
|
t.Fatal("startup-only database settings should require restart")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeRepositorySaveIsPartialImmediateAndDoesNotCreateSettingsFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
|
repository := &runtimeSettingsRepositoryStub{values: map[string]string{"EVOLINK_API_KEY": "keep-secret"}}
|
|
service := NewWithRuntimeRepository(path, nil, repository, nil)
|
|
value, err := service.Save(context.Background(), map[string]any{
|
|
"EVOLINK_API_KEY": " ",
|
|
"EVOLINK_BASE_URL": " https://saved.example ",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(repository.saved, map[string]string{"EVOLINK_BASE_URL": "https://saved.example"}) {
|
|
t.Fatalf("saved=%#v", repository.saved)
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("provider save unexpectedly created local settings file: %v", err)
|
|
}
|
|
if value.(Payload).RestartRequired {
|
|
t.Fatal("provider database update should apply immediately")
|
|
}
|
|
assertSecretProjection(t, value, "EVOLINK_API_KEY", true)
|
|
}
|
|
|
|
func TestRuntimeRepositoryFailureDoesNotFallBackToLocalFile(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
|
repository := &runtimeSettingsRepositoryStub{saveErr: errors.New("database unavailable")}
|
|
service := NewWithRuntimeRepository(path, nil, repository, nil)
|
|
_, err := service.Save(context.Background(), map[string]any{"ALI_OSS_BUCKET": "bucket"})
|
|
if err == nil || !strings.Contains(err.Error(), "persist runtime settings") {
|
|
t.Fatalf("Save() error=%v", err)
|
|
}
|
|
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
|
t.Fatalf("failed database save unexpectedly created local settings file: %v", statErr)
|
|
}
|
|
}
|
|
|
|
func TestServiceGetUsesInjectedEnvironmentWithoutMutatingProcess(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".env.local")
|
|
if err := os.WriteFile(path, []byte("EVOLINK_BASE_URL=file-value\nSEEDANCE_API_KEY=file-secret\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const sentinel = "settings-service-must-not-write-this"
|
|
old, present := os.LookupEnv(sentinel)
|
|
t.Cleanup(func() {
|
|
if present {
|
|
_ = os.Setenv(sentinel, old)
|
|
} else {
|
|
_ = os.Unsetenv(sentinel)
|
|
}
|
|
})
|
|
_ = os.Unsetenv(sentinel)
|
|
service := New(path, map[string]string{"EVOLINK_BASE_URL": "runtime-value", "SEEDANCE_API_KEY": "runtime-secret"}, nil)
|
|
value, err := service.Get(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := os.LookupEnv(sentinel); exists {
|
|
t.Fatal("process environment mutated")
|
|
}
|
|
if got := fieldValue(t, value, "EVOLINK_BASE_URL"); got != "runtime-value" {
|
|
t.Fatalf("value = %q", got)
|
|
}
|
|
assertSecretProjection(t, value, "SEEDANCE_API_KEY", true)
|
|
}
|
|
|
|
func TestLoadEnvironmentIgnoresUnknownRuntimeFlags(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".env.local")
|
|
if err := os.WriteFile(path, []byte("ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS=true\nUNKNOWN_RUNTIME_FLAG=true\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
values, err := LoadEnvironment(path, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, exists := values["ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS"]; exists {
|
|
t.Fatal("obsolete provider bootstrap flag was loaded")
|
|
}
|
|
if _, exists := values["UNKNOWN_RUNTIME_FLAG"]; exists {
|
|
t.Fatal("unknown runtime flag was loaded")
|
|
}
|
|
}
|
|
|
|
func TestServiceSynchronizesBillingAccountWithoutRequiringRestart(t *testing.T) {
|
|
writer := &billingWriterStub{}
|
|
service := New(filepath.Join(t.TempDir(), ".env.local"), nil, nil).WithBillingAccountWriter(writer)
|
|
value, err := service.Save(context.Background(), map[string]any{
|
|
"ZHINIAN_BILLING_ACCOUNT_NAME": " Acme ",
|
|
"ZHINIAN_BILLING_ACCOUNT_BANK": " Bank ",
|
|
"ZHINIAN_BILLING_ACCOUNT_NUMBER": " 123 ",
|
|
"ZHINIAN_BILLING_CONTACT": " Ops ",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if writer.value != (BillingAccount{AccountName: "Acme", BankName: "Bank", AccountNumber: "123", Contact: "Ops"}) {
|
|
t.Fatalf("billing account=%#v", writer.value)
|
|
}
|
|
if value.(Payload).RestartRequired {
|
|
t.Fatal("billing account update should be applied immediately")
|
|
}
|
|
}
|
|
|
|
func TestServicePartialBillingUpdatePreservesPersistedSiblingFields(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".env.local")
|
|
if err := os.WriteFile(path, []byte("ZHINIAN_BILLING_ACCOUNT_NAME=Acme\nZHINIAN_BILLING_ACCOUNT_BANK=Legacy Bank\nZHINIAN_BILLING_ACCOUNT_NUMBER=123\nZHINIAN_BILLING_CONTACT=Old Ops\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &billingWriterStub{}
|
|
service := New(path, nil, nil).WithBillingAccountWriter(writer)
|
|
if _, err := service.Save(context.Background(), map[string]any{"ZHINIAN_BILLING_CONTACT": "New Ops"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := BillingAccount{AccountName: "Acme", BankName: "Legacy Bank", AccountNumber: "123", Contact: "New Ops"}
|
|
if writer.value != want {
|
|
t.Fatalf("billing account=%#v want %#v", writer.value, want)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeRepositoryPartialBillingUpdatePreservesDatabaseSiblings(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "nested", ".env.local")
|
|
repository := &runtimeSettingsRepositoryStub{values: map[string]string{
|
|
"ZHINIAN_BILLING_ACCOUNT_NAME": "Acme",
|
|
"ZHINIAN_BILLING_ACCOUNT_BANK": "Legacy Bank",
|
|
"ZHINIAN_BILLING_ACCOUNT_NUMBER": "123",
|
|
"ZHINIAN_BILLING_CONTACT": "Old Ops",
|
|
}}
|
|
writer := &billingWriterStub{}
|
|
service := NewWithRuntimeRepository(path, nil, repository, nil).WithBillingAccountWriter(writer)
|
|
value, err := service.Save(context.Background(), map[string]any{"ZHINIAN_BILLING_CONTACT": "New Ops"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := BillingAccount{AccountName: "Acme", BankName: "Legacy Bank", AccountNumber: "123", Contact: "New Ops"}
|
|
if writer.value != want {
|
|
t.Fatalf("billing account=%#v want %#v", writer.value, want)
|
|
}
|
|
if value.(Payload).RestartRequired {
|
|
t.Fatal("billing account database update should apply immediately")
|
|
}
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Fatalf("billing database update unexpectedly created local settings file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRestartRequirementIsIndependentFromDatabasePersistence(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
want bool
|
|
}{
|
|
{key: "EVOLINK_API_KEY", want: false},
|
|
{key: "IMAGE_GENERATE_ENGINE", want: false},
|
|
{key: "ZHINIAN_BILLING_ACCOUNT_NAME", want: false},
|
|
{key: "ZHINIAN_AUTH_REQUIRED", want: true},
|
|
{key: "ZHINIAN_AUTH_SESSION_SECRET", want: true},
|
|
{key: "ZHINIAN_BILLING_REQUIRED", want: true},
|
|
{key: "ALI_OSS_ENDPOINT", want: true},
|
|
{key: "ALI_OSS_BUCKET", want: true},
|
|
{key: "ALI_OSS_ACCESS_KEY_ID", want: true},
|
|
{key: "ALI_OSS_ACCESS_KEY_SECRET", want: true},
|
|
{key: "ALI_OSS_PUBLIC_BASE_URL", want: true},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.key, func(t *testing.T) {
|
|
if got := requiresRestart(map[string]string{test.key: "value"}); got != test.want {
|
|
t.Fatalf("requiresRestart=%v want %v", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type billingWriterStub struct{ value BillingAccount }
|
|
|
|
func (writer *billingWriterStub) SaveBillingAccount(_ context.Context, value BillingAccount) error {
|
|
writer.value = value
|
|
return nil
|
|
}
|
|
|
|
type runtimeSettingsRepositoryStub struct {
|
|
values map[string]string
|
|
saved map[string]string
|
|
saveErr error
|
|
}
|
|
|
|
func (repository *runtimeSettingsRepositoryStub) LoadRuntimeSettings(_ context.Context, _ []string) (map[string]string, error) {
|
|
return clone(repository.values), nil
|
|
}
|
|
|
|
func (repository *runtimeSettingsRepositoryStub) SaveRuntimeSettings(_ context.Context, values map[string]string) error {
|
|
if repository.saveErr != nil {
|
|
return repository.saveErr
|
|
}
|
|
repository.saved = clone(values)
|
|
if repository.values == nil {
|
|
repository.values = map[string]string{}
|
|
}
|
|
for key, value := range values {
|
|
repository.values[key] = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fieldValue(t *testing.T, value any, key string) string {
|
|
t.Helper()
|
|
payload := value.(Payload)
|
|
for _, group := range payload.Groups {
|
|
for _, field := range group.Fields {
|
|
if field.Key == key {
|
|
return field.Value
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("field %s not found", key)
|
|
return ""
|
|
}
|
|
func assertSecretProjection(t *testing.T, value any, key string, configured bool) {
|
|
t.Helper()
|
|
payload := value.(Payload)
|
|
for _, group := range payload.Groups {
|
|
for _, field := range group.Fields {
|
|
if field.Key == key {
|
|
if field.Value != "" || field.Configured != configured {
|
|
t.Fatalf("field = %#v", field)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
t.Fatalf("field %s not found", key)
|
|
}
|
|
func clone(input map[string]string) map[string]string {
|
|
output := map[string]string{}
|
|
for key, value := range input {
|
|
output[key] = value
|
|
}
|
|
return output
|
|
}
|