Files
NianAIGC/backend/internal/application/runtime_test.go
2026-08-18 00:36:05 +08:00

229 lines
8.9 KiB
Go

package application
import (
"bytes"
"context"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings"
)
func TestDefaultSettingsServicePersistsRestartRequiredWithoutPartialHotReload(t *testing.T) {
path := filepath.Join(t.TempDir(), ".env.local")
key := "IMAGE_GENERATE_ENGINE"
service := defaultSettingsService(func(name string) string {
if name == "ZHINIAN_SETTINGS_FILE" {
return path
}
return ""
})
value, err := service.Save(context.Background(), map[string]any{key: "evolink"})
if err != nil {
t.Fatal(err)
}
payload, ok := value.(settings.Payload)
if !ok || !payload.RestartRequired {
t.Fatalf("payload=%#v", value)
}
}
func TestBillingAccountAndSettingsUseOneRuntimeSource(t *testing.T) {
path := filepath.Join(t.TempDir(), ".env.local")
if err := os.WriteFile(path, []byte("ZHINIAN_BILLING_ACCOUNT_NAME=Original\nZHINIAN_BILLING_ACCOUNT_BANK=Old Bank\n"), 0o600); err != nil {
t.Fatal(err)
}
service := defaultSettingsService(func(name string) string {
if name == "ZHINIAN_SETTINGS_FILE" {
return path
}
return ""
})
store := settingsBillingAccountStore{service: service}
if err := store.Save(context.Background(), billing.AccountConfig{AccountName: "Updated", BankName: "New Bank", AccountNumber: "123", Contact: "Ops"}); err != nil {
t.Fatal(err)
}
loaded, err := store.Load(context.Background())
if err != nil || loaded.AccountName != "Updated" || loaded.BankName != "New Bank" || loaded.AccountNumber != "123" || loaded.Contact != "Ops" {
t.Fatalf("loaded=%#v err=%v", loaded, err)
}
payload, err := service.Get(context.Background())
if err != nil || fieldValueFromSettings(t, payload, "ZHINIAN_BILLING_ACCOUNT_NAME") != "Updated" {
t.Fatalf("settings payload=%#v err=%v", payload, err)
}
}
func fieldValueFromSettings(t *testing.T, value any, key string) string {
t.Helper()
payload := value.(settings.Payload)
for _, group := range payload.Groups {
for _, field := range group.Fields {
if field.Key == key {
return field.Value
}
}
}
t.Fatalf("settings field %s not found", key)
return ""
}
func TestRuntimeHealthDetailsMatchTypeScriptDefaultsAndConfiguredModes(t *testing.T) {
values := map[string]string{
"VOLCENGINE_ACCESS_KEY_ID": "access",
"VOLCENGINE_SECRET_ACCESS_KEY": "secret",
"SEEDANCE_API_KEY": "seedance-key",
"ZHINIAN_AUTH_REQUIRED": "true",
"ZHINIAN_AUTH_SESSION_SECRET": "session-secret",
}
details := runtimeHealthDetails(func(name string) string { return values[name] })
if details.VisualAPIMode != "volcengine" || details.EvolinkMode != "missing" || details.SeedanceMode != "seedance" || details.BailianMode != "missing" || details.AuthMode != "configured" {
t.Fatalf("details = %+v", details)
}
if len(details.Capabilities) != 2 {
t.Fatalf("capabilities = %#v, want image and Seedance", details.Capabilities)
}
image := details.Capabilities[0].(map[string]any)
video := details.Capabilities[1].(map[string]any)
if image["id"] != "image.generate" || image["engineLabel"] != "即梦" || video["id"] != "video.generate" || video["engineLabel"] != "Seedance" {
t.Fatalf("capabilities = %#v", details.Capabilities)
}
}
func TestRuntimeHealthDetailsReportConfiguredProvidersAndImageEngine(t *testing.T) {
values := map[string]string{
"IMAGE_GENERATE_ENGINE": "bailian",
"BAILIAN_API_KEY": "bailian-key",
}
details := runtimeHealthDetails(func(name string) string { return values[name] })
if details.VisualAPIMode != "missing" || details.EvolinkMode != "missing" || details.SeedanceMode != "missing" || details.BailianMode != "bailian" || details.AuthMode != "disabled" {
t.Fatalf("details = %+v", details)
}
image := details.Capabilities[0].(map[string]any)
if image["engine"] != "bailian" || image["engineLabel"] != "阿里云百炼" || image["reqKey"] != "wan2.7-image-pro" {
t.Fatalf("image capability = %#v", image)
}
}
func TestValidateProductionProviderConfigurationRequiresAllRealCredentials(t *testing.T) {
if err := validateProductionProviderConfiguration(func(string) string { return "" }); err == nil {
t.Fatal("validateProductionProviderConfiguration() unexpectedly succeeded")
} else if !strings.Contains(err.Error(), "EVOLINK_API_KEY") || !strings.Contains(err.Error(), "SEEDANCE_API_KEY") {
t.Fatalf("error = %v", err)
}
values := map[string]string{
"VOLCENGINE_ACCESS_KEY_ID": "access",
"VOLCENGINE_SECRET_ACCESS_KEY": "secret",
"EVOLINK_API_KEY": "evolink",
"BAILIAN_API_KEY": "bailian",
"SEEDANCE_API_KEY": "seedance",
}
if err := validateProductionProviderConfiguration(func(name string) string { return values[name] }); err != nil {
t.Fatalf("validateProductionProviderConfiguration() error = %v", err)
}
}
func TestValidateProductionProviderConfigurationCanBeSkippedForBootstrap(t *testing.T) {
values := map[string]string{"ZHINIAN_ALLOW_UNCONFIGURED_PROVIDERS": "true"}
if err := validateProductionProviderConfiguration(func(name string) string { return values[name] }); err != nil {
t.Fatalf("validateProductionProviderConfiguration() with bootstrap flag = %v", err)
}
}
func TestProviderUnavailableMessagesIdentifyOnlyMissingCredentials(t *testing.T) {
values := map[string]string{
"VOLCENGINE_ACCESS_KEY_ID": "access",
"VOLCENGINE_SECRET_ACCESS_KEY": "secret",
"EVOLINK_API_KEY": "evolink",
"BAILIAN_API_KEY": "bailian",
}
missing := providerUnavailableMessages(func(name string) string { return values[name] })
if len(missing) != 1 || !strings.Contains(missing["seedance"], "SEEDANCE_API_KEY") {
t.Fatalf("missing = %#v", missing)
}
}
func TestProviderTargetsNeverSelectRemovedProvider(t *testing.T) {
getenv := func(string) string { return "" }
for engine, target := range providerImageTargets(getenv) {
if target.Provider == "mock" {
t.Fatalf("image engine %s selected removed provider", engine)
}
}
for engine, target := range providerVideoTargets(getenv) {
if target.Provider == "mock" {
t.Fatalf("video engine %s selected removed provider", engine)
}
}
}
func TestCapabilitySummaryMatchesConfiguredDefaultVideoEngine(t *testing.T) {
for _, test := range []struct {
name, engine, wantEngine, wantProvider, wantModel string
}{
{name: "Bailian default", engine: "", wantEngine: "bailian", wantProvider: "bailian", wantModel: "wan2.7-i2v-2026-04-25"},
{name: "Seedance configured", engine: "seedance", wantEngine: "seedance", wantProvider: "seedance", wantModel: "doubao-seedance-2-0-260128"},
} {
t.Run(test.name, func(t *testing.T) {
values := map[string]string{"VIDEO_GENERATE_ENGINE": test.engine}
value, err := capabilitySummary(func(name string) string { return values[name] })(context.Background())
if err != nil {
t.Fatal(err)
}
capabilities := value.([]any)
video := capabilities[1].(map[string]any)
if video["engine"] != test.wantEngine || video["provider"] != test.wantProvider || video["reqKey"] != test.wantModel {
t.Fatalf("video capability = %#v", video)
}
})
}
}
func TestRemoteAssetMaximumDefaultsToTwentyMiB(t *testing.T) {
getenv := func(string) string { return "" }
if got := remoteAssetMaxBytes(getenv); got != 20<<20 {
t.Fatalf("remote asset maximum = %d, want %d", got, int64(20<<20))
}
getenv = func(string) string { return "3145728" }
if got := remoteAssetMaxBytes(getenv); got != 3<<20 {
t.Fatalf("configured remote asset maximum = %d, want %d", got, int64(3<<20))
}
}
func TestPrefixedBlobStoreKeepsApplicationStoragePathStable(t *testing.T) {
inner := &recordingBlobStore{}
store := prefixedBlobStore{prefix: "tenant-prefix", store: inner}
stored, err := store.Put(context.Background(), "uploads/day/file.png", bytes.NewReader([]byte("x")), 1, "image/png")
if err != nil {
t.Fatal(err)
}
if stored.Key != "uploads/day/file.png" || inner.putKey != "tenant-prefix/uploads/day/file.png" {
t.Fatalf("stored=%#v inner=%q", stored, inner.putKey)
}
_, _ = store.Read(context.Background(), stored.Key)
_ = store.Delete(context.Background(), stored.Key)
if !reflect.DeepEqual([]string{inner.readKey, inner.deleteKey}, []string{"tenant-prefix/uploads/day/file.png", "tenant-prefix/uploads/day/file.png"}) {
t.Fatalf("read/delete=%q/%q", inner.readKey, inner.deleteKey)
}
}
type recordingBlobStore struct{ putKey, readKey, deleteKey string }
func (s *recordingBlobStore) Put(_ context.Context, key string, _ io.Reader, _ int64, _ string) (assets.StoredObject, error) {
s.putKey = key
return assets.StoredObject{Key: key, URL: "https://cdn.example/" + key}, nil
}
func (s *recordingBlobStore) Read(_ context.Context, key string) (assets.Blob, error) {
s.readKey = key
return assets.Blob{Body: io.NopCloser(bytes.NewReader(nil))}, nil
}
func (s *recordingBlobStore) Delete(_ context.Context, key string) error {
s.deleteKey = key
return nil
}