package service import ( "context" "errors" "net/http" "testing" "time" "github.com/QuantumNous/new-api/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" cmerrs "gitlab.ecloud.com/ecloud/ecloudsdkcore/errs" cmmodel "gitlab.ecloud.com/ecloud/ecloudsdkmaas/model" ) func TestChinaMobileAssetAdapterCreateAssetMapsRequestAndResponse(t *testing.T) { setChinaMobileAssetTestEnv(t, "") fake := &fakeChinaMobileAssetSDKClient{} adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { assert.Equal(t, "ak", credential.AK) assert.Equal(t, "sk", credential.SK) assert.Equal(t, defaultChinaMobileAssetPoolID, credential.PoolID) return fake }} spec, ok := ParseAssetAction("CreateAsset") require.True(t, ok) resp, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{ Action: spec, Version: "2024-01-01", Body: map[string]any{ "GroupId": "group-1", "Name": "asset", "URL": "https://example.com/a.png", "AssetType": "Image", }, }) require.Nil(t, assetErr) require.NotNil(t, resp) require.NotNil(t, fake.createAssetRequest) assert.Equal(t, "group-1", *fake.createAssetRequest.CreateAssetBody.GroupId) assert.Equal(t, "asset", *fake.createAssetRequest.CreateAssetBody.AssetName) assert.Equal(t, "https://example.com/a.png", *fake.createAssetRequest.CreateAssetBody.AssetUrl) assert.Equal(t, cmmodel.CreateAssetBodyAssetTypeEnumImage, *fake.createAssetRequest.CreateAssetBody.AssetType) assert.Contains(t, string(resp.Body), `"RequestId":"req-1"`) assert.Contains(t, string(resp.Body), `"Result":"asset-1"`) } func TestChinaMobileAssetCredentialFromEnvDefaultsCenterPool(t *testing.T) { setChinaMobileAssetTestEnv(t, "") credential, err := chinaMobileAssetCredentialFromEnv() require.NoError(t, err) assert.Equal(t, "ak", credential.AK) assert.Equal(t, "sk", credential.SK) assert.Equal(t, defaultChinaMobileAssetPoolID, credential.PoolID) } func TestChinaMobileAssetCredentialFromEnvSupportsPoolID(t *testing.T) { setChinaMobileAssetTestEnv(t, "CIDC-RP-29") credential, err := chinaMobileAssetCredentialFromEnv() require.NoError(t, err) assert.Equal(t, "ak", credential.AK) assert.Equal(t, "sk", credential.SK) assert.Equal(t, "CIDC-RP-29", credential.PoolID) } func TestChinaMobileAssetCredentialFromEnvRequiresAKAndSK(t *testing.T) { t.Setenv(chinaMobileAssetAKEnv, "") t.Setenv(chinaMobileAssetSKEnv, "") t.Setenv(chinaMobileAssetPoolIDEnv, "") _, err := chinaMobileAssetCredentialFromEnv() require.Error(t, err) assert.Contains(t, err.Error(), chinaMobileAssetAKEnv) assert.Contains(t, err.Error(), chinaMobileAssetSKEnv) } func TestChinaMobileAssetAdapterAllOfficialActions(t *testing.T) { cases := []struct { action string body map[string]any assert func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) }{ {"CreateAssetGroup", map[string]any{"Name": "g", "GroupType": "AIGC"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.createAssetGroupRequest) }}, {"CreateAsset", map[string]any{"GroupId": "g", "Name": "n", "URL": "https://example.com/a.png", "AssetType": "Image"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.createAssetRequest) }}, {"ListAssetGroups", map[string]any{"Filter": map[string]any{"GroupType": "AIGC"}, "PageNumber": float64(1), "PageSize": float64(10)}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.listAssetGroupsRequest) }}, {"ListAssets", map[string]any{"Filter": map[string]any{"GroupType": "AIGC"}, "PageNumber": float64(1), "PageSize": float64(10)}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.listAssetsRequest) }}, {"GetAsset", map[string]any{"Id": "asset-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.getAssetRequest) assert.Equal(t, "asset-1", *fake.getAssetRequest.GetAssetPath.AssetId) }}, {"GetAssetGroup", map[string]any{"Id": "group-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.getAssetGroupRequest) assert.Equal(t, "group-1", *fake.getAssetGroupRequest.GetAssetGroupPath.GroupId) }}, {"UpdateAsset", map[string]any{"Id": "asset-1", "Name": "n"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.updateAssetRequest) }}, {"UpdateAssetGroup", map[string]any{"Id": "group-1", "Name": "g"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.updateAssetGroupRequest) }}, {"DeleteAsset", map[string]any{"Id": "asset-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.deleteAssetRequest) }}, {"DeleteAssetGroup", map[string]any{"Id": "group-1"}, func(t *testing.T, fake *fakeChinaMobileAssetSDKClient) { require.NotNil(t, fake.deleteAssetGroupRequest) }}, } for _, tc := range cases { t.Run(tc.action, func(t *testing.T) { setChinaMobileAssetTestEnv(t, "") fake := &fakeChinaMobileAssetSDKClient{} adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return fake }} spec, ok := ParseAssetAction(tc.action) require.True(t, ok) resp, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{ Action: spec, Version: "2024-01-01", Body: tc.body, }) require.Nil(t, assetErr) require.NotNil(t, resp) tc.assert(t, fake) assert.Contains(t, string(resp.Body), `"ResponseMetadata"`) }) } } func TestChinaMobileAssetAdapterRejectsUnsupportedCompatibilityOptions(t *testing.T) { setChinaMobileAssetTestEnv(t, "") adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return &fakeChinaMobileAssetSDKClient{} }} spec, ok := ParseAssetAction("ListAssets") require.True(t, ok) cases := []map[string]any{ {"ProjectName": "prod"}, {"SortBy": "Name"}, {"SortOrder": "Asc"}, } for _, body := range cases { _, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "cm-key"}, AssetRequest{ Action: spec, Body: body, }) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorOperationNotSupported, assetErr.Type) } } func TestChinaMobileAssetAdapterMissingIdReturnsInvalidRequest(t *testing.T) { setChinaMobileAssetTestEnv(t, "") adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return &fakeChinaMobileAssetSDKClient{} }} spec, ok := ParseAssetAction("GetAsset") require.True(t, ok) _, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{ Action: spec, Body: map[string]any{}, }) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type) assert.Contains(t, assetErr.Message, "Id is required") } func TestChinaMobileAssetAdapterStateErrorMapsToUpstreamError(t *testing.T) { _, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "CreateAsset", Operation: AssetOperationAssetCreate}, "2024-01-01", []byte(`{"requestId":"req","state":"ERROR","errorCode":"Bad","errorMessage":"bad request"}`)) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorUpstream, assetErr.Type) assert.Equal(t, "bad request", assetErr.Message) } func TestChinaMobileAssetAdapterRequiresGroupTypeForListAssets(t *testing.T) { setChinaMobileAssetTestEnv(t, "") adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return &fakeChinaMobileAssetSDKClient{} }} spec, ok := ParseAssetAction("ListAssets") require.True(t, ok) _, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{ Action: spec, Body: map[string]any{"PageNumber": 1, "PageSize": 10}, }) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type) assert.Equal(t, http.StatusBadRequest, assetErr.HTTPStatus) } func TestChinaMobileAssetDeleteFalseMapsToUpstreamError(t *testing.T) { _, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "DeleteAsset", Operation: AssetOperationAssetDelete, Delete: true}, "2024-01-01", []byte(`{"requestId":"req","state":"OK","body":false}`)) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorUpstream, assetErr.Type) } func TestChinaMobileAssetUnknownStateMapsToUpstreamError(t *testing.T) { _, assetErr := normalizeChinaMobileAssetResponse(AssetActionSpec{Action: "CreateAsset", Operation: AssetOperationAssetCreate}, "2024-01-01", []byte(`{"requestId":"req","state":"","body":"asset-1"}`)) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorUpstream, assetErr.Type) } func TestChinaMobileAssetSDKBadRequestMapsToInvalidRequest(t *testing.T) { setChinaMobileAssetTestEnv(t, "") adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return &errorChinaMobileAssetSDKClient{ fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{}, err: cmerrs.NewServerResponseError("bad request", nil, http.StatusBadRequest, nil, `{"errorMessage":"invalid asset"}`), } }} spec, ok := ParseAssetAction("CreateAsset") require.True(t, ok) _, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()}) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorInvalidRequest, assetErr.Type) assert.Equal(t, http.StatusBadRequest, assetErr.HTTPStatus) } func TestChinaMobileAssetSDKServerErrorRemainsUpstreamError(t *testing.T) { setChinaMobileAssetTestEnv(t, "") adapter := &ChinaMobileAssetAdapter{newClient: func(credential chinaMobileAssetCredential) chinaMobileAssetSDKClient { return &errorChinaMobileAssetSDKClient{ fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{}, err: cmerrs.NewServerResponseError("server error", nil, http.StatusInternalServerError, nil, ""), } }} spec, ok := ParseAssetAction("CreateAsset") require.True(t, ok) _, assetErr := adapter.DoAssetRequest(context.Background(), &model.Channel{Key: "video-generation-key"}, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()}) require.NotNil(t, assetErr) assert.Equal(t, AssetErrorUpstream, assetErr.Type) assert.Equal(t, http.StatusBadGateway, assetErr.HTTPStatus) } func TestCallChinaMobileAssetSDKReturnsWhenContextCancelled(t *testing.T) { client := &blockingChinaMobileAssetSDKClient{fakeChinaMobileAssetSDKClient: fakeChinaMobileAssetSDKClient{}, release: make(chan struct{})} ctx, cancel := context.WithCancel(context.Background()) cancel() spec, ok := ParseAssetAction("CreateAsset") require.True(t, ok) started := time.Now() _, err := callChinaMobileAssetSDK(ctx, client, AssetRequest{Action: spec, Body: validChinaMobileCreateAssetBody()}) require.ErrorIs(t, err, context.Canceled) assert.Less(t, time.Since(started), 500*time.Millisecond) close(client.release) } func setChinaMobileAssetTestEnv(t *testing.T, poolID string) { t.Helper() t.Setenv(chinaMobileAssetAKEnv, "ak") t.Setenv(chinaMobileAssetSKEnv, "sk") t.Setenv(chinaMobileAssetPoolIDEnv, poolID) } type blockingChinaMobileAssetSDKClient struct { fakeChinaMobileAssetSDKClient release chan struct{} } type errorChinaMobileAssetSDKClient struct { fakeChinaMobileAssetSDKClient err error } func (f *errorChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) { if f.err == nil { return nil, errors.New("missing test error") } return nil, f.err } func (f *blockingChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) { <-f.release return f.fakeChinaMobileAssetSDKClient.CreateAsset(request) } func validChinaMobileCreateAssetBody() map[string]any { return map[string]any{ "GroupId": "group-1", "Name": "asset", "URL": "https://example.com/asset.png", "AssetType": "Image", } } type fakeChinaMobileAssetSDKClient struct { createAssetRequest *cmmodel.CreateAssetRequest listAssetsRequest *cmmodel.ListAssetsRequest getAssetRequest *cmmodel.GetAssetRequest updateAssetRequest *cmmodel.UpdateAssetRequest deleteAssetRequest *cmmodel.DeleteAssetRequest createAssetGroupRequest *cmmodel.CreateAssetGroupRequest listAssetGroupsRequest *cmmodel.ListAssetGroupsRequest getAssetGroupRequest *cmmodel.GetAssetGroupRequest updateAssetGroupRequest *cmmodel.UpdateAssetGroupRequest deleteAssetGroupRequest *cmmodel.DeleteAssetGroupRequest } func (f *fakeChinaMobileAssetSDKClient) CreateAsset(request *cmmodel.CreateAssetRequest) (*cmmodel.CreateAssetResponse, error) { f.createAssetRequest = request return (&cmmodel.CreateAssetResponse{}).SetRequestId("req-1").SetState("OK").SetBody("asset-1"), nil } func (f *fakeChinaMobileAssetSDKClient) ListAssets(request *cmmodel.ListAssetsRequest) (*cmmodel.ListAssetsResponse, error) { f.listAssetsRequest = request item := cmmodel.ListAssetsResponseData{} item.SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.ListAssetsResponseDataStatusEnumActive) body := (&cmmodel.ListAssetsResponseBody{}).SetData([]cmmodel.ListAssetsResponseData{item}).SetTotal(1) return (&cmmodel.ListAssetsResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) GetAsset(request *cmmodel.GetAssetRequest) (*cmmodel.GetAssetResponse, error) { f.getAssetRequest = request body := (&cmmodel.GetAssetResponseBody{}).SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.GetAssetResponseBodyStatusEnumActive) return (&cmmodel.GetAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) UpdateAsset(request *cmmodel.UpdateAssetRequest) (*cmmodel.UpdateAssetResponse, error) { f.updateAssetRequest = request body := (&cmmodel.UpdateAssetResponseBody{}).SetAssetId("asset-1").SetGroupId("group-1").SetAssetName("n").SetStatus(cmmodel.UpdateAssetResponseBodyStatusEnumActive) return (&cmmodel.UpdateAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) DeleteAsset(request *cmmodel.DeleteAssetRequest) (*cmmodel.DeleteAssetResponse, error) { f.deleteAssetRequest = request return (&cmmodel.DeleteAssetResponse{}).SetRequestId("req").SetState("OK").SetBody(true), nil } func (f *fakeChinaMobileAssetSDKClient) CreateAssetGroup(request *cmmodel.CreateAssetGroupRequest) (*cmmodel.CreateAssetGroupResponse, error) { f.createAssetGroupRequest = request body := (&cmmodel.CreateAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.CreateAssetGroupResponseBodyGroupTypeEnumAigc) return (&cmmodel.CreateAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) ListAssetGroups(request *cmmodel.ListAssetGroupsRequest) (*cmmodel.ListAssetGroupsResponse, error) { f.listAssetGroupsRequest = request item := cmmodel.ListAssetGroupsResponseData{} item.SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.ListAssetGroupsResponseDataGroupTypeEnumAigc) body := (&cmmodel.ListAssetGroupsResponseBody{}).SetData([]cmmodel.ListAssetGroupsResponseData{item}).SetTotal(1) return (&cmmodel.ListAssetGroupsResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) GetAssetGroup(request *cmmodel.GetAssetGroupRequest) (*cmmodel.GetAssetGroupResponse, error) { f.getAssetGroupRequest = request body := (&cmmodel.GetAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.GetAssetGroupResponseBodyGroupTypeEnumAigc) return (&cmmodel.GetAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) UpdateAssetGroup(request *cmmodel.UpdateAssetGroupRequest) (*cmmodel.UpdateAssetGroupResponse, error) { f.updateAssetGroupRequest = request body := (&cmmodel.UpdateAssetGroupResponseBody{}).SetGroupId("group-1").SetGroupName("g").SetGroupType(cmmodel.UpdateAssetGroupResponseBodyGroupTypeEnumAigc) return (&cmmodel.UpdateAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(body), nil } func (f *fakeChinaMobileAssetSDKClient) DeleteAssetGroup(request *cmmodel.DeleteAssetGroupRequest) (*cmmodel.DeleteAssetGroupResponse, error) { f.deleteAssetGroupRequest = request return (&cmmodel.DeleteAssetGroupResponse{}).SetRequestId("req").SetState("OK").SetBody(true), nil }