eb7fa98426
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m57s
The "watch for chips" rewarded payout and its per-day / per-hour caps live in the shared payments.config row and had no admin surface — they could only be changed by SQL, contrary to D32 (the rewarded rate is meant to be admin-configurable). Add a "Rewarded ads" form on the /_gm/catalog page: RewardConfig / SetRewardConfig on the service (non-negative validated), a setRewardConfig store writer (the singleton config row), the consoleSetReward handler + POST /_gm/catalog/reward route, and the form pre-filled from the current config. No migration — the columns already exist. The reward rate is a config value, not a sellable catalog atom (so a "no-ads forever" style product is still out of scope by D32/D33). Test: an integration test sets the config, checks the page pre-fill, and refuses a negative value.
154 lines
7.0 KiB
Go
154 lines
7.0 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"))
|
|
}
|
|
}
|
|
|
|
// TestConsoleRewardConfig checks the rewarded-ads config form on the catalog page: a POST updates the
|
|
// payout and caps, the page pre-fills the current values, and a negative value is refused.
|
|
func TestConsoleRewardConfig(t *testing.T) {
|
|
srv, _, pay := bannerServer(t)
|
|
h := srv.Handler()
|
|
const origin = "http://admin.test"
|
|
const reward = "http://admin.test/_gm/catalog/reward"
|
|
|
|
// Set the payout and caps.
|
|
if code, body := consoleDo(h, http.MethodPost, reward, "reward_payout=7&reward_daily=40&reward_hourly=9", origin); code != http.StatusOK || !strings.Contains(body, "Saved") {
|
|
t.Fatalf("set reward = %d, has 'Saved' = %v", code, strings.Contains(body, "Saved"))
|
|
}
|
|
if payout, daily, hourly, err := pay.RewardConfig(context.Background()); err != nil || payout != 7 || daily != 40 || hourly != 9 {
|
|
t.Fatalf("reward config = %d/%d/%d (err %v), want 7/40/9", payout, daily, hourly, err)
|
|
}
|
|
|
|
// The catalog page renders the form pre-filled with the current payout.
|
|
if _, body := consoleDo(h, http.MethodGet, "http://admin.test/_gm/catalog", "", ""); !strings.Contains(body, "reward_payout") || !strings.Contains(body, `value="7"`) {
|
|
t.Error("catalog page did not render the reward form with the current payout")
|
|
}
|
|
|
|
// A negative value is refused (the service validates non-negativity).
|
|
if code, body := consoleDo(h, http.MethodPost, reward, "reward_payout=-1", origin); code != http.StatusOK || !strings.Contains(body, "non-negative") {
|
|
t.Errorf("negative payout = %d, has 'non-negative' = %v", code, strings.Contains(body, "non-negative"))
|
|
}
|
|
}
|