owpengram-server/internal/store/postgres/star_gift_integration_test.go
2026-07-16 12:38:53 +08:00

177 lines
8.3 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 postgres
import (
"context"
"errors"
"fmt"
"testing"
"time"
"telesrv/internal/domain"
)
// TestStarGiftStorePostgres 回归迁移 0089目录不可变版本与用户收到礼物实例对真实 PG 的 CRUD
// (版本固定 / 创建 / keyset 分页 / excludeUnsaved / 隐藏切换 / 转换幂等)。
func TestStarGiftStorePostgres(t *testing.T) {
pool := testPool(t)
ctx := context.Background()
st := NewStarGiftStore(pool)
users := NewUserStore(pool)
suffix := randomSuffix(t)
owner, err := users.Create(ctx, domain.User{AccessHash: 81, Phone: "+1779" + suffix + "51", FirstName: "GiftOwner"})
if err != nil {
t.Fatalf("create owner: %v", err)
}
from, err := users.Create(ctx, domain.User{AccessHash: 82, Phone: "+1779" + suffix + "52", FirstName: "GiftSender"})
if err != nil {
t.Fatalf("create sender: %v", err)
}
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
docID := time.Now().UnixNano() & 0x7fffffffffffffff
documentIDs := []int64{docID}
locationKeys := []string{"doc:" + fmt.Sprint(docID)}
entry, err := st.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
Stars: 50, ConvertStars: 50, Enabled: true, Document: domain.Document{
ID: docID, AccessHash: docID + 1, MimeType: "application/x-tgsticker", Size: 4, DCID: 2,
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}},
},
Blob: domain.FileBlob{LocationKey: "doc:" + fmt.Sprint(docID), Backend: domain.MediaBackendLocalFS, ObjectKey: "test-star-gift", Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker"},
Animation: domain.StarGiftAnimation{JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`), SHA256: make([]byte, 32), SourceFormat: domain.StarGiftAnimationTGS, Width: 512, Height: 512},
Actor: "test", CommandID: "test-star-gift-" + suffix,
})
if err != nil {
t.Fatalf("create catalog gift: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, "DELETE FROM peer_star_gifts WHERE owner_peer_id IN ($1, $2)", owner.ID, int64(987654321))
tx, _ := pool.Begin(ctx)
if tx != nil {
_, _ = tx.Exec(ctx, "DELETE FROM star_gift_catalog WHERE gift_id=$1", entry.Gift.ID)
_, _ = tx.Exec(ctx, "DELETE FROM star_gift_catalog_revisions WHERE gift_id=$1", entry.Gift.ID)
_, _ = tx.Exec(ctx, "DELETE FROM file_blobs WHERE location_key = ANY($1::text[])", locationKeys)
_, _ = tx.Exec(ctx, "DELETE FROM documents WHERE id = ANY($1::bigint[])", documentIDs)
_ = tx.Commit(ctx)
}
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, from.ID})
})
// 创建三份礼物msg_id 递增)。
for i := 0; i < 3; i++ {
if _, err := st.Create(ctx, domain.SavedStarGift{
Owner: ownerPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 100 + i,
Date: 1700000000 + i, ConvertStars: 50,
}); err != nil {
t.Fatalf("create gift #%d: %v", i, err)
}
}
// active revision 更新后,已收到的礼物必须继续固定到购买瞬间的 immutable revision。
docID2 := docID + 1
documentIDs = append(documentIDs, docID2)
locationKeys = append(locationKeys, "doc:"+fmt.Sprint(docID2))
updated, err := st.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{
GiftID: entry.Gift.ID, Title: "Revision 2", Stars: 75, ConvertStars: 25, Enabled: true,
Document: domain.Document{
ID: docID2, AccessHash: docID2 + 1, MimeType: "application/x-tgsticker", Size: 4, DCID: 2,
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}},
},
Blob: domain.FileBlob{LocationKey: "doc:" + fmt.Sprint(docID2), Backend: domain.MediaBackendLocalFS, ObjectKey: "test-star-gift-v2", Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker"},
Animation: domain.StarGiftAnimation{JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`), SHA256: make([]byte, 32), SourceFormat: domain.StarGiftAnimationTGS, Width: 512, Height: 512},
Actor: "test", CommandID: "test-star-gift-v2-" + suffix,
})
if err != nil {
t.Fatalf("create catalog revision 2: %v", err)
}
if updated.Revision != 2 || updated.Gift.RevisionID == entry.Gift.RevisionID {
t.Fatalf("revision 2 = %+v, want a new immutable revision", updated)
}
if updated.ReceivedCount != 3 {
t.Fatalf("revision 2 received count = %d, want all 3 historical instances", updated.ReceivedCount)
}
historical, found, err := st.CatalogRevision(ctx, entry.Gift.RevisionID)
if err != nil || !found || historical.Stars != 50 || historical.Sticker.ID != docID {
t.Fatalf("historical revision = %+v found %v err %v", historical, found, err)
}
// keyset 分页:每页 2末页省略游标。
page1, err := st.ListByOwner(ctx, ownerPeer, false, "", 2)
if err != nil {
t.Fatalf("list page1: %v", err)
}
if len(page1.Gifts) != 2 || page1.Count != 3 || page1.NextOffset == "" {
t.Fatalf("page1 = %d count %d next %q, want 2/3/nonempty", len(page1.Gifts), page1.Count, page1.NextOffset)
}
if page1.Gifts[0].MsgID != 102 {
t.Fatalf("page1[0] msg_id = %d, want 102 (newest first)", page1.Gifts[0].MsgID)
}
page2, err := st.ListByOwner(ctx, ownerPeer, false, page1.NextOffset, 2)
if err != nil {
t.Fatalf("list page2: %v", err)
}
if len(page2.Gifts) != 1 || page2.NextOffset != "" {
t.Fatalf("page2 = %d next %q, want 1 + empty (terminal)", len(page2.Gifts), page2.NextOffset)
}
// 隐藏 msg_id=101 → excludeUnsaved 列表少一份。
if ok, err := st.SetUnsaved(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 101}, true); err != nil || !ok {
t.Fatalf("set unsaved = %v err %v", ok, err)
}
shown, err := st.ListByOwner(ctx, ownerPeer, true, "", 100)
if err != nil || len(shown.Gifts) != 2 || shown.Count != 2 {
t.Fatalf("excludeUnsaved = %d count %d err %v, want 2/2", len(shown.Gifts), shown.Count, err)
}
// GetByRef(user msg_id)。
g, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100})
if err != nil || !found || g.GiftID != entry.Gift.ID || g.RevisionID != entry.Gift.RevisionID || g.ConvertStars != 50 {
t.Fatalf("get = %+v found %v err %v", g, found, err)
}
// 转换 msg_id=100 → converted从列表消失重复转换被拒。
conv, err := st.MarkConverted(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100})
if err != nil || conv.ConvertStars != 50 || !conv.Converted {
t.Fatalf("convert = %+v err %v, want ConvertStars 50 converted", conv, err)
}
if _, err := st.MarkConverted(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100}); !errors.Is(err, domain.ErrStarGiftAlreadyConverted) {
t.Fatalf("double convert err = %v, want ErrStarGiftAlreadyConverted", err)
}
full, _ := st.ListByOwner(ctx, ownerPeer, false, "", 100)
if full.Count != 2 {
t.Fatalf("count after convert = %d, want 2", full.Count)
}
// 转换不存在的礼物。
if _, err := st.MarkConverted(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 999}); !errors.Is(err, domain.ErrStarGiftNotFound) {
t.Fatalf("convert missing err = %v, want ErrStarGiftNotFound", err)
}
// CountByOwner展示在资料 = 非转换、非隐藏100 已转换、101 已隐藏、102 仍展示 → 1。
n, err := st.CountByOwner(ctx, ownerPeer)
if err != nil || n != 1 {
t.Fatalf("CountByOwner = %d err %v, want 1 (100 converted, 101 hidden, 102 shown)", n, err)
}
// 频道礼物用 saved_id 定位,和用户 msg_id 身份键隔离。
channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: 987654321}
if _, err := st.Create(ctx, domain.SavedStarGift{
Owner: ownerPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 700,
Date: 1700000100, ConvertStars: 50,
}); err != nil {
t.Fatalf("create user gift with same msg_id namespace: %v", err)
}
channelSavedID, err := st.Create(ctx, domain.SavedStarGift{
Owner: channelPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 0, SavedID: 0,
Date: 1700000101, ConvertStars: 50,
})
if err != nil {
t.Fatalf("create channel gift: %v", err)
}
cg, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: channelPeer, SavedID: channelSavedID})
if err != nil || !found || cg.Owner != channelPeer || cg.MsgID != 0 || cg.SavedID != channelSavedID {
t.Fatalf("get channel gift = %+v found %v err %v", cg, found, err)
}
cn, err := st.CountByOwner(ctx, channelPeer)
if err != nil || cn != 1 {
t.Fatalf("channel CountByOwner = %d err %v, want 1", cn, err)
}
}