Ctrl K
ComunidadGuías…

internal/app/bots/premiumbot_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package bots

import (
	"context"
	"strings"
	"testing"

	"telesrv/internal/domain"
	"telesrv/internal/store/memory"
)

type fakePremiumStorefront struct {
	plans   []domain.PremiumPlan
	active  []domain.PremiumEntitlement
	history []domain.PremiumEntitlement
	balance int64
}

func (f *fakePremiumStorefront) BotUserID() int64    { return domain.PremiumBotConfiguredUserID() }
func (f *fakePremiumStorefront) BotUsername() string { return "premiumbot" }
func (f *fakePremiumStorefront) Plans(context.Context) ([]domain.PremiumPlan, error) {
	return append([]domain.PremiumPlan(nil), f.plans...), nil
}
func (f *fakePremiumStorefront) Plan(_ context.Context, months int) (domain.PremiumPlan, error) {
	for _, plan := range f.plans {
		if plan.Months == months {
			return plan, nil
		}
	}
	return domain.PremiumPlan{}, domain.ErrPremiumPlanUnavailable
}
func (f *fakePremiumStorefront) Balance(context.Context, int64) (domain.StarsBalance, error) {
	return domain.StarsBalance{Balance: f.balance}, nil
}
func (f *fakePremiumStorefront) ActiveEntitlements(context.Context, int64, int) ([]domain.PremiumEntitlement, error) {
	return append([]domain.PremiumEntitlement(nil), f.active...), nil
}
func (f *fakePremiumStorefront) PurchaseHistory(context.Context, int64, int) ([]domain.PremiumEntitlement, error) {
	return append([]domain.PremiumEntitlement(nil), f.history...), nil
}

func premiumBotTestPlan() domain.PremiumPlan {
	return domain.PremiumPlan{
		Months: 3, DurationDays: 90, AmountStars: 750, Enabled: true,
		SortOrder: 10, Label: "3 months", Version: 4,
	}
}

func TestPremiumBotStartHintsNeverSelectPrice(t *testing.T) {
	svc, _, _, _ := newTestService(t)
	svc.SetPremium(&fakePremiumStorefront{plans: []domain.PremiumPlan{premiumBotTestPlan()}})
	for _, body := range []string{
		"/start settings", "/start profile", "/start business",
		"/start deeplink_referrer", "/start doubled_limits",
	} {
		reply := svc.handlePremium(context.Background(), 1001, body)
		if reply.Media != nil {
			t.Fatalf("%q produced invoice media from a navigation hint", body)
		}
		if reply.ReplyMarkup == nil || len(reply.ReplyMarkup.Inline) < 3 {
			t.Fatalf("%q did not produce the safe main menu: %+v", body, reply)
		}
	}
}

func TestPremiumBotInvoiceUsesCatalogSnapshotAndBuyButton(t *testing.T) {
	svc, _, _, _ := newTestService(t)
	plan := premiumBotTestPlan()
	svc.SetPremium(&fakePremiumStorefront{plans: []domain.PremiumPlan{plan}, balance: 1250})

	reply := svc.handlePremium(context.Background(), 1001, "/start buy_3")
	if reply.Media == nil || reply.Media.Kind != domain.MessageMediaKindInvoice ||
		reply.Media.Invoice == nil {
		t.Fatalf("invoice reply media = %+v", reply.Media)
	}
	invoice := reply.Media.Invoice
	if invoice.Kind != domain.PremiumPurchaseSelf || invoice.Months != plan.Months ||
		invoice.DurationDays != plan.DurationDays || invoice.AmountStars != plan.AmountStars ||
		invoice.PlanVersion != plan.Version {
		t.Fatalf("invoice = %+v, want catalog snapshot %+v", invoice, plan)
	}
	if !strings.Contains(reply.Text, "Текущий баланс: 1250 Stars") {
		t.Fatalf("invoice text does not show Stars balance: %q", reply.Text)
	}
	if reply.ReplyMarkup == nil || len(reply.ReplyMarkup.Inline) != 1 ||
		len(reply.ReplyMarkup.Inline[0]) != 1 ||
		reply.ReplyMarkup.Inline[0][0].Type != domain.MarkupButtonBuy {
		t.Fatalf("invoice markup = %+v, want one buy button", reply.ReplyMarkup)
	}
	if err := domain.ValidateReplyMarkup(reply.ReplyMarkup); err != nil {
		t.Fatalf("invoice markup validation: %v", err)
	}
}

