fix: sync harden star gift upgrade projections
This commit is contained in:
parent
ee74d941bb
commit
35d3908660
17 changed files with 772 additions and 40 deletions
|
|
@ -243,7 +243,7 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass {
|
|||
Peers: tgPeerList(shared.Peers),
|
||||
}
|
||||
case domain.MessageServiceActionStarGift:
|
||||
return tgMessageActionStarGift(m.ServiceAction.StarGift)
|
||||
return tgMessageActionStarGiftForViewer(m.ServiceAction.StarGift, msg.OwnerUserID)
|
||||
case domain.MessageServiceActionStarGiftUnique:
|
||||
return tgMessageActionStarGiftUnique(m.ServiceAction.StarGiftUnique)
|
||||
case domain.MessageServiceActionStarGiftOffer:
|
||||
|
|
|
|||
|
|
@ -1118,6 +1118,7 @@ type GiftsService interface {
|
|||
GiftByID(ctx context.Context, id int64) (domain.StarGift, bool, error)
|
||||
GiftRevisionByID(ctx context.Context, revisionID int64) (domain.StarGift, bool, error)
|
||||
CollectiblePreview(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error)
|
||||
CollectiblePreviewSample(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error)
|
||||
CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error)
|
||||
UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error)
|
||||
UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error)
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ func (r *Router) onPaymentsGetStarGiftUpgradePreview(ctx context.Context, giftID
|
|||
if giftID <= 0 || r.deps.Gifts == nil {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, giftID)
|
||||
preview, found, err := r.deps.Gifts.CollectiblePreviewSample(ctx, giftID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -876,7 +876,7 @@ func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int6
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projected := tgSavedStarGifts(gifts, catalog, availability)
|
||||
projected := tgSavedStarGifts(viewerUserID, gifts, catalog, availability)
|
||||
out := &tg.PaymentsSavedStarGifts{
|
||||
Count: count,
|
||||
Gifts: projected,
|
||||
|
|
@ -990,6 +990,30 @@ func tgStarGift(g domain.StarGift) *tg.StarGift {
|
|||
}
|
||||
|
||||
// tgMessageActionStarGift 把礼物服务消息载荷投影为 messageActionStarGift。
|
||||
func tgMessageActionStarGiftForViewer(in *domain.MessageStarGiftAction, viewerUserID int64) tg.MessageActionClass {
|
||||
if in == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
ownerUserID := in.PeerUserID
|
||||
if ownerUserID == 0 && in.To.Type == domain.PeerTypeUser {
|
||||
ownerUserID = in.To.ID
|
||||
}
|
||||
if ownerUserID <= 0 || viewerUserID <= 0 {
|
||||
return tgMessageActionStarGift(in)
|
||||
}
|
||||
projected := *in
|
||||
if viewerUserID == ownerUserID {
|
||||
// The owner upgrades through InputSavedStarGift. Exposing the separate
|
||||
// prepayment hash makes DrKLO prefer the wrong invoice family and use the
|
||||
// private dialog peer (the sender) as the alleged gift owner.
|
||||
projected.PrepaidUpgradeHash = ""
|
||||
} else {
|
||||
// Telegram defines messageActionStarGift.can_upgrade as receiver-only.
|
||||
projected.CanUpgrade = false
|
||||
}
|
||||
return tgMessageActionStarGift(&projected)
|
||||
}
|
||||
|
||||
func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionClass {
|
||||
if in == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
|
|
@ -1088,7 +1112,7 @@ func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.Save
|
|||
}
|
||||
|
||||
// tgSavedStarGifts 把已收到礼物实例投影为 []tg.SavedStarGift。
|
||||
func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) []tg.SavedStarGift {
|
||||
func tgSavedStarGifts(viewerUserID int64, gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) []tg.SavedStarGift {
|
||||
out := make([]tg.SavedStarGift, 0, len(gifts))
|
||||
for _, g := range gifts {
|
||||
item := tg.SavedStarGift{
|
||||
|
|
@ -1126,7 +1150,11 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta
|
|||
item.SetUpgradeStars(g.PrepaidUpgradeStars)
|
||||
item.CanUpgrade = true
|
||||
}
|
||||
if g.PrepaidUpgradeHash != "" && g.PrepaidUpgradeStars == 0 && canIssue {
|
||||
// This hash starts the separate "prepay someone else's upgrade"
|
||||
// invoice. The owner must use InputSavedStarGift instead; otherwise
|
||||
// DrKLO prefers the hash path and substitutes the private dialog peer.
|
||||
ownerIsViewer := g.Owner.Type == domain.PeerTypeUser && g.Owner.ID == viewerUserID
|
||||
if g.PrepaidUpgradeHash != "" && g.PrepaidUpgradeStars == 0 && canIssue && !ownerIsViewer {
|
||||
item.SetPrepaidUpgradeHash(g.PrepaidUpgradeHash)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
|
|
@ -304,7 +305,7 @@ func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleA
|
|||
historical.ID: {UpgradeStars: 75, SupplyTotal: 500, Issued: 12},
|
||||
}
|
||||
|
||||
projected := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)
|
||||
projected := tgSavedStarGifts(0, []domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)
|
||||
if len(projected) != 1 || !projected[0].CanUpgrade {
|
||||
t.Fatalf("saved gift = %#v, want current pool to make historical gift upgradable", projected)
|
||||
}
|
||||
|
|
@ -339,7 +340,7 @@ func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleA
|
|||
}
|
||||
|
||||
availability[historical.ID] = domain.StarGiftCollectibleAvailability{UpgradeStars: 75, SupplyTotal: 500, Issued: 500}
|
||||
soldOut := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
soldOut := tgSavedStarGifts(0, []domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
if soldOut.CanUpgrade {
|
||||
t.Fatal("sold-out collectible pool must not advertise upgrade")
|
||||
}
|
||||
|
|
@ -349,7 +350,7 @@ func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleA
|
|||
t.Fatal("sold-out catalog projection must not expose upgrade_stars")
|
||||
}
|
||||
saved.PrepaidUpgradeStars = 75
|
||||
soldOutPrepaid := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
soldOutPrepaid := tgSavedStarGifts(0, []domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
if soldOutPrepaid.CanUpgrade {
|
||||
t.Fatal("sold-out prepaid gift must not advertise an upgrade the aggregate will reject")
|
||||
}
|
||||
|
|
@ -375,7 +376,7 @@ func TestSavedStarGiftProjectionPreservesCollectibleLifecycle(t *testing.T) {
|
|||
CanExportAt: exportAt, TransferStars: 25, CanTransferAt: transferAt, CanResellAt: resellAt,
|
||||
DropOriginalDetailsStars: 30, CanCraftAt: readyAt,
|
||||
}
|
||||
projected := tgSavedStarGifts([]domain.SavedStarGift{saved}, nil, nil)
|
||||
projected := tgSavedStarGifts(0, []domain.SavedStarGift{saved}, nil, nil)
|
||||
if len(projected) != 1 {
|
||||
t.Fatalf("saved lifecycle projection count = %d", len(projected))
|
||||
}
|
||||
|
|
@ -401,7 +402,7 @@ func TestSavedStarGiftProjectionPreservesCollectibleLifecycle(t *testing.T) {
|
|||
}
|
||||
}
|
||||
assertLifecycle(t, projected[0])
|
||||
zero := tgSavedStarGifts([]domain.SavedStarGift{{
|
||||
zero := tgSavedStarGifts(0, []domain.SavedStarGift{{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: 7102}, GiftID: giftID, RevisionID: revision,
|
||||
MsgID: 45, Date: 101, UniqueGiftID: unique.ID, Unique: &unique,
|
||||
}}, nil, nil)[0]
|
||||
|
|
@ -427,7 +428,7 @@ func TestSavedStarGiftProjectionPreservesCollectibleLifecycle(t *testing.T) {
|
|||
channelSaved.Owner = domain.Peer{Type: domain.PeerTypeChannel, ID: 8102}
|
||||
channelSaved.MsgID = 0
|
||||
channelSaved.SavedID = 51
|
||||
channelProjected := tgSavedStarGifts([]domain.SavedStarGift{channelSaved}, nil, nil)[0]
|
||||
channelProjected := tgSavedStarGifts(0, []domain.SavedStarGift{channelSaved}, nil, nil)[0]
|
||||
if _, ok := channelProjected.GetCanCraftAt(); ok {
|
||||
t.Fatal("channel can_craft_at must be absent until channel Craft is executable")
|
||||
}
|
||||
|
|
@ -584,6 +585,59 @@ func TestMessageStarGiftProjectionSeparatesPaidPriceFromPrepaidAmount(t *testing
|
|||
}
|
||||
}
|
||||
|
||||
func TestStarGiftPrepaidUpgradeProjectionIsViewerScoped(t *testing.T) {
|
||||
const (
|
||||
senderID = int64(7101)
|
||||
ownerID = int64(7102)
|
||||
giftID = int64(8101)
|
||||
revision = int64(9101)
|
||||
)
|
||||
action := &domain.MessageStarGiftAction{
|
||||
GiftID: giftID, PeerUserID: ownerID, To: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID},
|
||||
CanUpgrade: true, PrepaidUpgradeHash: "prepaid-upgrade-hash-0123456789",
|
||||
}
|
||||
ownerMessage := domain.Message{
|
||||
ID: 20, OwnerUserID: ownerID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: senderID},
|
||||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
|
||||
Kind: domain.MessageServiceActionStarGift, StarGift: action,
|
||||
}},
|
||||
}
|
||||
ownerAction := tgMessage(ownerMessage).(*tg.MessageService).Action.(*tg.MessageActionStarGift)
|
||||
if !ownerAction.CanUpgrade {
|
||||
t.Fatal("owner message lost receiver-only can_upgrade")
|
||||
}
|
||||
if hash, ok := ownerAction.GetPrepaidUpgradeHash(); ok || hash != "" {
|
||||
t.Fatalf("owner message exposed prepaid hash %q set=%v", hash, ok)
|
||||
}
|
||||
|
||||
senderMessage := ownerMessage
|
||||
senderMessage.OwnerUserID = senderID
|
||||
senderMessage.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: ownerID}
|
||||
senderMessage.Out = true
|
||||
senderAction := tgMessage(senderMessage).(*tg.MessageService).Action.(*tg.MessageActionStarGift)
|
||||
if senderAction.CanUpgrade {
|
||||
t.Fatal("sender message exposed receiver-only can_upgrade")
|
||||
}
|
||||
if hash, ok := senderAction.GetPrepaidUpgradeHash(); !ok || hash != action.PrepaidUpgradeHash {
|
||||
t.Fatalf("sender message prepaid hash = %q set=%v", hash, ok)
|
||||
}
|
||||
|
||||
saved := domain.SavedStarGift{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: ownerID}, GiftID: giftID, RevisionID: revision,
|
||||
MsgID: 20, Date: 100, PrepaidUpgradeHash: action.PrepaidUpgradeHash,
|
||||
}
|
||||
catalog := map[int64]domain.StarGift{revision: {ID: giftID, RevisionID: revision}}
|
||||
availability := map[int64]domain.StarGiftCollectibleAvailability{giftID: {UpgradeStars: 25, SupplyTotal: 10}}
|
||||
ownerSaved := tgSavedStarGifts(ownerID, []domain.SavedStarGift{saved}, catalog, availability)[0]
|
||||
if hash, ok := ownerSaved.GetPrepaidUpgradeHash(); ok || hash != "" {
|
||||
t.Fatalf("owner saved gift exposed prepaid hash %q set=%v", hash, ok)
|
||||
}
|
||||
viewerSaved := tgSavedStarGifts(senderID, []domain.SavedStarGift{saved}, catalog, availability)[0]
|
||||
if hash, ok := viewerSaved.GetPrepaidUpgradeHash(); !ok || hash != action.PrepaidUpgradeHash {
|
||||
t.Fatalf("non-owner saved gift prepaid hash = %q set=%v", hash, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftUpgradeRPCReplaysCommittedReceiptAfterTerminalTransition(t *testing.T) {
|
||||
r, sender, owner, gift := starGiftTestRouter(t)
|
||||
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
||||
|
|
@ -761,6 +815,76 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
}
|
||||
}
|
||||
|
||||
func TestStarGiftUpgradePreviewBoundsRandomSampleWithoutShrinkingFullAttributes(t *testing.T) {
|
||||
r, _, owner, gift := starGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
giftService, ok := r.deps.Gifts.(*appstargifts.Service)
|
||||
if !ok {
|
||||
t.Fatalf("gift service = %T", r.deps.Gifts)
|
||||
}
|
||||
|
||||
models := make([]domain.StarGiftCollectibleAttribute, 0, 7)
|
||||
patterns := make([]domain.StarGiftCollectibleAttribute, 0, 6)
|
||||
backdrops := make([]domain.StarGiftCollectibleAttribute, 0, 6)
|
||||
for i := 0; i < 6; i++ {
|
||||
models = append(models, collectibleRPCAttribute(domain.StarGiftCollectibleModel, int64(8200+i), fmt.Sprintf("Model %d", i)))
|
||||
patterns = append(patterns, collectibleRPCAttribute(domain.StarGiftCollectiblePattern, int64(8300+i), fmt.Sprintf("Pattern %d", i)))
|
||||
backdrops = append(backdrops, collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, int64(8400+i), fmt.Sprintf("Backdrop %d", i)))
|
||||
}
|
||||
crafted := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8299, "Crafted")
|
||||
crafted.Crafted = true
|
||||
crafted.RarityKind = domain.StarGiftRarityLegendary
|
||||
crafted.RarityPermille = 0
|
||||
models = append(models, crafted)
|
||||
if _, err := giftService.PublishCollectibleRevision(context.Background(), domain.StarGiftCollectibleWrite{
|
||||
GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 500, SlugPrefix: "bounded-preview",
|
||||
Models: models, Patterns: patterns, Backdrops: backdrops, Actor: "test", CommandID: "bounded-preview",
|
||||
}); err != nil {
|
||||
t.Fatalf("publish collectible pool: %v", err)
|
||||
}
|
||||
|
||||
preview, err := r.onPaymentsGetStarGiftUpgradePreview(ctx, gift.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get bounded upgrade preview: %v", err)
|
||||
}
|
||||
counts := map[domain.StarGiftCollectibleAttributeKind]int{}
|
||||
identities := map[string]struct{}{}
|
||||
for _, attribute := range preview.SampleAttributes {
|
||||
var kind domain.StarGiftCollectibleAttributeKind
|
||||
var identity string
|
||||
switch value := attribute.(type) {
|
||||
case *tg.StarGiftAttributeModel:
|
||||
kind = domain.StarGiftCollectibleModel
|
||||
if value.Crafted {
|
||||
t.Fatal("ordinary upgrade preview included crafted model")
|
||||
}
|
||||
identity = fmt.Sprintf("model:%d", value.Document.GetID())
|
||||
case *tg.StarGiftAttributePattern:
|
||||
kind = domain.StarGiftCollectiblePattern
|
||||
identity = fmt.Sprintf("pattern:%d", value.Document.GetID())
|
||||
case *tg.StarGiftAttributeBackdrop:
|
||||
kind = domain.StarGiftCollectibleBackdrop
|
||||
identity = fmt.Sprintf("backdrop:%d", value.BackdropID)
|
||||
default:
|
||||
t.Fatalf("preview attribute = %T", attribute)
|
||||
}
|
||||
if _, duplicate := identities[identity]; duplicate {
|
||||
t.Fatalf("duplicate preview identity %q", identity)
|
||||
}
|
||||
identities[identity] = struct{}{}
|
||||
counts[kind]++
|
||||
}
|
||||
if len(preview.SampleAttributes) != 9 || counts[domain.StarGiftCollectibleModel] != 3 ||
|
||||
counts[domain.StarGiftCollectiblePattern] != 3 || counts[domain.StarGiftCollectibleBackdrop] != 3 {
|
||||
t.Fatalf("bounded preview count = %d kinds=%v, want three per kind", len(preview.SampleAttributes), counts)
|
||||
}
|
||||
|
||||
all, err := r.onPaymentsGetStarGiftUpgradeAttributes(ctx, gift.ID)
|
||||
if err != nil || len(all.Attributes) != 19 {
|
||||
t.Fatalf("complete upgrade attributes = %d err=%v, want 19", len(all.Attributes), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftCollectionsCRUDFilterOrderAndPin(t *testing.T) {
|
||||
r, _, owner, gift := starGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue