From 526e2d9cc4301cab83b8a1033163d38436297b4424dafd8db6feb16420f6a903 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Mon, 29 Dec 2025 13:33:26 +0300 Subject: [PATCH] =?UTF-8?q?28=2012=2025=20=D0=92=D1=81=D1=91=20=D0=B5?= =?UTF-8?q?=D1=89=D1=91=20=D0=BD=D0=B5=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BB=D0=B8=D0=BD=D0=B8=D0=B9.=20=D0=9F=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BB=D1=8B=D0=B2=D0=B0=D1=8E=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D0=BF=D0=BE=D0=BD=D1=8F=D0=BB=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE=20=D0=BD=D0=B5=D1=82=D0=B0=D0=BA=20=D0=B2=20=D1=81?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B5.=20=D0=94=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D1=88=D0=B5=20=D0=B1=D1=83=D0=B4=D1=83=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F=D1=82=D1=8C=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B0=D0=BA.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/CreateNewDatabase.java | 9 - src/main/java/Test/TestJsonWsClient2.java | 126 -- .../java/Test/Test_AddBlock_new_NoAuth.java | 317 ----- .../Test_AddUser_and_Authorification.java | 1023 ----------------- .../java/Test/Test_SessionRefreshClient.java | 109 -- src/main/java/Test/test1.java | 5 - src/test/java/test/it/IT_RunAllCleanMain.java | 6 +- ...kScenarioRunner.java => AddBlockFlow.java} | 271 ++--- .../test/it/ws/IT_03_AddBlock_NoAuth.java | 3 +- src/test/java/test/it/ws/WsJsonOneShot.java | 62 + 10 files changed, 192 insertions(+), 1739 deletions(-) delete mode 100644 src/main/java/CreateNewDatabase.java delete mode 100644 src/main/java/Test/TestJsonWsClient2.java delete mode 100644 src/main/java/Test/Test_AddBlock_new_NoAuth.java delete mode 100644 src/main/java/Test/Test_AddUser_and_Authorification.java delete mode 100644 src/main/java/Test/Test_SessionRefreshClient.java delete mode 100644 src/main/java/Test/test1.java rename src/test/java/test/it/ws/{AddBlockScenarioRunner.java => AddBlockFlow.java} (59%) create mode 100644 src/test/java/test/it/ws/WsJsonOneShot.java diff --git a/src/main/java/CreateNewDatabase.java b/src/main/java/CreateNewDatabase.java deleted file mode 100644 index bb3d90d..0000000 --- a/src/main/java/CreateNewDatabase.java +++ /dev/null @@ -1,9 +0,0 @@ -//import shine.db.DatabaseInitializer; -// -//public class CreateNewDatabase { -// -// public static void main(String[] args) { -// // Просто прокидываем управление в DatabaseInitializer -// DatabaseInitializer.createNewDB(args); -// } -//} diff --git a/src/main/java/Test/TestJsonWsClient2.java b/src/main/java/Test/TestJsonWsClient2.java deleted file mode 100644 index dc0144c..0000000 --- a/src/main/java/Test/TestJsonWsClient2.java +++ /dev/null @@ -1,126 +0,0 @@ -//package Test; -// -//import java.net.URI; -//import java.net.http.HttpClient; -//import java.net.http.WebSocket; -//import java.net.http.WebSocket.Listener; -//import java.util.concurrent.CompletionStage; -//import java.util.concurrent.CountDownLatch; -// -//public class TestJsonWsClient2 { -// -// public static void main(String[] args) throws Exception { -// String uri = "ws://localhost:7070/ws"; -// -// String jsonRequestRefreshSession = """ -// { -// "op": "RefreshSession", -// "requestId": "test-1", -// "payload": { -// "sessionId": 123, -// "sessionPwd": "test-password" -// } -// } -// """; -// -// String jsonRequestAddUser = """ -// { -// "op": "AddUser", -// "requestId": "test-add-1", -// "payload": { -// "login": "anya1111", -// "loginId": 100211, -// "bchId": 4222, -// "pubkey0": "PUB0", -// "pubkey1": "PUB1", -// "bchLimit": 1000000 -// } -// } -// """; -// -// String jsonRequestAuthChallenge = """ -// { -// "op": "AuthChallenge", -// "requestId": "test-auth-1", -// "payload": { -// "login": "anya1111" -// } -// } -// """; -// -// // Что тестируем сейчас: -// String jsonRequest = jsonRequestAuthChallenge; -//// String jsonRequest = jsonRequestRefreshSession; -//// String jsonRequest = jsonRequestAddUser; -// -// System.out.println("Подключаемся к " + uri); -// -// CountDownLatch latch = new CountDownLatch(1); -// -// HttpClient client = HttpClient.newHttpClient(); -// -// WebSocket webSocket = client.newWebSocketBuilder() -// .buildAsync(URI.create(uri), new Listener() { -// -// // 0 — ещё ничего не получили -// // 1 — получили 1-й ответ, отправили повторно -// // 2 — получили 2-й ответ, закрываемся -// private int responsesCount = 0; -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ WebSocket подключен"); -// -// System.out.println("📤 Отправляем JSON-запрос (1 раз):"); -// System.out.println(jsonRequest); -// -// webSocket.sendText(jsonRequest, true); -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// responsesCount++; -// -// System.out.println("📥 Получен TEXT-ответ #" + responsesCount + " от сервера:"); -// System.out.println(message); -// -// if (responsesCount == 1) { -// // После первого ответа — отправляем тот же запрос ещё раз -// System.out.println("📤 Отправляем JSON-запрос второй раз:"); -// System.out.println(jsonRequest); -// webSocket.sendText(jsonRequest, true); -// } else { -// // После второго ответа — закрываем соединение -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done"); -// latch.countDown(); -// } -// -// return Listener.super.onText(webSocket, data, last); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return Listener.super.onClose(webSocket, statusCode, reason); -// } -// }).join(); -// -// // Ждём, пока получим ответ/ошибку/закрытие -// latch.await(); -// System.out.println("Тест завершён, выходим."); -// } -//} diff --git a/src/main/java/Test/Test_AddBlock_new_NoAuth.java b/src/main/java/Test/Test_AddBlock_new_NoAuth.java deleted file mode 100644 index 2b98e98..0000000 --- a/src/main/java/Test/Test_AddBlock_new_NoAuth.java +++ /dev/null @@ -1,317 +0,0 @@ -//package Test; -// -//import com.fasterxml.jackson.databind.JsonNode; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import utils.crypto.Ed25519Util; -//import blockchain.body.HeaderBody; -//import blockchain.body.TextBody; -//import blockchain.BchCryptoVerifier; -//import blockchain.BchBlockEntry; -// -//import java.net.URI; -//import java.net.http.HttpClient; -//import java.net.http.WebSocket; -//import java.nio.ByteBuffer; -//import java.nio.ByteOrder; -//import java.util.Base64; -//import java.util.concurrent.CompletableFuture; -//import java.util.concurrent.CompletionStage; -//import java.util.concurrent.CountDownLatch; -// -//public class Test_AddBlock_new_NoAuth { -// -// private static final String WS_URI = "ws://localhost:7070/ws"; -// private static final ObjectMapper JSON = new ObjectMapper(); -// -// private static final String TEST_LOGIN = "anya24"; -// // По твоему правилу: blockchainName = login + 4 цифры -// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; -// -// private static final byte[] LOGIN_PRIV_KEY; -// private static final byte[] LOGIN_PUB_KEY; -// -// static { -// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); -// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); -// } -// -// private static final byte[] ZERO32 = new byte[32]; -// private static final String ZERO64 = "0".repeat(64); -// -// public static void main(String[] args) throws Exception { -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { -// -// private int step = 0; -// -// // Эти значения обновим ПО ОТВЕТУ сервера на header -// private String lastGlobalHashHex = ZERO64; -// private String lastLineHashHex = ZERO64; -// -// @Override -// public void onOpen(WebSocket ws) { -// System.out.println("✅ WS connected: " + WS_URI); -// ws.request(1); -// -// // 1) HEADER (global=0, line=0, lineNumber=0) -// byte[] headerFull = buildHeaderBlockFullBytes( -// /*global*/0, -// /*lineIndex*/(short)0, -// /*lineBlock*/0, -// /*prevGlobal*/ZERO32, -// /*prevLine*/ZERO32 -// ); -// -// String json = buildAddBlockJson( -// "test-add-header", -// TEST_BCH_NAME, -// 0, -// ZERO64, // prevGlobalHash для первого блока — нули -// base64(headerFull) -// ); -// -// System.out.println("\n📤 SEND #1 (HEADER):\n" + json); -// ws.sendText(json, true); -// } -// -// @Override -// public CompletionStage onText(WebSocket ws, CharSequence data, boolean last) { -// String msg = data.toString(); -// System.out.println("\n📥 RECV:\n" + msg); -// System.out.println("-----------------------------------------------------"); -// -// try { -// int status = extractStatus(msg); -// -// if (step == 0) { -// if (status != 200) { -// System.out.println("❌ HEADER rejected, status=" + status); -// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail"); -// return CompletableFuture.completedFuture(null); -// } -// -// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state -// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash"); -// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash"); -// -// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) { -// System.out.println("❌ No serverLastGlobalHash in response"); -// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response"); -// return CompletableFuture.completedFuture(null); -// } -// if (serverLastLineHash == null || serverLastLineHash.isBlank()) { -// // fallback: пусть будет как global (если сервер так хранит) -// serverLastLineHash = serverLastGlobalHash; -// } -// -// lastGlobalHashHex = serverLastGlobalHash; -// lastLineHashHex = serverLastLineHash; -// -// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex); -// byte[] prevLine32 = hexToBytes32(lastLineHashHex); -// -// // 2) TEXT (global=1, line=0, lineNumber=1) -// byte[] textFull = buildTextBlockFullBytes( -// /*global*/1, -// /*lineIndex*/(short)0, -// /*lineBlock*/1, -// prevGlobal32, -// prevLine32, -// "Hello from test client" -// ); -// -// String json2 = buildAddBlockJson( -// "test-add-text", -// TEST_BCH_NAME, -// 1, -// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера -// base64(textFull) -// ); -// -// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2); -// step = 1; -// ws.sendText(json2, true); -// -// } else if (step == 1) { -// if (status != 200) { -// System.out.println("❌ TEXT rejected, status=" + status); -// } else { -// System.out.println("✅ Done. Closing."); -// } -// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok"); -// } -// -// } catch (Exception e) { -// e.printStackTrace(System.out); -// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception"); -// } -// -// ws.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket ws, Throwable error) { -// System.out.println("❌ WS error: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket ws, int statusCode, String reason) { -// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// // ================================================================================= -// // BUILD BLOCKS -// // ================================================================================= -// -// private static byte[] buildHeaderBlockFullBytes(int globalNumber, -// short lineIndex, -// int lineBlockNumber, -// byte[] prevGlobalHash32, -// byte[] prevLineHash32) { -// -// HeaderBody body = new HeaderBody( -// TEST_LOGIN -// ); -// byte[] bodyBytes = body.toBytes(); -// -// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); -// } -// -// private static byte[] buildTextBlockFullBytes(int globalNumber, -// short lineIndex, -// int lineBlockNumber, -// byte[] prevGlobalHash32, -// byte[] prevLineHash32, -// String text) { -// -// TextBody body = new TextBody(text); -// byte[] bodyBytes = body.toBytes(); -// -// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); -// } -// -// private static byte[] buildSignedBlockFullBytes(int globalNumber, -// short lineIndex, -// int lineBlockNumber, -// byte[] bodyBytes, -// byte[] prevGlobalHash32, -// byte[] prevLineHash32) { -// -// long ts = System.currentTimeMillis() / 1000L; -// -// int recordSize = -// BchBlockEntry.RAW_HEADER_SIZE + -// bodyBytes.length + -// BchBlockEntry.SIGNATURE_LEN + -// BchBlockEntry.HASH_LEN; -// -// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length) -// .order(ByteOrder.BIG_ENDIAN) -// .putInt(recordSize) -// .putInt(globalNumber) -// .putLong(ts) -// .putShort(lineIndex) -// .putInt(lineBlockNumber) -// .put(bodyBytes) -// .array(); -// -// byte[] preimage = BchCryptoVerifier.buildPreimage( -// TEST_LOGIN, -// prevGlobalHash32, -// prevLineHash32, -// rawBytes -// ); -// -// byte[] hash32 = BchCryptoVerifier.sha256(preimage); -// -// // если у тебя подпись должна быть по preimage — меняй тут -// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY); -// -// return new BchBlockEntry( -// globalNumber, -// ts, -// lineIndex, -// lineBlockNumber, -// bodyBytes, -// signature64, -// hash32 -// ).toBytes(); -// } -// -// // ================================================================================= -// // JSON BUILD -// // ================================================================================= -// -// private static String buildAddBlockJson(String requestId, -// String blockchainName, -// int globalNumber, -// String prevGlobalHashHex, -// String blockBytesB64) { -// return """ -// { -// "op": "AddBlock", -// "requestId": "%s", -// "payload": { -// "login": "%s", -// "blockchainName": "%s", -// "globalNumber": %d, -// "prevGlobalHash": "%s", -// "blockBytesB64": "%s" -// } -// } -// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); -// } -// -// // ================================================================================= -// // HELPERS -// // ================================================================================= -// -// private static int extractStatus(String json) { -// try { -// JsonNode root = JSON.readTree(json); -// if (root.has("status")) return root.get("status").asInt(); -// } catch (Exception ignore) {} -// return -1; -// } -// -// private static String extractPayloadString(String json, String field) { -// try { -// JsonNode root = JSON.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has(field)) { -// return payload.get(field).asText(); -// } -// } catch (Exception ignore) {} -// return null; -// } -// -// private static String base64(byte[] bytes) { -// return Base64.getEncoder().encodeToString(bytes); -// } -// -// private static byte[] hexToBytes32(String hex) { -// if (hex == null) throw new IllegalArgumentException("hex is null"); -// String s = hex.trim(); -// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length()); -// byte[] out = new byte[32]; -// for (int i = 0; i < 32; i++) { -// int hi = Character.digit(s.charAt(i * 2), 16); -// int lo = Character.digit(s.charAt(i * 2 + 1), 16); -// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2)); -// out[i] = (byte) ((hi << 4) | lo); -// } -// return out; -// } -//} diff --git a/src/main/java/Test/Test_AddUser_and_Authorification.java b/src/main/java/Test/Test_AddUser_and_Authorification.java deleted file mode 100644 index b9c097f..0000000 --- a/src/main/java/Test/Test_AddUser_and_Authorification.java +++ /dev/null @@ -1,1023 +0,0 @@ -//package Test; -// -//import com.fasterxml.jackson.databind.JsonNode; -//import com.fasterxml.jackson.databind.ObjectMapper; -//import utils.crypto.Ed25519Util; -// -//import java.net.URI; -//import java.net.http.HttpClient; -//import java.net.http.WebSocket; -//import java.net.http.WebSocket.Listener; -//import java.nio.charset.StandardCharsets; -//import java.util.Base64; -//import java.util.List; -//import java.util.ArrayList; -//import java.util.concurrent.CompletableFuture; -//import java.util.concurrent.CompletionStage; -//import java.util.concurrent.CountDownLatch; -// -///** -// * Большой сценарий тестирования авторизации и работы с сессиями: -// * -// * 1) AddUser — создаём пользователя в локальной БД. -// * -// * 2) Сессия 1: -// * - AuthChallenge + CreateAuthSession → первая сессия (SESSION1_ID/SESSION1_PWD). -// * -// * 3) Сессия 2: -// * - AuthChallenge + CreateAuthSession → вторая сессия (SESSION2_ID/SESSION2_PWD). -// * - ListSessions (внутри второй сессии, AUTH_STATUS_USER). -// * -// * 4) Новое подключение: -// * - AuthChallenge → AUTH_IN_PROGRESS. -// * - ListSessions c timeMs + signatureB64 (подпись по authNonce). -// * -// * 5) Новое подключение: -// * - RefreshSession по первой сессии. -// * - CloseActiveSession по второй сессии (закрываем SESSION2_ID). -// * -// * 6) Новое подключение: -// * - AuthChallenge → AUTH_IN_PROGRESS. -// * - ListSessions (ожидаем, что вторая сессия исчезла, осталась только первая). -// * -// * 7) Новое подключение: -// * - AuthChallenge → AUTH_IN_PROGRESS. -// * - CloseActiveSession по первой сессии (SESSION1_ID) без Refresh. -// * -// * 8) Новое подключение: -// * - AuthChallenge → AUTH_IN_PROGRESS. -// * - ListSessions (ожидаем пустой список сессий). -// */ -//public class Test_AddUser_and_Authorification { -// -// // Адрес сервера -// private static final String WS_URI = "ws://localhost:7070/ws"; -// -// private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); -// -// // Тестовые данные пользователя -// private static final String TEST_LOGIN = "anya24"; -// // По твоему правилу: blockchainName = login + 4 цифры -// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001"; -// private static final int TEST_BCH_LIMIT = 1_000_000; -// -// // Краткая строка clientInfo, которую клиент шлёт -// private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0"; -// -// // --- Тестовые пары ключей --- -// // loginKey — ключ аккаунта (например, "основной") -// // deviceKey — ключ устройства, которым подписываем авторизацию / управление сессиями -// -// private static final byte[] LOGIN_PRIV_KEY; -// private static final String LOGIN_PUBKEY_B64; -// -// private static final byte[] DEVICE_PRIV_KEY; -// private static final String DEVICE_PUBKEY_B64; -// -// static { -// // Детерминированное "семя" для логин-ключа -// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN); -// byte[] loginPub = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY); -// LOGIN_PUBKEY_B64 = Ed25519Util.keyToBase64(loginPub); -// -// // Детерминированное "семя" для девайс-ключа -// DEVICE_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-device-" + TEST_LOGIN); -// byte[] devicePub = Ed25519Util.derivePublicKey(DEVICE_PRIV_KEY); -// DEVICE_PUBKEY_B64 = Ed25519Util.keyToBase64(devicePub); -// } -// -// // --- Глобальные переменные между сценариями --- -// -// /** Первая сессия (создана в сценарии 1). */ -// private static String SESSION1_ID; -// private static String SESSION1_PWD; -// private static String SESSION1_STORAGE_PWD; -// -// /** Вторая сессия (создана в сценарии 2). */ -// private static String SESSION2_ID; -// private static String SESSION2_PWD; -// private static String SESSION2_STORAGE_PWD; -// -// public static void main(String[] args) throws Exception { -// System.out.println("Подключаемся к " + WS_URI); -// -// scenario1_AddUser_And_CreateFirstSession(); -// -// scenario2_CreateSecondSession_And_ListInside(); -// -// scenario3_ListSessions_AuthInProgress("S3: ListSessions (AUTH_IN_PROGRESS, две сессии ожидаются)", true, true); -// -// scenario4_RefreshFirstSession_And_CloseSecond(); -// -// scenario5_ListSessions_AuthInProgress_AfterClosingSecond(); -// -// scenario6_CloseFirstSession_AuthInProgress(); -// -// scenario7_ListSessions_AuthInProgress_NoSessions(); -// -// System.out.println("\n\nВсе сценарии завершены, выходим."); -// } -// -// // ========================================================== -// // SCENARIO 1 -// // ========================================================== -// -// private static void scenario1_AddUser_And_CreateFirstSession() throws Exception { -// printSection("СЦЕНАРИЙ 1: AddUser + AuthChallenge + CreateAuthSession (первая сессия)"); -// -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new Listener() { -// -// private int step = 0; // 0 - AddUser, 1 - AuthChallenge, 2 - CreateAuthSession -// private String authNonceLocal; -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ [S1] WebSocket подключен"); -// webSocket.request(1); -// -// String json = buildAddUserJson(); -// System.out.println("\n📤 [S1 / Шаг 1] Отправляем AddUser:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// System.out.println("\n📥 [S1] Ответ на шаг " + (step + 1) + ":"); -// System.out.println(message); -// System.out.println("-----------------------------------------------------"); -// -// try { -// if (step == 0) { -// // Ответ на AddUser -// int status = extractStatus(message); -// boolean ok = (status == 200); -// printTestResult( -// "S1/AddUser", -// ok, -// "status=" + status + (ok ? " (пользователь создан/добавлен)" : " (ожидали 200)") -// ); -// -// // Переходим к AuthChallenge -// step = 1; -// String json = buildAuthStep1Json(); -// System.out.println("\n📤 [S1 / Шаг 2] Отправляем AuthChallenge:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 1) { -// // Ответ на AuthChallenge -// int status = extractStatus(message); -// String nonce = extractAuthNonce(message); -// boolean ok = (status == 200 && nonce != null && !nonce.isBlank()); -// printTestResult( -// "S1/AuthChallenge", -// ok, -// "status=" + status + ", authNonce=" + nonce -// ); -// -// authNonceLocal = nonce; -// -// // Переходим к CreateAuthSession -// step = 2; -// SESSION1_STORAGE_PWD = generateFakeStoragePwd(); -// String json = buildAuthStep2Json(authNonceLocal, SESSION1_STORAGE_PWD); -// System.out.println("\n📤 [S1 / Шаг 3] Отправляем CreateAuthSession (первая сессия):"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 2) { -// // Ответ на CreateAuthSession — здесь мы получаем SESSION1_ID / SESSION1_PWD -// int status = extractStatus(message); -// String sessionId = extractSessionId(message); -// String sessionPwd = extractSessionPwd(message); -// -// boolean ok = (status == 200 -// && sessionId != null && !sessionId.isBlank() -// && sessionPwd != null && !sessionPwd.isBlank()); -// -// SESSION1_ID = sessionId; -// SESSION1_PWD = sessionPwd; -// -// printTestResult( -// "S1/CreateAuthSession (первая сессия)", -// ok, -// "status=" + status + -// ", sessionId=" + sessionId + -// ", sessionPwd=" + (sessionPwd != null ? "[получен]" : "null") -// ); -// -// System.out.println("🆔 [S1] SESSION1_ID=" + SESSION1_ID); -// System.out.println("🔐 [S1] SESSION1_PWD=" + SESSION1_PWD); -// -// step = 3; -// System.out.println("✅ [S1] Все шаги выполнены, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario1 done"); -// } -// -// } catch (Exception e) { -// e.printStackTrace(System.out); -// } -// -// webSocket.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ [S1] Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 [S1] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// // ========================================================== -// // SCENARIO 2 -// // ========================================================== -// -// private static void scenario2_CreateSecondSession_And_ListInside() throws Exception { -// printSection("СЦЕНАРИЙ 2: Создать вторую сессию и внутри неё вызвать ListSessions"); -// -// if (SESSION1_ID == null || SESSION1_PWD == null) { -// System.out.println("⚠️ [S2] Первая сессия не создана, пропускаем сценарий 2."); -// return; -// } -// -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new Listener() { -// -// private int step = 0; // 0 - AuthChallenge, 1 - CreateAuthSession(вторая), 2 - ListSessions -// private String authNonceLocal; -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ [S2] WebSocket подключен"); -// webSocket.request(1); -// -// String json = buildAuthStep1Json(); -// System.out.println("\n📤 [S2 / Шаг 1] Отправляем AuthChallenge:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// System.out.println("\n📥 [S2] Ответ на шаг " + (step + 1) + ":"); -// System.out.println(message); -// System.out.println("-----------------------------------------------------"); -// -// try { -// if (step == 0) { -// int status = extractStatus(message); -// String nonce = extractAuthNonce(message); -// boolean ok = (status == 200 && nonce != null && !nonce.isBlank()); -// printTestResult( -// "S2/AuthChallenge", -// ok, -// "status=" + status + ", authNonce=" + nonce -// ); -// authNonceLocal = nonce; -// -// step = 1; -// SESSION2_STORAGE_PWD = generateFakeStoragePwd(); -// String json = buildAuthStep2Json(authNonceLocal, SESSION2_STORAGE_PWD); -// System.out.println("\n📤 [S2 / Шаг 2] Отправляем CreateAuthSession (вторая сессия):"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 1) { -// int status = extractStatus(message); -// String sessionId = extractSessionId(message); -// String sessionPwd = extractSessionPwd(message); -// -// boolean ok = (status == 200 -// && sessionId != null && !sessionId.isBlank() -// && sessionPwd != null && !sessionPwd.isBlank()); -// -// SESSION2_ID = sessionId; -// SESSION2_PWD = sessionPwd; -// -// printTestResult( -// "S2/CreateAuthSession (вторая сессия)", -// ok, -// "status=" + status + -// ", sessionId=" + sessionId + -// ", sessionPwd=" + (sessionPwd != null ? "[получен]" : "null") -// ); -// -// System.out.println("🆔 [S2] SESSION2_ID=" + SESSION2_ID); -// System.out.println("🔐 [S2] SESSION2_PWD=" + SESSION2_PWD); -// -// // Теперь вызываем ListSessions внутри второй сессии (AUTH_STATUS_USER) -// step = 2; -// String json = buildListSessionsJson(0L, "", "test-list-in-session2"); -// System.out.println("\n📤 [S2 / Шаг 3] Отправляем ListSessions (внутри второй сессии):"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 2) { -// int status = extractStatus(message); -// List sessionIds = extractSessionIds(message); -// -// boolean has1 = sessionIds.contains(SESSION1_ID); -// boolean has2 = sessionIds.contains(SESSION2_ID); -// -// boolean ok = (status == 200 && has1 && has2); -// -// printTestResult( -// "S2/ListSessions (ожидаем 1 и 2 сессии)", -// ok, -// "status=" + status + -// ", sessions=" + sessionIds + -// ", contains SESSION1=" + has1 + -// ", contains SESSION2=" + has2 -// ); -// -// step = 3; -// System.out.println("✅ [S2] Все шаги выполнены, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario2 done"); -// } -// -// } catch (Exception e) { -// e.printStackTrace(System.out); -// } -// -// webSocket.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ [S2] Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 [S2] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// // ========================================================== -// // SCENARIO 3 / 5 / 7: ListSessions -// // ========================================================== -// -// private static void scenario3_ListSessions_AuthInProgress( -// String title, -// boolean expectSession1Present, -// boolean expectSession2Present -// ) throws Exception { -// -// printSection(title); -// -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new Listener() { -// -// private int step = 0; // 0 - AuthChallenge, 1 - ListSessions (AUTH_IN_PROGRESS) -// private String authNonceLocal; -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ [S-List] WebSocket подключен"); -// webSocket.request(1); -// -// String json = buildAuthStep1Json(); -// System.out.println("\n📤 [S-List / Шаг 1] Отправляем AuthChallenge:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// System.out.println("\n📥 [S-List] Ответ на шаг " + (step + 1) + ":"); -// System.out.println(message); -// System.out.println("-----------------------------------------------------"); -// -// try { -// if (step == 0) { -// int status = extractStatus(message); -// String nonce = extractAuthNonce(message); -// boolean ok = (status == 200 && nonce != null && !nonce.isBlank()); -// printTestResult( -// "S-List/AuthChallenge", -// ok, -// "status=" + status + ", authNonce=" + nonce -// ); -// authNonceLocal = nonce; -// -// // Теперь в статусе AUTH_IN_PROGRESS вызываем ListSessions -// long timeMs = System.currentTimeMillis(); -// String sig = signAuthorificated(authNonceLocal, timeMs); -// -// step = 1; -// String json = buildListSessionsJson(timeMs, sig, "test-list-auth-in-progress"); -// System.out.println("\n📤 [S-List / Шаг 2] Отправляем ListSessions (AUTH_IN_PROGRESS):"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 1) { -// int status = extractStatus(message); -// List sessionIds = extractSessionIds(message); -// -// boolean has1 = (SESSION1_ID != null && sessionIds.contains(SESSION1_ID)); -// boolean has2 = (SESSION2_ID != null && sessionIds.contains(SESSION2_ID)); -// -// boolean ok = -// status == 200 -// && (expectSession1Present == has1) -// && (expectSession2Present == has2); -// -// printTestResult( -// "S-List/ListSessions (ожидаемые сессии)", -// ok, -// "status=" + status + -// ", sessions=" + sessionIds + -// ", expect1=" + expectSession1Present + ", has1=" + has1 + -// ", expect2=" + expectSession2Present + ", has2=" + has2 -// ); -// -// step = 2; -// System.out.println("✅ [S-List] Все шаги выполнены, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario-list done"); -// } -// -// } catch (Exception e) { -// e.printStackTrace(System.out); -// } -// -// webSocket.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ [S-List] Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 [S-List] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// private static void scenario5_ListSessions_AuthInProgress_AfterClosingSecond() throws Exception { -// scenario3_ListSessions_AuthInProgress( -// "СЦЕНАРИЙ 5: ListSessions (AUTH_IN_PROGRESS) после закрытия второй сессии — должна остаться только первая", -// true, -// false -// ); -// } -// -// private static void scenario7_ListSessions_AuthInProgress_NoSessions() throws Exception { -// scenario3_ListSessions_AuthInProgress( -// "СЦЕНАРИЙ 7: ListSessions (AUTH_IN_PROGRESS) после закрытия обеих сессий — ожидаем пустой список", -// false, -// false -// ); -// } -// -// // ========================================================== -// // SCENARIO 4 -// // ========================================================== -// -// private static void scenario4_RefreshFirstSession_And_CloseSecond() throws Exception { -// printSection("СЦЕНАРИЙ 4: Refresh первой сессии и Close второй сессии (из первой)"); -// -// if (SESSION1_ID == null || SESSION1_PWD == null || SESSION2_ID == null) { -// System.out.println("⚠️ [S4] Нет нужных сессий (SESSION1/SESSION2), пропускаем сценарий 4."); -// return; -// } -// -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new Listener() { -// -// private int step = 0; // 0 - Refresh(1), 1 - Close(2) -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ [S4] WebSocket подключен"); -// webSocket.request(1); -// -// String json = buildRefreshSessionJson(SESSION1_ID, SESSION1_PWD, "test-refresh-session1"); -// System.out.println("\n📤 [S4 / Шаг 1] Отправляем RefreshSession для SESSION1:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// System.out.println("\n📥 [S4] Ответ на шаг " + (step + 1) + ":"); -// System.out.println(message); -// System.out.println("-----------------------------------------------------"); -// -// try { -// if (step == 0) { -// int status = extractStatus(message); -// String storagePwd = extractStoragePwd(message); -// boolean ok = (status == 200 && storagePwd != null); -// printTestResult( -// "S4/RefreshSession (SESSION1)", -// ok, -// "status=" + status + ", storagePwd=" + (storagePwd != null ? "[получен]" : "null") -// ); -// -// // Теперь, находясь внутри первой сессии (AUTH_STATUS_USER), -// // закрываем вторую сессию -// step = 1; -// String json = buildCloseSessionJson( -// SESSION2_ID, -// 0L, -// "", -// "test-close-session2-from-session1" -// ); -// System.out.println("\n📤 [S4 / Шаг 2] Отправляем CloseActiveSession для SESSION2:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 1) { -// int status = extractStatus(message); -// boolean ok = (status == 200); -// printTestResult( -// "S4/CloseActiveSession (SESSION2)", -// ok, -// "status=" + status + " (ожидали 200)" -// ); -// -// step = 2; -// System.out.println("✅ [S4] Все шаги выполнены, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario4 done"); -// } -// } catch (Exception e) { -// e.printStackTrace(System.out); -// } -// -// webSocket.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ [S4] Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 [S4] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// // ========================================================== -// // SCENARIO 6 -// // ========================================================== -// -// private static void scenario6_CloseFirstSession_AuthInProgress() throws Exception { -// printSection("СЦЕНАРИЙ 6: Close первой сессии (SESSION1) в статусе AUTH_IN_PROGRESS без Refresh"); -// -// if (SESSION1_ID == null) { -// System.out.println("⚠️ [S6] Первая сессия не создана, пропускаем сценарий 6."); -// return; -// } -// -// CountDownLatch latch = new CountDownLatch(1); -// HttpClient client = HttpClient.newHttpClient(); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), new Listener() { -// -// private int step = 0; // 0 - AuthChallenge, 1 - CloseActiveSession(SESSION1) -// private String authNonceLocal; -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ [S6] WebSocket подключен"); -// webSocket.request(1); -// -// String json = buildAuthStep1Json(); -// System.out.println("\n📤 [S6 / Шаг 1] Отправляем AuthChallenge:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// String message = data.toString(); -// System.out.println("\n📥 [S6] Ответ на шаг " + (step + 1) + ":"); -// System.out.println(message); -// System.out.println("-----------------------------------------------------"); -// -// try { -// if (step == 0) { -// int status = extractStatus(message); -// String nonce = extractAuthNonce(message); -// boolean ok = (status == 200 && nonce != null && !nonce.isBlank()); -// printTestResult( -// "S6/AuthChallenge", -// ok, -// "status=" + status + ", authNonce=" + nonce -// ); -// authNonceLocal = nonce; -// -// // Теперь в AUTH_IN_PROGRESS закрываем первую сессию -// long timeMs = System.currentTimeMillis(); -// String sig = signAuthorificated(authNonceLocal, timeMs); -// -// step = 1; -// String json = buildCloseSessionJson( -// SESSION1_ID, -// timeMs, -// sig, -// "test-close-session1-auth-in-progress" -// ); -// System.out.println("\n📤 [S6 / Шаг 2] Отправляем CloseActiveSession для SESSION1 (AUTH_IN_PROGRESS):"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// } else if (step == 1) { -// int status = extractStatus(message); -// boolean ok = (status == 200); -// printTestResult( -// "S6/CloseActiveSession (SESSION1, AUTH_IN_PROGRESS)", -// ok, -// "status=" + status + " (ожидали 200)" -// ); -// -// step = 2; -// System.out.println("✅ [S6] Все шаги выполнены, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario6 done"); -// } -// } catch (Exception e) { -// e.printStackTrace(System.out); -// } -// -// webSocket.request(1); -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ [S6] Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 [S6] Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// }).join(); -// -// latch.await(); -// } -// -// // ========================================================== -// // JSON BUILDERS -// // ========================================================== -// -// // 1) AddUser с payload (loginKey != deviceKey) -// private static String buildAddUserJson() { -// return """ -// { -// "op": "AddUser", -// "requestId": "test-add-1", -// "payload": { -// "login": "%s", -// "blockchainName": "%s", -// "loginKey": "%s", -// "deviceKey": "%s", -// "bchLimit": %d -// } -// } -// """.formatted( -// TEST_LOGIN, -// TEST_BCH_NAME, -// LOGIN_PUBKEY_B64, // loginKey -// DEVICE_PUBKEY_B64, // deviceKey -// TEST_BCH_LIMIT -// ); -// } -// -// // 2) Шаг 1 авторизации: запрос authNonce -// private static String buildAuthStep1Json() { -// return """ -// { -// "op": "AuthChallenge", -// "requestId": "test-auth-1", -// "payload": { -// "login": "%s" -// } -// } -// """.formatted(TEST_LOGIN); -// } -// -// /** -// * 3) Шаг 2 авторизации: подтверждение подписью. -// * -// * @param authNonce одноразовый nonce с шага 1 -// * @param storagePwd клиентский storagePwd -// */ -// private static String buildAuthStep2Json(String authNonce, String storagePwd) { -// if (authNonce == null) { -// authNonce = ""; -// } -// if (storagePwd == null || storagePwd.isBlank()) { -// storagePwd = generateFakeStoragePwd(); -// } -// -// long timeMs = System.currentTimeMillis(); -// String sigB64 = signAuthorificated(authNonce, timeMs); -// -// return """ -// { -// "op": "CreateAuthSession", -// "requestId": "test-auth-2", -// "payload": { -// "storagePwd": "%s", -// "timeMs": %d, -// "signatureB64": "%s", -// "clientInfo": "%s" -// } -// } -// """.formatted( -// storagePwd, -// timeMs, -// sigB64, -// TEST_CLIENT_INFO -// ); -// } -// -// // 4) RefreshSession: всё в payload -// private static String buildRefreshSessionJson(String sessionId, String sessionPwd, String requestId) { -// return """ -// { -// "op": "RefreshSession", -// "requestId": "%s", -// "payload": { -// "sessionId": "%s", -// "sessionPwd": "%s", -// "clientInfo": "%s" -// } -// } -// """.formatted( -// requestId, -// sessionId, -// sessionPwd, -// TEST_CLIENT_INFO -// ); -// } -// -// // 5) ListSessions -// private static String buildListSessionsJson(long timeMs, String signatureB64, String requestId) { -// if (signatureB64 == null) { -// signatureB64 = ""; -// } -// return """ -// { -// "op": "ListSessions", -// "requestId": "%s", -// "payload": { -// "timeMs": %d, -// "signatureB64": "%s" -// } -// } -// """.formatted( -// requestId, -// timeMs, -// signatureB64 -// ); -// } -// -// // 6) CloseActiveSession -// private static String buildCloseSessionJson(String sessionId, -// long timeMs, -// String signatureB64, -// String requestId) { -// if (signatureB64 == null) { -// signatureB64 = ""; -// } -// return """ -// { -// "op": "CloseActiveSession", -// "requestId": "%s", -// "payload": { -// "sessionId": "%s", -// "timeMs": %d, -// "signatureB64": "%s" -// } -// } -// """.formatted( -// requestId, -// sessionId, -// timeMs, -// signatureB64 -// ); -// } -// -// // просто для теста: base64 от 32 байт "storage" ключа -// private static String generateFakeStoragePwd() { -// byte[] data = new byte[32]; -// for (int i = 0; i < data.length; i++) { -// data[i] = (byte) (i + 1); -// } -// return Base64.getEncoder().encodeToString(data); -// } -// -// /** -// * Подписывает строку "AUTHORIFICATED:" + timeMs + authNonce приватным ключом устройства. -// */ -// private static String signAuthorificated(String authNonce, long timeMs) { -// String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; -// byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); -// -// byte[] sig = Ed25519Util.sign(preimage, DEVICE_PRIV_KEY); -// return Base64.getEncoder().encodeToString(sig); -// } -// -// // ========================================================== -// // JSON HELPERS -// // ========================================================== -// -// private static String extractAuthNonce(String json) { -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has("authNonce")) { -// return payload.get("authNonce").asText(); -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить authNonce из ответа: " + e.getMessage()); -// } -// return null; -// } -// -// private static String extractSessionPwd(String json) { -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has("sessionPwd")) { -// return payload.get("sessionPwd").asText(); -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить sessionPwd из ответа: " + e.getMessage()); -// } -// return null; -// } -// -// private static String extractSessionId(String json) { -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has("sessionId")) { -// return payload.get("sessionId").asText(); -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить sessionId из ответа: " + e.getMessage()); -// } -// return null; -// } -// -// private static String extractStoragePwd(String json) { -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload != null && payload.has("storagePwd")) { -// return payload.get("storagePwd").asText(); -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить storagePwd из ответа: " + e.getMessage()); -// } -// return null; -// } -// -// private static int extractStatus(String json) { -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// if (root.has("status")) { -// return root.get("status").asInt(); -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить status из ответа: " + e.getMessage()); -// } -// return -1; -// } -// -// private static List extractSessionIds(String json) { -// List result = new ArrayList<>(); -// try { -// JsonNode root = JSON_MAPPER.readTree(json); -// JsonNode payload = root.get("payload"); -// if (payload == null || payload.isNull()) { -// return result; -// } -// JsonNode sessionsNode = payload.get("sessions"); -// if (sessionsNode == null || !sessionsNode.isArray()) { -// return result; -// } -// for (JsonNode s : sessionsNode) { -// JsonNode idNode = s.get("sessionId"); -// if (idNode != null && !idNode.isNull()) { -// result.add(idNode.asText()); -// } -// } -// } catch (Exception e) { -// System.out.println("⚠️ Не удалось распарсить список sessions из ответа: " + e.getMessage()); -// } -// return result; -// } -// -// // ========================================================== -// // OUTPUT HELPERS -// // ========================================================== -// -// private static void printSection(String title) { -// System.out.println("\n\n=================================================="); -// System.out.println(title); -// System.out.println("==================================================\n"); -// } -// -// private static void printTestResult(String name, boolean ok, String details) { -// if (ok) { -// System.out.println("✅ " + name + " — " + details); -// } else { -// System.out.println("❌ " + name + " — " + details); -// } -// } -//} \ No newline at end of file diff --git a/src/main/java/Test/Test_SessionRefreshClient.java b/src/main/java/Test/Test_SessionRefreshClient.java deleted file mode 100644 index a229229..0000000 --- a/src/main/java/Test/Test_SessionRefreshClient.java +++ /dev/null @@ -1,109 +0,0 @@ -//package Test; -// -//import java.net.URI; -//import java.net.http.HttpClient; -//import java.net.http.WebSocket; -//import java.net.http.WebSocket.Listener; -//import java.util.concurrent.CompletableFuture; -//import java.util.concurrent.CompletionStage; -//import java.util.concurrent.CountDownLatch; -// -//public class Test_SessionRefreshClient { -// -// // Адрес сервера -// private static final String WS_URI = "ws://localhost:7070/ws"; -// -// // ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ==== -// private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId -// private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd -// // ============================================= -// -// public static void main(String[] args) throws Exception { -// System.out.println("Подключаемся к " + WS_URI); -// -// CountDownLatch latch = new CountDownLatch(1); -// -// HttpClient client = HttpClient.newHttpClient(); -// -// ClientListener listener = new ClientListener(latch); -// -// client.newWebSocketBuilder() -// .buildAsync(URI.create(WS_URI), listener) -// .join(); -// -// latch.await(); -// System.out.println("Тест RefreshSession завершён, выходим."); -// } -// -// private static String buildRefreshSessionJson() { -// return """ -// { -// "op": "RefreshSession", -// "requestId": "test-session-refresh-1", -// "payload": { -// "sessionId": %d, -// "sessionPwd": "%s" -// } -// } -// """.formatted(SESSION_ID, SESSION_PWD); -// } -// -// private static class ClientListener implements Listener { -// -// private final CountDownLatch latch; -// -// ClientListener(CountDownLatch latch) { -// this.latch = latch; -// } -// -// @Override -// public void onOpen(WebSocket webSocket) { -// System.out.println("✅ WebSocket подключен"); -// -// webSocket.request(1); // разрешаем принимать одно сообщение -// -// // сразу отправляем запрос RefreshSession -// String json = buildRefreshSessionJson(); -// System.out.println(); -// System.out.println("📤 Отправляем RefreshSession:"); -// System.out.println(json); -// webSocket.sendText(json, true); -// -// Listener.super.onOpen(webSocket); -// } -// -// @Override -// public CompletionStage onText(WebSocket webSocket, -// CharSequence data, -// boolean last) { -// System.out.println("📥 Ответ от сервера:"); -// System.out.println(data.toString()); -// System.out.println("-----------------------------------------------------"); -// -// // После одного ответа просто закрываем соединение -// System.out.println("✅ Получен ответ на RefreshSession, закрываем соединение"); -// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done"); -// -// // запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся) -// webSocket.request(1); -// -// return CompletableFuture.completedFuture(null); -// } -// -// @Override -// public void onError(WebSocket webSocket, Throwable error) { -// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage()); -// error.printStackTrace(System.out); -// latch.countDown(); -// } -// -// @Override -// public CompletionStage onClose(WebSocket webSocket, -// int statusCode, -// String reason) { -// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason); -// latch.countDown(); -// return CompletableFuture.completedFuture(null); -// } -// } -//} diff --git a/src/main/java/Test/test1.java b/src/main/java/Test/test1.java deleted file mode 100644 index 2307c78..0000000 --- a/src/main/java/Test/test1.java +++ /dev/null @@ -1,5 +0,0 @@ -//package Test; -// -//public class test1 { -// -//} \ No newline at end of file diff --git a/src/test/java/test/it/IT_RunAllCleanMain.java b/src/test/java/test/it/IT_RunAllCleanMain.java index aee0510..6db0dbb 100644 --- a/src/test/java/test/it/IT_RunAllCleanMain.java +++ b/src/test/java/test/it/IT_RunAllCleanMain.java @@ -20,7 +20,7 @@ public class IT_RunAllCleanMain { private static final String DATA_DIR = "data"; public static void main(String[] args) { - ItRunContext.initIfNeeded(); +// ItRunContext.initIfNeeded(); TestLog.title("IT RUN CLEAN: очистка data/ + запуск всех тестов"); @@ -32,8 +32,8 @@ public class IT_RunAllCleanMain { System.exit(1); } - int failed = IT_RunAllMain.runAll(); - System.exit(failed); +// int failed = IT_RunAllMain.runAll(); +// System.exit(failed); } private static void cleanupDataDir(String dirName) throws IOException { diff --git a/src/test/java/test/it/ws/AddBlockScenarioRunner.java b/src/test/java/test/it/ws/AddBlockFlow.java similarity index 59% rename from src/test/java/test/it/ws/AddBlockScenarioRunner.java rename to src/test/java/test/it/ws/AddBlockFlow.java index 80e50aa..b94fe73 100644 --- a/src/test/java/test/it/ws/AddBlockScenarioRunner.java +++ b/src/test/java/test/it/ws/AddBlockFlow.java @@ -5,6 +5,7 @@ import blockchain.BchCryptoVerifier; import blockchain.body.HeaderBody; import blockchain.body.ReactionBody; import blockchain.body.TextBody; +import test.it.utils.JsonParsers; import test.it.utils.TestConfig; import utils.crypto.Ed25519Util; @@ -13,31 +14,35 @@ import java.nio.ByteOrder; import java.time.Duration; import java.util.Base64; -/** - * AddBlockScenarioRunner - * - * Хранит локальное состояние: - * - globalLastHashHex / globalLastNumber - * - lineLastNumber[line] / lineLastHashHex[line] - * - headerHash32 (нужен как prevLineHash для первых блоков линий) - * - * Умеет: - * - собрать блок (header/text/react) - * - отправить AddBlock по сети (каждый запрос = новое WS соединение) - * - обновить локальное состояние - */ -public final class AddBlockScenarioRunner { +import static org.junit.jupiter.api.Assertions.*; - // requestId делаем фиксированный (как ты попросил) - private static final String FIXED_REQUEST_ID = "it03"; +/** + * AddBlockFlow + * + * Держит локальное состояние цепочки: + * - last globalNumber / last globalHash + * - last lineNum / last lineHash для каждой линии + * + * И умеет: + * - собрать следующий блок (HEADER / TEXT / REACTION) + * - отправить AddBlock в сервер (через WsJsonOneShot) + * - проверить serverLastGlobalHash == localHash + * - обновить локальное состояние + * + * Важно: + * - Этот класс НЕ занимается красивыми логами. Только логика + проверки. + */ +public final class AddBlockFlow { private static final byte[] ZERO32 = new byte[32]; private static final String ZERO64 = "0".repeat(64); - private final String wsUri; - private final String blockchainName; + // линии как у тебя + public static final short LINE_HEADER = 0; + public static final short LINE_TEXT = 1; + public static final short LINE_REACT = 2; - // Локальное состояние (как и было в тесте) + // локальное состояние private final int[] lineLastNumber = new int[8]; private final String[] lineLastHashHex = new String[8]; @@ -46,10 +51,7 @@ public final class AddBlockScenarioRunner { private byte[] headerHash32 = null; - public AddBlockScenarioRunner(String wsUri, String blockchainName) { - this.wsUri = wsUri; - this.blockchainName = blockchainName; - + public AddBlockFlow() { for (int i = 0; i < 8; i++) lineLastHashHex[i] = ""; } @@ -57,90 +59,94 @@ public final class AddBlockScenarioRunner { // PUBLIC API // ================================================================================= - public String getGlobalLastHashHex() { - return globalLastHashHex; - } + /** Шлём HEADER (global=0, line=0, lineNum=0). Должно быть ПЕРВЫМ. */ + public void sendHeader0(Duration timeout) { + assertEquals(-1, globalLastNumber, "HEADER должен идти первым: globalLastNumber сейчас уже " + globalLastNumber); - public int getGlobalLastNumber() { - return globalLastNumber; - } - - public int getLineLastNumber(int lineIndex) { - return lineLastNumber[lineIndex]; - } - - public String getLineLastHashHex(int lineIndex) { - return lineLastHashHex[lineIndex]; - } - - /** Добавить HEADER (global=0, line=0, lineNum=0). */ - public AddBlockResult addHeader(short lineHeader) { BuiltBlock header = buildHeaderBlock( 0, - lineHeader, + LINE_HEADER, 0, ZERO32, ZERO32 ); - String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, 0, ZERO64, base64(header.fullBytes)); - String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8)); + String req = buildAddBlockJson(TestConfig.BCH_NAME(), 0, ZERO64, base64(header.fullBytes)); + String resp = WsJsonOneShot.request(req, timeout); + + assert200("AddBlock(HEADER)", resp); + + String serverLastGlobalHash0 = extractPayloadString(resp, "serverLastGlobalHash"); + assertNotNull(serverLastGlobalHash0, "HEADER: payload.serverLastGlobalHash must not be null"); + assertEquals(64, serverLastGlobalHash0.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars"); - // локальный hash String localHash0 = bytesToHex64(header.hash32); + assertEquals(localHash0, serverLastGlobalHash0, "HEADER: serverLastGlobalHash должен совпасть с локальным hash"); - // обновляем состояние (как раньше) + // обновляем локальное состояние headerHash32 = header.hash32; globalLastNumber = 0; globalLastHashHex = localHash0; - lineLastNumber[0] = 0; - lineLastHashHex[0] = localHash0; - return new AddBlockResult(reqJson, resp, localHash0); + lineLastNumber[LINE_HEADER] = 0; + lineLastHashHex[LINE_HEADER] = localHash0; } - /** Добавить TEXT в lineText, следующим lineNum, global=globalNumber. */ - public AddBlockResult addText(int globalNumber, short lineText, String text) { - int lineNum = nextLineNum(lineText); - byte[] prevLineHash = prevLineHash32(lineText); + /** Шлём следующий TEXT блок в line=1. */ + public BuiltBlock sendNextText(String text, Duration timeout) { + assertNotNull(headerHash32, "TEXT нельзя слать до HEADER (headerHash32 == null)"); + + int nextGlobal = globalLastNumber + 1; + int lineNum = nextLineNum(LINE_TEXT); + byte[] prevLineHash = prevLineHash32(LINE_TEXT); BuiltBlock b = buildTextBlock( - globalNumber, - lineText, + nextGlobal, + LINE_TEXT, lineNum, hexToBytes32(globalLastHashHex), prevLineHash, text ); - String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes)); - String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8)); + String req = buildAddBlockJson(TestConfig.BCH_NAME(), nextGlobal, globalLastHashHex, base64(b.fullBytes)); + String resp = WsJsonOneShot.request(req, timeout); + + assert200("AddBlock(TEXT)", resp); + + String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash"); + assertNotNull(serverLastGlobalHash, "TEXT: payload.serverLastGlobalHash must not be null"); + assertEquals(64, serverLastGlobalHash.trim().length(), "TEXT: serverLastGlobalHash must be 64 hex chars"); String localHash = bytesToHex64(b.hash32); + assertEquals(localHash, serverLastGlobalHash, "TEXT: serverLastGlobalHash должен совпасть с локальным hash"); // обновляем состояние - globalLastNumber = globalNumber; + globalLastNumber = nextGlobal; globalLastHashHex = localHash; - lineLastNumber[lineText] = lineNum; - lineLastHashHex[lineText] = localHash; + lineLastNumber[LINE_TEXT] = lineNum; + lineLastHashHex[LINE_TEXT] = localHash; - return new AddBlockResult(reqJson, resp, localHash, b.hash32); + return b; } - /** Добавить REACT в lineReact, следующим lineNum, global=globalNumber, ссылка на (toGlobal,toHash32). */ - public AddBlockResult addReaction(int globalNumber, - short lineReact, - int reactionCode, - String toBlockchainName, - int toBlockGlobalNumber, - byte[] toBlockHash32) { + /** Шлём следующий REACTION блок в line=2, ссылаясь на конкретный блок. */ + public BuiltBlock sendNextReaction(int reactionCode, + String toBlockchainName, + int toBlockGlobalNumber, + byte[] toBlockHash32, + Duration timeout) { + assertNotNull(headerHash32, "REACTION нельзя слать до HEADER (headerHash32 == null)"); + assertNotNull(toBlockHash32, "toBlockHash32 is null"); + assertEquals(32, toBlockHash32.length, "toBlockHash32 must be 32 bytes"); - int lineNum = nextLineNum(lineReact); - byte[] prevLineHash = prevLineHash32(lineReact); + int nextGlobal = globalLastNumber + 1; + int lineNum = nextLineNum(LINE_REACT); + byte[] prevLineHash = prevLineHash32(LINE_REACT); BuiltBlock b = buildReactionBlock( - globalNumber, - lineReact, + nextGlobal, + LINE_REACT, lineNum, hexToBytes32(globalLastHashHex), prevLineHash, @@ -150,51 +156,38 @@ public final class AddBlockScenarioRunner { toBlockHash32 ); - String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes)); - String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8)); + String req = buildAddBlockJson(TestConfig.BCH_NAME(), nextGlobal, globalLastHashHex, base64(b.fullBytes)); + String resp = WsJsonOneShot.request(req, timeout); + + assert200("AddBlock(REACT)", resp); + + String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash"); + assertNotNull(serverLastGlobalHash, "REACT: payload.serverLastGlobalHash must not be null"); + assertEquals(64, serverLastGlobalHash.trim().length(), "REACT: serverLastGlobalHash must be 64 hex chars"); String localHash = bytesToHex64(b.hash32); + assertEquals(localHash, serverLastGlobalHash, "REACT: serverLastGlobalHash должен совпасть с локальным hash"); // обновляем состояние - globalLastNumber = globalNumber; + globalLastNumber = nextGlobal; globalLastHashHex = localHash; - lineLastNumber[lineReact] = lineNum; - lineLastHashHex[lineReact] = localHash; + lineLastNumber[LINE_REACT] = lineNum; + lineLastHashHex[LINE_REACT] = localHash; - return new AddBlockResult(reqJson, resp, localHash, b.hash32); + return b; } - // ================================================================================= - // RESULT HOLDER - // ================================================================================= - - public static final class AddBlockResult { - public final String requestJson; - public final String responseJson; - - /** локально вычисленный hash (HEX64) именно для этого блока */ - public final String localHashHex; - - /** локальный hash32 (если надо ссылаться на блок дальше) */ - public final byte[] localHash32; - - public AddBlockResult(String requestJson, String responseJson, String localHashHex) { - this(requestJson, responseJson, localHashHex, null); - } - - public AddBlockResult(String requestJson, String responseJson, String localHashHex, byte[] localHash32) { - this.requestJson = requestJson; - this.responseJson = responseJson; - this.localHashHex = localHashHex; - this.localHash32 = localHash32; - } - } + // getters для итогов/логов (если надо) + public int globalLastNumber() { return globalLastNumber; } + public String globalLastHashHex() { return globalLastHashHex; } + public int lineLastNumber(short line) { return lineLastNumber[line]; } + public String lineLastHashHex(short line) { return lineLastHashHex[line]; } // ================================================================================= - // LINE HELPERS + // INTERNALS: line helpers // ================================================================================= - /** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 тут только 0. */ + /** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 здесь не используется. */ private int nextLineNum(short lineIndex) { if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); if (lineIndex == 0) return 0; @@ -228,15 +221,15 @@ public final class AddBlockScenarioRunner { } // ================================================================================= - // BUILD BLOCKS + // INTERNALS: build blocks // ================================================================================= - /** Небольшой холдер, чтобы сценарий мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */ - private static final class BuiltBlock { - final byte[] fullBytes; - final byte[] hash32; + /** Небольшой холдер, чтобы flow мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */ + public static final class BuiltBlock { + public final byte[] fullBytes; + public final byte[] hash32; - BuiltBlock(byte[] fullBytes, byte[] hash32) { + public BuiltBlock(byte[] fullBytes, byte[] hash32) { this.fullBytes = fullBytes; this.hash32 = hash32; } @@ -264,12 +257,6 @@ public final class AddBlockScenarioRunner { TextBody body = new TextBody(text); byte[] bodyBytes = body.toBytes(); - // ⚠️ ВАЖНО: - // У тебя сервер ругается: "Body is in wrong lineIndex expected=1 actual=0 (type=1 ver=1)". - // Это значит, что lineIndex хранится ВНУТРИ bodyBytes. - // Ниже — безопасный патч: предполагаем формат "type(1) + ver(1) + lineIndex(2)" и проставляем lineIndex. - bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex); - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); } @@ -292,9 +279,6 @@ public final class AddBlockScenarioRunner { byte[] bodyBytes = body.toBytes(); - // Аналогично TextBody — если внутри есть lineIndex, проставляем. - bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex); - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); } @@ -321,7 +305,7 @@ public final class AddBlockScenarioRunner { // Ключевой момент: preimage должен совпасть с серверным правилом. // Сервер НЕ получает prevLineHash по сети — он берёт его из своего состояния линии. - // Поэтому мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()). + // Поэтому в тесте мы обязаны передавать сюда ровно тот же prevLineHash32. byte[] preimage = BchCryptoVerifier.buildPreimage( TestConfig.LOGIN(), prevGlobalHash32, @@ -346,33 +330,11 @@ public final class AddBlockScenarioRunner { return new BuiltBlock(full, hash32); } - /** - * Патч lineIndex внутри bodyBytes. - * - * Предположение (по твоей ошибке type=1 ver=1): - * bodyBytes[0] = type - * bodyBytes[1] = ver - * bodyBytes[2..3] = lineIndex (big-endian short) - * - * Если формат другой — скажешь, поменяю оффсет/проверки. - */ - private static byte[] patchBodyLineIndexIfPresent(byte[] bodyBytes, short lineIndex) { - if (bodyBytes == null) return null; - if (bodyBytes.length < 4) return bodyBytes; - - // Патчим только для line>0 (для header line=0 и так норм). - if (lineIndex <= 0) return bodyBytes; - - ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN).putShort(2, lineIndex); - return bodyBytes; - } - // ================================================================================= - // JSON HELPERS + // INTERNALS: json helpers // ================================================================================= - private static String buildAddBlockJson(String requestId, - String blockchainName, + private static String buildAddBlockJson(String blockchainName, int globalNumber, String prevGlobalHashHex, String blockBytesB64) { @@ -387,7 +349,24 @@ public final class AddBlockScenarioRunner { "blockBytesB64": "%s" } } - """.formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); + """.formatted(WsJsonOneShot.FIXED_REQUEST_ID, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); + } + + private static void assert200(String op, String resp) { + int st = JsonParsers.status(resp); + assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp); + } + + private static String extractPayloadString(String json, String field) { + try { + com.fasterxml.jackson.databind.JsonNode root = + new com.fasterxml.jackson.databind.ObjectMapper().readTree(json); + com.fasterxml.jackson.databind.JsonNode payload = root.get("payload"); + if (payload != null && payload.has(field)) { + return payload.get(field).asText(); + } + } catch (Exception ignore) {} + return null; } private static String base64(byte[] bytes) { @@ -395,7 +374,7 @@ public final class AddBlockScenarioRunner { } // ================================================================================= - // HEX HELPERS + // INTERNALS: hex helpers // ================================================================================= private static byte[] hexToBytes32(String hex) { diff --git a/src/test/java/test/it/ws/IT_03_AddBlock_NoAuth.java b/src/test/java/test/it/ws/IT_03_AddBlock_NoAuth.java index deb963b..262fa87 100644 --- a/src/test/java/test/it/ws/IT_03_AddBlock_NoAuth.java +++ b/src/test/java/test/it/ws/IT_03_AddBlock_NoAuth.java @@ -48,7 +48,7 @@ public class IT_03_AddBlock_NoAuth { // - requestId тут не важен, но пусть будет. // - отдельная авторизация не нужна, но пользователь должен существовать. String reqJson = JsonBuilders.addUser("it03-adduser-beforeall"); - +/** String resp = WsJsonOneShot.request(reqJson, Duration.ofSeconds(5)); int st = JsonParsers.status(resp); @@ -63,6 +63,7 @@ public class IT_03_AddBlock_NoAuth { } fail("User precondition failed. status=" + st + ", resp=" + resp); + */ } @Test diff --git a/src/test/java/test/it/ws/WsJsonOneShot.java b/src/test/java/test/it/ws/WsJsonOneShot.java new file mode 100644 index 0000000..00cc767 --- /dev/null +++ b/src/test/java/test/it/ws/WsJsonOneShot.java @@ -0,0 +1,62 @@ +package test.it.ws; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import test.it.utils.TestConfig; +import test.it.utils.WsTestClient; + +import java.time.Duration; + +/** + * WsJsonOneShot + * + * Утилита "отправил JSON -> получил JSON", строго: + * - на каждый request создаём НОВОЕ WS соединение + * - отправляем + * - ждём ответ + * - закрываем соединение + * + * Важно: + * - requestId тут не важен для человека, но важен для WsTestClient, чтобы сопоставить ответ. + * - поэтому ставим ВСЕГДА один и тот же requestId (FIXED_REQUEST_ID). + * - requestId НЕ логируем. + */ +public final class WsJsonOneShot { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** Всегда один и тот же requestId. */ + public static final String FIXED_REQUEST_ID = "it"; + + private WsJsonOneShot() {} + + /** + * Отправить JSON строкой и вернуть JSON ответ строкой. + * Соединение создаётся и закрывается ВНУТРИ. + */ + public static String request(String json, Duration timeout) { + String patched = forceRequestId(json, FIXED_REQUEST_ID); + + try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { + // requestId нам нужен только как ключ ожидания в WsTestClient + return client.request(FIXED_REQUEST_ID, patched, timeout); + } + } + + /** + * Гарантируем, что requestId есть и равен FIXED_REQUEST_ID. + * Если JSON кривой — вернём как есть (тогда упадёт выше по логике, и это нормально для теста). + */ + private static String forceRequestId(String json, String requestId) { + try { + JsonNode root = MAPPER.readTree(json); + if (!(root instanceof ObjectNode obj)) return json; + + obj.put("requestId", requestId); + return MAPPER.writeValueAsString(obj); + } catch (Exception ignore) { + return json; + } + } +} \ No newline at end of file