561 lines
22 KiB
Go
561 lines
22 KiB
Go
package application
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers"
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings"
|
|
)
|
|
|
|
func TestDefaultSettingsServiceAppliesProviderSettingsWithoutRestart(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 TestRuntimeSettingsGetenvProvidesDatabaseBackedStartupConfiguration(t *testing.T) {
|
|
settingsPath := filepath.Join(t.TempDir(), ".env.local")
|
|
fallbackValues := map[string]string{
|
|
"ZHINIAN_SETTINGS_FILE": settingsPath,
|
|
"ZHINIAN_AUTH_REQUIRED": "0",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": "environment-session-secret-that-is-long-enough",
|
|
"ZHINIAN_BILLING_REQUIRED": "1",
|
|
"ALI_OSS_ENDPOINT": "https://environment-oss.example.test",
|
|
"ALI_OSS_BUCKET": "environment-bucket",
|
|
"ALI_OSS_ACCESS_KEY_ID": "environment-access-key",
|
|
"ALI_OSS_ACCESS_KEY_SECRET": "environment-access-secret",
|
|
"ALI_OSS_PUBLIC_BASE_URL": "https://environment-cdn.example.test",
|
|
"ZHINIAN_PROVIDER_TIMEOUT_MS": "1234",
|
|
}
|
|
fallback := func(name string) string { return fallbackValues[name] }
|
|
repository := &applicationRuntimeSettingsRepository{values: map[string]string{
|
|
"ZHINIAN_AUTH_REQUIRED": "1",
|
|
"ZHINIAN_AUTH_SESSION_SECRET": "database-session-secret-that-is-long-enough",
|
|
"ZHINIAN_BILLING_REQUIRED": "0",
|
|
"ALI_OSS_ENDPOINT": "https://oss-cn-test.aliyuncs.com",
|
|
"ALI_OSS_BUCKET": "database-bucket",
|
|
"ALI_OSS_ACCESS_KEY_ID": "database-access-key",
|
|
"ALI_OSS_ACCESS_KEY_SECRET": "database-access-secret",
|
|
"ALI_OSS_PUBLIC_BASE_URL": "https://database-cdn.example.test",
|
|
}}
|
|
service := databaseSettingsService(fallback, repository)
|
|
getenv, err := runtimeSettingsGetenv(context.Background(), fallback, service)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(repository.requestedKeys, settings.RuntimeSettingKeys()) {
|
|
t.Fatalf("requested keys=%#v want %#v", repository.requestedKeys, settings.RuntimeSettingKeys())
|
|
}
|
|
auth, err := ParseAuthConfig(getenv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !auth.Required || auth.SessionSecret != "database-session-secret-that-is-long-enough" {
|
|
t.Fatalf("auth=%#v", auth)
|
|
}
|
|
if getenv("ZHINIAN_BILLING_REQUIRED") != "0" || getenv("ZHINIAN_PROVIDER_TIMEOUT_MS") != "1234" {
|
|
t.Fatalf("billing=%q timeout=%q", getenv("ZHINIAN_BILLING_REQUIRED"), getenv("ZHINIAN_PROVIDER_TIMEOUT_MS"))
|
|
}
|
|
if _, configured, err := configuredOSSBlobStore(getenv); err != nil || !configured {
|
|
t.Fatalf("configured OSS = %v err=%v", configured, err)
|
|
}
|
|
}
|
|
|
|
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",
|
|
"VIDEO_GENERATE_ENGINE": "seedance",
|
|
"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 TestRuntimeProviderJobBuilderRefreshesDatabaseSettings(t *testing.T) {
|
|
settingsPath := filepath.Join(t.TempDir(), ".env.local")
|
|
fallback := func(name string) string {
|
|
if name == "ZHINIAN_SETTINGS_FILE" {
|
|
return settingsPath
|
|
}
|
|
return ""
|
|
}
|
|
repository := &applicationRuntimeSettingsRepository{values: map[string]string{
|
|
"EVOLINK_API_KEY": "evolink-secret",
|
|
"EVOLINK_IMAGE_MODEL": "database-image-model",
|
|
"EVOLINK_IMAGE_QUALITY": "high",
|
|
"IMAGE_GENERATE_ENGINE": "evolink",
|
|
}}
|
|
service := databaseSettingsService(fallback, repository)
|
|
builder := runtimeProviderJobBuilder{fallback: fallback, settings: service, enforceAvailability: true}
|
|
command, err := builder.Build(context.Background(), "owner", "", "image.generate", "", map[string]any{"prompt": "draw"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var request providers.Request
|
|
if err := json.Unmarshal(command.Job.RequestPayload, &request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if command.Job.Provider != "evolink" || command.Job.ReqKey != "database-image-model" || request.Settings["quality"] != "high" {
|
|
t.Fatalf("job=%#v request=%#v", command.Job, request)
|
|
}
|
|
repository.values = map[string]string{
|
|
"BAILIAN_API_KEY": "bailian-secret",
|
|
"BAILIAN_IMAGE_MODEL": "database-bailian-model",
|
|
"IMAGE_GENERATE_ENGINE": "bailian",
|
|
}
|
|
command, err = builder.Build(context.Background(), "owner", "", "image.generate", "", map[string]any{"prompt": "draw again"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if command.Job.Provider != "bailian" || command.Job.ReqKey != "database-bailian-model" {
|
|
t.Fatalf("refreshed job=%#v", command.Job)
|
|
}
|
|
}
|
|
|
|
func TestEvoLinkImageModelOptionsAndConfiguredDefault(t *testing.T) {
|
|
getenv := func(name string) string {
|
|
switch name {
|
|
case "IMAGE_GENERATE_ENGINE":
|
|
return "evolink"
|
|
case "EVOLINK_IMAGE_MODEL":
|
|
return "custom-image-model"
|
|
case "EVOLINK_IMAGE_QUALITY":
|
|
return "high"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
options := evolinkImageModelOptions(getenv)
|
|
if len(options) != 4 || options[3].(map[string]any)["id"] != "custom-image-model" {
|
|
t.Fatalf("options=%#v", options)
|
|
}
|
|
capabilities, err := capabilitySummary(getenv)(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
image := capabilities.([]any)[0].(map[string]any)
|
|
if image["reqKey"] != "custom-image-model" || len(image["models"].([]any)) != 4 {
|
|
t.Fatalf("image capability=%#v", image)
|
|
}
|
|
builder := configuredProviderJobBuilder(getenv, false)
|
|
for _, model := range []string{"gpt-image-2", "gpt-image-2.5-flare", "gpt-image-2.5-sunburst", "custom-image-model"} {
|
|
command, err := builder.Build(context.Background(), "owner", "", "image.generate", "", map[string]any{
|
|
"engine": "evolink", "model": model, "prompt": "draw",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("model %s: %v", model, err)
|
|
}
|
|
var request providers.Request
|
|
if err := json.Unmarshal(command.Job.RequestPayload, &request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if command.Job.ReqKey != model || request.Model != model || request.Settings["quality"] != "high" {
|
|
t.Fatalf("model %s: job=%#v request=%#v", model, command.Job, request)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImageCapabilityExposesEvoLinkDefaultWhenPrimaryIsJimeng(t *testing.T) {
|
|
getenv := func(name string) string {
|
|
if name == "EVOLINK_IMAGE_MODEL" {
|
|
return "gpt-image-2.5-sunburst"
|
|
}
|
|
return ""
|
|
}
|
|
capabilities, err := capabilitySummary(getenv)(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
image := capabilities.([]any)[0].(map[string]any)
|
|
evolink := image["evolink"].(map[string]any)
|
|
if image["engine"] != "jimeng" || evolink["reqKey"] != "gpt-image-2.5-sunburst" || len(evolink["models"].([]any)) != 3 {
|
|
t.Fatalf("image capability=%#v", image)
|
|
}
|
|
health := runtimeHealthDetails(getenv)
|
|
healthImage := health.Capabilities[0].(map[string]any)
|
|
if healthImage["evolink"].(map[string]any)["reqKey"] != "gpt-image-2.5-sunburst" {
|
|
t.Fatalf("health capability=%#v", healthImage)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeProviderJobBuilderRoutesSeedreamWithoutReplacingJimeng(t *testing.T) {
|
|
values := map[string]string{
|
|
"SEEDANCE_API_KEY": "ark-secret",
|
|
"IMAGE_GENERATE_ENGINE": "seedream",
|
|
}
|
|
getenv := func(name string) string { return values[name] }
|
|
builder := configuredProviderJobBuilder(getenv, true)
|
|
command, err := builder.Build(context.Background(), "owner", "", "image.generate", "", map[string]any{
|
|
"prompt": "draw", "engine": "seedream", "settings": map[string]any{"size": "1.5K", "outputFormat": "png", "optimizeMode": "standard"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if command.Job.Provider != "seedream" || command.Job.ReqKey != providers.Seedream50ProModel {
|
|
t.Fatalf("job=%#v", command.Job)
|
|
}
|
|
jimeng, err := builder.Build(context.Background(), "owner", "", "image.generate", "", map[string]any{"prompt": "draw", "engine": "jimeng"})
|
|
if err == nil || !strings.Contains(err.Error(), "VOLCENGINE_ACCESS_KEY_ID") {
|
|
t.Fatalf("jimeng availability error=%v job=%#v", err, jimeng.Job)
|
|
}
|
|
if _, ok := buildProviderRegistryWithClient(getenv, http.DefaultClient, 2<<20)["seedream"].(*providers.Seedream); !ok {
|
|
t.Fatalf("seedream adapter is not registered")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeProviderJobBuilderRoutesMinimaxH3FromDatabaseSettings(t *testing.T) {
|
|
values := map[string]string{
|
|
"MINIMAX_API_KEY": "minimax-secret",
|
|
"MINIMAX_BASE_URL": "https://api.minimax.test",
|
|
"VIDEO_GENERATE_ENGINE": "minimax",
|
|
}
|
|
getenv := func(name string) string { return values[name] }
|
|
builder := configuredProviderJobBuilder(getenv, true)
|
|
command, err := builder.Build(context.Background(), "owner", "", "video.generate", "", map[string]any{
|
|
"engine": "minimax", "prompt": "animate", "settings": map[string]any{"duration": 5.0, "resolution": "768P", "ratio": "16:9"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if command.Job.Provider != "minimax" || command.Job.ReqKey != providers.MinimaxH3Model {
|
|
t.Fatalf("job=%#v", command.Job)
|
|
}
|
|
if _, ok := buildProviderRegistryWithClient(getenv, http.DefaultClient, 2<<20)["minimax"].(*providers.Minimax); !ok {
|
|
t.Fatalf("minimax adapter is not registered")
|
|
}
|
|
details := runtimeHealthDetails(getenv)
|
|
if details.MinimaxMode != "minimax" {
|
|
t.Fatalf("health=%#v", details)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeProviderResolverUsesLatestDatabaseCredential(t *testing.T) {
|
|
var authorizations []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
authorizations = append(authorizations, request.Header.Get("Authorization"))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"task-1","status":"queued"}`))
|
|
}))
|
|
defer server.Close()
|
|
settingsPath := filepath.Join(t.TempDir(), ".env.local")
|
|
fallback := func(name string) string {
|
|
if name == "ZHINIAN_SETTINGS_FILE" {
|
|
return settingsPath
|
|
}
|
|
return ""
|
|
}
|
|
repository := &applicationRuntimeSettingsRepository{values: map[string]string{
|
|
"EVOLINK_API_KEY": "database-secret-1",
|
|
"EVOLINK_BASE_URL": server.URL,
|
|
}}
|
|
service := databaseSettingsService(fallback, repository)
|
|
resolver := newRuntimeProviderResolver(fallback, service)
|
|
adapter, err := resolver.Resolve(context.Background(), "evolink")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := adapter.Submit(context.Background(), providers.Request{Capability: "image.generate", Prompt: "draw"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repository.values["EVOLINK_API_KEY"] = "database-secret-2"
|
|
adapter, err = resolver.Resolve(context.Background(), "evolink")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := adapter.Submit(context.Background(), providers.Request{Capability: "image.generate", Prompt: "draw again"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(authorizations, []string{"Bearer database-secret-1", "Bearer database-secret-2"}) {
|
|
t.Fatalf("authorizations=%#v", authorizations)
|
|
}
|
|
}
|
|
|
|
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 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) != 3 || !strings.Contains(missing["seedance"], "SEEDANCE_API_KEY") || !strings.Contains(missing["seedream"], "SEEDANCE_API_KEY") || !strings.Contains(missing["minimax"], "MINIMAX_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 TestProviderVideoTargetsUseDocumentedSeedanceDefaults(t *testing.T) {
|
|
values := map[string]string{
|
|
"SEEDANCE_RATIO": "16:9",
|
|
"SEEDANCE_DURATION": "12",
|
|
"SEEDANCE_RESOLUTION": "1080p",
|
|
}
|
|
target := providerVideoTargets(func(name string) string { return values[name] })["seedance"]
|
|
if target.Model != "doubao-seedance-2-0-260128" {
|
|
t.Fatalf("model=%q", target.Model)
|
|
}
|
|
if target.Settings["ratio"] != "16:9" || target.Settings["duration"] != float64(12) || target.Settings["resolution"] != "1080p" {
|
|
t.Fatalf("settings=%#v", target.Settings)
|
|
}
|
|
}
|
|
|
|
func TestProviderVideoTargetsIncludeMinimaxH3Defaults(t *testing.T) {
|
|
target := providerVideoTargets(func(string) string { return "" })["minimax"]
|
|
if target.Provider != "minimax" || target.Model != providers.MinimaxH3Model {
|
|
t.Fatalf("target=%#v", target)
|
|
}
|
|
want := map[string]any{"ratio": "16:9", "duration": float64(5), "resolution": "768P"}
|
|
if !reflect.DeepEqual(target.Settings, want) {
|
|
t.Fatalf("settings=%#v want=%#v", target.Settings, want)
|
|
}
|
|
}
|
|
|
|
func TestProviderVideoModelsKeepSeedance20And25Available(t *testing.T) {
|
|
targets := providerVideoModels(func(string) string { return "" })
|
|
for _, model := range []string{"doubao-seedance-2-0-260128", "doubao-seedance-2-5-260628"} {
|
|
target, ok := targets[model]
|
|
if !ok || target.Provider != "seedance" || target.Model != model {
|
|
t.Fatalf("target[%q] = %#v", model, target)
|
|
}
|
|
}
|
|
if seedanceCapabilityLimits("doubao-seedance-2-0-260128")["durationSeconds"].(map[string]int)["max"] != 15 {
|
|
t.Fatal("Seedance 2.0 duration limit changed")
|
|
}
|
|
if seedanceCapabilityLimits("doubao-seedance-2-5-260628")["durationSeconds"].(map[string]int)["max"] != 30 {
|
|
t.Fatal("Seedance 2.5 duration limit is not 30 seconds")
|
|
}
|
|
}
|
|
|
|
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"},
|
|
{name: "MiniMax configured", engine: "minimax", wantEngine: "minimax", wantProvider: "minimax", wantModel: providers.MinimaxH3Model},
|
|
} {
|
|
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)
|
|
}
|
|
if _, err := store.SignReadURL(stored.Key, time.Hour); err != nil || inner.signedKey != "tenant-prefix/uploads/day/file.png" || inner.signedTTL != time.Hour {
|
|
t.Fatalf("signed URL delegation = %q / %s, err=%v", inner.signedKey, inner.signedTTL, err)
|
|
}
|
|
}
|
|
|
|
func TestConfiguredOSSBlobStoreDoesNotRequestPublicObjectACL(t *testing.T) {
|
|
var requests []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
|
requests = append(requests, request.Method+" "+request.URL.RequestURI())
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
values := map[string]string{
|
|
"ALI_OSS_ENDPOINT": server.URL + "/private-bucket",
|
|
"ALI_OSS_BUCKET": "private-bucket",
|
|
"ALI_OSS_ACCESS_KEY_ID": "test-access-key",
|
|
"ALI_OSS_ACCESS_KEY_SECRET": "test-access-secret",
|
|
"ALI_OSS_PUBLIC_BASE_URL": server.URL + "/private-bucket",
|
|
}
|
|
store, configured, err := configuredOSSBlobStore(func(name string) string { return values[name] })
|
|
if err != nil || !configured {
|
|
t.Fatalf("configured store = %T, %v, configured=%v", store, err, configured)
|
|
}
|
|
if _, err := store.Put(context.Background(), "uploads/day/private.png", bytes.NewReader([]byte("png")), 3, "image/png"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(requests, []string{"PUT /private-bucket/zhinian/uploads/day/private.png"}) {
|
|
t.Fatalf("OSS requests = %#v, want one private PutObject request", requests)
|
|
}
|
|
}
|
|
|
|
type recordingBlobStore struct {
|
|
putKey, readKey, deleteKey, signedKey string
|
|
signedTTL time.Duration
|
|
}
|
|
|
|
type applicationRuntimeSettingsRepository struct {
|
|
values map[string]string
|
|
requestedKeys []string
|
|
}
|
|
|
|
func (repository *applicationRuntimeSettingsRepository) LoadRuntimeSettings(_ context.Context, keys []string) (map[string]string, error) {
|
|
repository.requestedKeys = append([]string(nil), keys...)
|
|
values := map[string]string{}
|
|
for key, value := range repository.values {
|
|
values[key] = value
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func (repository *applicationRuntimeSettingsRepository) SaveRuntimeSettings(_ context.Context, values map[string]string) error {
|
|
if repository.values == nil {
|
|
repository.values = map[string]string{}
|
|
}
|
|
for key, value := range values {
|
|
repository.values[key] = value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
func (s *recordingBlobStore) SignReadURL(key string, ttl time.Duration) (string, error) {
|
|
s.signedKey, s.signedTTL = key, ttl
|
|
return "https://signed.example/" + key, nil
|
|
}
|