func TestPremiumBotGiftPickerAndCatalogInvoice(t *testing.T) {
	svc, users, _, _ := newTestService(t)
	buyer := newOwner(t, users, "+15550001")
	recipient := newOwner(t, users, "+15550002")
	plan := premiumBotTestPlan()
	svc.SetPremium(&fakePremiumStorefront{plans: []domain.PremiumPlan{plan}, balance: 2200})

	picker := svc.handlePremium(context.Background(), buyer.ID, "/gift")
	if picker.ReplyMarkup == nil || picker.ReplyMarkup.Type != domain.MessageReplyMarkupKeyboard ||
		len(picker.ReplyMarkup.Keyboard) != 1 ||
		picker.ReplyMarkup.Keyboard[0][0].Type != domain.MarkupButtonRequestPeer {
		t.Fatalf("gift picker = %+v", picker.ReplyMarkup)
	}
	if picker.ReplyMarkup.Selective {
		t.Fatal("gift picker must be visible in a private bot chat")
	}
	if err := domain.ValidateReplyMarkup(picker.ReplyMarkup); err != nil {
		t.Fatalf("gift picker markup: %v", err)
	}

	selected := svc.handlePremiumMessage(context.Background(), buyer.ID, domain.Message{
		Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService,
			ServiceAction: &domain.MessageServiceAction{
				Kind: domain.MessageServiceActionRequestedPeer,
				RequestedPeer: &domain.MessageRequestedPeerAction{
					ButtonID: premiumGiftPeerButtonID,
					Peers:    []domain.Peer{{Type: domain.PeerTypeUser, ID: recipient.ID}},
				},
			}},
	})
	if selected.ReplyMarkup == nil || selected.ReplyMarkup.Type != domain.MessageReplyMarkupInline ||
		len(selected.ReplyMarkup.Inline) != 1 ||
		!strings.HasPrefix(string(selected.ReplyMarkup.Inline[0][0].Data), premiumCallbackGiftBuy) {
		t.Fatalf("selected recipient plans = %+v", selected)
	}
	if err := domain.ValidateReplyMarkup(selected.ReplyMarkup); err != nil {
		t.Fatalf("gift plans markup: %v", err)
	}

	invoiceReply := svc.premiumBotGiftInvoice(
		context.Background(), buyer.ID, recipient.ID, plan.Months, "Happy birthday",
	)
	if invoiceReply.Media == nil || invoiceReply.Media.Invoice == nil {
		t.Fatalf("gift invoice reply = %+v", invoiceReply)
	}
	invoice := invoiceReply.Media.Invoice
	if invoice.Kind != domain.PremiumPurchaseGift || invoice.RecipientUserID != recipient.ID ||
		invoice.Months != plan.Months || invoice.AmountStars != plan.AmountStars ||
		invoice.PlanVersion != plan.Version || invoice.Message.Text != "Happy birthday" {
		t.Fatalf("gift invoice = %+v", invoice)
	}
	if !containsAll(invoiceReply.Text, "Цена: 750 Stars", "Текущий баланс: 2200 Stars") {
		t.Fatalf("gift invoice text does not show price and balance: %q", invoiceReply.Text)
	}
	if err := domain.ValidateReplyMarkup(invoiceReply.ReplyMarkup); err != nil {
		t.Fatalf("gift invoice markup: %v", err)
	}
}

func TestPremiumBotStatusAndHistory(t *testing.T) {
	svc, _, _, _ := newTestService(t)
	now := int(svc.now().Unix())
	svc.SetPremium(&fakePremiumStorefront{
		plans:  []domain.PremiumPlan{premiumBotTestPlan()},
		active: []domain.PremiumEntitlement{{Status: domain.PremiumEntitlementActive, ExpiresAt: now + 86400}},
		history: []domain.PremiumEntitlement{{
			UserID: 2002, SourceUserID: 1001, Source: domain.PremiumEntitlementGift,
			Months: 3, Status: domain.PremiumEntitlementActive, CreatedAt: now,
		}},
	})
	if reply := svc.handlePremium(context.Background(), 1001, "/status"); !containsAll(reply.Text, "активен до", "UTC") {
		t.Fatalf("/status = %q", reply.Text)
	}
	if reply := svc.handlePremium(context.Background(), 1001, "/history"); !containsAll(reply.Text, "3 мес.", "подарок пользователю 2002") {
		t.Fatalf("/history = %q", reply.Text)
	}
	if !svc.HandlesBot(domain.PremiumBotConfiguredUserID()) {
		t.Fatal("Premium bot is not registered as a built-in responder")
	}
}

