f60327c1ce
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
A product title containing & < > " ' silently breaks VK purchases: VK
HTML-escapes the character in its order_status_change payment-callback echo
(e.g. " -> ") but signs the notification over a different form, so the
gateway's signature check no longer matches ("vk callback: bad signature")
and the purchase never confirms — while get_item (whose request carries no
title) still passes.
validateProduct now rejects those characters at the admin catalog editor —
the single write path for CreateProduct/UpdateProduct/SetProductActive — so a
title can't reintroduce the break. Reject, not transform: the operator retypes
a clean title (« » guillemets, not straight quotes), so the stored value stays
identical everywhere (storefront, wallet, VK box).
Docs: docs/PAYMENTS.md §7 + agent field notes; Go Doc on validateProduct.
Test: validateProduct HTML-special-char cases. No wire/schema change.
54 lines
3.2 KiB
Go
54 lines
3.2 KiB
Go
package payments
|
|
|
|
import "testing"
|
|
|
|
func TestValidateProduct(t *testing.T) {
|
|
pack := ProductInput{
|
|
Title: "100 chips",
|
|
Atoms: []AtomLine{{Atom: "chips", Quantity: 100}},
|
|
Prices: []PriceLine{{Method: "direct", Currency: CurrencyRUB, Amount: 14900}},
|
|
}
|
|
value := ProductInput{
|
|
Title: "5 hints",
|
|
Atoms: []AtomLine{{Atom: "hints", Quantity: 5}},
|
|
Prices: []PriceLine{{Method: "", Currency: CurrencyChip, Amount: 50}},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
in ProductInput
|
|
sellable bool
|
|
wantErr bool
|
|
}{
|
|
{"valid pack", pack, true, false},
|
|
{"valid value", value, true, false},
|
|
{"pack with a benefit atom", ProductInput{Title: "x", Atoms: []AtomLine{{"chips", 100}, {"hints", 5}}, Prices: pack.Prices}, true, true},
|
|
{"pack without money price", ProductInput{Title: "x", Atoms: pack.Atoms, Prices: value.Prices}, true, true},
|
|
{"value without chip price", ProductInput{Title: "x", Atoms: value.Atoms, Prices: pack.Prices}, true, true},
|
|
{"tournament sellable is refused", ProductInput{Title: "cup", Atoms: []AtomLine{{"tournament", 1}}, Prices: value.Prices}, true, true},
|
|
{"tournament draft is allowed", ProductInput{Title: "cup", Atoms: []AtomLine{{"tournament", 1}}}, false, false},
|
|
{"unknown atom", ProductInput{Title: "x", Atoms: []AtomLine{{"gold", 1}}}, false, true},
|
|
{"duplicate atom", ProductInput{Title: "x", Atoms: []AtomLine{{"hints", 1}, {"hints", 2}}}, false, true},
|
|
{"non-positive quantity", ProductInput{Title: "x", Atoms: []AtomLine{{"hints", 0}}}, false, true},
|
|
{"chip price with a method", ProductInput{Title: "x", Atoms: value.Atoms, Prices: []PriceLine{{Method: "direct", Currency: CurrencyChip, Amount: 50}}}, true, true},
|
|
{"money price without a method", ProductInput{Title: "x", Atoms: pack.Atoms, Prices: []PriceLine{{Method: "", Currency: CurrencyRUB, Amount: 149}}}, true, true},
|
|
{"duplicate price", ProductInput{Title: "x", Atoms: pack.Atoms, Prices: []PriceLine{{Method: "direct", Currency: CurrencyRUB, Amount: 1}, {Method: "direct", Currency: CurrencyRUB, Amount: 2}}}, true, true},
|
|
{"empty title", ProductInput{Title: " ", Atoms: value.Atoms, Prices: value.Prices}, true, true},
|
|
{"title with a double quote", ProductInput{Title: `50 "фишек"`, Atoms: value.Atoms, Prices: value.Prices}, true, true},
|
|
{"title with an ampersand", ProductInput{Title: "фишки & подсказки", Atoms: value.Atoms, Prices: value.Prices}, true, true},
|
|
{"title with an angle bracket", ProductInput{Title: "50 <фишек>", Atoms: value.Atoms, Prices: value.Prices}, true, true},
|
|
{"title with an apostrophe", ProductInput{Title: "player's pack", Atoms: value.Atoms, Prices: value.Prices}, true, true},
|
|
{"title with a special char is refused even for a draft", ProductInput{Title: `x "y"`, Atoms: value.Atoms}, false, true},
|
|
{"title with guillemets is allowed", ProductInput{Title: "50 «фишек»", Atoms: value.Atoms, Prices: value.Prices}, true, false},
|
|
{"no atoms", ProductInput{Title: "x", Prices: value.Prices}, true, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateProduct(tt.in, tt.sellable)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("validateProduct = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|