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 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") c.Next() }) router.PUT("/admin/resources/mp4-layouts/batch", handler.UpdateMP4ResourceLayouts) return router } type mockResourceWallet struct { walletclient.Client resources map[int64]*walletv1.Resource updates []*walletv1.UpdateResourceRequest } 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 }