838 lines
46 KiB
Go
838 lines
46 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
users := NewUserStore(pool)
|
|
sender := createTestUser(t, ctx, users, "+1778"+suffix+"41", "CollectibleSender", "")
|
|
owner := createTestUser(t, ctx, users, "+1778"+suffix+"42", "CollectibleOwner", "")
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
|
|
gifts := NewStarGiftStore(pool)
|
|
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
|
|
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
|
Title: "Comet", Stars: 50, ConvertStars: 25, Enabled: true,
|
|
Document: collectibleTestDocument(baseDocumentID, "gift.tgs"),
|
|
Blob: collectibleTestBlob(baseDocumentID, "gift"),
|
|
Animation: collectibleTestAnimation("gift.tgs"),
|
|
Actor: "integration", CommandID: "catalog-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create collectible catalog gift: %v", err)
|
|
}
|
|
poolRevision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
|
GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "comet-" + suffix,
|
|
Models: []domain.StarGiftCollectibleAttribute{{
|
|
Kind: domain.StarGiftCollectibleModel, Name: "Aurora", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 922,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+1, "model.tgs"),
|
|
Blob: collectibleTestBlobPtr(baseDocumentID+1, "model"), Animation: collectibleTestAnimationPtr("model.tgs"),
|
|
OfficialDocumentID: 5100000000000000001,
|
|
}, {
|
|
Kind: domain.StarGiftCollectibleModel, Name: "Crafted Aurora", RarityKind: domain.StarGiftRarityLegendary, Crafted: true,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+3, "crafted-model.tgs"),
|
|
Blob: collectibleTestBlobPtr(baseDocumentID+3, "crafted-model"), Animation: collectibleTestAnimationPtr("crafted-model.tgs"),
|
|
OfficialDocumentID: 5100000000000000003,
|
|
}, {
|
|
Kind: domain.StarGiftCollectibleModel, Name: "Solar", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 78,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+4, "model-two.tgs"),
|
|
Blob: collectibleTestBlobPtr(baseDocumentID+4, "model-two"), Animation: collectibleTestAnimationPtr("model-two.tgs"),
|
|
OfficialDocumentID: 5100000000000000004,
|
|
}},
|
|
Patterns: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 989,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs")},
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Rings", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 11,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+5, "pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+5, "pattern-two"), Animation: collectibleTestAnimationPtr("pattern-two.tgs")},
|
|
},
|
|
Backdrops: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Midnight", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 999},
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Daylight", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1},
|
|
},
|
|
Actor: "integration", CommandID: "collectibles-" + suffix,
|
|
OfficialGiftID: 5170145012310081615, SourceManifestSHA256: make([]byte, 32),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("publish collectible pool: %v", err)
|
|
}
|
|
if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 3 || len(poolRevision.Patterns) != 2 || len(poolRevision.Backdrops) != 2 ||
|
|
!poolRevision.Models[1].Crafted || poolRevision.Models[1].RarityKind != domain.StarGiftRarityLegendary ||
|
|
poolRevision.Models[1].RarityPermille != 0 || poolRevision.Models[0].OfficialDocumentID != 5100000000000000001 {
|
|
t.Fatalf("published pool = %+v", poolRevision)
|
|
}
|
|
storedRevision, found, err := gifts.ActiveCollectibleRevision(ctx, entry.Gift.ID)
|
|
if err != nil || !found || storedRevision.Models[0].Animation == nil || len(storedRevision.Models[0].Animation.JSON) == 0 {
|
|
t.Fatalf("full active collectible revision = found:%v err:%v value:%+v", found, err, storedRevision)
|
|
}
|
|
fullProjection, found, err := gifts.ActiveCollectibleProjection(ctx, entry.Gift.ID, 0)
|
|
if err != nil || !found || len(fullProjection.Models) != 3 || len(fullProjection.Patterns) != 2 || len(fullProjection.Backdrops) != 2 ||
|
|
fullProjection.Models[0].Animation == nil || len(fullProjection.Models[0].Animation.JSON) != 0 ||
|
|
fullProjection.Patterns[0].Animation == nil || len(fullProjection.Patterns[0].Animation.JSON) != 0 {
|
|
t.Fatalf("complete collectible projection = found:%v err:%v value:%+v", found, err, fullProjection)
|
|
}
|
|
sampleProjection, found, err := gifts.ActiveCollectibleProjection(ctx, entry.Gift.ID, 1)
|
|
if err != nil || !found || len(sampleProjection.Models) != 1 || len(sampleProjection.Patterns) != 1 || len(sampleProjection.Backdrops) != 1 ||
|
|
sampleProjection.Models[0].Crafted || sampleProjection.Models[0].RarityKind != domain.StarGiftRarityPermille ||
|
|
sampleProjection.Models[0].Animation == nil || len(sampleProjection.Models[0].Animation.JSON) != 0 {
|
|
t.Fatalf("sampled collectible projection = found:%v err:%v value:%+v", found, err, sampleProjection)
|
|
}
|
|
availability, err := gifts.CollectibleAvailability(ctx, []int64{entry.Gift.ID, entry.Gift.ID + 1})
|
|
if err != nil {
|
|
t.Fatalf("collectible availability: %v", err)
|
|
}
|
|
if got, ok := availability[entry.Gift.ID]; !ok || got.UpgradeStars != 100 || got.SupplyTotal != 10 || got.Issued != 0 {
|
|
t.Fatalf("collectible availability = %+v, want active published pool", availability)
|
|
}
|
|
if _, ok := availability[entry.Gift.ID+1]; ok {
|
|
t.Fatalf("unknown gift must not have collectible availability: %+v", availability)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE star_gift_collectible_revisions SET issued=issued WHERE id=$1`, poolRevision.ID); err == nil {
|
|
t.Fatal("published collectible revision accepted a non-advancing issuance update")
|
|
}
|
|
var guardedIssued int
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&guardedIssued); err != nil || guardedIssued != 0 {
|
|
t.Fatalf("issued after rejected manual update = %d err %v, want 0", guardedIssued, err)
|
|
}
|
|
|
|
messages := NewMessageStore(pool)
|
|
saved := createCollectibleSavedGift(t, ctx, messages, gifts, entry.Gift, domain.SavedStarGift{
|
|
Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID,
|
|
Date: 1700001000, ConvertStars: 25, Message: "original",
|
|
})
|
|
savedID := saved.ID
|
|
stars := NewStarsStore(pool)
|
|
if _, _, err := stars.EnsureGrant(ctx, owner.ID, 1000, 1700001001); err != nil {
|
|
t.Fatalf("grant upgrade stars: %v", err)
|
|
}
|
|
upgrades := NewStarGiftUpgradeStore(pool, messages)
|
|
req := domain.StarGiftUpgradeRequest{
|
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: saved.MsgID},
|
|
KeepOriginalDetails: true, ChargeStars: 100, FormID: 991,
|
|
CommandKey: "paid-" + suffix, Date: 1700001002,
|
|
}
|
|
upgraded, err := upgrades.UpgradeStarGift(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("upgrade star gift: %v", err)
|
|
}
|
|
if upgraded.Duplicate || upgraded.Unique.Num != 1 || upgraded.Unique.Slug != "comet-"+suffix+"-1" ||
|
|
upgraded.Unique.Model.Name != "Aurora" || upgraded.Unique.Pattern.Name != "Orbit" ||
|
|
upgraded.Unique.Backdrop.Name != "Midnight" || upgraded.Balance.Balance != 900 ||
|
|
upgraded.Saved.ID != savedID || upgraded.Saved.UniqueGiftID != upgraded.Unique.ID || upgraded.Saved.UpgradeMsgID <= 0 {
|
|
t.Fatalf("upgrade result = %+v", upgraded)
|
|
}
|
|
ownerMessage := upgraded.Send.RecipientMessage
|
|
if ownerMessage.OwnerUserID != owner.ID || ownerMessage.Pts <= 0 || ownerMessage.Media == nil ||
|
|
ownerMessage.Media.ServiceAction == nil || ownerMessage.Media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique ||
|
|
ownerMessage.Media.ServiceAction.StarGiftUnique == nil || ownerMessage.Media.ServiceAction.StarGiftUnique.Gift.ID != upgraded.Unique.ID {
|
|
t.Fatalf("owner upgrade service message = %+v", ownerMessage)
|
|
}
|
|
uniqueAction := ownerMessage.Media.ServiceAction.StarGiftUnique
|
|
if uniqueAction.SavedID != 0 || uniqueAction.Peer.Type != "" || uniqueAction.Peer.ID != 0 {
|
|
t.Fatalf("user unique action leaked channel peer/saved_id: %+v", uniqueAction)
|
|
}
|
|
senderUniqueAction := upgraded.Send.SenderMessage.Media.ServiceAction.StarGiftUnique
|
|
if senderUniqueAction == nil || senderUniqueAction.SavedID != 0 {
|
|
t.Fatalf("sender unique action leaked owner-only saved_id: %+v", senderUniqueAction)
|
|
}
|
|
if byOutput, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: ownerMessage.ID}); err != nil || !found || byOutput.ID != savedID {
|
|
t.Fatalf("owner upgrade output ref = %+v found %v err %v", byOutput, found, err)
|
|
}
|
|
var ownerAliasCount, senderAliasCount int
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs
|
|
WHERE owner_user_id=$1 AND msg_id=$2 AND saved_gift_id=$3`, owner.ID, ownerMessage.ID, savedID).Scan(&ownerAliasCount); err != nil {
|
|
t.Fatalf("load owner upgrade output alias: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_user_message_refs
|
|
WHERE owner_user_id=$1 AND msg_id=$2`, sender.ID, ownerMessage.ID).Scan(&senderAliasCount); err != nil {
|
|
t.Fatalf("load sender upgrade output alias: %v", err)
|
|
}
|
|
if ownerAliasCount != 1 || senderAliasCount != 0 {
|
|
t.Fatalf("upgrade output aliases owner=%d sender=%d, want owner-only", ownerAliasCount, senderAliasCount)
|
|
}
|
|
ownerSourceEdit := upgradedSourceEditForUser(upgraded, owner.ID)
|
|
if ownerSourceEdit.Event.Pts <= ownerMessage.Pts || ownerSourceEdit.Message.Media == nil ||
|
|
ownerSourceEdit.Message.Media.ServiceAction == nil || ownerSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
|
ownerSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != ownerMessage.ID ||
|
|
ownerSourceEdit.Message.Media.ServiceAction.StarGift.CanUpgrade {
|
|
t.Fatalf("owner source gift was not durably marked upgraded: %+v", ownerSourceEdit)
|
|
}
|
|
senderSourceEdit := upgradedSourceEditForUser(upgraded, sender.ID)
|
|
if senderSourceEdit.Message.Media == nil || senderSourceEdit.Message.Media.ServiceAction == nil ||
|
|
senderSourceEdit.Message.Media.ServiceAction.StarGift == nil ||
|
|
senderSourceEdit.Message.Media.ServiceAction.StarGift.UpgradeMsgID != upgraded.Send.SenderMessage.ID {
|
|
t.Fatalf("sender source gift has wrong box-local upgrade link: %+v", senderSourceEdit)
|
|
}
|
|
difference, err := NewUpdateEventStore(pool).ListAfter(ctx, owner.ID, ownerMessage.Pts-1, 4)
|
|
if err != nil || len(difference) < 2 || difference[0].Type != domain.UpdateEventNewMessage ||
|
|
difference[0].Message.ID != ownerMessage.ID || difference[1].Type != domain.UpdateEventEditMessage ||
|
|
difference[1].Message.ID != saved.MsgID || difference[1].Message.Media == nil ||
|
|
difference[1].Message.Media.ServiceAction == nil || difference[1].Message.Media.ServiceAction.StarGift == nil ||
|
|
difference[1].Message.Media.ServiceAction.StarGift.UpgradeMsgID != ownerMessage.ID {
|
|
t.Fatalf("owner upgrade difference = %+v err %v", difference, err)
|
|
}
|
|
|
|
var (
|
|
issued, uniqueCount, commandCount int
|
|
reason string
|
|
)
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&issued); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM unique_star_gifts WHERE source_saved_gift_id=$1`, savedID).Scan(&uniqueCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_upgrade_commands WHERE source_saved_gift_id=$1`, savedID).Scan(&commandCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 ORDER BY id DESC LIMIT 1`, owner.ID).Scan(&reason); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if issued != 1 || uniqueCount != 1 || commandCount != 1 || reason != string(domain.StarsReasonGiftUpgrade) {
|
|
t.Fatalf("durable aggregate issued=%d unique=%d command=%d reason=%q", issued, uniqueCount, commandCount, reason)
|
|
}
|
|
receipt, found, err := upgrades.StarGiftUpgradeReceipt(ctx, owner.ID, req.CommandKey)
|
|
if err != nil || !found || receipt.SourceSavedGiftID != savedID || receipt.UniqueGiftID != upgraded.Unique.ID ||
|
|
receipt.FormID != req.FormID || receipt.ChargeStars != req.ChargeStars || receipt.RequirePrepaid ||
|
|
!receipt.KeepOriginalDetails || receipt.BalanceAfter != 900 || receipt.SourceEditPts != ownerSourceEdit.Event.Pts {
|
|
t.Fatalf("upgrade receipt = %+v found=%v err=%v", receipt, found, err)
|
|
}
|
|
|
|
replayed, err := upgrades.UpgradeStarGift(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("replay upgrade: %v", err)
|
|
}
|
|
if !replayed.Duplicate || replayed.Unique.ID != upgraded.Unique.ID || replayed.Balance.Balance != 900 ||
|
|
upgradedSourceEditForUser(replayed, owner.ID).Event.Pts != ownerSourceEdit.Event.Pts {
|
|
t.Fatalf("replayed upgrade = %+v", replayed)
|
|
}
|
|
conflictingReplay := req
|
|
conflictingReplay.KeepOriginalDetails = false
|
|
if _, err := upgrades.UpgradeStarGift(ctx, conflictingReplay); err == nil {
|
|
t.Fatal("same command key with a changed semantic payload must not replay")
|
|
}
|
|
if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: owner.ID, Ref: req.Ref, ChargeStars: 100, FormID: 992,
|
|
CommandKey: "different-" + suffix, Date: 1700001003,
|
|
}); !errors.Is(err, domain.ErrStarGiftAlreadyUpgraded) {
|
|
t.Fatalf("second logical upgrade err = %v", err)
|
|
}
|
|
bal, err := stars.GetBalance(ctx, owner.ID)
|
|
if err != nil || bal.Balance != 900 {
|
|
t.Fatalf("balance after retries = %+v err %v", bal, err)
|
|
}
|
|
|
|
prepaidSaved := createCollectibleSavedGift(t, ctx, messages, gifts, entry.Gift, domain.SavedStarGift{
|
|
Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID,
|
|
// A later pool revision may raise the current price; the historical paid
|
|
// amount remains an entitlement instead of being compared to that price.
|
|
Date: 1700001004, ConvertStars: 25, PrepaidUpgradeStars: 50,
|
|
})
|
|
prepaidSavedID := prepaidSaved.ID
|
|
prepaid, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: prepaidSaved.MsgID},
|
|
RequirePrepaid: true, CommandKey: "prepaid-" + suffix, Date: 1700001005,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("free prepaid upgrade: %v", err)
|
|
}
|
|
if prepaid.Saved.ID != prepaidSavedID || prepaid.Unique.Num != 2 || prepaid.Balance.Balance != 900 ||
|
|
prepaid.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique == nil ||
|
|
!prepaid.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique.PrepaidUpgrade {
|
|
t.Fatalf("prepaid upgrade = %+v", prepaid)
|
|
}
|
|
|
|
insufficientSaved := createCollectibleSavedGift(t, ctx, messages, gifts, entry.Gift, domain.SavedStarGift{
|
|
Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID,
|
|
Date: 1700001006, ConvertStars: 25,
|
|
})
|
|
insufficientSavedID := insufficientSaved.ID
|
|
if _, err := stars.Debit(ctx, owner.ID, 850, domain.StarsReasonReaction,
|
|
domain.Peer{Type: domain.PeerTypeChannel, ID: 777001}, 1700001007, "paid reaction", ""); err != nil {
|
|
t.Fatalf("seed isolated paid reaction debit: %v", err)
|
|
}
|
|
if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: insufficientSaved.MsgID},
|
|
ChargeStars: 100, FormID: 994, CommandKey: "insufficient-" + suffix, Date: 1700001008,
|
|
}); !errors.Is(err, domain.ErrStarsInsufficient) {
|
|
t.Fatalf("insufficient upgrade err = %v", err)
|
|
}
|
|
insufficientAfter, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: insufficientSaved.MsgID})
|
|
if err != nil || !found || insufficientAfter.ID != insufficientSavedID || insufficientAfter.UniqueGiftID != 0 {
|
|
t.Fatalf("saved gift after rejected upgrade = %+v found %v err %v", insufficientAfter, found, err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&issued); err != nil || issued != 2 {
|
|
t.Fatalf("issued after rejected upgrade = %d err %v, want 2", issued, err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 ORDER BY id DESC LIMIT 1`, owner.ID).Scan(&reason); err != nil || reason != string(domain.StarsReasonReaction) {
|
|
t.Fatalf("paid reaction ledger reason after rejected upgrade = %q err %v", reason, err)
|
|
}
|
|
|
|
collection, err := gifts.CreateCollection(ctx, ownerPeer, "Favorites", []int64{savedID})
|
|
if err != nil {
|
|
t.Fatalf("create unique collection: %v", err)
|
|
}
|
|
filtered, err := gifts.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{Owner: ownerPeer, CollectionID: collection.CollectionID, Limit: 10})
|
|
if err != nil || filtered.Count != 1 || len(filtered.Gifts) != 1 || filtered.Gifts[0].UniqueGiftID != upgraded.Unique.ID {
|
|
t.Fatalf("collection filter = %+v err %v", filtered, err)
|
|
}
|
|
if err := gifts.SetPinned(ctx, ownerPeer, []int64{savedID}); err != nil {
|
|
t.Fatalf("pin unique gift: %v", err)
|
|
}
|
|
pinned, found, err := gifts.GetByRef(ctx, req.Ref)
|
|
if err != nil || !found || pinned.PinnedOrder != 1 || len(pinned.CollectionIDs) != 1 || pinned.CollectionIDs[0] != collection.CollectionID {
|
|
t.Fatalf("pinned saved gift = %+v found %v err %v", pinned, found, err)
|
|
}
|
|
|
|
concurrentOwner := createTestUser(t, ctx, users, "+1778"+suffix+"43", "ConcurrentOwner", "")
|
|
concurrentPeer := domain.Peer{Type: domain.PeerTypeUser, ID: concurrentOwner.ID}
|
|
concurrentSaved := createCollectibleSavedGift(t, ctx, messages, gifts, entry.Gift, domain.SavedStarGift{
|
|
Owner: concurrentPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID,
|
|
Date: 1700001010, ConvertStars: 25,
|
|
})
|
|
if _, _, err := stars.EnsureGrant(ctx, concurrentOwner.ID, 150, 1700001011); err != nil {
|
|
t.Fatalf("grant concurrent balance: %v", err)
|
|
}
|
|
type concurrentDebitResult struct {
|
|
kind string
|
|
err error
|
|
}
|
|
start := make(chan struct{})
|
|
results := make(chan concurrentDebitResult, 2)
|
|
go func() {
|
|
<-start
|
|
_, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: concurrentOwner.ID, Ref: domain.SavedStarGiftRef{Owner: concurrentPeer, MsgID: concurrentSaved.MsgID},
|
|
ChargeStars: 100, FormID: 993, CommandKey: "concurrent-upgrade-" + suffix, Date: 1700001012,
|
|
})
|
|
results <- concurrentDebitResult{kind: "gift_upgrade", err: err}
|
|
}()
|
|
go func() {
|
|
<-start
|
|
_, err := stars.Debit(ctx, concurrentOwner.ID, 100, domain.StarsReasonReaction,
|
|
domain.Peer{Type: domain.PeerTypeChannel, ID: 777002}, 1700001012, "paid reaction", "")
|
|
results <- concurrentDebitResult{kind: "paid_reaction", err: err}
|
|
}()
|
|
close(start)
|
|
firstResult, secondResult := <-results, <-results
|
|
successes := 0
|
|
for _, result := range []concurrentDebitResult{firstResult, secondResult} {
|
|
if result.err == nil {
|
|
successes++
|
|
continue
|
|
}
|
|
if !errors.Is(result.err, domain.ErrStarsInsufficient) {
|
|
t.Fatalf("concurrent %s err = %v, want Stars insufficient for loser", result.kind, result.err)
|
|
}
|
|
}
|
|
if successes != 1 {
|
|
t.Fatalf("concurrent debit results = %+v / %+v, want exactly one success", firstResult, secondResult)
|
|
}
|
|
concurrentBalance, err := stars.GetBalance(ctx, concurrentOwner.ID)
|
|
if err != nil || concurrentBalance.Balance != 50 {
|
|
t.Fatalf("concurrent balance = %+v err %v, want 50", concurrentBalance, err)
|
|
}
|
|
reasonRows, err := pool.Query(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 AND amount<0 ORDER BY id`, concurrentOwner.ID)
|
|
if err != nil {
|
|
t.Fatalf("list concurrent debit reasons: %v", err)
|
|
}
|
|
var debitReasons []string
|
|
for reasonRows.Next() {
|
|
var got string
|
|
if err := reasonRows.Scan(&got); err != nil {
|
|
reasonRows.Close()
|
|
t.Fatal(err)
|
|
}
|
|
debitReasons = append(debitReasons, got)
|
|
}
|
|
if err := reasonRows.Err(); err != nil {
|
|
reasonRows.Close()
|
|
t.Fatal(err)
|
|
}
|
|
reasonRows.Close()
|
|
if len(debitReasons) != 1 || (debitReasons[0] != string(domain.StarsReasonGiftUpgrade) && debitReasons[0] != string(domain.StarsReasonReaction)) {
|
|
t.Fatalf("concurrent debit reasons = %+v, want exactly one isolated business reason", debitReasons)
|
|
}
|
|
|
|
soldOutEntry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
|
Title: "Nova", Stars: 25, ConvertStars: 10, Enabled: true,
|
|
Document: collectibleTestDocument(baseDocumentID+100, "nova.tgs"),
|
|
Blob: collectibleTestBlob(baseDocumentID+100, "nova"), Animation: collectibleTestAnimation("nova.tgs"),
|
|
Actor: "integration", CommandID: "soldout-catalog-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create sold-out catalog: %v", err)
|
|
}
|
|
soldOutRevision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
|
GiftID: soldOutEntry.Gift.ID, UpgradeStars: 10, SupplyTotal: 1, SlugPrefix: "nova-" + suffix,
|
|
Models: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Nova", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+101, "nova-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+101, "nova-model"), Animation: collectibleTestAnimationPtr("nova-model.tgs")},
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Nova Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+103, "nova-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+103, "nova-model-two"), Animation: collectibleTestAnimationPtr("nova-model-two.tgs")},
|
|
},
|
|
Patterns: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Ray", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+102, "nova-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+102, "nova-pattern"), Animation: collectibleTestAnimationPtr("nova-pattern.tgs")},
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Ray Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+104, "nova-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+104, "nova-pattern-two"), Animation: collectibleTestAnimationPtr("nova-pattern-two.tgs")},
|
|
},
|
|
Backdrops: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Void", BackdropID: 2, CenterColor: 0x101010, EdgeColor: 0x202020, PatternColor: 0x303030, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Light", BackdropID: 3, CenterColor: 0xeeeeee, EdgeColor: 0xcccccc, PatternColor: 0xaaaaaa, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
},
|
|
Actor: "integration", CommandID: "soldout-pool-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("publish sold-out pool: %v", err)
|
|
}
|
|
soldOutOwner := createTestUser(t, ctx, users, "+1778"+suffix+"44", "SoldOutOwner", "")
|
|
soldOutPeer := domain.Peer{Type: domain.PeerTypeUser, ID: soldOutOwner.ID}
|
|
soldOutSaved := make([]domain.SavedStarGift, 0, 2)
|
|
for index := range 2 {
|
|
soldOutSaved = append(soldOutSaved, createCollectibleSavedGift(t, ctx, messages, gifts, soldOutEntry.Gift, domain.SavedStarGift{
|
|
Owner: soldOutPeer, FromUserID: sender.ID, GiftID: soldOutEntry.Gift.ID, RevisionID: soldOutEntry.Gift.RevisionID,
|
|
Date: 1700001020 + index, ConvertStars: 10,
|
|
}))
|
|
}
|
|
if _, _, err := stars.EnsureGrant(ctx, soldOutOwner.ID, 100, 1700001022); err != nil {
|
|
t.Fatalf("grant sold-out owner balance: %v", err)
|
|
}
|
|
if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: soldOutOwner.ID, Ref: domain.SavedStarGiftRef{Owner: soldOutPeer, MsgID: soldOutSaved[0].MsgID},
|
|
ChargeStars: 10, FormID: 995, CommandKey: "soldout-first-" + suffix, Date: 1700001023,
|
|
}); err != nil {
|
|
t.Fatalf("fill collectible supply: %v", err)
|
|
}
|
|
balanceBeforeSoldOut, _ := stars.GetBalance(ctx, soldOutOwner.ID)
|
|
if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: soldOutOwner.ID, Ref: domain.SavedStarGiftRef{Owner: soldOutPeer, MsgID: soldOutSaved[1].MsgID},
|
|
ChargeStars: 10, FormID: 996, CommandKey: "soldout-second-" + suffix, Date: 1700001024,
|
|
}); !errors.Is(err, domain.ErrStarGiftCollectibleSoldOut) {
|
|
t.Fatalf("sold-out upgrade err = %v", err)
|
|
}
|
|
balanceAfterSoldOut, _ := stars.GetBalance(ctx, soldOutOwner.ID)
|
|
var soldOutIssued int
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, soldOutRevision.ID).Scan(&soldOutIssued); err != nil || soldOutIssued != 1 || balanceAfterSoldOut.Balance != balanceBeforeSoldOut.Balance {
|
|
t.Fatalf("sold-out state issued=%d balance=%d->%d err=%v", soldOutIssued, balanceBeforeSoldOut.Balance, balanceAfterSoldOut.Balance, err)
|
|
}
|
|
|
|
ordinaryCollection, err := gifts.CreateCollection(ctx, ownerPeer, "Ordinary", []int64{insufficientSavedID})
|
|
if err != nil {
|
|
t.Fatalf("create ordinary collection: %v", err)
|
|
}
|
|
converted, err := gifts.MarkConverted(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: insufficientSaved.MsgID})
|
|
if err != nil || !converted.Converted || converted.PinnedOrder != 0 || len(converted.CollectionIDs) != 0 {
|
|
t.Fatalf("convert collection member = %+v err %v", converted, err)
|
|
}
|
|
collections, err := gifts.ListCollections(ctx, ownerPeer)
|
|
if err != nil {
|
|
t.Fatalf("list collections after conversion: %v", err)
|
|
}
|
|
foundOrdinary := false
|
|
for _, got := range collections {
|
|
if got.CollectionID != ordinaryCollection.CollectionID {
|
|
continue
|
|
}
|
|
foundOrdinary = true
|
|
if len(got.GiftIDs) != 0 || got.Hash != domain.StarGiftCollectionHash(got.Title, nil) || got.Hash == ordinaryCollection.Hash {
|
|
t.Fatalf("ordinary collection after conversion = %+v", got)
|
|
}
|
|
}
|
|
if !foundOrdinary {
|
|
t.Fatal("ordinary collection disappeared after member conversion")
|
|
}
|
|
filteredAfterConvert, err := gifts.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{
|
|
Owner: ownerPeer, CollectionID: ordinaryCollection.CollectionID, Limit: 10,
|
|
})
|
|
if err != nil || filteredAfterConvert.Count != 0 || len(filteredAfterConvert.Gifts) != 0 {
|
|
t.Fatalf("converted collection filter = %+v err %v, want empty", filteredAfterConvert, err)
|
|
}
|
|
}
|
|
|
|
func TestStarGiftCollectiblePreviewActivationGuardPostgres(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
|
|
gifts := NewStarGiftStore(pool)
|
|
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
|
Title: "Unsafe Preview " + suffix, Stars: 10, ConvertStars: 5, Enabled: true,
|
|
Document: collectibleTestDocument(baseDocumentID, "unsafe-preview.tgs"), Blob: collectibleTestBlob(baseDocumentID, "unsafe-preview"),
|
|
Animation: collectibleTestAnimation("unsafe-preview.tgs"), Actor: "integration", CommandID: "unsafe-preview-catalog-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create unsafe-preview catalog: %v", err)
|
|
}
|
|
var revisionID int64
|
|
if err := pool.QueryRow(ctx, `
|
|
INSERT INTO star_gift_collectible_revisions
|
|
(gift_id, revision, upgrade_stars, supply_total, slug_prefix, status, created_by, command_id)
|
|
VALUES ($1, 1, 10, 10, $2, 'draft', 'integration', $3)
|
|
RETURNING id`, entry.Gift.ID, "unsafe-"+suffix, "unsafe-preview-pool-"+suffix).Scan(&revisionID); err != nil {
|
|
t.Fatalf("insert unsafe-preview revision: %v", err)
|
|
}
|
|
animation := collectibleTestAnimation("unsafe-preview-attribute.tgs")
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO star_gift_collectible_models
|
|
(collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format,
|
|
width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, crafted, sort_order)
|
|
VALUES ($1, 'Only Model', $2, $3::jsonb, $4, 'model.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, false, 0)`,
|
|
revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil {
|
|
t.Fatalf("insert unsafe-preview model: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO star_gift_collectible_patterns
|
|
(collectible_revision_id, name, document_id, animation_json, animation_sha256, source_name, source_format,
|
|
width, height, frame_rate, in_point, out_point, rarity_kind, rarity_permille, sort_order)
|
|
VALUES ($1, 'Only Pattern', $2, $3::jsonb, $4, 'pattern.tgs', 'tgs', 512, 512, 30, 0, 60, 'permille', 1000, 0)`,
|
|
revisionID, baseDocumentID, string(animation.JSON), animation.SHA256); err != nil {
|
|
t.Fatalf("insert unsafe-preview pattern: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
INSERT INTO star_gift_collectible_backdrops
|
|
(collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color, text_color,
|
|
rarity_kind, rarity_permille, sort_order)
|
|
VALUES ($1, 'Only Backdrop', 1, 1, 2, 3, 4, 'permille', 1000, 0)`, revisionID); err != nil {
|
|
t.Fatalf("insert unsafe-preview backdrop: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `
|
|
UPDATE star_gift_collectible_revisions SET status='published', published_at=now() WHERE id=$1`, revisionID); err != nil {
|
|
t.Fatalf("publish unsafe-preview revision directly: %v", err)
|
|
}
|
|
if _, err := pool.Exec(ctx, `UPDATE star_gift_catalog SET collectible_revision_id=$2 WHERE gift_id=$1`, entry.Gift.ID, revisionID); err == nil {
|
|
t.Fatal("database activated a collectible preview pool with one client-distinct item per category")
|
|
}
|
|
var activeRevisionID *int64
|
|
if err := pool.QueryRow(ctx, `SELECT collectible_revision_id FROM star_gift_catalog WHERE gift_id=$1`, entry.Gift.ID).Scan(&activeRevisionID); err != nil || activeRevisionID != nil {
|
|
t.Fatalf("unsafe preview activation pointer=%v err=%v, want null", activeRevisionID, err)
|
|
}
|
|
}
|
|
|
|
func TestStarGiftUpgradeWithoutCraftedModelDoesNotAdvertiseCraft(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
now := int(time.Now().Unix())
|
|
users := NewUserStore(pool)
|
|
sender := createTestUser(t, ctx, users, "+1779"+suffix+"51", "NoCraftSender", "")
|
|
owner := createTestUser(t, ctx, users, "+1779"+suffix+"52", "NoCraftOwner", "")
|
|
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
|
|
|
gifts := NewStarGiftStore(pool)
|
|
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
|
|
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
|
Title: "No Craft " + suffix, Stars: 50, ConvertStars: 25, Enabled: true,
|
|
Document: collectibleTestDocument(baseDocumentID, "no-craft-gift.tgs"),
|
|
Blob: collectibleTestBlob(baseDocumentID, "no-craft-gift"), Animation: collectibleTestAnimation("no-craft-gift.tgs"),
|
|
Actor: "integration", CommandID: "no-craft-catalog-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create no-craft catalog gift: %v", err)
|
|
}
|
|
revision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
|
GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "no-craft-" + suffix,
|
|
Models: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Ordinary", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+1, "no-craft-model.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "no-craft-model"), Animation: collectibleTestAnimationPtr("no-craft-model.tgs")},
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Ordinary Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+3, "no-craft-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "no-craft-model-two"), Animation: collectibleTestAnimationPtr("no-craft-model-two.tgs")},
|
|
},
|
|
Patterns: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+2, "no-craft-pattern.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "no-craft-pattern"), Animation: collectibleTestAnimationPtr("no-craft-pattern.tgs")},
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+4, "no-craft-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "no-craft-pattern-two"), Animation: collectibleTestAnimationPtr("no-craft-pattern-two.tgs")},
|
|
},
|
|
Backdrops: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
},
|
|
Actor: "integration", CommandID: "no-craft-pool-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("publish no-craft pool: %v", err)
|
|
}
|
|
if len(revision.Models) != 2 || revision.Models[0].Crafted || revision.Models[1].Crafted {
|
|
t.Fatalf("no-craft pool models = %+v", revision.Models)
|
|
}
|
|
|
|
messages := NewMessageStore(pool)
|
|
saved := createCollectibleSavedGift(t, ctx, messages, gifts, entry.Gift, domain.SavedStarGift{
|
|
Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID,
|
|
Date: now, ConvertStars: 25,
|
|
})
|
|
stars := NewStarsStore(pool)
|
|
if _, _, err := stars.EnsureGrant(ctx, owner.ID, 1000, now); err != nil {
|
|
t.Fatalf("grant no-craft upgrade stars: %v", err)
|
|
}
|
|
upgrades := NewStarGiftUpgradeStore(pool, messages, WithStarGiftLifecyclePolicy(domain.StarGiftLifecyclePolicy{
|
|
TransferStars: 25, DropOriginalDetailsStars: 25, OfferMinStars: 1, CraftChancePermille: 750,
|
|
}))
|
|
upgraded, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{
|
|
UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: saved.MsgID},
|
|
ChargeStars: 100, FormID: 551, CommandKey: "no-craft-upgrade-" + suffix, Date: now + 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upgrade no-craft gift: %v", err)
|
|
}
|
|
uniqueAction := upgraded.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique
|
|
if upgraded.Unique.CraftChancePermille != 0 || upgraded.Saved.CanCraftAt != 0 ||
|
|
uniqueAction == nil || uniqueAction.Gift.CraftChancePermille != 0 || uniqueAction.CanCraftAt != 0 {
|
|
t.Fatalf("no-craft capability leaked: saved=%+v unique=%+v action=%+v", upgraded.Saved, upgraded.Unique, uniqueAction)
|
|
}
|
|
|
|
lifecycle := NewStarGiftLifecycleStore(pool, messages, 1_000_000)
|
|
page, err := lifecycle.ListCraftStarGifts(ctx, owner.ID, entry.Gift.ID, "", 10)
|
|
if err != nil || page.Count != 0 || len(page.Gifts) != 0 {
|
|
t.Fatalf("no-craft candidate page = %+v err %v", page, err)
|
|
}
|
|
if _, err := lifecycle.CraftStarGift(ctx, domain.StarGiftCraftRequest{
|
|
UserID: owner.ID, Refs: []domain.SavedStarGiftRef{{Owner: ownerPeer, MsgID: saved.MsgID}},
|
|
CommandKey: "no-craft-attempt-" + suffix, Date: now + 2,
|
|
}); !errors.Is(err, domain.ErrStarGiftCraftUnavailable) {
|
|
t.Fatalf("no-craft attempt err = %v", err)
|
|
}
|
|
var lifecycleStatus string
|
|
var burned bool
|
|
var commandCount int
|
|
if err := pool.QueryRow(ctx, `SELECT p.lifecycle_status,u.burned
|
|
FROM peer_star_gifts p JOIN unique_star_gifts u ON u.id=p.unique_gift_id WHERE p.id=$1`, upgraded.Saved.ID).
|
|
Scan(&lifecycleStatus, &burned); err != nil {
|
|
t.Fatalf("load no-craft aggregate: %v", err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_craft_commands WHERE user_id=$1 AND command_key=$2`,
|
|
owner.ID, "no-craft-attempt-"+suffix).Scan(&commandCount); err != nil {
|
|
t.Fatalf("count no-craft commands: %v", err)
|
|
}
|
|
if lifecycleStatus != "active" || burned || commandCount != 0 {
|
|
t.Fatalf("no-craft attempt mutated aggregate: status=%q burned=%t commands=%d", lifecycleStatus, burned, commandCount)
|
|
}
|
|
}
|
|
|
|
func TestAdminUniqueStarGiftGrantIsAtomicAndReplayable(t *testing.T) {
|
|
pool := testPool(t)
|
|
ctx := context.Background()
|
|
suffix := randomSuffix(t)
|
|
now := int(time.Now().Unix())
|
|
recipient := createTestUser(t, ctx, NewUserStore(pool), "+1780"+suffix+"61", "AdminGiftRecipient", "")
|
|
gifts := NewStarGiftStore(pool)
|
|
baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000
|
|
entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
|
|
Title: "Admin Grant " + suffix, Stars: 50, ConvertStars: 25, Enabled: true,
|
|
Document: collectibleTestDocument(baseDocumentID, "admin-grant-gift.tgs"),
|
|
Blob: collectibleTestBlob(baseDocumentID, "admin-grant-gift"), Animation: collectibleTestAnimation("admin-grant-gift.tgs"),
|
|
Actor: "integration", CommandID: "admin-grant-catalog-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create admin grant catalog gift: %v", err)
|
|
}
|
|
revision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
|
GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 2, SlugPrefix: "admin-grant-" + suffix,
|
|
Models: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Model One", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+1, "admin-model-one.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+1, "admin-model-one"), Animation: collectibleTestAnimationPtr("admin-model-one.tgs")},
|
|
{Kind: domain.StarGiftCollectibleModel, Name: "Model Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestDocumentPtr(baseDocumentID+2, "admin-model-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+2, "admin-model-two"), Animation: collectibleTestAnimationPtr("admin-model-two.tgs")},
|
|
},
|
|
Patterns: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern One", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+3, "admin-pattern-one.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+3, "admin-pattern-one"), Animation: collectibleTestAnimationPtr("admin-pattern-one.tgs")},
|
|
{Kind: domain.StarGiftCollectiblePattern, Name: "Pattern Two", RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500,
|
|
Document: collectibleTestPatternDocumentPtr(baseDocumentID+4, "admin-pattern-two.tgs"), Blob: collectibleTestBlobPtr(baseDocumentID+4, "admin-pattern-two"), Animation: collectibleTestAnimationPtr("admin-pattern-two.tgs")},
|
|
},
|
|
Backdrops: []domain.StarGiftCollectibleAttribute{
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop One", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
{Kind: domain.StarGiftCollectibleBackdrop, Name: "Backdrop Two", BackdropID: 2, CenterColor: 0xaabbcc, EdgeColor: 0x778899, PatternColor: 0xddeeff, TextColor: 0x111111, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 500},
|
|
},
|
|
Actor: "integration", CommandID: "admin-grant-pool-" + suffix,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("publish admin grant collectible pool: %v", err)
|
|
}
|
|
upgrades := NewStarGiftUpgradeStore(pool, NewMessageStore(pool))
|
|
invalid := domain.AdminStarGiftGrant{
|
|
SenderID: domain.OfficialSystemUserID, Recipient: domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID},
|
|
GiftID: entry.Gift.ID, Upgrade: true, CommandKey: "admin-invalid-" + suffix, Date: now,
|
|
ModelAttributeID: revision.Models[0].ID + 9_999_999,
|
|
}
|
|
if _, err := upgrades.GrantUniqueStarGift(ctx, invalid); !errors.Is(err, domain.ErrStarGiftCollectibleInvalid) {
|
|
t.Fatalf("invalid admin grant error=%v", err)
|
|
}
|
|
var issued, messageCount, savedCount, uniqueCount, commandCount int
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, revision.ID).Scan(&issued); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
invalidRandomID := lifecycleCommandRandomID("admin-collectible-grant", recipient.ID, invalid.CommandKey)
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM private_messages WHERE sender_user_id=$1 AND random_id=$2`,
|
|
domain.OfficialSystemUserID, invalidRandomID).Scan(&messageCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM peer_star_gifts WHERE owner_peer_type='user' AND owner_peer_id=$1 AND gift_id=$2`,
|
|
recipient.ID, entry.Gift.ID).Scan(&savedCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM unique_star_gifts WHERE gift_id=$1`, entry.Gift.ID).Scan(&uniqueCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_admin_grant_commands WHERE recipient_user_id=$1`,
|
|
recipient.ID).Scan(&commandCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if issued != 0 || messageCount != 0 || savedCount != 0 || uniqueCount != 0 || commandCount != 0 {
|
|
t.Fatalf("failed grant leaked state: issued=%d messages=%d saved=%d unique=%d commands=%d",
|
|
issued, messageCount, savedCount, uniqueCount, commandCount)
|
|
}
|
|
|
|
req := invalid
|
|
req.CommandKey = "admin-success-" + suffix
|
|
req.Message = "atomic collectible"
|
|
req.ModelAttributeID = revision.Models[0].ID
|
|
req.PatternAttributeID = revision.Patterns[0].ID
|
|
req.BackdropAttributeID = revision.Backdrops[0].ID
|
|
granted, err := upgrades.GrantUniqueStarGift(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("grant admin unique gift: %v", err)
|
|
}
|
|
action := granted.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique
|
|
if granted.Duplicate || granted.Saved.MsgID <= 0 || granted.Saved.MsgID != granted.Saved.UpgradeMsgID ||
|
|
granted.Saved.UniqueGiftID != granted.Unique.ID || granted.Unique.Num != 1 ||
|
|
action == nil || !action.Assigned || !action.Saved || action.Gift.ID != granted.Unique.ID {
|
|
t.Fatalf("admin grant result=%+v action=%+v", granted, action)
|
|
}
|
|
events, err := NewUpdateEventStore(pool).ListAfter(ctx, recipient.ID, granted.Send.RecipientMessage.Pts-1, 1)
|
|
if err != nil || len(events) != 1 || events[0].Message.Media == nil ||
|
|
events[0].Message.Media.ServiceAction == nil ||
|
|
events[0].Message.Media.ServiceAction.StarGiftUnique == nil ||
|
|
events[0].Message.Media.ServiceAction.StarGiftUnique.Gift.ID != granted.Unique.ID {
|
|
t.Fatalf("admin grant durable update=%+v err=%v", events, err)
|
|
}
|
|
|
|
replay, err := upgrades.GrantUniqueStarGift(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("replay admin unique gift: %v", err)
|
|
}
|
|
if !replay.Duplicate || replay.Saved.ID != granted.Saved.ID || replay.Unique.ID != granted.Unique.ID ||
|
|
replay.Send.RecipientMessage.ID != granted.Send.RecipientMessage.ID {
|
|
t.Fatalf("admin grant replay=%+v want saved=%d unique=%d msg=%d",
|
|
replay, granted.Saved.ID, granted.Unique.ID, granted.Send.RecipientMessage.ID)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, revision.ID).Scan(&issued); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_admin_grant_commands WHERE recipient_user_id=$1`,
|
|
recipient.ID).Scan(&commandCount); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if issued != 1 || commandCount != 1 {
|
|
t.Fatalf("replay duplicated aggregate: issued=%d commands=%d", issued, commandCount)
|
|
}
|
|
}
|
|
|
|
func collectibleTestAnimation(name string) domain.StarGiftAnimation {
|
|
return domain.StarGiftAnimation{
|
|
SourceName: name, SourceFormat: domain.StarGiftAnimationTGS,
|
|
JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`),
|
|
TGS: []byte("test"), SHA256: make([]byte, 32), Width: 512, Height: 512, FrameRate: 30, OutPoint: 30,
|
|
}
|
|
}
|
|
|
|
func collectibleTestAnimationPtr(name string) *domain.StarGiftAnimation {
|
|
animation := collectibleTestAnimation(name)
|
|
return &animation
|
|
}
|
|
|
|
func collectibleTestDocument(id int64, name string) domain.Document {
|
|
return domain.Document{
|
|
ID: id, AccessHash: id + 100, FileReference: []byte("collectible-test"), Date: 1700001000,
|
|
MimeType: "application/x-tgsticker", Size: 4, DCID: 2,
|
|
Attributes: []domain.DocumentAttribute{
|
|
{Kind: domain.DocAttrImageSize, W: 512, H: 512},
|
|
{Kind: domain.DocAttrSticker, Alt: "🎁"},
|
|
{Kind: domain.DocAttrFilename, FileName: name},
|
|
},
|
|
}
|
|
}
|
|
|
|
func collectibleTestDocumentPtr(id int64, name string) *domain.Document {
|
|
document := collectibleTestDocument(id, name)
|
|
return &document
|
|
}
|
|
|
|
func collectibleTestPatternDocumentPtr(id int64, name string) *domain.Document {
|
|
document := collectibleTestDocument(id, name)
|
|
document.Attributes[1] = domain.DocumentAttribute{Kind: domain.DocAttrCustomEmoji, Alt: "🎁", TextColor: true}
|
|
document.Thumbs = []domain.PhotoSize{{Kind: domain.PhotoSizeKindPath, Type: "j", Bytes: []byte{1}}}
|
|
return &document
|
|
}
|
|
|
|
func collectibleTestBlob(id int64, suffix string) domain.FileBlob {
|
|
return domain.FileBlob{
|
|
LocationKey: fmt.Sprintf("doc:%d", id), Backend: domain.MediaBackendLocalFS,
|
|
ObjectKey: "collectible-integration-" + suffix, Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker",
|
|
}
|
|
}
|
|
|
|
func collectibleTestBlobPtr(id int64, suffix string) *domain.FileBlob {
|
|
blob := collectibleTestBlob(id, suffix)
|
|
return &blob
|
|
}
|
|
|
|
// createCollectibleSavedGift seeds the same valid source-message + saved-gift
|
|
// invariant as the purchase aggregate. Tests must not invent a peer_star_gifts
|
|
// msg_id that has no durable message box behind it.
|
|
func createCollectibleSavedGift(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
messages *MessageStore,
|
|
gifts *StarGiftStore,
|
|
gift domain.StarGift,
|
|
saved domain.SavedStarGift,
|
|
) domain.SavedStarGift {
|
|
t.Helper()
|
|
sticker := gift.Sticker
|
|
sent, err := messages.SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
|
SenderUserID: saved.FromUserID,
|
|
RecipientUserID: saved.Owner.ID,
|
|
RandomID: (time.Now().UnixNano() & 0x7fffffffffffffff) ^ saved.Owner.ID ^ int64(saved.Date),
|
|
Date: saved.Date,
|
|
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
|
|
Kind: domain.MessageServiceActionStarGift,
|
|
StarGift: &domain.MessageStarGiftAction{
|
|
GiftID: gift.ID, Stars: gift.Stars, ConvertStars: saved.ConvertStars,
|
|
Title: gift.Title, Sticker: &sticker, Message: saved.Message,
|
|
FromUserID: saved.FromUserID, PeerUserID: saved.Owner.ID, Saved: true,
|
|
CanUpgrade: gift.UpgradeStars > 0, PrepaidUpgrade: saved.PrepaidUpgradeStars > 0,
|
|
UpgradePriceStars: gift.UpgradeStars, UpgradeStars: saved.PrepaidUpgradeStars,
|
|
},
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create collectible source message: %v", err)
|
|
}
|
|
saved.MsgID = sent.RecipientMessage.ID
|
|
id, err := gifts.Create(ctx, saved)
|
|
if err != nil {
|
|
t.Fatalf("create saved gift: %v", err)
|
|
}
|
|
saved.ID = id
|
|
return saved
|
|
}
|
|
|
|
func upgradedSourceEditForUser(result domain.StarGiftUpgradeResult, userID int64) domain.EditedMessageForUser {
|
|
for _, edit := range result.SourceEdits {
|
|
if edit.UserID == userID {
|
|
return edit
|
|
}
|
|
}
|
|
return domain.EditedMessageForUser{UserID: userID}
|
|
}
|