func TestPremiumBotUsesClientSessionLanguageForMessagesAndCallbacks(t *testing.T) {
	svc, users, _, messages := newTestService(t)
	owner := newOwner(t, users, "+15550888")
	svc.SetPremium(&fakePremiumStorefront{
		plans: []domain.PremiumPlan{premiumBotTestPlan()}, balance: 900,
	})
	locale := premiumLocaleFromSession(domain.ClientSessionMetadata{
		SessionID: 77, LangPack: "tdesktop", LangCode: "en-US",
	})
	home := svc.handlePremium(context.Background(), owner.ID, "/start", locale)
	if !strings.Contains(home.Text, "Welcome to the Premium store") ||
		home.ReplyMarkup == nil || home.ReplyMarkup.Inline[0][0].Text != "Buy Premium" {
		t.Fatalf("English Premium home = %+v", home)
	}

	_, handled, err := svc.OnCallbackQuery(context.Background(), domain.BotCallbackQuery{
		ID: 8, BotUserID: svc.premium.BotUserID(), UserID: owner.ID,
		Peer:      domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
		MessageID: 1, Data: []byte(premiumCallbackPlans),
		ClientSession: domain.ClientSessionMetadata{SessionID: 77, LangCode: "en"},
	})
	if err != nil || !handled {
		t.Fatalf("English callback handled=%v err=%v", handled, err)
	}
	reply := latestPremiumBotReply(t, messages, owner.ID, svc.premium.BotUserID())
	if !strings.Contains(reply.Body, "Choose a Premium plan") {
		t.Fatalf("English Premium callback reply = %q", reply.Body)
	}
}

func TestPremiumBotMenuCallbacksExecuteInsideLocalServer(t *testing.T) {
	svc, users, _, messages := newTestService(t)
	owner := newOwner(t, users, "+15550999")
	svc.SetPremium(&fakePremiumStorefront{
		plans: []domain.PremiumPlan{premiumBotTestPlan()}, balance: 900,
	})

	home := svc.handlePremium(context.Background(), owner.ID, "/start")
	if home.ReplyMarkup == nil || len(home.ReplyMarkup.Inline) != 3 {
		t.Fatalf("home markup = %+v", home.ReplyMarkup)
	}
	for _, row := range home.ReplyMarkup.Inline {
		for _, button := range row {
			if button.Type != domain.MarkupButtonCallback || button.URL != "" ||
				!strings.HasPrefix(string(button.Data), premiumCallbackPrefix) {
				t.Fatalf("home button is not a local callback: %+v", button)
			}
		}
	}

	answer, handled, err := svc.OnCallbackQuery(context.Background(), domain.BotCallbackQuery{
		ID: 1, BotUserID: svc.premium.BotUserID(), UserID: owner.ID,
		Peer:      domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
		MessageID: 1, Data: []byte(premiumCallbackPlans),
	})
	if err != nil || !handled || answer.Alert {
		t.Fatalf("plans callback answer = %+v handled=%v err=%v", answer, handled, err)
	}
	reply := latestPremiumBotReply(t, messages, owner.ID, svc.premium.BotUserID())
	if !strings.Contains(reply.Body, "Выберите тариф Premium") ||
		reply.ReplyMarkup == nil || len(reply.ReplyMarkup.Inline) != 1 ||
		string(reply.ReplyMarkup.Inline[0][0].Data) != premiumCallbackBuy+"3" {
		t.Fatalf("plans callback reply = %+v", reply)
	}

	_, handled, err = svc.OnCallbackQuery(context.Background(), domain.BotCallbackQuery{
		ID: 2, BotUserID: svc.premium.BotUserID(), UserID: owner.ID,
		Peer:      domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
		MessageID: reply.ID, Data: []byte(premiumCallbackBuy + "3"),
	})
	if err != nil || !handled {
		t.Fatalf("buy callback handled=%v err=%v", handled, err)
	}
	invoice := latestPremiumBotReply(t, messages, owner.ID, svc.premium.BotUserID())
	if invoice.Media == nil || invoice.Media.Invoice == nil ||
		invoice.Media.Invoice.AmountStars != 750 {
		t.Fatalf("buy callback did not create catalog invoice: %+v", invoice)
	}
}

func latestPremiumBotReply(
	t *testing.T,
	messages *memory.MessageStore,
	userID, botID int64,
) domain.Message {
	t.Helper()
	list, err := messages.ListByUser(context.Background(), userID, domain.MessageFilter{
		HasPeer: true,
		Peer:    domain.Peer{Type: domain.PeerTypeUser, ID: botID},
		Limit:   100,
	})
	if err != nil {
		t.Fatalf("list premium bot replies: %v", err)
	}
	var latest domain.Message
	for _, message := range list.Messages {
		if message.From.ID == botID && message.ID > latest.ID {
			latest = message
		}
	}
	if latest.ID == 0 {
		t.Fatal("no Premium bot reply")
	}
	return latest
}

func containsAll(text string, parts ...string) bool {
	for _, part := range parts {
		if !strings.Contains(text, part) {
			return false
		}
	}
	return true
}