owpengram-server/internal/rpc/payments_stars_rpc_test.go
A 0c99ae0a9d feat: sync monoforum and collectible emoji status
Sync telesrv bd15657 (feat(account): implement collectible emoji status).

Skipped telesrv docs changes per public sync rules.
2026-07-19 20:37:10 +08:00

226 lines
9.4 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 rpc
import (
"context"
"testing"
"github.com/iamxvbaba/td/clock"
"github.com/iamxvbaba/td/tg"
"go.uber.org/zap/zaptest"
appstars "telesrv/internal/app/stars"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
func starsRouter(t *testing.T, grant int64) *Router {
t.Helper()
svc := appstars.NewService(memory.NewStarsStore(), appstars.WithStartingGrant(grant))
return New(Config{}, Deps{Stars: svc}, zaptest.NewLogger(t), clock.System)
}
// getStarsStatus 首读惰性授予后返回真实余额;响应必须是合法 starsStatus
// balance 必填 + chats/users 非 nil vector
func TestOnPaymentsGetStarsStatusGranted(t *testing.T) {
r := starsRouter(t, 1000)
ctx := WithUserID(context.Background(), 1000000001)
status, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}})
if err != nil {
t.Fatalf("getStarsStatus: %v", err)
}
amount, ok := status.Balance.(*tg.StarsAmount)
if !ok || amount.Amount != 1000 {
t.Fatalf("balance = %#v, want StarsAmount 1000", status.Balance)
}
if status.Chats == nil || status.Users == nil {
t.Fatalf("chats/users must be non-nil vectors, got chats=%v users=%v", status.Chats, status.Users)
}
// 余额是 flag 外必填字段,不能省略。
if _, hasHistory := status.GetHistory(); hasHistory {
t.Fatalf("status (not transactions) should carry no history")
}
}
// TON 余额未建模:返回 starsTonAmount 的合法响应(不崩客户端)。
func TestOnPaymentsGetStarsStatusTon(t *testing.T) {
r := starsRouter(t, 1000)
ctx := WithUserID(context.Background(), 1000000001)
// SetTon 同时置 flag 位+字段gotd true-flagGetTon 读 flag 位,手工 struct 字面量不置位)。
req := &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}}
req.SetTon(true)
status, err := r.onPaymentsGetStarsStatus(ctx, req)
if err != nil {
t.Fatalf("getStarsStatus ton: %v", err)
}
if _, ok := status.Balance.(*tg.StarsTonAmount); !ok {
t.Fatalf("ton balance = %#v, want StarsTonAmount", status.Balance)
}
}
// getStarsTransactions 返回授予流水keyset 分页末页省略 next_offset防 DrKLO 死循环)。
func TestOnPaymentsGetStarsTransactions(t *testing.T) {
r := starsRouter(t, 1000)
ctx := WithUserID(context.Background(), 1000000001)
status, err := r.onPaymentsGetStarsTransactions(ctx, &tg.PaymentsGetStarsTransactionsRequest{Peer: &tg.InputPeerSelf{}})
if err != nil {
t.Fatalf("getStarsTransactions: %v", err)
}
history, ok := status.GetHistory()
if !ok || len(history) != 1 {
t.Fatalf("history = %d ok=%v, want 1 grant txn", len(history), ok)
}
txn := history[0]
if amount, ok := txn.Amount.(*tg.StarsAmount); !ok || amount.Amount != 1000 {
t.Fatalf("grant txn amount = %#v, want +1000", txn.Amount)
}
// grant 走 Fragment 对手方Peer 必填,不可 nil
if _, ok := txn.Peer.(*tg.StarsTransactionPeerFragment); !ok {
t.Fatalf("grant peer = %#v, want StarsTransactionPeerFragment", txn.Peer)
}
// 单页装得下 → 无 next_offset。
if off, ok := status.GetNextOffset(); ok {
t.Fatalf("single-page next_offset = %q, want absent (no infinite paging)", off)
}
}
func TestTGStarsTransactionsPaidMessage(t *testing.T) {
out := tgStarsTransactions([]domain.StarsTransaction{{
ID: 1, UserID: 42, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 50},
Amount: -10, Date: 1700002002, Reason: domain.StarsReasonPaidMessage, Title: "Paid message",
}})
if len(out) != 1 {
t.Fatalf("paid-message transactions = %d, want 1", len(out))
}
if paid, ok := out[0].GetPaidMessages(); !ok || paid != 1 {
t.Fatalf("paid_messages = %d/%v, want 1/true", paid, ok)
}
if amount, ok := out[0].Amount.(*tg.StarsAmount); !ok || amount.Amount != -10 {
t.Fatalf("paid-message amount = %#v, want -10", out[0].Amount)
}
}
// deps.Stars==nil 兜底:返回合法的空 starsStatus余额 0不崩。
func TestOnPaymentsGetStarsStatusNilDeps(t *testing.T) {
r := New(Config{}, Deps{}, zaptest.NewLogger(t), clock.System)
ctx := WithUserID(context.Background(), 1000000001)
status, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}})
if err != nil {
t.Fatalf("nil-deps getStarsStatus: %v", err)
}
if amount, ok := status.Balance.(*tg.StarsAmount); !ok || amount.Amount != 0 {
t.Fatalf("nil-deps balance = %#v, want StarsAmount 0", status.Balance)
}
_ = domain.DefaultStarsStartingGrant
}
type channelLedgerGifts struct {
GiftsService
starsBalance int64
tonBalance int64
starsPage domain.StarsTransactionPage
tonPage domain.TonTransactionPage
}
func (s *channelLedgerGifts) ChannelStarsBalance(context.Context, int64) (int64, error) {
return s.starsBalance, nil
}
func (s *channelLedgerGifts) ChannelStarsTransactions(context.Context, int64, string, int) (domain.StarsTransactionPage, error) {
return s.starsPage, nil
}
func (s *channelLedgerGifts) ChannelTonBalance(context.Context, int64) (int64, error) {
return s.tonBalance, nil
}
func (s *channelLedgerGifts) ChannelTonTransactions(context.Context, int64, string, int) (domain.TonTransactionPage, error) {
return s.tonPage, nil
}
type channelLedgerChannels struct {
ChannelsService
view domain.ChannelView
}
func (s *channelLedgerChannels) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
return s.view, nil
}
func (s *channelLedgerChannels) GetChannels(context.Context, int64, []int64) ([]domain.ChannelView, error) {
return []domain.ChannelView{s.view}, nil
}
func TestPaymentsStarsLedgerUsesRequestedChannelOwner(t *testing.T) {
const viewerID, channelID int64 = 1000000001, 2000000001
view := domain.ChannelView{
Channel: domain.Channel{ID: channelID, AccessHash: 9876, Title: "Gift Channel", Broadcast: true, CreatorUserID: viewerID},
Self: domain.ChannelMember{ChannelID: channelID, UserID: viewerID, Role: domain.ChannelRoleCreator, Status: domain.ChannelMemberActive},
}
gifts := &channelLedgerGifts{
starsBalance: 20,
tonBalance: 900,
starsPage: domain.StarsTransactionPage{Balance: 20, Transactions: []domain.StarsTransaction{{
ID: 1, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}, Amount: 20, Date: 10, Reason: domain.StarsReasonGift,
}}},
tonPage: domain.TonTransactionPage{Balance: 900, Transactions: []domain.TonTransaction{{
ID: 2, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 2000000002}, GiftID: 9, Amount: 900, Date: 11, Reason: domain.StarsReasonGiftResale,
}}},
}
r := New(Config{}, Deps{Gifts: gifts, Channels: &channelLedgerChannels{view: view}}, zaptest.NewLogger(t), clock.System)
ctx := WithUserID(context.Background(), viewerID)
peer := &tg.InputPeerChannel{ChannelID: channelID, AccessHash: view.Channel.AccessHash}
status, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: peer})
if err != nil {
t.Fatalf("get channel stars status: %v", err)
}
if amount, ok := status.Balance.(*tg.StarsAmount); !ok || amount.Amount != 20 || len(status.Chats) != 1 {
t.Fatalf("channel stars status = %+v chats=%d", status.Balance, len(status.Chats))
}
revenue, err := r.onPaymentsGetStarsRevenueStats(ctx, &tg.PaymentsGetStarsRevenueStatsRequest{Peer: peer})
if err != nil {
t.Fatalf("get channel stars revenue: %v", err)
}
if current, ok := revenue.Status.CurrentBalance.(*tg.StarsAmount); !ok || current.Amount != 20 {
t.Fatalf("channel stars revenue current = %+v", revenue.Status.CurrentBalance)
}
if overall, ok := revenue.Status.OverallRevenue.(*tg.StarsAmount); !ok || overall.Amount != 20 || revenue.Status.WithdrawalEnabled {
t.Fatalf("channel stars revenue overall = %+v withdrawal=%v", revenue.Status.OverallRevenue, revenue.Status.WithdrawalEnabled)
}
txnReq := &tg.PaymentsGetStarsTransactionsRequest{Peer: peer, Limit: 20}
txnReq.SetTon(true)
transactions, err := r.onPaymentsGetStarsTransactions(ctx, txnReq)
if err != nil {
t.Fatalf("get channel ton transactions: %v", err)
}
history, ok := transactions.GetHistory()
if amount, amountOK := transactions.Balance.(*tg.StarsTonAmount); !amountOK || amount.Amount != 900 || !ok || len(history) != 1 || !history[0].StargiftResale {
t.Fatalf("channel ton transactions = balance=%+v history=%+v", transactions.Balance, history)
}
revenueReq := &tg.PaymentsGetStarsRevenueStatsRequest{Peer: peer}
revenueReq.SetTon(true)
tonRevenue, err := r.onPaymentsGetStarsRevenueStats(ctx, revenueReq)
if err != nil {
t.Fatalf("get channel ton revenue: %v", err)
}
if current, ok := tonRevenue.Status.CurrentBalance.(*tg.StarsTonAmount); !ok || current.Amount != 900 {
t.Fatalf("channel ton revenue current = %+v", tonRevenue.Status.CurrentBalance)
}
}
func TestPaymentsStarsLedgerRejectsNonAdminChannelReader(t *testing.T) {
const viewerID, channelID int64 = 1000000001, 2000000001
view := domain.ChannelView{
Channel: domain.Channel{ID: channelID, AccessHash: 9876, Title: "Gift Channel", Broadcast: true},
Self: domain.ChannelMember{ChannelID: channelID, UserID: viewerID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive},
}
r := New(Config{}, Deps{Gifts: &channelLedgerGifts{}, Channels: &channelLedgerChannels{view: view}}, zaptest.NewLogger(t), clock.System)
ctx := WithUserID(context.Background(), viewerID)
_, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerChannel{ChannelID: channelID, AccessHash: view.Channel.AccessHash}})
if err == nil {
t.Fatal("non-admin channel ledger read unexpectedly succeeded")
}
}