258 lines
10 KiB
Go
258 lines
10 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
"yingce/backend/internal/model"
|
||
)
|
||
|
||
func TestArkPrivateAssetUsesRegionalArkControlPlane(t *testing.T) {
|
||
value, err := arkPrivateAssetControlPlaneURL("test-region")
|
||
if err != nil || value != "https://ark.test-region.volcengineapi.com" {
|
||
t.Fatalf("arkPrivateAssetControlPlaneURL() = %q, %v", value, err)
|
||
}
|
||
}
|
||
|
||
func TestCallArkPrivateAssetAPISignsAndUsesAssetContract(t *testing.T) {
|
||
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||
if request.Method != http.MethodPost {
|
||
t.Errorf("method = %s, want POST", request.Method)
|
||
}
|
||
if request.URL.Query().Get("Action") != "CreateAsset" || request.URL.Query().Get("Version") != arkPrivateAssetAPIVersion {
|
||
t.Errorf("query = %s", request.URL.RawQuery)
|
||
}
|
||
if !strings.HasPrefix(request.Header.Get("Authorization"), "HMAC-SHA256") {
|
||
t.Errorf("missing Volcengine request signature")
|
||
}
|
||
var body map[string]interface{}
|
||
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if body["ProjectName"] != "project-test" || body["AssetType"] != "Image" || body["GroupId"] != "group-test" {
|
||
t.Errorf("body = %#v", body)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"Result":{"AssetId":"asset-test"}}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
previousBaseURL := arkPrivateAssetAPIBaseURLOverride
|
||
arkPrivateAssetAPIBaseURLOverride = server.URL
|
||
t.Cleanup(func() { arkPrivateAssetAPIBaseURLOverride = previousBaseURL })
|
||
|
||
response, err := callArkPrivateAssetAPI(context.Background(), arkPrivateAssetSettingValue{
|
||
Region: "test-region",
|
||
ProjectName: "project-test",
|
||
AccessKeyID: "test-access-key",
|
||
AccessKeySecret: "test-secret-key",
|
||
}, "CreateAsset", map[string]interface{}{
|
||
"GroupId": "group-test", "AssetType": "Image", "URL": "https://media.example.test/reference.png", "ProjectName": "project-test",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("callArkPrivateAssetAPI() error = %v", err)
|
||
}
|
||
if got := arkPrivateAssetResponseField(response, "AssetId"); got != "asset-test" {
|
||
t.Fatalf("asset ID = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestCallArkPrivateAssetAPIUsesIDForGetAsset(t *testing.T) {
|
||
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||
var body map[string]interface{}
|
||
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if request.URL.Query().Get("Action") != "GetAsset" || body["Id"] != "asset-test" || body["AssetId"] != nil {
|
||
t.Errorf("GetAsset request = %#v", body)
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_, _ = w.Write([]byte(`{"Result":{"Status":"Processing"}}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
previousBaseURL := arkPrivateAssetAPIBaseURLOverride
|
||
arkPrivateAssetAPIBaseURLOverride = server.URL
|
||
t.Cleanup(func() { arkPrivateAssetAPIBaseURLOverride = previousBaseURL })
|
||
|
||
_, err := callArkPrivateAssetAPI(context.Background(), arkPrivateAssetSettingValue{
|
||
Region: "test-region", AccessKeyID: "test-access-key", AccessKeySecret: "test-secret-key",
|
||
}, "GetAsset", map[string]interface{}{"Id": "asset-test", "ProjectName": "project-test"})
|
||
if err != nil {
|
||
t.Fatalf("callArkPrivateAssetAPI() error = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetControlPlaneDisablesGenerationAnalytics(t *testing.T) {
|
||
ctx := context.WithValue(context.Background(), providerAnalyticsKey{}, providerAnalyticsContext{Service: &Service{}, Capability: "video"})
|
||
ctx = withoutProviderAnalytics(ctx)
|
||
metadata, ok := ctx.Value(providerAnalyticsKey{}).(providerAnalyticsContext)
|
||
if !ok || metadata.Service != nil || metadata.Capability != "" {
|
||
t.Fatalf("provider analytics = %#v, %v", metadata, ok)
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetAutomaticSyncIsOptionalWhenSettingDisabled(t *testing.T) {
|
||
enabled, err := arkPrivateAssetAutomaticSyncEnabled(arkPrivateAssetSettingValue{})
|
||
if err != nil || enabled {
|
||
t.Fatalf("arkPrivateAssetAutomaticSyncEnabled() = %v, %v; want false, nil", enabled, err)
|
||
}
|
||
|
||
_, err = arkPrivateAssetAutomaticSyncEnabled(arkPrivateAssetSettingValue{Enabled: true})
|
||
if err == nil || !strings.Contains(err.Error(), "尚未配置") {
|
||
t.Fatalf("enabled incomplete setting error = %v, want configuration error", err)
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetSettingsEncryptSecret(t *testing.T) {
|
||
svc := &Service{dataDir: t.TempDir()}
|
||
value, err := arkPrivateAssetSettingFromRequest(ArkPrivateAssetSettingRequest{
|
||
Enabled: true, Region: "test-region", ProjectName: "project-test", AccessKeyID: "test-access-key", AccessKeySecret: "test-secret-key",
|
||
}, defaultArkPrivateAssetSetting())
|
||
if err != nil {
|
||
t.Fatalf("arkPrivateAssetSettingFromRequest() error = %v", err)
|
||
}
|
||
ciphertext, err := svc.encryptSettingSecret(value.AccessKeySecret)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.HasPrefix(ciphertext, encryptedSettingPrefix) || strings.Contains(ciphertext, value.AccessKeySecret) {
|
||
t.Fatalf("encrypted secret = %q", ciphertext)
|
||
}
|
||
plaintext, err := svc.decryptSettingSecret(ciphertext)
|
||
if err != nil || plaintext != value.AccessKeySecret {
|
||
t.Fatalf("decryptSettingSecret() = %q, %v", plaintext, err)
|
||
}
|
||
public := publicArkPrivateAssetSetting(nil, value)
|
||
if !public.HasAccessKeySecret || public.AccessKeyID != "test-access-key" {
|
||
t.Fatalf("public setting = %#v", public)
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetResponseFieldReadsNestedAssetStatus(t *testing.T) {
|
||
response := map[string]interface{}{
|
||
"Result": map[string]interface{}{
|
||
"Asset": map[string]interface{}{"Status": "Active", "AssetId": "asset-nested"},
|
||
},
|
||
}
|
||
if got := arkPrivateAssetResponseField(response, "Status"); got != "Active" {
|
||
t.Fatalf("status = %q", got)
|
||
}
|
||
if got := arkPrivateAssetResponseField(response, "AssetId"); got != "asset-nested" {
|
||
t.Fatalf("asset ID = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetResponseFieldReadsCreateGroupID(t *testing.T) {
|
||
response := map[string]interface{}{
|
||
"Result": map[string]interface{}{
|
||
"Group": map[string]interface{}{"Id": "group-created"},
|
||
},
|
||
}
|
||
if got := arkPrivateAssetResponseField(response, "GroupId", "Id"); got != "group-created" {
|
||
t.Fatalf("group ID = %q", got)
|
||
}
|
||
}
|
||
|
||
func TestShouldRetryArkPrivateAssetBinding(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
binding *model.ArkPrivateAssetBinding
|
||
want bool
|
||
}{
|
||
{
|
||
name: "素材组未建成时网络失败可重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, Error: `创建方舟素材组失败:Post "https://ark.cn-shanghai.volcengineapi.com/": EOF`},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "素材组未建成时上游业务拒绝也可重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, Error: "创建方舟素材组失败:方舟素材库请求失败(HTTP 403):SubscriptionRequired"},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "素材组已建时上传素材失败可重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, AssetGroupID: "group-1", Error: "上传方舟可信素材失败:Post \"https://ark.cn-beijing.volcengineapi.com/\": EOF"},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "素材组已建时素材 ID 解析失败可重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, AssetGroupID: "group-1", Error: "方舟素材库没有返回素材 ID"},
|
||
want: true,
|
||
},
|
||
{
|
||
name: "审核拒绝保持终态不重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, AssetGroupID: "group-1", ArkAssetID: "asset-1", Error: "FaceMismatch: Face consistency verification failed."},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "素材组已建时的其他失败不重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusFail, AssetGroupID: "group-1", Error: "生成方舟素材临时地址失败:对象存储不可用"},
|
||
want: false,
|
||
},
|
||
{
|
||
name: "非失败状态不重试",
|
||
binding: &model.ArkPrivateAssetBinding{Status: arkPrivateAssetStatusLive, ArkAssetID: "asset-1"},
|
||
want: false,
|
||
},
|
||
}
|
||
for _, item := range cases {
|
||
if got := shouldRetryArkPrivateAssetBinding(item.binding); got != item.want {
|
||
t.Fatalf("%s: shouldRetryArkPrivateAssetBinding() = %v, want %v", item.name, got, item.want)
|
||
}
|
||
}
|
||
if shouldRetryArkPrivateAssetBinding(nil) {
|
||
t.Fatal("shouldRetryArkPrivateAssetBinding(nil) = true, want false")
|
||
}
|
||
}
|
||
|
||
func TestCallArkPrivateAssetAPIPassesThroughUpstreamHTTPErrors(t *testing.T) {
|
||
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusForbidden)
|
||
_, _ = w.Write([]byte(`{"ResponseMetadata":{"Error":{"Code":"SubscriptionRequired","Message":"This API requires an active subscription."}}}`))
|
||
}))
|
||
defer server.Close()
|
||
|
||
previousBaseURL := arkPrivateAssetAPIBaseURLOverride
|
||
arkPrivateAssetAPIBaseURLOverride = server.URL
|
||
t.Cleanup(func() { arkPrivateAssetAPIBaseURLOverride = previousBaseURL })
|
||
|
||
_, err := callArkPrivateAssetAPI(context.Background(), arkPrivateAssetSettingValue{
|
||
Region: "test-region", AccessKeyID: "test-access-key", AccessKeySecret: "test-secret-key",
|
||
}, "CreateAssetGroup", map[string]interface{}{"ProjectName": "project-test"})
|
||
if err == nil {
|
||
t.Fatal("callArkPrivateAssetAPI() error = nil, want upstream passthrough error")
|
||
}
|
||
for _, want := range []string{"403", "SubscriptionRequired", "This API requires an active subscription."} {
|
||
if !strings.Contains(err.Error(), want) {
|
||
t.Fatalf("callArkPrivateAssetAPI() error = %q, want contains %q", err.Error(), want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestArkPrivateAssetUpstreamDetail(t *testing.T) {
|
||
if got := arkPrivateAssetUpstreamDetail(map[string]interface{}{}); got != "" {
|
||
t.Fatalf("empty response detail = %q, want empty", got)
|
||
}
|
||
response := map[string]interface{}{
|
||
"ResponseMetadata": map[string]interface{}{
|
||
"Error": map[string]interface{}{"Code": "FaceMismatch", "Message": "Face consistency verification failed."},
|
||
},
|
||
}
|
||
if got := arkPrivateAssetUpstreamDetail(response); got != "FaceMismatch Face consistency verification failed." {
|
||
t.Fatalf("detail = %q", got)
|
||
}
|
||
emptyError := map[string]interface{}{"ResponseMetadata": map[string]interface{}{"Error": map[string]interface{}{}}}
|
||
if got := arkPrivateAssetUpstreamDetail(emptyError); got != "方舟素材库请求失败" {
|
||
t.Fatalf("empty upstream error detail = %q", got)
|
||
}
|
||
}
|