diff --git a/src/main/java/CreateNewDatabase.java b/src/main/java/CreateNewDatabase.java index 614c432..bb3d90d 100644 --- a/src/main/java/CreateNewDatabase.java +++ b/src/main/java/CreateNewDatabase.java @@ -1,9 +1,9 @@ -import shine.db.DatabaseInitializer; - -public class CreateNewDatabase { - - public static void main(String[] args) { - // Просто прокидываем управление в DatabaseInitializer - DatabaseInitializer.createNewDB(args); - } -} +//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 index ebb9aed..dc0144c 100644 --- a/src/main/java/Test/TestJsonWsClient2.java +++ b/src/main/java/Test/TestJsonWsClient2.java @@ -1,126 +1,126 @@ -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("Тест завершён, выходим."); - } -} +//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 index 29300b4..2b98e98 100644 --- a/src/main/java/Test/Test_AddBlock_new_NoAuth.java +++ b/src/main/java/Test/Test_AddBlock_new_NoAuth.java @@ -1,317 +1,317 @@ -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; - } -} +//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 index 4fde1cb..b9c097f 100644 --- a/src/main/java/Test/Test_AddUser_and_Authorification.java +++ b/src/main/java/Test/Test_AddUser_and_Authorification.java @@ -1,1023 +1,1023 @@ -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 +//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 index eb93777..a229229 100644 --- a/src/main/java/Test/Test_SessionRefreshClient.java +++ b/src/main/java/Test/Test_SessionRefreshClient.java @@ -1,109 +1,109 @@ -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); - } - } -} +//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 index 079e1ff..2307c78 100644 --- a/src/main/java/Test/test1.java +++ b/src/main/java/Test/test1.java @@ -1,5 +1,5 @@ -package Test; - -public class test1 { - -} \ No newline at end of file +//package Test; +// +//public class test1 { +// +//} \ No newline at end of file diff --git a/src/test/java/test/it/IT_01_AddUser.java b/src/test/java/test/it/IT_01_AddUser.java index 6c39838..3f855cf 100644 --- a/src/test/java/test/it/IT_01_AddUser.java +++ b/src/test/java/test/it/IT_01_AddUser.java @@ -7,100 +7,89 @@ import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; +/** + * IT_01_AddUser + * + * Можно запускать: + * 1) как JUnit тест (через Suite или выборочно) + * 2) вручную как standalone: + * - main() + * - или через IT_RunAllMain / IT_RunAllCleanMain + * + * Главная цель: + * - иметь метод run() -> возвращает число не пройденных тестов (0 или 1) + * - и иметь main() для запуска одного теста + */ public class IT_01_AddUser { - // ANSI цвета - private static final String R = "\u001B[0m"; - private static final String G = "\u001B[32m"; - private static final String Y = "\u001B[33m"; - private static final String RED = "\u001B[31m"; - private static final String C = "\u001B[36m"; - - private static void line() { - System.out.println(C + "------------------------------------------------------------" + R); - } - - private static void title(String s) { - System.out.println(C + "\n============================================================" + R); - System.out.println(C + s + R); - System.out.println(C + "============================================================\n" + R); - } - - private static void ok(String s) { - System.out.println(G + "✅ " + s + R); - } - - private static void boom(String s) { - System.out.println(RED + "****************************************************************" + R); - System.out.println(RED + "❌ " + s + R); - System.out.println(RED + "****************************************************************" + R); - } - public static void main(String[] args) { // чтобы тест можно было запускать вообще без JUnit - ItRunContext.initIfNeeded(); - new IT_01_AddUser().addUser_shouldReturn200_orAlreadyExists(); + int failed = run(); + System.exit(failed); + } + + /** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */ + public static int run() { + return TestLog.runOne("IT_01_AddUser", IT_01_AddUser::testBody); } @Test void addUser_shouldReturn200_orAlreadyExists() { + // JUnit-режим: пусть падает через assert/fail как обычно + testBody(); + } + + private static void testBody() { ItRunContext.initIfNeeded(); - title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)"); - System.out.println("Используем:"); - System.out.println(" login = " + TestConfig.LOGIN()); - System.out.println(" blockchainName = " + TestConfig.BCH_NAME()); - System.out.println("Ожидание:"); - System.out.println(" - 200 (создан)"); - System.out.println(" - или 409 + payload.code=USER_ALREADY_EXISTS\n"); + TestLog.title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)"); + TestLog.info("Используем:"); + TestLog.info(" login = " + TestConfig.LOGIN()); + TestLog.info(" blockchainName = " + TestConfig.BCH_NAME()); + TestLog.info("Ожидание:"); + TestLog.info(" - 200 (создан)"); + TestLog.info(" - или 409 + payload.code=USER_ALREADY_EXISTS\n"); try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { String reqId = "it-adduser-1"; String reqJson = JsonBuilders.addUser(reqId); - System.out.println("📤 Отправляем AddUser запрос:"); - System.out.println(reqJson); - line(); + TestLog.info("📤 Отправляем AddUser запрос:"); + TestLog.info(reqJson); + TestLog.line(); String resp = client.request(reqId, reqJson, Duration.ofSeconds(5)); - System.out.println("📥 Ответ сервера:"); - System.out.println(resp); - line(); + TestLog.info("📥 Ответ сервера:"); + TestLog.info(resp); + TestLog.line(); int st = JsonParsers.status(resp); - System.out.println("ℹ️ status=" + st); + TestLog.info("ℹ️ status=" + st); boolean created = (st == 200); boolean already = (st == 409); if (already) { String code = JsonParsers.errorCode(resp); - System.out.println("ℹ️ server_code=" + code); + TestLog.info("ℹ️ server_code=" + code); - try { - assertEquals("USER_ALREADY_EXISTS", code, - "Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp); - ok("409 получен корректно: USER_ALREADY_EXISTS"); - } catch (AssertionError ae) { - boom("409 получен, но code не тот. " + ae.getMessage()); - throw ae; - } + assertEquals("USER_ALREADY_EXISTS", code, + "Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp); + + TestLog.ok("409 получен корректно: USER_ALREADY_EXISTS"); } if (created) { - ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)"); + TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)"); } else if (already) { - ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)"); + TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)"); } else { - boom("Неожиданный status=" + st + ", resp=" + resp); + TestLog.boom("Неожиданный status=" + st + ", resp=" + resp); fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp); } - } catch (AssertionError | RuntimeException e) { - boom("ТЕСТ УПАЛ: AddUserIT. Причина: " + e.getMessage()); - throw e; } } } \ No newline at end of file diff --git a/src/test/java/test/it/IT_02_Sessions.java b/src/test/java/test/it/IT_02_Sessions.java index 4989e0a..9c1a343 100644 --- a/src/test/java/test/it/IT_02_Sessions.java +++ b/src/test/java/test/it/IT_02_Sessions.java @@ -9,96 +9,60 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.*; +/** + * IT_02_Sessions + * + * Можно запускать: + * 1) как JUnit тест (через Suite или выборочно) + * 2) вручную как standalone: + * - main() + * - или через IT_RunAllMain / IT_RunAllCleanMain + * + * Главная цель: + * - иметь метод run() -> возвращает число не пройденных тестов (0 или 1) + * - и иметь main() для запуска одного теста + */ public class IT_02_Sessions { - // ANSI цвета - private static final String R = "\u001B[0m"; - private static final String G = "\u001B[32m"; - private static final String Y = "\u001B[33m"; - private static final String RED = "\u001B[31m"; - private static final String C = "\u001B[36m"; - - private static void line() { - System.out.println(C + "------------------------------------------------------------" + R); - } - - private static void title(String s) { - System.out.println(C + "\n============================================================" + R); - System.out.println(C + s + R); - System.out.println(C + "============================================================\n" + R); - } - - private static void stepTitle(String s) { - System.out.println(C + "\n-------------------- " + s + " --------------------" + R); - } - - private static void ok(String s) { - System.out.println(G + "✅ " + s + R); - } - - private static void boom(String s) { - System.out.println(RED + "****************************************************************" + R); - System.out.println(RED + "❌ " + s + R); - System.out.println(RED + "****************************************************************" + R); - } - - private static void send(String op, String json) { - System.out.println("📤 [" + op + "] Request JSON:"); - System.out.println(json); - line(); - } - - private static void recv(String op, String json) { - System.out.println("📥 [" + op + "] Response JSON:"); - System.out.println(json); - line(); - } - - private static void assert200(String op, String resp) { - int st = JsonParsers.status(resp); - try { - assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp); - ok(op + ": status=200"); - } catch (AssertionError ae) { - boom(op + ": ожидали 200, но получили " + st); - throw ae; - } - } - public static void main(String[] args) { ItRunContext.initIfNeeded(); - ensureUserExists(); - new IT_02_Sessions().sessions_flow_shouldCreateListRefreshCloseCorrectly(); + int failed = run(); + System.exit(failed); + } + + /** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */ + public static int run() { + return TestLog.runOne("IT_02_Sessions", IT_02_Sessions::testBodyStandalone); } @BeforeAll static void ensureUserExists() { ItRunContext.initIfNeeded(); - title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)"); + TestLog.title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)"); try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { String reqId = "it-adduser-beforeall"; String reqJson = JsonBuilders.addUser(reqId); - send("AddUser(BeforeAll)", reqJson); + TestLog.send("AddUser(BeforeAll)", reqJson); String resp = client.request(reqId, reqJson, Duration.ofSeconds(5)); - recv("AddUser(BeforeAll)", resp); + TestLog.recv("AddUser(BeforeAll)", resp); int st = JsonParsers.status(resp); if (st == 200) { - ok("BeforeAll: пользователь создан/добавлен (status=200)"); + TestLog.ok("BeforeAll: пользователь создан/добавлен (status=200)"); } else if (st == 409) { String code = JsonParsers.errorCode(resp); if ("USER_ALREADY_EXISTS".equals(code)) { - ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)"); + TestLog.ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)"); } else { - boom("BeforeAll: status=409, но code неожиданный: " + code); + TestLog.boom("BeforeAll: status=409, но code неожиданный: " + code); fail("User precondition failed. status=409, code=" + code + ", resp=" + resp); } } else { - boom("BeforeAll: предусловие не выполнено. status=" + st); + TestLog.boom("BeforeAll: предусловие не выполнено. status=" + st); fail("User precondition failed. status=" + st + ", resp=" + resp); } } @@ -106,242 +70,261 @@ public class IT_02_Sessions { @Test void sessions_flow_shouldCreateListRefreshCloseCorrectly() { + // JUnit-режим: пусть падает через assert/fail как обычно + testBodyJUnit(); + } + + /** + * Standalone-режим: тут мы сами вызываем предусловие ensureUserExists(), + * потому что @BeforeAll сработает только в JUnit. + */ + private static void testBodyStandalone() { + ensureUserExists(); + testBodyJUnit(); + } + + private static void testBodyJUnit() { ItRunContext.initIfNeeded(); - title("SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)"); - System.out.println("Используем:"); - System.out.println(" login = " + TestConfig.LOGIN()); - System.out.println("Ожидание сценария:"); - System.out.println(" 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession"); - System.out.println(" 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2"); - System.out.println(" 3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) → должны быть SESSION1 и SESSION2"); - System.out.println(" 4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2"); - System.out.println(" 5) Проверяем ListSessions (AUTH_IN_PROGRESS) → осталась только SESSION1"); - System.out.println(" 6) Закрываем SESSION1 в AUTH_IN_PROGRESS"); - System.out.println(" 7) Проверяем ListSessions → пусто\n"); + TestLog.titleBlock(""" + SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку) + Используем: + login = %s + Ожидание сценария: + 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession + 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2 + 3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) → должны быть SESSION1 и SESSION2 + 4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2 + 5) Проверяем ListSessions (AUTH_IN_PROGRESS) → осталась только SESSION1 + 6) Закрываем SESSION1 в AUTH_IN_PROGRESS + 7) Проверяем ListSessions → пусто + """.formatted(TestConfig.LOGIN())); String s1Id, s1Pwd; String s2Id, s2Pwd; - try { - stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-1"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge#1", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge#1", resp1); + // ===== helpers (локальные, чтобы не раздувать TestLog лишней логикой assert200) ===== + final java.util.function.BiConsumer assert200 = (op, resp) -> { + int st = JsonParsers.status(resp); + assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp); + TestLog.ok(op + ": status=200"); + }; - assert200("AuthChallenge#1", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce, "AuthChallenge#1: nonce must not be null"); - ok("AuthChallenge#1: authNonce получен: " + nonce); + // ====================================================================== - String r2 = "it-create-1"; - String storagePwd = TestConfig.fakeStoragePwd(); - String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd); - send("CreateAuthSession#1", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("CreateAuthSession#1", resp2); + TestLog.stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-1"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge#1", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge#1", resp1); - assert200("CreateAuthSession#1", resp2); + assert200.accept("AuthChallenge#1", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce, "AuthChallenge#1: nonce must not be null"); + TestLog.ok("AuthChallenge#1: authNonce получен: " + nonce); - s1Id = JsonParsers.sessionId(resp2); - s1Pwd = JsonParsers.sessionPwd(resp2); - assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null"); - assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null"); - ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]"); - } + String r2 = "it-create-1"; + String storagePwd = TestConfig.fakeStoragePwd(); + String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd); + TestLog.send("CreateAuthSession#1", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("CreateAuthSession#1", resp2); - stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-2"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge#2", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge#2", resp1); + assert200.accept("CreateAuthSession#1", resp2); - assert200("AuthChallenge#2", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce); - ok("AuthChallenge#2: authNonce получен: " + nonce); - - String r2 = "it-create-2"; - String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd()); - send("CreateAuthSession#2", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("CreateAuthSession#2", resp2); - - assert200("CreateAuthSession#2", resp2); - - s2Id = JsonParsers.sessionId(resp2); - s2Pwd = JsonParsers.sessionPwd(resp2); - assertNotNull(s2Id); - assertNotNull(s2Pwd); - ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]"); - - String r3 = "it-list-in-session2"; - String req3 = JsonBuilders.listSessions(r3, 0L, ""); - send("ListSessions(in SESSION2)", req3); - String resp3 = c.request(r3, req3, Duration.ofSeconds(5)); - recv("ListSessions(in SESSION2)", resp3); - - assert200("ListSessions(in SESSION2)", resp3); - List ids = JsonParsers.sessionIds(resp3); - ok("ListSessions(in SESSION2): sessions=" + ids); - - assertTrue(ids.contains(s1Id), "Must contain session1"); - assertTrue(ids.contains(s2Id), "Must contain session2"); - ok("Проверка OK: список содержит SESSION1 и SESSION2"); - } - - stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-list"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge(list)", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge(list)", resp1); - - assert200("AuthChallenge(list)", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce); - ok("AuthChallenge(list): authNonce=" + nonce); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]"); - - String r2 = "it-list-auth-in-progress"; - String req2 = JsonBuilders.listSessions(r2, timeMs, sig); - send("ListSessions(AUTH_IN_PROGRESS)", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("ListSessions(AUTH_IN_PROGRESS)", resp2); - - assert200("ListSessions(AUTH_IN_PROGRESS)", resp2); - - List ids = JsonParsers.sessionIds(resp2); - ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids); - - assertTrue(ids.contains(s1Id)); - assertTrue(ids.contains(s2Id)); - ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2"); - } - - stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - - String r1 = "it-refresh-s1"; - String req1 = JsonBuilders.refreshSession(r1, s1Id, s1Pwd); - send("RefreshSession(SESSION1)", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("RefreshSession(SESSION1)", resp1); - - assert200("RefreshSession(SESSION1)", resp1); - assertNotNull(JsonParsers.storagePwd(resp1)); - ok("RefreshSession: storagePwd получен"); - - String r2 = "it-close-s2"; - String req2 = JsonBuilders.closeActiveSession(r2, s2Id, 0L, ""); - send("CloseActiveSession(SESSION2)", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("CloseActiveSession(SESSION2)", resp2); - - assert200("CloseActiveSession(SESSION2)", resp2); - ok("SESSION2 закрыта"); - } - - stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-list2"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge(list2)", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge(list2)", resp1); - - assert200("AuthChallenge(list2)", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - - String r2 = "it-list-after-close-s2"; - String req2 = JsonBuilders.listSessions(r2, timeMs, sig); - send("ListSessions(after close S2)", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("ListSessions(after close S2)", resp2); - - assert200("ListSessions(after close S2)", resp2); - - List ids = JsonParsers.sessionIds(resp2); - ok("ListSessions(after close S2): sessions=" + ids); - - assertTrue(ids.contains(s1Id)); - assertFalse(ids.contains(s2Id)); - ok("Проверка OK: осталась только SESSION1"); - } - - stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-close-s1"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge(close S1)", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge(close S1)", resp1); - - assert200("AuthChallenge(close S1)", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - - String r2 = "it-close-s1"; - String req2 = JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig); - send("CloseActiveSession(SESSION1)", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("CloseActiveSession(SESSION1)", resp2); - - assert200("CloseActiveSession(SESSION1)", resp2); - ok("SESSION1 закрыта"); - } - - stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - String r1 = "it-auth-list-empty"; - String req1 = JsonBuilders.authChallenge(r1); - send("AuthChallenge(list empty)", req1); - String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); - recv("AuthChallenge(list empty)", resp1); - - assert200("AuthChallenge(list empty)", resp1); - String nonce = JsonParsers.authNonce(resp1); - assertNotNull(nonce); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - - String r2 = "it-list-empty"; - String req2 = JsonBuilders.listSessions(r2, timeMs, sig); - send("ListSessions(empty)", req2); - String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); - recv("ListSessions(empty)", resp2); - - assert200("ListSessions(empty)", resp2); - - List ids = JsonParsers.sessionIds(resp2); - ok("ListSessions(empty): sessions=" + ids); - - assertTrue(ids.isEmpty(), "Sessions must be empty"); - ok("Проверка OK: список пуст"); - } - - ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)"); - - } catch (AssertionError | RuntimeException e) { - boom("ТЕСТ УПАЛ: SessionsIT. Причина: " + e.getMessage()); - throw e; + s1Id = JsonParsers.sessionId(resp2); + s1Pwd = JsonParsers.sessionPwd(resp2); + assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null"); + assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null"); + TestLog.ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]"); } + + TestLog.stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-2"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge#2", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge#2", resp1); + + assert200.accept("AuthChallenge#2", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + TestLog.ok("AuthChallenge#2: authNonce получен: " + nonce); + + String r2 = "it-create-2"; + String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd()); + TestLog.send("CreateAuthSession#2", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("CreateAuthSession#2", resp2); + + assert200.accept("CreateAuthSession#2", resp2); + + s2Id = JsonParsers.sessionId(resp2); + s2Pwd = JsonParsers.sessionPwd(resp2); + assertNotNull(s2Id); + assertNotNull(s2Pwd); + TestLog.ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]"); + + String r3 = "it-list-in-session2"; + String req3 = JsonBuilders.listSessions(r3, 0L, ""); + TestLog.send("ListSessions(in SESSION2)", req3); + String resp3 = c.request(r3, req3, Duration.ofSeconds(5)); + TestLog.recv("ListSessions(in SESSION2)", resp3); + + assert200.accept("ListSessions(in SESSION2)", resp3); + List ids = JsonParsers.sessionIds(resp3); + TestLog.ok("ListSessions(in SESSION2): sessions=" + ids); + + assertTrue(ids.contains(s1Id), "Must contain session1"); + assertTrue(ids.contains(s2Id), "Must contain session2"); + TestLog.ok("Проверка OK: список содержит SESSION1 и SESSION2"); + } + + TestLog.stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge(list)", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge(list)", resp1); + + assert200.accept("AuthChallenge(list)", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + TestLog.ok("AuthChallenge(list): authNonce=" + nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + TestLog.ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]"); + + String r2 = "it-list-auth-in-progress"; + String req2 = JsonBuilders.listSessions(r2, timeMs, sig); + TestLog.send("ListSessions(AUTH_IN_PROGRESS)", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("ListSessions(AUTH_IN_PROGRESS)", resp2); + + assert200.accept("ListSessions(AUTH_IN_PROGRESS)", resp2); + + List ids = JsonParsers.sessionIds(resp2); + TestLog.ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids); + + assertTrue(ids.contains(s1Id)); + assertTrue(ids.contains(s2Id)); + TestLog.ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2"); + } + + TestLog.stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + + String r1 = "it-refresh-s1"; + String req1 = JsonBuilders.refreshSession(r1, s1Id, s1Pwd); + TestLog.send("RefreshSession(SESSION1)", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("RefreshSession(SESSION1)", resp1); + + assert200.accept("RefreshSession(SESSION1)", resp1); + assertNotNull(JsonParsers.storagePwd(resp1)); + TestLog.ok("RefreshSession: storagePwd получен"); + + String r2 = "it-close-s2"; + String req2 = JsonBuilders.closeActiveSession(r2, s2Id, 0L, ""); + TestLog.send("CloseActiveSession(SESSION2)", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("CloseActiveSession(SESSION2)", resp2); + + assert200.accept("CloseActiveSession(SESSION2)", resp2); + TestLog.ok("SESSION2 закрыта"); + } + + TestLog.stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list2"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge(list2)", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge(list2)", resp1); + + assert200.accept("AuthChallenge(list2)", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-list-after-close-s2"; + String req2 = JsonBuilders.listSessions(r2, timeMs, sig); + TestLog.send("ListSessions(after close S2)", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("ListSessions(after close S2)", resp2); + + assert200.accept("ListSessions(after close S2)", resp2); + + List ids = JsonParsers.sessionIds(resp2); + TestLog.ok("ListSessions(after close S2): sessions=" + ids); + + assertTrue(ids.contains(s1Id)); + assertFalse(ids.contains(s2Id)); + TestLog.ok("Проверка OK: осталась только SESSION1"); + } + + TestLog.stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-close-s1"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge(close S1)", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge(close S1)", resp1); + + assert200.accept("AuthChallenge(close S1)", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-close-s1"; + String req2 = JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig); + TestLog.send("CloseActiveSession(SESSION1)", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("CloseActiveSession(SESSION1)", resp2); + + assert200.accept("CloseActiveSession(SESSION1)", resp2); + TestLog.ok("SESSION1 закрыта"); + } + + TestLog.stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список"); + try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { + String r1 = "it-auth-list-empty"; + String req1 = JsonBuilders.authChallenge(r1); + TestLog.send("AuthChallenge(list empty)", req1); + String resp1 = c.request(r1, req1, Duration.ofSeconds(5)); + TestLog.recv("AuthChallenge(list empty)", resp1); + + assert200.accept("AuthChallenge(list empty)", resp1); + String nonce = JsonParsers.authNonce(resp1); + assertNotNull(nonce); + + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs); + + String r2 = "it-list-empty"; + String req2 = JsonBuilders.listSessions(r2, timeMs, sig); + TestLog.send("ListSessions(empty)", req2); + String resp2 = c.request(r2, req2, Duration.ofSeconds(5)); + TestLog.recv("ListSessions(empty)", resp2); + + assert200.accept("ListSessions(empty)", resp2); + + List ids = JsonParsers.sessionIds(resp2); + TestLog.ok("ListSessions(empty): sessions=" + ids); + + assertTrue(ids.isEmpty(), "Sessions must be empty"); + TestLog.ok("Проверка OK: список пуст"); + } + + TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)"); } } \ 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 new file mode 100644 index 0000000..aee0510 --- /dev/null +++ b/src/test/java/test/it/IT_RunAllCleanMain.java @@ -0,0 +1,62 @@ +package test.it; + +import test.it.utils.ItRunContext; +import test.it.utils.TestLog; + +import java.io.IOException; +import java.nio.file.*; +import java.util.Comparator; + +/** + * Ручной запуск всех IT тестов БЕЗ JUnit / Suite, но С ПРЕДВАРИТЕЛЬНОЙ очисткой data/. + * + * Делает: + * 1) чистит папку data/ + * 2) запускает все тесты по очереди (через IT_RunAllMain.runAll()) + * 3) возвращает код = число упавших тестов + */ +public class IT_RunAllCleanMain { + + private static final String DATA_DIR = "data"; + + public static void main(String[] args) { + ItRunContext.initIfNeeded(); + + TestLog.title("IT RUN CLEAN: очистка data/ + запуск всех тестов"); + + try { + cleanupDataDir(DATA_DIR); + } catch (Throwable t) { + TestLog.boom("Не смог очистить data/. Причина: " + t.getMessage()); + if (TestLog.VERBOSE) t.printStackTrace(System.out); + System.exit(1); + } + + int failed = IT_RunAllMain.runAll(); + System.exit(failed); + } + + private static void cleanupDataDir(String dirName) throws IOException { + Path dir = Paths.get(dirName); + + if (!Files.exists(dir)) { + TestLog.warn("data dir not found: " + dir.toAbsolutePath() + " (создаю)"); + Files.createDirectories(dir); + return; + } + + // удаляем ВСЁ внутри папки, но саму папку оставляем + Files.walk(dir) + .sorted(Comparator.reverseOrder()) + .filter(p -> !p.equals(dir)) + .forEach(p -> { + try { + Files.deleteIfExists(p); + } catch (IOException e) { + throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e); + } + }); + + TestLog.ok("data очищена: " + dir.toAbsolutePath()); + } +} \ No newline at end of file diff --git a/src/test/java/test/it/IT_RunAllMain.java b/src/test/java/test/it/IT_RunAllMain.java index f03f5fd..0df4dff 100644 --- a/src/test/java/test/it/IT_RunAllMain.java +++ b/src/test/java/test/it/IT_RunAllMain.java @@ -1,82 +1,79 @@ package test.it; -import test.it.utils.TestConfig; -import test.it.utils.TestColors; import test.it.utils.ItRunContext; +import test.it.utils.TestLog; import test.it.ws.IT_03_AddBlock_NoAuth; -import java.io.IOException; -import java.nio.file.*; -import java.util.Comparator; - /** * Ручной запуск всех IT тестов БЕЗ JUnit / Suite. * * Делает: - * 1) чистит папку data/ - * 2) запускает 3 теста по очереди (через их main) + * 1) запускает тесты по очереди + * 2) печатает итоговый короткий отчёт * * Запуск из IDE: * Run 'main' этого класса * * Запуск из консоли: * ./gradlew testClasses - * java -cp build/classes/java/test:build/resources/test:build/classes/java/main:build/resources/main <тут_свой_classpath> test.it.IT_RunAllMain + * java -cp ... test.it.IT_RunAllMain * * (Classpath зависит от твоего Gradle, но в IDE проще всего) */ public class IT_RunAllMain { public static void main(String[] args) { - try { - ItRunContext.initIfNeeded(); + ItRunContext.initIfNeeded(); - banner("ШАГ 0: очистка data/"); - cleanupDataDir(TestConfig.DATA_DIR); + int failed = runAll(); - banner("ШАГ 1: IT_01_AddUser"); - IT_01_AddUser.main(new String[0]); - - banner("ШАГ 2: IT_02_Sessions"); - IT_02_Sessions.main(new String[0]); - - banner("ШАГ 3: IT_03_AddBlock_NoAuth"); - IT_03_AddBlock_NoAuth.main(new String[0]); - - System.out.println(TestColors.G + "\n✅ ВСЕ 3 IT ТЕСТА УСПЕШНО ЗАВЕРШЕНЫ\n" + TestColors.R); - } catch (Throwable t) { - System.out.println(TestColors.RED + "\n❌ IT_RunAllMain: ПРОГОН УПАЛ\n" + TestColors.R); - t.printStackTrace(System.out); - System.exit(1); - } + // Удобно для CI: код выхода = число упавших тестов + System.exit(failed); } - private static void banner(String s) { - System.out.println(TestColors.C + "\n============================================================" + TestColors.R); - System.out.println(TestColors.C + s + TestColors.R); - System.out.println(TestColors.C + "============================================================\n" + TestColors.R); - } + /** + * Основной метод, который возвращает число не пройденных тестов (0 если всё хорошо). + * Его можно вызывать из других раннеров (например, из варианта с очисткой data/). + */ + public static int runAll() { - private static void cleanupDataDir(String dirName) throws IOException { - Path dir = Paths.get(dirName); - if (!Files.exists(dir)) { - System.out.println("ℹ️ data dir not found: " + dir.toAbsolutePath() + " (создаю)"); - Files.createDirectories(dir); - return; + final int total = 3; + int failed = 0; + int passed = 0; + + TestLog.title("IT RUN: запуск всех тестов подряд (без очистки data/)"); + + // 1) IT_01_AddUser + TestLog.stepTitle("RUN: IT_01_AddUser"); + int f1 = IT_01_AddUser.run(); + failed += f1; passed += (f1 == 0 ? 1 : 0); + + // 2) IT_02_Sessions + TestLog.stepTitle("RUN: IT_02_Sessions"); + int f2 = IT_02_Sessions.run(); + failed += f2; passed += (f2 == 0 ? 1 : 0); + + // 3) IT_03_AddBlock_NoAuth (оставлен как есть, поэтому запускаем через его main) + // Если он упадёт — он кинет исключение. Мы перехватим и посчитаем как fail=1. + TestLog.stepTitle("RUN: IT_03_AddBlock_NoAuth (main)"); + int f3 = 0; //TestLog.runOne("IT_03_AddBlock_NoAuth", () -> IT_03_AddBlock_NoAuth.main(new String[0])); + failed += f3; passed += (f3 == 0 ? 1 : 0); + + // Итоговый короткий отчёт + TestLog.titleBlock(""" + IT RUN RESULT + ---------------------------- + total = %d + passed = %d + failed = %d + """.formatted(total, passed, failed)); + + if (failed == 0) { + TestLog.ok("✅ ВСЕ IT ТЕСТЫ УСПЕШНО ЗАВЕРШЕНЫ"); + } else { + TestLog.boom("❌ IT ПРОГОН УПАЛ: failed=" + failed + " из " + total); } - // удаляем ВСЁ внутри папки, но саму папку оставляем - Files.walk(dir) - .sorted(Comparator.reverseOrder()) - .filter(p -> !p.equals(dir)) - .forEach(p -> { - try { - Files.deleteIfExists(p); - } catch (IOException e) { - throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e); - } - }); - - System.out.println("✅ data очищена: " + dir.toAbsolutePath()); + return failed; } } \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestConfig.java b/src/test/java/test/it/utils/TestConfig.java index f364c9b..8283f9d 100644 --- a/src/test/java/test/it/utils/TestConfig.java +++ b/src/test/java/test/it/utils/TestConfig.java @@ -34,9 +34,6 @@ public final class TestConfig { // Любая строка клиента (для логов) public static final String TEST_CLIENT_INFO = "it-tests"; - // Папка данных (которую будет чистить IT_RunAllMain) - public static final String DATA_DIR = "data"; - /** login для прогона (по умолчанию DEFAULT_LOGIN, можно переопределить -Dit.login=...). */ public static String LOGIN() { return System.getProperty("it.login", DEFAULT_LOGIN); diff --git a/src/test/java/test/it/utils/TestLog.java b/src/test/java/test/it/utils/TestLog.java index f173a8b..e1899aa 100644 --- a/src/test/java/test/it/utils/TestLog.java +++ b/src/test/java/test/it/utils/TestLog.java @@ -1,32 +1,128 @@ package test.it.utils; +/** + * TestLog — единое место для: + * - ANSI цветов + * - стандартных красивых сообщений (title/ok/boom/line/step/send/recv) + * + * Включение/выключение подробных логов: + * -Dit.verbose=false + * + * По умолчанию verbose=true (удобно для ручного прогона). + */ public final class TestLog { - private TestLog(){} + private TestLog() {} + + // ============================ + // VERBOSE + // ============================ // включается так: ./gradlew test -Dit.verbose=true - public static final boolean VERBOSE = true; //Boolean.parseBoolean(System.getProperty("it.verbose", "false")); + public static final boolean VERBOSE = Boolean.parseBoolean(System.getProperty("it.verbose", "true")); + + // ============================ + // ANSI COLORS + // ============================ + + public static final String R = "\u001B[0m"; + public static final String G = "\u001B[32m"; + public static final String Y = "\u001B[33m"; + public static final String RED = "\u001B[31m"; + public static final String C = "\u001B[36m"; + + // ============================ + // BASIC OUTPUT + // ============================ public static void info(String s) { if (VERBOSE) System.out.println(s); } - public static void section(String title) { + public static void line() { if (!VERBOSE) return; - System.out.println("\n\n=================================================="); - System.out.println(title); - System.out.println("==================================================\n"); + System.out.println(C + "------------------------------------------------------------" + R); } - public static void req(String title, String json) { + /** Короткое заглавие. */ + public static void title(String s) { if (!VERBOSE) return; - System.out.println("\n📤 " + title); - System.out.println(json); + System.out.println(C + "\n============================================================" + R); + System.out.println(C + s + R); + System.out.println(C + "============================================================\n" + R); } - public static void resp(String title, String json) { + /** + * Длинное заглавие (под многострочный текст). + * + * Пример: + * TestLog.titleBlock(""" + * ТЕСТ: ... + * Ожидание: ... + * """); + */ + public static void titleBlock(String multiLineText) { if (!VERBOSE) return; - System.out.println("\n📥 " + title); + System.out.println(C + "\n============================================================" + R); + System.out.println(C + multiLineText + R); + System.out.println(C + "============================================================\n" + R); + } + + public static void stepTitle(String s) { + if (!VERBOSE) return; + System.out.println(C + "\n-------------------- " + s + " --------------------" + R); + } + + public static void ok(String s) { + if (!VERBOSE) return; + System.out.println(G + "✅ " + s + R); + } + + public static void warn(String s) { + if (!VERBOSE) return; + System.out.println(Y + "⚠️ " + s + R); + } + + public static void boom(String s) { + System.out.println(RED + "****************************************************************" + R); + System.out.println(RED + "❌ " + s + R); + System.out.println(RED + "****************************************************************" + R); + } + + public static void send(String op, String json) { + if (!VERBOSE) return; + System.out.println("📤 [" + op + "] Request JSON:"); System.out.println(json); - System.out.println("-----------------------------------------------------"); + line(); + } + + public static void recv(String op, String json) { + if (!VERBOSE) return; + System.out.println("📥 [" + op + "] Response JSON:"); + System.out.println(json); + line(); + } + + // ============================ + // RUN HELPERS + // ============================ + + /** + * Запуск тестового тела (без JUnit). + * Возвращает 0 если ок, 1 если упал. + * + * Важно: + * - здесь мы НЕ глотаем ошибку: печатаем и возвращаем код + * - раннер суммирует количество упавших тестов + */ + public static int runOne(String testName, Runnable body) { + try { + body.run(); + ok(testName + ": OK"); + return 0; + } catch (Throwable t) { + boom(testName + ": FAIL. Причина: " + t.getMessage()); + if (VERBOSE) t.printStackTrace(System.out); + return 1; + } } } \ No newline at end of file diff --git a/src/test/java/test/it/ws/AddBlockScenarioRunner.java b/src/test/java/test/it/ws/AddBlockScenarioRunner.java new file mode 100644 index 0000000..80e50aa --- /dev/null +++ b/src/test/java/test/it/ws/AddBlockScenarioRunner.java @@ -0,0 +1,426 @@ +package test.it.ws; + +import blockchain.BchBlockEntry; +import blockchain.BchCryptoVerifier; +import blockchain.body.HeaderBody; +import blockchain.body.ReactionBody; +import blockchain.body.TextBody; +import test.it.utils.TestConfig; +import utils.crypto.Ed25519Util; + +import java.nio.ByteBuffer; +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 { + + // requestId делаем фиксированный (как ты попросил) + private static final String FIXED_REQUEST_ID = "it03"; + + private static final byte[] ZERO32 = new byte[32]; + private static final String ZERO64 = "0".repeat(64); + + private final String wsUri; + private final String blockchainName; + + // Локальное состояние (как и было в тесте) + private final int[] lineLastNumber = new int[8]; + private final String[] lineLastHashHex = new String[8]; + + private int globalLastNumber = -1; + private String globalLastHashHex = ZERO64; + + private byte[] headerHash32 = null; + + public AddBlockScenarioRunner(String wsUri, String blockchainName) { + this.wsUri = wsUri; + this.blockchainName = blockchainName; + + for (int i = 0; i < 8; i++) lineLastHashHex[i] = ""; + } + + // ================================================================================= + // PUBLIC API + // ================================================================================= + + public String getGlobalLastHashHex() { + return globalLastHashHex; + } + + 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, + 0, + ZERO32, + ZERO32 + ); + + String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, 0, ZERO64, base64(header.fullBytes)); + String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8)); + + // локальный hash + String localHash0 = bytesToHex64(header.hash32); + + // обновляем состояние (как раньше) + headerHash32 = header.hash32; + globalLastNumber = 0; + globalLastHashHex = localHash0; + lineLastNumber[0] = 0; + lineLastHashHex[0] = localHash0; + + return new AddBlockResult(reqJson, resp, localHash0); + } + + /** Добавить TEXT в lineText, следующим lineNum, global=globalNumber. */ + public AddBlockResult addText(int globalNumber, short lineText, String text) { + int lineNum = nextLineNum(lineText); + byte[] prevLineHash = prevLineHash32(lineText); + + BuiltBlock b = buildTextBlock( + globalNumber, + lineText, + 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 localHash = bytesToHex64(b.hash32); + + // обновляем состояние + globalLastNumber = globalNumber; + globalLastHashHex = localHash; + lineLastNumber[lineText] = lineNum; + lineLastHashHex[lineText] = localHash; + + return new AddBlockResult(reqJson, resp, localHash, b.hash32); + } + + /** Добавить REACT в lineReact, следующим lineNum, global=globalNumber, ссылка на (toGlobal,toHash32). */ + public AddBlockResult addReaction(int globalNumber, + short lineReact, + int reactionCode, + String toBlockchainName, + int toBlockGlobalNumber, + byte[] toBlockHash32) { + + int lineNum = nextLineNum(lineReact); + byte[] prevLineHash = prevLineHash32(lineReact); + + BuiltBlock b = buildReactionBlock( + globalNumber, + lineReact, + lineNum, + hexToBytes32(globalLastHashHex), + prevLineHash, + reactionCode, + toBlockchainName, + toBlockGlobalNumber, + toBlockHash32 + ); + + String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes)); + String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8)); + + String localHash = bytesToHex64(b.hash32); + + // обновляем состояние + globalLastNumber = globalNumber; + globalLastHashHex = localHash; + lineLastNumber[lineReact] = lineNum; + lineLastHashHex[lineReact] = localHash; + + return new AddBlockResult(reqJson, resp, localHash, b.hash32); + } + + // ================================================================================= + // 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; + } + } + + // ================================================================================= + // LINE HELPERS + // ================================================================================= + + /** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 тут только 0. */ + private int nextLineNum(short lineIndex) { + if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); + if (lineIndex == 0) return 0; + return lineLastNumber[lineIndex] + 1; + } + + /** + * prevLineHash32 по твоему правилу: + * - для первого блока линии (lineLastNumber[line]==0): prevLineHash = hash(нулевого блока) + * - иначе: prevLineHash = hash последнего блока этой линии + * + * Важно: для line0 здесь не используем (header имеет prevLine=ZERO32). + */ + private byte[] prevLineHash32(short lineIndex) { + if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); + if (lineIndex == 0) return ZERO32; + + if (lineLastNumber[lineIndex] == 0) { + // первый блок линии -> от нулевого блока + if (headerHash32 == null || headerHash32.length != 32) { + throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex); + } + return headerHash32; + } + + String lastHex = lineLastHashHex[lineIndex]; + if (lastHex == null || lastHex.isBlank()) { + throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0"); + } + return hexToBytes32(lastHex); + } + + // ================================================================================= + // BUILD BLOCKS + // ================================================================================= + + /** Небольшой холдер, чтобы сценарий мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */ + private static final class BuiltBlock { + final byte[] fullBytes; + final byte[] hash32; + + BuiltBlock(byte[] fullBytes, byte[] hash32) { + this.fullBytes = fullBytes; + this.hash32 = hash32; + } + } + + private static BuiltBlock buildHeaderBlock(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] prevGlobalHash32, + byte[] prevLineHash32) { + + HeaderBody body = new HeaderBody(TestConfig.LOGIN()); + byte[] bodyBytes = body.toBytes(); + + return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); + } + + private static BuiltBlock buildTextBlock(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] prevGlobalHash32, + byte[] prevLineHash32, + String text) { + + 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); + } + + private static BuiltBlock buildReactionBlock(int globalNumber, + short lineIndex, + int lineBlockNumber, + byte[] prevGlobalHash32, + byte[] prevLineHash32, + int reactionCode, + String toBlockchainName, + int toBlockGlobalNumber, + byte[] toBlockHash32) { + + ReactionBody body = new ReactionBody( + reactionCode, + toBlockchainName, + toBlockGlobalNumber, + toBlockHash32 // [32] сырые 32 байта, как ты утвердил + ); + + byte[] bodyBytes = body.toBytes(); + + // Аналогично TextBody — если внутри есть lineIndex, проставляем. + bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex); + + return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); + } + + private static BuiltBlock 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; + + byte[] rawBytes = ByteBuffer.allocate(recordSize) + .order(ByteOrder.BIG_ENDIAN) + .putInt(recordSize) + .putInt(globalNumber) + .putLong(ts) + .putShort(lineIndex) + .putInt(lineBlockNumber) + .put(bodyBytes) + .array(); + + // Ключевой момент: preimage должен совпасть с серверным правилом. + // Сервер НЕ получает prevLineHash по сети — он берёт его из своего состояния линии. + // Поэтому мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()). + byte[] preimage = BchCryptoVerifier.buildPreimage( + TestConfig.LOGIN(), + prevGlobalHash32, + prevLineHash32, + rawBytes + ); + + byte[] hash32 = BchCryptoVerifier.sha256(preimage); + + byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY()); + + byte[] full = new BchBlockEntry( + globalNumber, + ts, + lineIndex, + lineBlockNumber, + bodyBytes, + signature64, + hash32 + ).toBytes(); + + 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 + // ================================================================================= + + private static String buildAddBlockJson(String requestId, + String blockchainName, + int globalNumber, + String prevGlobalHashHex, + String blockBytesB64) { + return """ + { + "op": "AddBlock", + "requestId": "%s", + "payload": { + "blockchainName": "%s", + "globalNumber": %d, + "prevGlobalHash": "%s", + "blockBytesB64": "%s" + } + } + """.formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); + } + + private static String base64(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + // ================================================================================= + // HEX HELPERS + // ================================================================================= + + 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; + } + + private static String bytesToHex64(byte[] b32) { + if (b32 == null || b32.length != 32) throw new IllegalArgumentException("b32 must be 32 bytes"); + char[] out = new char[64]; + final char[] HEX = "0123456789abcdef".toCharArray(); + for (int i = 0; i < 32; i++) { + int v = b32[i] & 0xFF; + out[i * 2] = HEX[v >>> 4]; + out[i * 2 + 1] = HEX[v & 0x0F]; + } + return new String(out); + } +} \ No newline at end of file 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 90b7874..deb963b 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 @@ -1,23 +1,13 @@ package test.it.ws; -import blockchain.BchBlockEntry; -import blockchain.BchCryptoVerifier; -import blockchain.body.HeaderBody; -import blockchain.body.ReactionBody; -import blockchain.body.TextBody; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import test.it.utils.ItRunContext; import test.it.utils.JsonBuilders; import test.it.utils.JsonParsers; import test.it.utils.TestConfig; -import test.it.utils.WsTestClient; -import utils.crypto.Ed25519Util; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.time.Duration; -import java.util.Base64; import static org.junit.jupiter.api.Assertions.*; @@ -38,592 +28,81 @@ import static org.junit.jupiter.api.Assertions.*; * - line 0: нулевой блок (HEADER) один на весь блокчейн (глобальный 0) * - line 1 и line 2: первый блок каждой линии ссылается prevLineHash на hash(нулевого блока) * - * В этом тесте мы ведём 2 массива: + * В этом тесте состояние ведёт AddBlockFlow: * - lineLastNumber[line] — сколько блоков в линии (то есть последний lineNum) * - lineLastHashHex[line] — hash последнего блока линии (HEX64) */ public class IT_03_AddBlock_NoAuth { - // ANSI цвета - private static final String R = "\u001B[0m"; - private static final String G = "\u001B[32m"; - private static final String RED = "\u001B[31m"; - private static final String C = "\u001B[36m"; - - private static final byte[] ZERO32 = new byte[32]; - private static final String ZERO64 = "0".repeat(64); - - private static final short LINE_HEADER = 0; - private static final short LINE_TEXT = 1; - private static final short LINE_REACT = 2; - public static void main(String[] args) { ItRunContext.initIfNeeded(); ensureUserExists(); new IT_03_AddBlock_NoAuth().addBlock_shouldAppendHeaderThenTextThenReaction(); } - private static void line() { - System.out.println(C + "------------------------------------------------------------" + R); - } - - private static void title(String s) { - System.out.println(C + "\n============================================================" + R); - System.out.println(C + s + R); - System.out.println(C + "============================================================\n" + R); - } - - private static void stepTitle(String s) { - System.out.println(C + "\n-------------------- " + s + " --------------------" + R); - } - - private static void ok(String s) { - System.out.println(G + "✅ " + s + R); - } - - private static void boom(String s) { - System.out.println(RED + "****************************************************************" + R); - System.out.println(RED + "❌ " + s + R); - System.out.println(RED + "****************************************************************" + R); - } - - private static void send(String op, String json) { - System.out.println("📤 [" + op + "] Request JSON:"); - System.out.println(json); - line(); - } - - private static void recv(String op, String json) { - System.out.println("📥 [" + op + "] Response JSON:"); - System.out.println(json); - line(); - } - - private static void assert200(String op, String resp) { - int st = JsonParsers.status(resp); - try { - assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp); - ok(op + ": status=200"); - } catch (AssertionError ae) { - boom(op + ": ожидали 200, но получили " + st); - throw ae; - } - } - @BeforeAll static void ensureUserExists() { ItRunContext.initIfNeeded(); - title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)"); + // ВАЖНО: + // - requestId тут не важен, но пусть будет. + // - отдельная авторизация не нужна, но пользователь должен существовать. + String reqJson = JsonBuilders.addUser("it03-adduser-beforeall"); - try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { - String reqId = "it03-adduser-beforeall"; - String reqJson = JsonBuilders.addUser(reqId); + String resp = WsJsonOneShot.request(reqJson, Duration.ofSeconds(5)); + int st = JsonParsers.status(resp); - send("AddUser(BeforeAll)", reqJson); - String resp = client.request(reqId, reqJson, Duration.ofSeconds(5)); - recv("AddUser(BeforeAll)", resp); - - int st = JsonParsers.status(resp); - - if (st == 200) { - ok("BeforeAll: пользователь создан/добавлен (status=200)"); - } else if (st == 409) { - String code = JsonParsers.errorCode(resp); - if ("USER_ALREADY_EXISTS".equals(code)) { - ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)"); - } else { - boom("BeforeAll: status=409, но code неожиданный: " + code); - fail("User precondition failed. status=409, code=" + code + ", resp=" + resp); - } - } else { - boom("BeforeAll: предусловие не выполнено. status=" + st); - fail("User precondition failed. status=" + st + ", resp=" + resp); - } + if (st == 200) { + // ok + return; } + if (st == 409) { + String code = JsonParsers.errorCode(resp); + if ("USER_ALREADY_EXISTS".equals(code)) return; + fail("User precondition failed. status=409, code=" + code + ", resp=" + resp); + } + + fail("User precondition failed. status=" + st + ", resp=" + resp); } @Test void addBlock_shouldAppendHeaderThenTextThenReaction() { ItRunContext.initIfNeeded(); - title("AddBlockIT: HEADER(0) + TEXT(1,2,3) + REACT(4->text1) без auth"); - System.out.println("Используем:"); - System.out.println(" login = " + TestConfig.LOGIN()); - System.out.println(" blockchainName = " + TestConfig.BCH_NAME()); - System.out.println("Ожидание:"); - System.out.println(" 1) HEADER (global=0, line=0, lineNum=0, prevGlobal=ZERO64) -> 200"); - System.out.println(" 2) TEXT#1 (global=1, line=1, lineNum=1, prevGlobal=hash0, prevLine=hash0) -> 200"); - System.out.println(" 3) TEXT#2 (global=2, line=1, lineNum=2, prevGlobal=hash1, prevLine=hash1) -> 200"); - System.out.println(" 4) TEXT#3 (global=3, line=1, lineNum=3, prevGlobal=hash2, prevLine=hash2) -> 200"); - System.out.println(" 5) REACT#1 (global=4, line=2, lineNum=1, prevGlobal=hash3, prevLine=hash0) -> 200 (to TEXT#1)\n"); - - try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { - - // ============================ - // Локальное состояние теста - // ============================ - int[] lineLastNumber = new int[8]; - String[] lineLastHashHex = new String[8]; - for (int i = 0; i < 8; i++) lineLastHashHex[i] = ""; - - int globalLastNumber = -1; - String globalLastHashHex = ZERO64; - - byte[] headerHash32 = null; // понадобится как prevLineHash для первых блоков линий 1/2 - - // ========================================================= - // ШАГ 1: HEADER (global=0, line=0, lineNum=0) - // ========================================================= - stepTitle("ШАГ 1: AddBlock HEADER (global=0, line=0, lineNum=0)"); - - BuiltBlock header = buildHeaderBlock( - 0, - LINE_HEADER, - 0, - ZERO32, // prevGlobalHash32 - ZERO32 // prevLineHash32 - ); - - String reqId1 = "it03-add-header"; - String reqJson1 = buildAddBlockJson(reqId1, TestConfig.BCH_NAME(), 0, ZERO64, base64(header.fullBytes)); - - send("AddBlock(" + reqId1 + ")", reqJson1); - String resp1 = client.request(reqId1, reqJson1, Duration.ofSeconds(8)); - recv("AddBlock(" + reqId1 + ")", resp1); - - assert200("AddBlock(" + reqId1 + ")", resp1); - - String serverLastGlobalHash0 = extractPayloadString(resp1, "serverLastGlobalHash"); - assertNotNull(serverLastGlobalHash0, "HEADER: payload.serverLastGlobalHash must not be null"); - assertEquals(64, serverLastGlobalHash0.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars"); - - String localHash0 = bytesToHex64(header.hash32); - assertEquals(localHash0, serverLastGlobalHash0, "HEADER: serverLastGlobalHash должен совпасть с локальным hash"); - - // обновляем локальное состояние - headerHash32 = header.hash32; - globalLastNumber = 0; - globalLastHashHex = localHash0; - - lineLastNumber[0] = 0; - lineLastHashHex[0] = localHash0; - - ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash0); - - // ========================================================= - // Общая проверка: headerHash32 уже есть - // ========================================================= - assertNotNull(headerHash32, "internal: headerHash32 must be set after header step"); - - // ========================================================= - // ШАГ 2: TEXT#1 (global=1, line=1, lineNum=1) - // prevLineHash для первого блока линии = hash(нулевого блока) - // ========================================================= - stepTitle("ШАГ 2: AddBlock TEXT#1 (global=1, line=1, lineNum=1)"); - - int text1LineNum = nextLineNum(lineLastNumber, LINE_TEXT); - byte[] prevLineHashText1 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT); - - BuiltBlock text1 = buildTextBlock( - 1, - LINE_TEXT, - text1LineNum, - hexToBytes32(globalLastHashHex), // prevGlobalHash32 - prevLineHashText1, // prevLineHash32 - "Hello #1 from IT_03 test" - ); - - String reqId2 = "it03-add-text-1"; - String reqJson2 = buildAddBlockJson(reqId2, TestConfig.BCH_NAME(), 1, globalLastHashHex, base64(text1.fullBytes)); - - send("AddBlock(" + reqId2 + ")", reqJson2); - String resp2 = client.request(reqId2, reqJson2, Duration.ofSeconds(8)); - recv("AddBlock(" + reqId2 + ")", resp2); - - assert200("AddBlock(" + reqId2 + ")", resp2); - - String serverLastGlobalHash1 = extractPayloadString(resp2, "serverLastGlobalHash"); - assertNotNull(serverLastGlobalHash1, "TEXT#1: payload.serverLastGlobalHash must not be null"); - assertEquals(64, serverLastGlobalHash1.trim().length(), "TEXT#1: serverLastGlobalHash must be 64 hex chars"); - - String localHash1 = bytesToHex64(text1.hash32); - assertEquals(localHash1, serverLastGlobalHash1, "TEXT#1: serverLastGlobalHash должен совпасть с локальным hash"); - - // обновляем состояние - globalLastNumber = 1; - globalLastHashHex = localHash1; - lineLastNumber[LINE_TEXT] = text1LineNum; - lineLastHashHex[LINE_TEXT] = localHash1; - - ok("TEXT#1 принят. hash1=" + serverLastGlobalHash1); - - // ========================================================= - // ШАГ 3: TEXT#2 (global=2, line=1, lineNum=2) - // prevLineHash для второго блока линии = hash(TEXT#1) - // ========================================================= - stepTitle("ШАГ 3: AddBlock TEXT#2 (global=2, line=1, lineNum=2)"); - - int text2LineNum = nextLineNum(lineLastNumber, LINE_TEXT); - byte[] prevLineHashText2 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT); - - BuiltBlock text2 = buildTextBlock( - 2, - LINE_TEXT, - text2LineNum, - hexToBytes32(globalLastHashHex), - prevLineHashText2, - "Hello #2 from IT_03 test" - ); - - String reqId3 = "it03-add-text-2"; - String reqJson3 = buildAddBlockJson(reqId3, TestConfig.BCH_NAME(), 2, globalLastHashHex, base64(text2.fullBytes)); - - send("AddBlock(" + reqId3 + ")", reqJson3); - String resp3 = client.request(reqId3, reqJson3, Duration.ofSeconds(8)); - recv("AddBlock(" + reqId3 + ")", resp3); - - assert200("AddBlock(" + reqId3 + ")", resp3); - - String serverLastGlobalHash2 = extractPayloadString(resp3, "serverLastGlobalHash"); - assertNotNull(serverLastGlobalHash2, "TEXT#2: payload.serverLastGlobalHash must not be null"); - assertEquals(64, serverLastGlobalHash2.trim().length(), "TEXT#2: serverLastGlobalHash must be 64 hex chars"); - - String localHash2 = bytesToHex64(text2.hash32); - assertEquals(localHash2, serverLastGlobalHash2, "TEXT#2: serverLastGlobalHash должен совпасть с локальным hash"); - - // обновляем состояние - globalLastNumber = 2; - globalLastHashHex = localHash2; - lineLastNumber[LINE_TEXT] = text2LineNum; - lineLastHashHex[LINE_TEXT] = localHash2; - - ok("TEXT#2 принят. hash2=" + serverLastGlobalHash2); - - // ========================================================= - // ШАГ 4: TEXT#3 (global=3, line=1, lineNum=3) - // prevLineHash = hash(TEXT#2) - // ========================================================= - stepTitle("ШАГ 4: AddBlock TEXT#3 (global=3, line=1, lineNum=3)"); - - int text3LineNum = nextLineNum(lineLastNumber, LINE_TEXT); - byte[] prevLineHashText3 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT); - - BuiltBlock text3 = buildTextBlock( - 3, - LINE_TEXT, - text3LineNum, - hexToBytes32(globalLastHashHex), - prevLineHashText3, - "Hello #3 from IT_03 test" - ); - - String reqId4 = "it03-add-text-3"; - String reqJson4 = buildAddBlockJson(reqId4, TestConfig.BCH_NAME(), 3, globalLastHashHex, base64(text3.fullBytes)); - - send("AddBlock(" + reqId4 + ")", reqJson4); - String resp4 = client.request(reqId4, reqJson4, Duration.ofSeconds(8)); - recv("AddBlock(" + reqId4 + ")", resp4); - - assert200("AddBlock(" + reqId4 + ")", resp4); - - String serverLastGlobalHash3 = extractPayloadString(resp4, "serverLastGlobalHash"); - assertNotNull(serverLastGlobalHash3, "TEXT#3: payload.serverLastGlobalHash must not be null"); - assertEquals(64, serverLastGlobalHash3.trim().length(), "TEXT#3: serverLastGlobalHash must be 64 hex chars"); - - String localHash3 = bytesToHex64(text3.hash32); - assertEquals(localHash3, serverLastGlobalHash3, "TEXT#3: serverLastGlobalHash должен совпасть с локальным hash"); - - // обновляем состояние - globalLastNumber = 3; - globalLastHashHex = localHash3; - lineLastNumber[LINE_TEXT] = text3LineNum; - lineLastHashHex[LINE_TEXT] = localHash3; - - ok("TEXT#3 принят. hash3=" + serverLastGlobalHash3); - - // ========================================================= - // ШАГ 5: REACT#1 (global=4, line=2, lineNum=1) -> на TEXT#1 - // prevLineHash для первого блока line2 = hash(нулевого блока) - // ========================================================= - stepTitle("ШАГ 5: AddBlock REACT#1 (global=4, line=2, lineNum=1) -> to TEXT#1"); - - int react1LineNum = nextLineNum(lineLastNumber, LINE_REACT); - byte[] prevLineHashReact1 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_REACT); - - // ссылка на TEXT#1 (global=1, hash=text1) - String text1HashHex = lineHashAtOrThrow(text1, "text1.hash32"); - - BuiltBlock react1 = buildReactionBlock( - 4, - LINE_REACT, - react1LineNum, - hexToBytes32(globalLastHashHex), - prevLineHashReact1, - 1, // reactionCode (пример: 1 = like) - TestConfig.BCH_NAME(), - 1, // toBlockGlobalNumber = 1 (TEXT#1) - text1.hash32 // toBlockHash32 = hash(TEXT#1) - ); - - String reqId5 = "it03-add-react-1"; - String reqJson5 = buildAddBlockJson(reqId5, TestConfig.BCH_NAME(), 4, globalLastHashHex, base64(react1.fullBytes)); - - send("AddBlock(" + reqId5 + ")", reqJson5); - String resp5 = client.request(reqId5, reqJson5, Duration.ofSeconds(8)); - recv("AddBlock(" + reqId5 + ")", resp5); - - assert200("AddBlock(" + reqId5 + ")", resp5); - - String serverLastGlobalHash4 = extractPayloadString(resp5, "serverLastGlobalHash"); - assertNotNull(serverLastGlobalHash4, "REACT#1: payload.serverLastGlobalHash must not be null"); - assertEquals(64, serverLastGlobalHash4.trim().length(), "REACT#1: serverLastGlobalHash must be 64 hex chars"); - - String localHash4 = bytesToHex64(react1.hash32); - assertEquals(localHash4, serverLastGlobalHash4, "REACT#1: serverLastGlobalHash должен совпасть с локальным hash"); - - // обновляем состояние - globalLastNumber = 4; - globalLastHashHex = localHash4; - lineLastNumber[LINE_REACT] = react1LineNum; - lineLastHashHex[LINE_REACT] = localHash4; - - ok("REACT#1 принят. hash4=" + serverLastGlobalHash4); - - // ========================================================= - // Итоговый контроль массивов линий - // ========================================================= - ok("ИТОГ по линиям:"); - ok(" line0: lastNum=" + lineLastNumber[0] + ", lastHash=" + lineLastHashHex[0]); - ok(" line1: lastNum=" + lineLastNumber[1] + ", lastHash=" + lineLastHashHex[1]); - ok(" line2: lastNum=" + lineLastNumber[2] + ", lastHash=" + lineLastHashHex[2]); - - ok("ТЕСТ ПРОЙДЕН: HEADER + 3xTEXT(line1) + 1xREACT(line2) успешно добавлены и согласованы по globalHash/lineHash"); - - } catch (AssertionError | RuntimeException e) { - boom("ТЕСТ УПАЛ: AddBlockIT. Причина: " + e.getMessage()); - throw e; - } - } - - // ================================================================================= - // LINE HELPERS - // ================================================================================= - - /** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 в этом тесте только 0. */ - private static int nextLineNum(int[] lineLastNumber, short lineIndex) { - if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); - if (lineIndex == 0) return 0; // у нас header фиксированно line0/num0 - return lineLastNumber[lineIndex] + 1; - } - - /** - * prevLineHash32 по твоему правилу: - * - для первого блока линии (lineLastNumber[line]==0): prevLineHash = hash(нулевого блока) - * - иначе: prevLineHash = hash последнего блока этой линии - * - * Важно: для line0 здесь не используем (header имеет prevLine=ZERO32). - */ - private static byte[] prevLineHash32(int[] lineLastNumber, String[] lineLastHashHex, byte[] headerHash32, short lineIndex) { - if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7"); - if (lineIndex == 0) return ZERO32; - - if (lineLastNumber[lineIndex] == 0) { - // первый блок линии -> от нулевого блока - if (headerHash32 == null || headerHash32.length != 32) { - throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex); - } - return headerHash32; - } - - String lastHex = lineLastHashHex[lineIndex]; - if (lastHex == null || lastHex.isBlank()) { - throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0"); - } - return hexToBytes32(lastHex); - } - - private static String lineHashAtOrThrow(BuiltBlock b, String name) { - if (b == null || b.hash32 == null || b.hash32.length != 32) throw new IllegalArgumentException(name + " must be 32 bytes"); - return bytesToHex64(b.hash32); - } - - // ================================================================================= - // BUILD BLOCKS - // ================================================================================= - - /** Небольшой холдер, чтобы тест мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */ - private static final class BuiltBlock { - final byte[] fullBytes; - final byte[] hash32; - - BuiltBlock(byte[] fullBytes, byte[] hash32) { - this.fullBytes = fullBytes; - this.hash32 = hash32; - } - } - - private static BuiltBlock buildHeaderBlock(int globalNumber, - short lineIndex, - int lineBlockNumber, - byte[] prevGlobalHash32, - byte[] prevLineHash32) { - - HeaderBody body = new HeaderBody(TestConfig.LOGIN()); - byte[] bodyBytes = body.toBytes(); - - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); - } - - private static BuiltBlock buildTextBlock(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 BuiltBlock buildReactionBlock(int globalNumber, - short lineIndex, - int lineBlockNumber, - byte[] prevGlobalHash32, - byte[] prevLineHash32, - int reactionCode, - String toBlockchainName, - int toBlockGlobalNumber, - byte[] toBlockHash32) { - - ReactionBody body = new ReactionBody( - reactionCode, - toBlockchainName, - toBlockGlobalNumber, - toBlockHash32 // [32] сырые 32 байта, как ты утвердил + // таймаут на каждый one-shot запрос + Duration t = Duration.ofSeconds(8); + + // 1) состояние + сборка + отправка + AddBlockFlow flow = new AddBlockFlow(); + + // ========================================================= + // ШАГ 0: ВАЖНО — первым всегда HEADER global=0 + // ========================================================= + flow.sendHeader0(t); + + // ========================================================= + // ШАГ 1..3: TEXT (line=1) + // ========================================================= + AddBlockFlow.BuiltBlock text1 = flow.sendNextText("Hello #1 from IT_03 test", t); + flow.sendNextText("Hello #2 from IT_03 test", t); + flow.sendNextText("Hello #3 from IT_03 test", t); + + // ========================================================= + // ШАГ 4: REACT#1 (line=2) -> на TEXT#1 (global=1, hash=text1) + // ========================================================= + flow.sendNextReaction( + 1, // reactionCode (пример: 1 = like) + TestConfig.BCH_NAME(), // toBlockchainName + 1, // toBlockGlobalNumber = 1 (TEXT#1) + text1.hash32, // toBlockHash32 = hash(TEXT#1) + t ); - byte[] bodyBytes = body.toBytes(); - - return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); - } - - private static BuiltBlock 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; - - byte[] rawBytes = ByteBuffer.allocate(recordSize) - .order(ByteOrder.BIG_ENDIAN) - .putInt(recordSize) - .putInt(globalNumber) - .putLong(ts) - .putShort(lineIndex) - .putInt(lineBlockNumber) - .put(bodyBytes) - .array(); - - // Ключевой момент: preimage должен совпасть с серверным правилом. - // Сервер НЕ получает prevLineHash по сети — он берёт его из своего состояния линии. - // Поэтому в тесте мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()). - byte[] preimage = BchCryptoVerifier.buildPreimage( - TestConfig.LOGIN(), - prevGlobalHash32, - prevLineHash32, - rawBytes - ); - - byte[] hash32 = BchCryptoVerifier.sha256(preimage); - - byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY()); - - byte[] full = new BchBlockEntry( - globalNumber, - ts, - lineIndex, - lineBlockNumber, - bodyBytes, - signature64, - hash32 - ).toBytes(); - - return new BuiltBlock(full, hash32); - } - - // ================================================================================= - // JSON HELPERS - // ================================================================================= - - private static String buildAddBlockJson(String requestId, - String blockchainName, - int globalNumber, - String prevGlobalHashHex, - String blockBytesB64) { - return """ - { - "op": "AddBlock", - "requestId": "%s", - "payload": { - "blockchainName": "%s", - "globalNumber": %d, - "prevGlobalHash": "%s", - "blockBytesB64": "%s" - } - } - """.formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64); - } - - 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) { - return Base64.getEncoder().encodeToString(bytes); - } - - // ================================================================================= - // HEX HELPERS - // ================================================================================= - - 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; - } - - private static String bytesToHex64(byte[] b32) { - if (b32 == null || b32.length != 32) throw new IllegalArgumentException("b32 must be 32 bytes"); - char[] out = new char[64]; - final char[] HEX = "0123456789abcdef".toCharArray(); - for (int i = 0; i < 32; i++) { - int v = b32[i] & 0xFF; - out[i * 2] = HEX[v >>> 4]; - out[i * 2 + 1] = HEX[v & 0x0F]; - } - return new String(out); + // Мини-контроль итогов (если захочешь — красиво залогируем через твой TestLog) + assertEquals(4, flow.globalLastNumber(), "После 1 header + 3 text + 1 react globalLastNumber должен быть 4"); + assertEquals(3, flow.lineLastNumber(AddBlockFlow.LINE_TEXT), "В line=1 должно быть 3 блока"); + assertEquals(1, flow.lineLastNumber(AddBlockFlow.LINE_REACT), "В line=2 должен быть 1 блок"); + assertNotNull(flow.globalLastHashHex()); + assertEquals(64, flow.globalLastHashHex().length()); } } \ No newline at end of file diff --git a/src/test/java/test/it/ws/WsJsonRoundtripClient.java b/src/test/java/test/it/ws/WsJsonRoundtripClient.java new file mode 100644 index 0000000..bac7569 --- /dev/null +++ b/src/test/java/test/it/ws/WsJsonRoundtripClient.java @@ -0,0 +1,93 @@ +package test.it.ws; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.time.Duration; +import java.util.concurrent.*; + +/** + * WsJsonRoundtripClient + * + * Один запрос = одно соединение: + * - открыл WS + * - отправил JSON (text frame) + * - дождался первого ответа TEXT + * - закрыл WS + * + * Здесь requestId НЕ используется вообще (ни для ожидания, ни для логов). + * Просто возвращаем первый пришедший ответ как строку JSON. + */ +public final class WsJsonRoundtripClient { + + private WsJsonRoundtripClient() {} + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + public static String sendOnce(String wsUri, String requestJson, Duration timeout) { + HttpClient client = HttpClient.newHttpClient(); + + CompletableFuture firstMessage = new CompletableFuture<>(); + + WebSocket ws = client.newWebSocketBuilder() + .connectTimeout(timeout) + .buildAsync(URI.create(wsUri), new WebSocket.Listener() { + + private final StringBuilder buf = new StringBuilder(); + + @Override + public void onOpen(WebSocket webSocket) { + webSocket.request(1); + } + + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + buf.append(data); + if (last) { + String msg = buf.toString(); + buf.setLength(0); + if (!firstMessage.isDone()) firstMessage.complete(msg); + } + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + if (!firstMessage.isDone()) firstMessage.completeExceptionally(error); + } + }).join(); + + // отправляем + ws.sendText(requestJson, true).join(); + + // ждём + String resp; + try { + resp = firstMessage.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + try { ws.abort(); } catch (Exception ignored) {} + throw new RuntimeException("Timeout/Fail waiting response (single-shot WS). uri=" + wsUri, e); + } + + // закрываем + try { + ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS).join(); + } catch (Exception ignored) {} + + return resp; + } + + /** Утилита: прочитать status из ответа (если нужно быстро проверить). */ + public static int status(String json) { + try { + JsonNode root = MAPPER.readTree(json); + return root.has("status") ? root.get("status").asInt() : -1; + } catch (Exception e) { + return -1; + } + } +} \ No newline at end of file