From a4ac702907fe7adb277496d57b9b692a00dc6820 Mon Sep 17 00:00:00 2001 From: subochev Date: Thu, 17 Sep 2026 00:11:56 +0300 Subject: [PATCH] =?UTF-8?q?test(gateway):=20=D0=B6=D0=B8=D0=B2=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D1=91=D0=BC=D0=BA=D0=B0=20API=20v2=20?= =?UTF-8?q?=D1=88=D0=BB=D1=8E=D0=B7=D0=B0=20smart-updown=20=E2=80=94=2026/?= =?UTF-8?q?26?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скрипт проверяет на живом стенде (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 --- tools/gateway-api-v2-check.cjs | 179 +++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tools/gateway-api-v2-check.cjs diff --git a/tools/gateway-api-v2-check.cjs b/tools/gateway-api-v2-check.cjs new file mode 100644 index 0000000..eb63b46 --- /dev/null +++ b/tools/gateway-api-v2-check.cjs @@ -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); +});