package payments import ( "strings" "testing" "github.com/google/uuid" ) // TestProjectOfferPricing checks the happy path: a chip pack priced on every rail and a chip-priced // value render into the two tables, pack table first, money formatted through Money. func TestProjectOfferPricing(t *testing.T) { entries := []catalogEntry{ { id: uuid.New(), title: "50 «Фишек»", atoms: []atomQty{{atomType: atomChips, quantity: 50}}, prices: []priceRow{ {method: string(SourceDirect), currency: CurrencyRUB, amount: 20000}, {method: string(SourceVK), currency: CurrencyVote, amount: 30}, {method: string(SourceTelegram), currency: CurrencyStar, amount: 100}, }, }, { id: uuid.New(), title: "200 подсказок", atoms: []atomQty{{atomType: "hints", quantity: 200}}, prices: []priceRow{{method: "", currency: CurrencyChip, amount: 50}}, }, } md := projectOfferPricing(entries) for _, want := range []string{ "| Наименование | Рубли | Голоса в VK | Stars в Telegram |", "| --- | ---: | ---: | ---: |", // price columns right-aligned "| 50 «Фишек» | 200.00 | 30 | 100 |", "| Наименование | «Фишки» |", "| --- | ---: |", "| 200 подсказок | 50 |", } { if !strings.Contains(md, want) { t.Errorf("projection missing %q\n---\n%s", want, md) } } if strings.Index(md, "Приобретение") > strings.Index(md, "Использование") { t.Errorf("the pack table must precede the values table:\n%s", md) } } // TestProjectOfferPricingOrdering checks packs sort by ascending rouble price, and values sort by // group (hints only → no-ads only → no-ads + hints) then ascending chip price within a group. func TestProjectOfferPricingOrdering(t *testing.T) { pack := func(title string, rub int64) catalogEntry { return catalogEntry{ id: uuid.New(), title: title, atoms: []atomQty{{atomType: atomChips, quantity: 1}}, prices: []priceRow{{method: string(SourceDirect), currency: CurrencyRUB, amount: rub}}, } } value := func(title string, chips int64, atoms ...string) catalogEntry { e := catalogEntry{id: uuid.New(), title: title, prices: []priceRow{{method: "", currency: CurrencyChip, amount: chips}}} for _, a := range atoms { e.atoms = append(e.atoms, atomQty{atomType: a, quantity: 1}) } return e } // Deliberately out of order on input. entries := []catalogEntry{ pack("packDear", 30000), pack("packCheap", 10000), value("bundle", 500, "hints", "noads_days"), value("adsOnly", 150, "noads_days"), value("hintsBig", 200, "hints"), value("hintsSmall", 50, "hints"), } md := projectOfferPricing(entries) order := []string{"packCheap", "packDear", "hintsSmall", "hintsBig", "adsOnly", "bundle"} last := -1 for _, title := range order { i := strings.Index(md, "| "+title+" |") if i < 0 { t.Fatalf("row %q missing:\n%s", title, md) } if i < last { t.Errorf("row %q out of order (want sequence %v):\n%s", title, order, md) } last = i } } // TestProjectOfferPricingMissingRailAndEscaping checks a pack missing a rail shows an em dash and a // title carrying a pipe is escaped so the table layout survives. func TestProjectOfferPricingMissingRailAndEscaping(t *testing.T) { entries := []catalogEntry{{ id: uuid.New(), title: "Bonus | pack", atoms: []atomQty{{atomType: atomChips, quantity: 10}}, // A roubles price only — no VK, no Telegram. prices: []priceRow{{method: string(SourceDirect), currency: CurrencyRUB, amount: 9900}}, }} md := projectOfferPricing(entries) if want := `| Bonus \| pack | 99.00 | — | — |`; !strings.Contains(md, want) { t.Errorf("want row %q in:\n%s", want, md) } } // TestProjectOfferPricingEscapesHTMLAndLinks checks an admin title carrying HTML or a markdown link // is neutralised so it cannot inject markup into the public offer: the tag becomes entities and the // link brackets are escaped (so no "javascript:" anchor forms). The raw metacharacters must not // survive into the projected markdown. func TestProjectOfferPricingEscapesHTMLAndLinks(t *testing.T) { entries := []catalogEntry{{ id: uuid.New(), title: ` [x](javascript:alert(2)) & "q"`, atoms: []atomQty{{atomType: "hints", quantity: 1}}, prices: []priceRow{{method: "", currency: CurrencyChip, amount: 5}}, }} md := projectOfferPricing(entries) for _, bad := range []string{"", "[x]", `& "q"`} { if strings.Contains(md, bad) { t.Errorf("unescaped %q survived into the projection:\n%s", bad, md) } } for _, want := range []string{"<script>", `\[x\]`, "&", ""q""} { if !strings.Contains(md, want) { t.Errorf("want escaped %q in:\n%s", want, md) } } } // TestProjectOfferPricingEmpty checks an empty catalog projects to the empty string (no stray table // headers), so the offer's pricing marker is replaced with nothing. func TestProjectOfferPricingEmpty(t *testing.T) { if got := projectOfferPricing(nil); got != "" { t.Errorf("empty catalog must project to empty string, got %q", got) } }