40acbcccdd
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m47s
- packs sorted by ascending rouble price; - values grouped hints-only -> no-ads-only -> no-ads+hints -> tournament (the tournament group is empty until such products become sellable), ascending chip price within each group; - price columns right-aligned (GFM "---:" separators); - tables span the full content width; the name column shrinks to its content and never wraps; - muted-but-visible cell borders on the dark theme, where the section rule colour blends into the background.
138 lines
5.0 KiB
Go
138 lines
5.0 KiB
Go
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: `<script>alert(1)</script> [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{"<script>", "</script>", "[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)
|
|
}
|
|
}
|