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:
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user