This commit is contained in:
andy committed 2026-08-18 18:23:11 +08:00
1 parent 648b274c55
commit 18cb51670c
14 files changed
+420 -171

No files matched your search

+124 -11
View File
@@ -3,9 +3,11 @@ package settings
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
)
@@ -16,10 +18,11 @@ func TestFixtureIsConsumedByGo(t *testing.T) {
t.Fatal(err)
}
var fixture struct {
Version int `json:"version"`
FileName string `json:"fileName"`
AllowedKeys []string `json:"allowedKeys"`
SecretKeys []string `json:"secretKeys"`
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)
@@ -27,6 +30,12 @@ func TestFixtureIsConsumedByGo(t *testing.T) {
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) {
@@ -73,13 +82,22 @@ func TestServiceSavePreservesSecretsAndUnrelatedEnvLines(t *testing.T) {
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",
"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"},
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,
)
@@ -92,11 +110,37 @@ func TestRuntimeRepositoryOverridesEnvironmentAndProjectsSecrets(t *testing.T) {
}
assertSecretProjection(t, value, "EVOLINK_API_KEY", true)
runtime, err := service.RuntimeValues(context.Background())
if err != nil || runtime["EVOLINK_API_KEY"] != "database-secret" {
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"}}
@@ -120,6 +164,19 @@ func TestRuntimeRepositorySaveIsPartialImmediateAndDoesNotCreateSettingsFile(t *
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 {
@@ -202,6 +259,58 @@ func TestServicePartialBillingUpdatePreservesPersistedSiblingFields(t *testing.T
}
}
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 {
@@ -210,8 +319,9 @@ func (writer *billingWriterStub) SaveBillingAccount(_ context.Context, value Bil
}
type runtimeSettingsRepositoryStub struct {
values map[string]string
saved map[string]string
values map[string]string
saved map[string]string
saveErr error
}
func (repository *runtimeSettingsRepositoryStub) LoadRuntimeSettings(_ context.Context, _ []string) (map[string]string, error) {
@@ -219,6 +329,9 @@ func (repository *runtimeSettingsRepositoryStub) LoadRuntimeSettings(_ context.C
}
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{}