Files
ttstd-infinite-canvas/backend/internal/app/provider_ark_private_assets_test.go

258 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}