test(gateway): живая приёмка API v2 шлюза smart-updown — 26/26

Скрипт проверяет на живом стенде (BASE=https://smart-updown.binom.pw/api):
/state с canBet/canBetReason/betPercent/balance/openBet/lastBet + status,
POST /bet без суммы (30% от баланса, кламп), отказы 200 {accepted:false,
reasonCode, retryAfterSeconds}, авто-закрытие ставки по экспирации,
переход openBet -> lastBet. Запуск: BASE=... node tools/gateway-api-v2-check.cjs
This commit is contained in:
subochev
2026-09-17 00:11:56 +03:00
parent af5dc5db4c
commit a4ac702907
+179
View File
@@ -0,0 +1,179 @@
/**
* ЖИВАЯ приёмка API v2 шлюза smart-updown (ставка без суммы, 30% от баланса).
*
* BASE=https://smart-updown.binom.pw/api node gateway-api-v2-check.cjs
*
* Проверяет:
* 1. GET /state — все СТАРЫЕ поля на месте (обратная совместимость)
* 2. GET /state — новые поля: bettor, betPercent, canBet, canBetReasonCode,
* canBetReason, betAmountUnits, balance, openBet, lastBet
* 3. /state — openBet и lastBet взаимоисключающие; у ставки есть status
* 4. /state — можно ли ставить (canBet) и что баланс читается
* 5. POST /bet {side:"up"} — БЕЗ суммы: accepted=true, есть betId и txSig
* 6. POST /bet — retryAfterSeconds это ЧИСЛО (дельта), 0 < r <= expiry
* 7. GET /state сразу после — openBet не null, status="open"
* 8. POST /bet повторно — accepted=false, reasonCode=OPEN_BET, retryAfterSeconds > 0
* 9. POST /bet {side:"кривой"} — 400
* 10. ждать экспирацию ~20 с — /state: openBet=null, lastBet.status="closed"
*/
const BASE = process.env.BASE || "https://smart-updown.binom.pw/api";
const results = [];
function check(name, ok, detail) {
results.push({ name, ok, detail });
console.log(`${ok ? "OK " : "FAIL"} | ${name}${detail ? " — " + detail : ""}`);
}
const j = async (path, init) => {
const r = await fetch(BASE + path, init);
let body = null;
try { body = await r.json(); } catch { /* не JSON */ }
return { status: r.status, body };
};
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Поля, которые читает тестовый фронт и которые НЕЛЬЗЯ терять.
const LEGACY_STATE = [
"counter", "nextBetId", "minAmount", "maxAmount", "minAmountHuman",
"maxAmountHuman", "multiplierBps", "expirySeconds", "paused", "admin",
"relayer", "vaultBalance", "vaultBalanceHuman",
];
const NEW_STATE = [
"bettor", "betPercent", "betAmountUnits", "betAmountHuman",
"canBet", "canBetReasonCode", "canBetReason", "balance",
"openBet", "lastBet",
];
(async () => {
console.log(`BASE=${BASE}\n`);
// --- 1-2. /state: старые и новые поля ---
const st = await j("/state");
const b = st.body || {};
const missingLegacy = LEGACY_STATE.filter((k) => !(k in b));
check("GET /state → 200", st.status === 200, `status=${st.status}`);
check("нет потери старых полей", missingLegacy.length === 0,
missingLegacy.length ? "нет: " + missingLegacy.join(",") : `${LEGACY_STATE.length} полей на месте`);
const missingNew = NEW_STATE.filter((k) => !(k in b));
check("новые поля присутствуют", missingNew.length === 0,
missingNew.length ? "нет: " + missingNew.join(",") : `${NEW_STATE.length} полей на месте`);
console.log(` bettor=${b.bettor} betPercent=${b.betPercent}`);
console.log(` canBet=${b.canBet} code=${b.canBetReasonCode} reason=${b.canBetReason}`);
console.log(` balance=${JSON.stringify(b.balance)}`);
console.log(` betAmountUnits=${b.betAmountUnits} (${b.betAmountHuman})`);
// --- 3. взаимоисключение openBet / lastBet + поле status ---
const openBet = b.openBet || null;
const lastBet = b.lastBet || null;
check("openBet и lastBet не оба заполнены", !(openBet && lastBet),
`openBet=${openBet ? openBet.id : "null"} lastBet=${lastBet ? lastBet.id : "null"}`);
const shown = openBet || lastBet;
if (shown) {
check("у ставки есть поле status (open|closed)",
shown.status === "open" || shown.status === "closed", `status=${shown.status}`);
check("у ставки есть side/amountUnits/entryPrice",
!!shown.side && !!shown.amountUnits && shown.entryPrice !== undefined,
`side=${shown.side} amount=${shown.amountUnits} entry=${shown.entryPrice}`);
} else {
console.log(" (ставок ещё нет — пропускаем проверки формы)");
}
// --- 4. можно ли ставить ---
if (b.canBet !== true) {
check("canBet=true (готовы ставить)", false, `code=${b.canBetReasonCode} reason=${b.canBetReason}`);
console.log("\nдальше невозможно: шлюз отказывает. Причина выше.");
process.exit(1);
}
check("canBet=true (готовы ставить)", true, `размер=${b.betAmountUnits}`);
// --- 5. ставка БЕЗ суммы ---
const bet1 = await j("/bet", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ side: "up" }),
});
const r1 = bet1.body || {};
console.log(` ответ /bet: ${JSON.stringify(r1).slice(0, 300)}`);
check("POST /bet {side} → 200", bet1.status === 200, `status=${bet1.status}`);
check("ставка принята (accepted=true)", r1.accepted === true,
`accepted=${r1.accepted} code=${r1.reasonCode} reason=${r1.reason}`);
const betId = r1.betId || r1.id;
check("есть betId (п.6)", !!betId, `betId=${betId}`);
check("есть txSig (п.6)", typeof r1.txSig === "string" && r1.txSig.length > 20,
`txSig=${r1.txSig ? r1.txSig.slice(0, 24) + "…" : r1.txSig}`);
// --- 6. retryAfterSeconds — ДЕЛЬТА ---
const retry = r1.retryAfterSeconds;
const expiry = Number(b.expirySeconds || 15);
check("retryAfterSeconds — число-дельта (п.5)",
typeof retry === "number" && retry > 0 && retry <= expiry,
`retryAfterSeconds=${retry} (expiry=${expiry})`);
// --- 7. /state сразу после: открытая ставка ---
await sleep(1500);
const st2 = (await j("/state")).body || {};
const ob = st2.openBet;
check("openBet заполнен после ставки (п.7)", !!ob, `openBet=${ob ? ob.id : "null"}`);
if (ob) {
check("openBet.status = \"open\"", ob.status === "open", `status=${ob.status}`);
check("openBet.id совпадает с ответом /bet", String(ob.id) === String(betId),
`openBet.id=${ob.id} betId=${betId}`);
check("openBet.retryAfterSeconds > 0",
typeof ob.retryAfterSeconds === "number" && ob.retryAfterSeconds >= 0,
`retryAfterSeconds=${ob.retryAfterSeconds}`);
}
// --- 8. вторая ставка при открытой: отказ OPEN_BET ---
const bet2 = await j("/bet", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ side: "down" }),
});
const r2 = bet2.body || {};
check("вторая ставка отклонена (accepted=false)", r2.accepted === false,
`accepted=${r2.accepted}`);
check("reasonCode=OPEN_BET", r2.reasonCode === "OPEN_BET", `code=${r2.reasonCode}`);
check("есть reason по-русски (п.2)",
typeof r2.reason === "string" && r2.reason.length > 5, `reason=${r2.reason}`);
check("retryAfterSeconds > 0 при отказе по открытой (п.5)",
typeof r2.retryAfterSeconds === "number" && r2.retryAfterSeconds > 0,
`retryAfterSeconds=${r2.retryAfterSeconds}`);
// --- 9. кривой side ---
const bet3 = await j("/bet", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ side: "вбок" }),
});
check("кривой side → 400", bet3.status === 400, `status=${bet3.status}`);
// --- 10. дождаться авто-закрытия ---
console.log("\nждём авто-закрытие (~22 с)…");
await sleep(22000);
const st3 = (await j("/state")).body || {};
check("после экспирации openBet = null", st3.openBet === null,
`openBet=${st3.openBet ? st3.openBet.id : "null"}`);
const lb = st3.lastBet;
check("lastBet заполнен после закрытия (п.7)", !!lb, `lastBet=${lb ? lb.id : "null"}`);
if (lb) {
check("lastBet.status = \"closed\"", lb.status === "closed", `status=${lb.status}`);
check("lastBet.result заполнен", !!lb.result,
`result=${lb.result} statusCode=${lb.statusCode}`);
}
check("canBet снова true после расчёта", st3.canBet === true,
`canBet=${st3.canBet} code=${st3.canBetReasonCode}`);
// --- итог ---
const failed = results.filter((r) => !r.ok);
console.log(`\n=== ИТОГ: ${results.length - failed.length}/${results.length} пройдено ===`);
if (failed.length) {
console.log("ПРОВАЛЫ:");
for (const f of failed) console.log(` - ${f.name} :: ${f.detail || ""}`);
process.exit(1);
}
console.log("все проверки пройдены");
})().catch((e) => {
console.error("исключение:", e);
process.exit(2);
});