2026-06-25 17:00:11 +08:00

293 lines
11 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/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 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-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
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) 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
}