2026-06-25 16:14:07 +08:00

239 lines
8.9 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 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/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 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 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-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
deletedGifts []*walletv1.DeleteGiftConfigRequest
}
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) 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
}