b6af682381
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
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 1m49s
The wallet storefront listed products in creation order — its purchases and its chip-exchange values were unsorted, unlike the admin console and the public offer. Extract the ordering (chip packs first by rouble price, then chip-priced values grouped hints → no-ads → combo → tournament and by chip price) into one comparator, compareCatalogRank over a small rank key, and apply it at all three sites (projectCatalog, projectOfferPricing, SortAdminCatalog) — so the subgroup ranking (valueGroup) and the full order now live in one place. The rewarded "watch for chips" CTA is a wallet-driven element rendered above the packs, so it is unaffected and stays on top. Also add the admin catalog active/all toggle: AdminCatalog takes includeInactive, the console shows active products by default and ?all=1 lists archived ones too (the detail and grant forms keep loading all products). Tests: a storefront canonical-order unit test (reproduced the unsorted bug first); an integration test for the active/all toggle. Existing offer/admin order tests unchanged — behaviour preserved.
127 lines
5.5 KiB
Go
127 lines
5.5 KiB
Go
//go:build integration
|
|
|
|
package inttest
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"scrabble/backend/internal/payments"
|
|
)
|
|
|
|
// productByTitle finds a catalog product by its (unique in the test) title.
|
|
func productByTitle(t *testing.T, pay *payments.Service, title string) payments.AdminProduct {
|
|
t.Helper()
|
|
all, err := pay.AdminCatalog(context.Background(), true)
|
|
if err != nil {
|
|
t.Fatalf("admin catalog: %v", err)
|
|
}
|
|
for _, p := range all {
|
|
if p.Title == title {
|
|
return p
|
|
}
|
|
}
|
|
t.Fatalf("product %q not found", title)
|
|
return payments.AdminProduct{}
|
|
}
|
|
|
|
// TestConsoleCatalogEditor drives the catalog editor end to end: create is CSRF-guarded; a value and
|
|
// a pack are created and edited; an invalid active product is refused; archive hides it; a
|
|
// never-transacted product deletes; a transacted product is refused deletion (archive only).
|
|
func TestConsoleCatalogEditor(t *testing.T) {
|
|
ctx := context.Background()
|
|
srv, _, pay := bannerServer(t)
|
|
h := srv.Handler()
|
|
const origin = "http://admin.test"
|
|
const catalog = "http://admin.test/_gm/catalog"
|
|
|
|
// CSRF: a create without the origin header is refused.
|
|
if code, _ := consoleDo(h, http.MethodPost, catalog, "title=x&hints=1&price_chip=10&active=true", ""); code != http.StatusForbidden {
|
|
t.Fatalf("create without origin = %d, want 403", code)
|
|
}
|
|
|
|
// Create a value product: 5 hints for 50 chips, active.
|
|
if code, body := consoleDo(h, http.MethodPost, catalog, "title=cat-hints-5&hints=5&price_chip=50&active=true", origin); code != http.StatusOK || !strings.Contains(body, "Created") {
|
|
t.Fatalf("create value = %d, has 'Created' = %v", code, strings.Contains(body, "Created"))
|
|
}
|
|
val := productByTitle(t, pay, "cat-hints-5")
|
|
if !val.Active || len(val.Atoms) != 1 || val.Atoms[0].Atom != "hints" || val.Atoms[0].Quantity != 5 {
|
|
t.Fatalf("created value = %+v", val)
|
|
}
|
|
|
|
// An invalid active pack (chips, no money price) is refused.
|
|
if code, body := consoleDo(h, http.MethodPost, catalog, "title=cat-bad&chips=100&active=true", origin); code != http.StatusOK || !strings.Contains(body, "Invalid product") {
|
|
t.Fatalf("bad pack = %d, has 'Invalid product' = %v", code, strings.Contains(body, "Invalid product"))
|
|
}
|
|
if _, err := pay.AdminCatalog(ctx, true); err != nil {
|
|
t.Fatalf("catalog: %v", err)
|
|
}
|
|
|
|
// Edit the value: raise to 8 hints.
|
|
base := catalog + "/" + val.ID.String()
|
|
if code, _ := consoleDo(h, http.MethodPost, base, "title=cat-hints-8&hints=8&price_chip=50&active=true", origin); code != http.StatusOK {
|
|
t.Fatalf("edit = %d", code)
|
|
}
|
|
if got := productByTitle(t, pay, "cat-hints-8"); len(got.Atoms) != 1 || got.Atoms[0].Quantity != 8 {
|
|
t.Fatalf("after edit atoms = %+v, want 8 hints", got.Atoms)
|
|
}
|
|
|
|
// Archive it → hidden from the storefront (the user Catalog filters active).
|
|
if code, _ := consoleDo(h, http.MethodPost, base+"/archive", "active=false", origin); code != http.StatusOK {
|
|
t.Fatalf("archive = %d", code)
|
|
}
|
|
if productByTitle(t, pay, "cat-hints-8").Active {
|
|
t.Error("product still active after archive")
|
|
}
|
|
|
|
// Delete the never-transacted product.
|
|
if code, body := consoleDo(h, http.MethodPost, base+"/delete", "", origin); code != http.StatusOK || !strings.Contains(body, "Deleted") {
|
|
t.Fatalf("delete clean = %d, has 'Deleted' = %v", code, strings.Contains(body, "Deleted"))
|
|
}
|
|
|
|
// A transacted product cannot be deleted — only archived.
|
|
if code, _ := consoleDo(h, http.MethodPost, catalog, "title=cat-pack&chips=100&price_rub=14900&active=true", origin); code != http.StatusOK {
|
|
t.Fatal("create pack failed")
|
|
}
|
|
pack := productByTitle(t, pay, "cat-pack")
|
|
if _, err := pay.CreateOrder(ctx, uuid.New(), payments.NewContext("direct", "web"), []payments.Source{payments.SourceDirect}, pack.ID, "robokassa"); err != nil {
|
|
t.Fatalf("create order: %v", err)
|
|
}
|
|
if code, body := consoleDo(h, http.MethodPost, catalog+"/"+pack.ID.String()+"/delete", "", origin); code != http.StatusOK || !strings.Contains(body, "Cannot delete") {
|
|
t.Fatalf("delete transacted = %d, has 'Cannot delete' = %v", code, strings.Contains(body, "Cannot delete"))
|
|
}
|
|
}
|
|
|
|
// TestConsoleCatalogActiveToggle checks the catalog console lists only active products by default and
|
|
// includes the archived ones under ?all=1 (the active/all toggle).
|
|
func TestConsoleCatalogActiveToggle(t *testing.T) {
|
|
srv, _, _ := bannerServer(t)
|
|
h := srv.Handler()
|
|
const origin = "http://admin.test"
|
|
const catalog = "http://admin.test/_gm/catalog"
|
|
|
|
// An active value and an archived one (created without the active flag).
|
|
if code, _ := consoleDo(h, http.MethodPost, catalog, "title=toggle-active&hints=5&price_chip=50&active=true", origin); code != http.StatusOK {
|
|
t.Fatal("create active failed")
|
|
}
|
|
if code, _ := consoleDo(h, http.MethodPost, catalog, "title=toggle-archived&hints=9&price_chip=90", origin); code != http.StatusOK {
|
|
t.Fatal("create archived failed")
|
|
}
|
|
|
|
// Default view: active only.
|
|
if _, body := consoleDo(h, http.MethodGet, catalog, "", ""); !strings.Contains(body, "toggle-active") || strings.Contains(body, "toggle-archived") {
|
|
t.Errorf("default view: active listed=%v, archived listed=%v (want true/false)",
|
|
strings.Contains(body, "toggle-active"), strings.Contains(body, "toggle-archived"))
|
|
}
|
|
|
|
// ?all=1: active and archived.
|
|
if _, body := consoleDo(h, http.MethodGet, catalog+"?all=1", "", ""); !strings.Contains(body, "toggle-active") || !strings.Contains(body, "toggle-archived") {
|
|
t.Errorf("all view: active listed=%v, archived listed=%v (want true/true)",
|
|
strings.Contains(body, "toggle-active"), strings.Contains(body, "toggle-archived"))
|
|
}
|
|
}
|