402 lines
16 KiB
Go
402 lines
16 KiB
Go
package resource
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/integration/walletclient"
|
||
"hyapp-admin-server/internal/middleware"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
|
||
"github.com/DATA-DOG/go-sqlmock"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
func TestUpdateMP4ResourceLayoutsPreservesResourceFields(t *testing.T) {
|
||
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
|
||
11: {
|
||
AppCode: "lalu",
|
||
ResourceId: 11,
|
||
ResourceCode: "profile_card_star",
|
||
ResourceType: resourceTypeProfileCard,
|
||
Name: "星光资料卡",
|
||
Status: "active",
|
||
Grantable: true,
|
||
GrantStrategy: "extend_expiry",
|
||
WalletAssetType: "RESOURCE",
|
||
WalletAssetAmount: 1,
|
||
UsageScopes: []string{resourceTypeProfileCard},
|
||
AssetUrl: "https://cdn.example.test/profile_card.png",
|
||
PreviewUrl: "https://cdn.example.test/profile_card_cover.png",
|
||
AnimationUrl: "https://cdn.example.test/profile_card.mp4?token=1",
|
||
MetadataJson: `{"profile_card_layout":{"source_width":1136,"source_height":1680,"color_content_width":750,"content_top":117,"content_bottom":1666,"content_height":1550,"detect_version":1}}`,
|
||
SortOrder: 9,
|
||
ManagerGrantEnabled: true,
|
||
PriceType: resourcePriceTypeCoin,
|
||
CoinPrice: 100,
|
||
},
|
||
}}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
metadataJSON := `{
|
||
"animation_format":"mp4",
|
||
"mp4_alpha_layout":{
|
||
"alpha_layout":"alpha_left_rgb_right",
|
||
"video_w":1500,
|
||
"video_h":1334,
|
||
"rgb_frame":[750,0,750,1334],
|
||
"alpha_frame":[0,0,750,1334],
|
||
"confirmed":true,
|
||
"detect_version":1
|
||
},
|
||
"debug":true
|
||
}`
|
||
body := fmt.Sprintf(`{"items":[{"resourceId":11,"metadataJson":%q}]}`, metadataJSON)
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodPut, "/admin/resources/mp4-layouts/batch", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("batch mp4 layout update status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.updates) != 1 {
|
||
t.Fatalf("expected one wallet update, got %d", len(wallet.updates))
|
||
}
|
||
update := wallet.updates[0]
|
||
if update.GetResourceId() != 11 || update.GetName() != "星光资料卡" || update.GetStatus() != "active" || update.GetCoinPrice() != 100 {
|
||
t.Fatalf("resource fields should be preserved: %+v", update)
|
||
}
|
||
if update.ManagerGrantEnabled == nil || !update.GetManagerGrantEnabled() || update.GetOperatorUserId() != 7 {
|
||
t.Fatalf("operator and manager grant should be preserved: %+v", update)
|
||
}
|
||
if !strings.Contains(update.GetMetadataJson(), `"profile_card_layout"`) || !strings.Contains(update.GetMetadataJson(), `"mp4_alpha_layout"`) {
|
||
t.Fatalf("profile card and mp4 metadata should both be kept: %s", update.GetMetadataJson())
|
||
}
|
||
if strings.Contains(update.GetMetadataJson(), "debug") {
|
||
t.Fatalf("debug metadata should be dropped: %s", update.GetMetadataJson())
|
||
}
|
||
}
|
||
|
||
func TestUpdateMP4ResourceLayoutsIgnoresNonMP4Resource(t *testing.T) {
|
||
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
|
||
12: {
|
||
AppCode: "lalu",
|
||
ResourceId: 12,
|
||
ResourceCode: "gift_rose",
|
||
ResourceType: "gift",
|
||
Name: "玫瑰",
|
||
Status: "active",
|
||
AnimationUrl: "https://cdn.example.test/gift.svga",
|
||
MetadataJson: "{}",
|
||
GrantStrategy: "increase_quantity",
|
||
PriceType: resourcePriceTypeCoin,
|
||
CoinPrice: 10,
|
||
},
|
||
}}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
body := fmt.Sprintf(`{"items":[{"resourceId":12,"metadataJson":%q}]}`, mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodPut, "/admin/resources/mp4-layouts/batch", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("non-mp4 resource should be ignored, got %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.updates) != 0 {
|
||
t.Fatalf("non-mp4 resource should not update wallet")
|
||
}
|
||
}
|
||
|
||
func TestDeleteGiftOnlyCallsGiftConfigDelete(t *testing.T) {
|
||
wallet := &mockResourceWallet{}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodDelete, "/admin/gifts/rose", nil)
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("delete gift status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.deletedGifts) != 1 {
|
||
t.Fatalf("expected one gift delete request, got %d", len(wallet.deletedGifts))
|
||
}
|
||
deleteReq := wallet.deletedGifts[0]
|
||
if deleteReq.GetAppCode() != "lalu" || deleteReq.GetGiftId() != "rose" || deleteReq.GetOperatorUserId() != 7 {
|
||
t.Fatalf("delete gift request mismatch: %+v", deleteReq)
|
||
}
|
||
}
|
||
|
||
func TestDeleteResourceCallsWalletDeleteResource(t *testing.T) {
|
||
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
|
||
11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
|
||
}}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodDelete, "/admin/resources/11", nil)
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("delete resource status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.deletedResources) != 1 {
|
||
t.Fatalf("expected one resource delete request, got %d", len(wallet.deletedResources))
|
||
}
|
||
deleteReq := wallet.deletedResources[0]
|
||
if deleteReq.GetAppCode() != "lalu" || deleteReq.GetResourceId() != 11 || deleteReq.GetOperatorUserId() != 7 {
|
||
t.Fatalf("delete resource request mismatch: %+v", deleteReq)
|
||
}
|
||
}
|
||
|
||
func TestBatchDeleteResourcesCallsWalletBatchDelete(t *testing.T) {
|
||
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
|
||
11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
|
||
12: {AppCode: "lalu", ResourceId: 12, ResourceCode: "ring_resource", ResourceType: "gift", Name: "Ring"},
|
||
}}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodPost, "/admin/resources/batch-delete", strings.NewReader(`{"resourceIds":[11,12]}`))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("batch delete resources status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.batchDeletedResources) != 1 {
|
||
t.Fatalf("expected one batch resource delete request, got %d", len(wallet.batchDeletedResources))
|
||
}
|
||
deleteReq := wallet.batchDeletedResources[0]
|
||
if deleteReq.GetAppCode() != "lalu" || fmt.Sprint(deleteReq.GetResourceIds()) != "[11 12]" || deleteReq.GetOperatorUserId() != 7 {
|
||
t.Fatalf("batch delete resource request mismatch: %+v", deleteReq)
|
||
}
|
||
}
|
||
|
||
func TestBatchCreateGiftsCallsWalletBatchCreate(t *testing.T) {
|
||
wallet := &mockResourceWallet{}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
|
||
|
||
body := `{"items":[{"giftId":"rose","resourceId":11,"status":"active","name":"Rose","priceVersion":"default","giftTypeCode":"normal","chargeAssetType":"COIN","coinPrice":10,"regionIds":[0]},{"giftId":"ring","resourceId":12,"status":"active","name":"Ring","priceVersion":"default","giftTypeCode":"normal","chargeAssetType":"COIN","coinPrice":20,"regionIds":[0]}]}`
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodPost, "/admin/gifts/batch", strings.NewReader(body))
|
||
request.Header.Set("Content-Type", "application/json")
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusCreated {
|
||
t.Fatalf("batch create gifts status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.batchCreatedGifts) != 1 {
|
||
t.Fatalf("expected one batch gift create request, got %d", len(wallet.batchCreatedGifts))
|
||
}
|
||
createReq := wallet.batchCreatedGifts[0]
|
||
if createReq.GetAppCode() != "lalu" || len(createReq.GetItems()) != 2 || createReq.GetItems()[0].GetGiftId() != "rose" || createReq.GetItems()[1].GetGiftId() != "ring" {
|
||
t.Fatalf("batch create gift request mismatch: %+v", createReq)
|
||
}
|
||
}
|
||
|
||
func TestIdentityAutoGrantConfigRouteDoesNotHitResourceGroupID(t *testing.T) {
|
||
router := newResourceHandlerTestRouter(New(&mockResourceWallet{}, nil, nil, time.Second, nil))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodGet, "/admin/resource-groups/identity-auto-grant-config", nil)
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
// 这里故意使用 nil store,让配置 handler 返回 500;如果路由被 :group_id 抢走,会变成 400 的 ID 参数错误。
|
||
if recorder.Code != http.StatusInternalServerError {
|
||
t.Fatalf("identity config route should hit config handler, got %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if strings.Contains(recorder.Body.String(), "ID 参数不正确") {
|
||
t.Fatalf("identity config route should not be parsed as group_id: %s", recorder.Body.String())
|
||
}
|
||
}
|
||
|
||
func TestListResourceGrantsResolvesDisplayIDBeforeWalletFilter(t *testing.T) {
|
||
db, sqlMock, err := sqlmock.New()
|
||
if err != nil {
|
||
t.Fatalf("create sql mock failed: %v", err)
|
||
}
|
||
defer db.Close()
|
||
|
||
wallet := &mockResourceWallet{}
|
||
router := newResourceHandlerTestRouter(New(wallet, nil, db, time.Second, nil))
|
||
resolvedUserID := int64(327702592329093120)
|
||
sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
|
||
WithArgs(
|
||
"lalu",
|
||
"111",
|
||
"111",
|
||
"111",
|
||
sqlmock.AnyArg(),
|
||
"111",
|
||
"%111%",
|
||
"111",
|
||
"111",
|
||
"111",
|
||
sqlmock.AnyArg(),
|
||
"111",
|
||
).
|
||
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(resolvedUserID))
|
||
|
||
recorder := httptest.NewRecorder()
|
||
request := httptest.NewRequest(http.MethodGet, "/admin/resource-grants?target_user_id=111", nil)
|
||
router.ServeHTTP(recorder, request)
|
||
|
||
if recorder.Code != http.StatusOK {
|
||
t.Fatalf("list grants status mismatch: %d %s", recorder.Code, recorder.Body.String())
|
||
}
|
||
if len(wallet.listGrantRequests) != 1 {
|
||
t.Fatalf("expected one wallet list request, got %d", len(wallet.listGrantRequests))
|
||
}
|
||
if got := wallet.listGrantRequests[0].GetTargetUserId(); got != resolvedUserID {
|
||
t.Fatalf("target display id should be resolved before wallet filter: got %d want %d", got, resolvedUserID)
|
||
}
|
||
if err := sqlMock.ExpectationsWereMet(); err != nil {
|
||
t.Fatalf("sql expectations mismatch: %v", err)
|
||
}
|
||
}
|
||
|
||
func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
|
||
gin.SetMode(gin.TestMode)
|
||
router := gin.New()
|
||
router.Use(func(c *gin.Context) {
|
||
c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
|
||
c.Set(middleware.ContextRequestID, "resource-handler-test")
|
||
c.Set(middleware.ContextUserID, uint(7))
|
||
c.Set(middleware.ContextUsername, "tester")
|
||
// 测试路由走真实 RegisterRoutes,权限一次性放开,避免中间件短路导致无法覆盖路由优先级。
|
||
c.Set(middleware.ContextPermissions, []string{
|
||
"emoji-pack:create",
|
||
"emoji-pack:view",
|
||
"gift:delete",
|
||
"gift:view",
|
||
"gift:update",
|
||
"gift:create",
|
||
"gift:status",
|
||
"resource:create",
|
||
"resource:delete",
|
||
"resource-grant:create",
|
||
"resource-grant:revoke",
|
||
"resource-grant:view",
|
||
"resource-group:create",
|
||
"resource-group:update",
|
||
"resource-group:view",
|
||
"resource-shop:update",
|
||
"resource-shop:view",
|
||
"resource:update",
|
||
"resource:view",
|
||
})
|
||
c.Next()
|
||
})
|
||
RegisterRoutes(router.Group(""), handler)
|
||
return router
|
||
}
|
||
|
||
type mockResourceWallet struct {
|
||
walletclient.Client
|
||
resources map[int64]*walletv1.Resource
|
||
updates []*walletv1.UpdateResourceRequest
|
||
deletedResources []*walletv1.DeleteResourceRequest
|
||
batchDeletedResources []*walletv1.BatchDeleteResourcesRequest
|
||
batchCreatedGifts []*walletv1.BatchCreateGiftConfigsRequest
|
||
deletedGifts []*walletv1.DeleteGiftConfigRequest
|
||
listGrantRequests []*walletv1.ListResourceGrantsRequest
|
||
}
|
||
|
||
func (m *mockResourceWallet) GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
|
||
resource := m.resources[req.GetResourceId()]
|
||
if resource == nil {
|
||
return nil, fmt.Errorf("resource not found")
|
||
}
|
||
return &walletv1.GetResourceResponse{Resource: resource}, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) UpdateResource(ctx context.Context, req *walletv1.UpdateResourceRequest) (*walletv1.ResourceResponse, error) {
|
||
m.updates = append(m.updates, req)
|
||
return &walletv1.ResourceResponse{Resource: &walletv1.Resource{
|
||
AppCode: req.GetAppCode(),
|
||
ResourceId: req.GetResourceId(),
|
||
ResourceCode: req.GetResourceCode(),
|
||
ResourceType: req.GetResourceType(),
|
||
Name: req.GetName(),
|
||
Status: req.GetStatus(),
|
||
Grantable: req.GetGrantable(),
|
||
GrantStrategy: req.GetGrantStrategy(),
|
||
WalletAssetType: req.GetWalletAssetType(),
|
||
WalletAssetAmount: req.GetWalletAssetAmount(),
|
||
UsageScopes: req.GetUsageScopes(),
|
||
AssetUrl: req.GetAssetUrl(),
|
||
PreviewUrl: req.GetPreviewUrl(),
|
||
AnimationUrl: req.GetAnimationUrl(),
|
||
MetadataJson: req.GetMetadataJson(),
|
||
SortOrder: req.GetSortOrder(),
|
||
ManagerGrantEnabled: req.GetManagerGrantEnabled(),
|
||
PriceType: req.GetPriceType(),
|
||
CoinPrice: req.GetCoinPrice(),
|
||
}}, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error) {
|
||
m.deletedResources = append(m.deletedResources, req)
|
||
resource := m.resources[req.GetResourceId()]
|
||
if resource == nil {
|
||
return nil, fmt.Errorf("resource not found")
|
||
}
|
||
return &walletv1.ResourceResponse{Resource: resource}, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error) {
|
||
m.batchDeletedResources = append(m.batchDeletedResources, req)
|
||
resp := &walletv1.BatchDeleteResourcesResponse{Resources: make([]*walletv1.Resource, 0, len(req.GetResourceIds()))}
|
||
for _, resourceID := range req.GetResourceIds() {
|
||
resource := m.resources[resourceID]
|
||
if resource == nil {
|
||
return nil, fmt.Errorf("resource not found")
|
||
}
|
||
resp.Resources = append(resp.Resources, resource)
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error) {
|
||
m.batchCreatedGifts = append(m.batchCreatedGifts, req)
|
||
resp := &walletv1.BatchCreateGiftConfigsResponse{Gifts: make([]*walletv1.GiftConfig, 0, len(req.GetItems()))}
|
||
for _, item := range req.GetItems() {
|
||
resp.Gifts = append(resp.Gifts, &walletv1.GiftConfig{
|
||
AppCode: item.GetAppCode(),
|
||
GiftId: item.GetGiftId(),
|
||
ResourceId: item.GetResourceId(),
|
||
Status: item.GetStatus(),
|
||
Name: item.GetName(),
|
||
})
|
||
}
|
||
return resp, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) DeleteGiftConfig(ctx context.Context, req *walletv1.DeleteGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
|
||
m.deletedGifts = append(m.deletedGifts, req)
|
||
return &walletv1.GiftConfigResponse{Gift: &walletv1.GiftConfig{
|
||
AppCode: req.GetAppCode(),
|
||
GiftId: req.GetGiftId(),
|
||
ResourceId: 11,
|
||
Status: "active",
|
||
Name: "Rose",
|
||
}}, nil
|
||
}
|
||
|
||
func (m *mockResourceWallet) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) {
|
||
// 列表过滤的修正点发生在 admin-server 调 wallet 之前;测试只需要捕获请求形状,
|
||
// 返回空页即可避免后续用户资料补全查询干扰断言。
|
||
m.listGrantRequests = append(m.listGrantRequests, req)
|
||
return &walletv1.ListResourceGrantsResponse{Grants: []*walletv1.ResourceGrant{}, Total: 0}, nil
|
||
}
|