28 12 25
Всё ещё не работает проверка линий. Переделал первые два теста! Третий (АддБлокс) ещё не работает
This commit is contained in:
parent
795341dd8d
commit
3f374f48e1
@ -1,9 +1,9 @@
|
|||||||
import shine.db.DatabaseInitializer;
|
//import shine.db.DatabaseInitializer;
|
||||||
|
//
|
||||||
public class CreateNewDatabase {
|
//public class CreateNewDatabase {
|
||||||
|
//
|
||||||
public static void main(String[] args) {
|
// public static void main(String[] args) {
|
||||||
// Просто прокидываем управление в DatabaseInitializer
|
// // Просто прокидываем управление в DatabaseInitializer
|
||||||
DatabaseInitializer.createNewDB(args);
|
// DatabaseInitializer.createNewDB(args);
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -1,126 +1,126 @@
|
|||||||
package Test;
|
//package Test;
|
||||||
|
//
|
||||||
import java.net.URI;
|
//import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
//import java.net.http.HttpClient;
|
||||||
import java.net.http.WebSocket;
|
//import java.net.http.WebSocket;
|
||||||
import java.net.http.WebSocket.Listener;
|
//import java.net.http.WebSocket.Listener;
|
||||||
import java.util.concurrent.CompletionStage;
|
//import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.CountDownLatch;
|
//import java.util.concurrent.CountDownLatch;
|
||||||
|
//
|
||||||
public class TestJsonWsClient2 {
|
//public class TestJsonWsClient2 {
|
||||||
|
//
|
||||||
public static void main(String[] args) throws Exception {
|
// public static void main(String[] args) throws Exception {
|
||||||
String uri = "ws://localhost:7070/ws";
|
// String uri = "ws://localhost:7070/ws";
|
||||||
|
//
|
||||||
String jsonRequestRefreshSession = """
|
// String jsonRequestRefreshSession = """
|
||||||
{
|
// {
|
||||||
"op": "RefreshSession",
|
// "op": "RefreshSession",
|
||||||
"requestId": "test-1",
|
// "requestId": "test-1",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"sessionId": 123,
|
// "sessionId": 123,
|
||||||
"sessionPwd": "test-password"
|
// "sessionPwd": "test-password"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""";
|
// """;
|
||||||
|
//
|
||||||
String jsonRequestAddUser = """
|
// String jsonRequestAddUser = """
|
||||||
{
|
// {
|
||||||
"op": "AddUser",
|
// "op": "AddUser",
|
||||||
"requestId": "test-add-1",
|
// "requestId": "test-add-1",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"login": "anya1111",
|
// "login": "anya1111",
|
||||||
"loginId": 100211,
|
// "loginId": 100211,
|
||||||
"bchId": 4222,
|
// "bchId": 4222,
|
||||||
"pubkey0": "PUB0",
|
// "pubkey0": "PUB0",
|
||||||
"pubkey1": "PUB1",
|
// "pubkey1": "PUB1",
|
||||||
"bchLimit": 1000000
|
// "bchLimit": 1000000
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""";
|
// """;
|
||||||
|
//
|
||||||
String jsonRequestAuthChallenge = """
|
// String jsonRequestAuthChallenge = """
|
||||||
{
|
// {
|
||||||
"op": "AuthChallenge",
|
// "op": "AuthChallenge",
|
||||||
"requestId": "test-auth-1",
|
// "requestId": "test-auth-1",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"login": "anya1111"
|
// "login": "anya1111"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""";
|
// """;
|
||||||
|
//
|
||||||
// Что тестируем сейчас:
|
// // Что тестируем сейчас:
|
||||||
String jsonRequest = jsonRequestAuthChallenge;
|
// String jsonRequest = jsonRequestAuthChallenge;
|
||||||
// String jsonRequest = jsonRequestRefreshSession;
|
//// String jsonRequest = jsonRequestRefreshSession;
|
||||||
// String jsonRequest = jsonRequestAddUser;
|
//// String jsonRequest = jsonRequestAddUser;
|
||||||
|
//
|
||||||
System.out.println("Подключаемся к " + uri);
|
// System.out.println("Подключаемся к " + uri);
|
||||||
|
//
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
// CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
//
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
// HttpClient client = HttpClient.newHttpClient();
|
||||||
|
//
|
||||||
WebSocket webSocket = client.newWebSocketBuilder()
|
// WebSocket webSocket = client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(uri), new Listener() {
|
// .buildAsync(URI.create(uri), new Listener() {
|
||||||
|
//
|
||||||
// 0 — ещё ничего не получили
|
// // 0 — ещё ничего не получили
|
||||||
// 1 — получили 1-й ответ, отправили повторно
|
// // 1 — получили 1-й ответ, отправили повторно
|
||||||
// 2 — получили 2-й ответ, закрываемся
|
// // 2 — получили 2-й ответ, закрываемся
|
||||||
private int responsesCount = 0;
|
// private int responsesCount = 0;
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onOpen(WebSocket webSocket) {
|
// public void onOpen(WebSocket webSocket) {
|
||||||
System.out.println("✅ WebSocket подключен");
|
// System.out.println("✅ WebSocket подключен");
|
||||||
|
//
|
||||||
System.out.println("📤 Отправляем JSON-запрос (1 раз):");
|
// System.out.println("📤 Отправляем JSON-запрос (1 раз):");
|
||||||
System.out.println(jsonRequest);
|
// System.out.println(jsonRequest);
|
||||||
|
//
|
||||||
webSocket.sendText(jsonRequest, true);
|
// webSocket.sendText(jsonRequest, true);
|
||||||
Listener.super.onOpen(webSocket);
|
// Listener.super.onOpen(webSocket);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onText(WebSocket webSocket,
|
// public CompletionStage<?> onText(WebSocket webSocket,
|
||||||
CharSequence data,
|
// CharSequence data,
|
||||||
boolean last) {
|
// boolean last) {
|
||||||
String message = data.toString();
|
// String message = data.toString();
|
||||||
responsesCount++;
|
// responsesCount++;
|
||||||
|
//
|
||||||
System.out.println("📥 Получен TEXT-ответ #" + responsesCount + " от сервера:");
|
// System.out.println("📥 Получен TEXT-ответ #" + responsesCount + " от сервера:");
|
||||||
System.out.println(message);
|
// System.out.println(message);
|
||||||
|
//
|
||||||
if (responsesCount == 1) {
|
// if (responsesCount == 1) {
|
||||||
// После первого ответа — отправляем тот же запрос ещё раз
|
// // После первого ответа — отправляем тот же запрос ещё раз
|
||||||
System.out.println("📤 Отправляем JSON-запрос второй раз:");
|
// System.out.println("📤 Отправляем JSON-запрос второй раз:");
|
||||||
System.out.println(jsonRequest);
|
// System.out.println(jsonRequest);
|
||||||
webSocket.sendText(jsonRequest, true);
|
// webSocket.sendText(jsonRequest, true);
|
||||||
} else {
|
// } else {
|
||||||
// После второго ответа — закрываем соединение
|
// // После второго ответа — закрываем соединение
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done");
|
// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done");
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return Listener.super.onText(webSocket, data, last);
|
// return Listener.super.onText(webSocket, data, last);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
// public void onError(WebSocket webSocket, Throwable error) {
|
||||||
System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
|
// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
|
||||||
error.printStackTrace(System.out);
|
// error.printStackTrace(System.out);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onClose(WebSocket webSocket,
|
// public CompletionStage<?> onClose(WebSocket webSocket,
|
||||||
int statusCode,
|
// int statusCode,
|
||||||
String reason) {
|
// String reason) {
|
||||||
System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
return Listener.super.onClose(webSocket, statusCode, reason);
|
// return Listener.super.onClose(webSocket, statusCode, reason);
|
||||||
}
|
// }
|
||||||
}).join();
|
// }).join();
|
||||||
|
//
|
||||||
// Ждём, пока получим ответ/ошибку/закрытие
|
// // Ждём, пока получим ответ/ошибку/закрытие
|
||||||
latch.await();
|
// latch.await();
|
||||||
System.out.println("Тест завершён, выходим.");
|
// System.out.println("Тест завершён, выходим.");
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -1,317 +1,317 @@
|
|||||||
package Test;
|
//package Test;
|
||||||
|
//
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
//import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import utils.crypto.Ed25519Util;
|
//import utils.crypto.Ed25519Util;
|
||||||
import blockchain.body.HeaderBody;
|
//import blockchain.body.HeaderBody;
|
||||||
import blockchain.body.TextBody;
|
//import blockchain.body.TextBody;
|
||||||
import blockchain.BchCryptoVerifier;
|
//import blockchain.BchCryptoVerifier;
|
||||||
import blockchain.BchBlockEntry;
|
//import blockchain.BchBlockEntry;
|
||||||
|
//
|
||||||
import java.net.URI;
|
//import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
//import java.net.http.HttpClient;
|
||||||
import java.net.http.WebSocket;
|
//import java.net.http.WebSocket;
|
||||||
import java.nio.ByteBuffer;
|
//import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
//import java.nio.ByteOrder;
|
||||||
import java.util.Base64;
|
//import java.util.Base64;
|
||||||
import java.util.concurrent.CompletableFuture;
|
//import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
//import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.CountDownLatch;
|
//import java.util.concurrent.CountDownLatch;
|
||||||
|
//
|
||||||
public class Test_AddBlock_new_NoAuth {
|
//public class Test_AddBlock_new_NoAuth {
|
||||||
|
//
|
||||||
private static final String WS_URI = "ws://localhost:7070/ws";
|
// private static final String WS_URI = "ws://localhost:7070/ws";
|
||||||
private static final ObjectMapper JSON = new ObjectMapper();
|
// private static final ObjectMapper JSON = new ObjectMapper();
|
||||||
|
//
|
||||||
private static final String TEST_LOGIN = "anya24";
|
// private static final String TEST_LOGIN = "anya24";
|
||||||
// По твоему правилу: blockchainName = login + 4 цифры
|
// // По твоему правилу: blockchainName = login + 4 цифры
|
||||||
private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
|
// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
|
||||||
|
//
|
||||||
private static final byte[] LOGIN_PRIV_KEY;
|
// private static final byte[] LOGIN_PRIV_KEY;
|
||||||
private static final byte[] LOGIN_PUB_KEY;
|
// private static final byte[] LOGIN_PUB_KEY;
|
||||||
|
//
|
||||||
static {
|
// static {
|
||||||
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
|
// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
|
||||||
LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
|
// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static final byte[] ZERO32 = new byte[32];
|
// private static final byte[] ZERO32 = new byte[32];
|
||||||
private static final String ZERO64 = "0".repeat(64);
|
// private static final String ZERO64 = "0".repeat(64);
|
||||||
|
//
|
||||||
public static void main(String[] args) throws Exception {
|
// public static void main(String[] args) throws Exception {
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
// CountDownLatch latch = new CountDownLatch(1);
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
// HttpClient client = HttpClient.newHttpClient();
|
||||||
|
//
|
||||||
client.newWebSocketBuilder()
|
// client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
|
// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
|
||||||
|
//
|
||||||
private int step = 0;
|
// private int step = 0;
|
||||||
|
//
|
||||||
// Эти значения обновим ПО ОТВЕТУ сервера на header
|
// // Эти значения обновим ПО ОТВЕТУ сервера на header
|
||||||
private String lastGlobalHashHex = ZERO64;
|
// private String lastGlobalHashHex = ZERO64;
|
||||||
private String lastLineHashHex = ZERO64;
|
// private String lastLineHashHex = ZERO64;
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onOpen(WebSocket ws) {
|
// public void onOpen(WebSocket ws) {
|
||||||
System.out.println("✅ WS connected: " + WS_URI);
|
// System.out.println("✅ WS connected: " + WS_URI);
|
||||||
ws.request(1);
|
// ws.request(1);
|
||||||
|
//
|
||||||
// 1) HEADER (global=0, line=0, lineNumber=0)
|
// // 1) HEADER (global=0, line=0, lineNumber=0)
|
||||||
byte[] headerFull = buildHeaderBlockFullBytes(
|
// byte[] headerFull = buildHeaderBlockFullBytes(
|
||||||
/*global*/0,
|
// /*global*/0,
|
||||||
/*lineIndex*/(short)0,
|
// /*lineIndex*/(short)0,
|
||||||
/*lineBlock*/0,
|
// /*lineBlock*/0,
|
||||||
/*prevGlobal*/ZERO32,
|
// /*prevGlobal*/ZERO32,
|
||||||
/*prevLine*/ZERO32
|
// /*prevLine*/ZERO32
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
String json = buildAddBlockJson(
|
// String json = buildAddBlockJson(
|
||||||
"test-add-header",
|
// "test-add-header",
|
||||||
TEST_BCH_NAME,
|
// TEST_BCH_NAME,
|
||||||
0,
|
// 0,
|
||||||
ZERO64, // prevGlobalHash для первого блока — нули
|
// ZERO64, // prevGlobalHash для первого блока — нули
|
||||||
base64(headerFull)
|
// base64(headerFull)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
|
// System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
|
||||||
ws.sendText(json, true);
|
// ws.sendText(json, true);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
|
// public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
|
||||||
String msg = data.toString();
|
// String msg = data.toString();
|
||||||
System.out.println("\n📥 RECV:\n" + msg);
|
// System.out.println("\n📥 RECV:\n" + msg);
|
||||||
System.out.println("-----------------------------------------------------");
|
// System.out.println("-----------------------------------------------------");
|
||||||
|
//
|
||||||
try {
|
// try {
|
||||||
int status = extractStatus(msg);
|
// int status = extractStatus(msg);
|
||||||
|
//
|
||||||
if (step == 0) {
|
// if (step == 0) {
|
||||||
if (status != 200) {
|
// if (status != 200) {
|
||||||
System.out.println("❌ HEADER rejected, status=" + status);
|
// System.out.println("❌ HEADER rejected, status=" + status);
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
|
// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
|
||||||
String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
|
// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
|
||||||
String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
|
// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
|
||||||
|
//
|
||||||
if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
|
// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
|
||||||
System.out.println("❌ No serverLastGlobalHash in response");
|
// System.out.println("❌ No serverLastGlobalHash in response");
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
|
// if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
|
||||||
// fallback: пусть будет как global (если сервер так хранит)
|
// // fallback: пусть будет как global (если сервер так хранит)
|
||||||
serverLastLineHash = serverLastGlobalHash;
|
// serverLastLineHash = serverLastGlobalHash;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
lastGlobalHashHex = serverLastGlobalHash;
|
// lastGlobalHashHex = serverLastGlobalHash;
|
||||||
lastLineHashHex = serverLastLineHash;
|
// lastLineHashHex = serverLastLineHash;
|
||||||
|
//
|
||||||
byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
|
// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
|
||||||
byte[] prevLine32 = hexToBytes32(lastLineHashHex);
|
// byte[] prevLine32 = hexToBytes32(lastLineHashHex);
|
||||||
|
//
|
||||||
// 2) TEXT (global=1, line=0, lineNumber=1)
|
// // 2) TEXT (global=1, line=0, lineNumber=1)
|
||||||
byte[] textFull = buildTextBlockFullBytes(
|
// byte[] textFull = buildTextBlockFullBytes(
|
||||||
/*global*/1,
|
// /*global*/1,
|
||||||
/*lineIndex*/(short)0,
|
// /*lineIndex*/(short)0,
|
||||||
/*lineBlock*/1,
|
// /*lineBlock*/1,
|
||||||
prevGlobal32,
|
// prevGlobal32,
|
||||||
prevLine32,
|
// prevLine32,
|
||||||
"Hello from test client"
|
// "Hello from test client"
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
String json2 = buildAddBlockJson(
|
// String json2 = buildAddBlockJson(
|
||||||
"test-add-text",
|
// "test-add-text",
|
||||||
TEST_BCH_NAME,
|
// TEST_BCH_NAME,
|
||||||
1,
|
// 1,
|
||||||
lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
|
// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
|
||||||
base64(textFull)
|
// base64(textFull)
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
|
// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
|
||||||
step = 1;
|
// step = 1;
|
||||||
ws.sendText(json2, true);
|
// ws.sendText(json2, true);
|
||||||
|
//
|
||||||
} else if (step == 1) {
|
// } else if (step == 1) {
|
||||||
if (status != 200) {
|
// if (status != 200) {
|
||||||
System.out.println("❌ TEXT rejected, status=" + status);
|
// System.out.println("❌ TEXT rejected, status=" + status);
|
||||||
} else {
|
// } else {
|
||||||
System.out.println("✅ Done. Closing.");
|
// System.out.println("✅ Done. Closing.");
|
||||||
}
|
// }
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
e.printStackTrace(System.out);
|
// e.printStackTrace(System.out);
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
|
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
ws.request(1);
|
// ws.request(1);
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onError(WebSocket ws, Throwable error) {
|
// public void onError(WebSocket ws, Throwable error) {
|
||||||
System.out.println("❌ WS error: " + error.getMessage());
|
// System.out.println("❌ WS error: " + error.getMessage());
|
||||||
error.printStackTrace(System.out);
|
// error.printStackTrace(System.out);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
|
// public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
|
||||||
System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
|
// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
}).join();
|
// }).join();
|
||||||
|
//
|
||||||
latch.await();
|
// latch.await();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// BUILD BLOCKS
|
// // BUILD BLOCKS
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
|
// private static byte[] buildHeaderBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32) {
|
// byte[] prevLineHash32) {
|
||||||
|
//
|
||||||
HeaderBody body = new HeaderBody(
|
// HeaderBody body = new HeaderBody(
|
||||||
TEST_LOGIN
|
// TEST_LOGIN
|
||||||
);
|
// );
|
||||||
byte[] bodyBytes = body.toBytes();
|
// byte[] bodyBytes = body.toBytes();
|
||||||
|
//
|
||||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] buildTextBlockFullBytes(int globalNumber,
|
// private static byte[] buildTextBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32,
|
// byte[] prevLineHash32,
|
||||||
String text) {
|
// String text) {
|
||||||
|
//
|
||||||
TextBody body = new TextBody(text);
|
// TextBody body = new TextBody(text);
|
||||||
byte[] bodyBytes = body.toBytes();
|
// byte[] bodyBytes = body.toBytes();
|
||||||
|
//
|
||||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] buildSignedBlockFullBytes(int globalNumber,
|
// private static byte[] buildSignedBlockFullBytes(int globalNumber,
|
||||||
short lineIndex,
|
// short lineIndex,
|
||||||
int lineBlockNumber,
|
// int lineBlockNumber,
|
||||||
byte[] bodyBytes,
|
// byte[] bodyBytes,
|
||||||
byte[] prevGlobalHash32,
|
// byte[] prevGlobalHash32,
|
||||||
byte[] prevLineHash32) {
|
// byte[] prevLineHash32) {
|
||||||
|
//
|
||||||
long ts = System.currentTimeMillis() / 1000L;
|
// long ts = System.currentTimeMillis() / 1000L;
|
||||||
|
//
|
||||||
int recordSize =
|
// int recordSize =
|
||||||
BchBlockEntry.RAW_HEADER_SIZE +
|
// BchBlockEntry.RAW_HEADER_SIZE +
|
||||||
bodyBytes.length +
|
// bodyBytes.length +
|
||||||
BchBlockEntry.SIGNATURE_LEN +
|
// BchBlockEntry.SIGNATURE_LEN +
|
||||||
BchBlockEntry.HASH_LEN;
|
// BchBlockEntry.HASH_LEN;
|
||||||
|
//
|
||||||
byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length)
|
// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
// .order(ByteOrder.BIG_ENDIAN)
|
||||||
.putInt(recordSize)
|
// .putInt(recordSize)
|
||||||
.putInt(globalNumber)
|
// .putInt(globalNumber)
|
||||||
.putLong(ts)
|
// .putLong(ts)
|
||||||
.putShort(lineIndex)
|
// .putShort(lineIndex)
|
||||||
.putInt(lineBlockNumber)
|
// .putInt(lineBlockNumber)
|
||||||
.put(bodyBytes)
|
// .put(bodyBytes)
|
||||||
.array();
|
// .array();
|
||||||
|
//
|
||||||
byte[] preimage = BchCryptoVerifier.buildPreimage(
|
// byte[] preimage = BchCryptoVerifier.buildPreimage(
|
||||||
TEST_LOGIN,
|
// TEST_LOGIN,
|
||||||
prevGlobalHash32,
|
// prevGlobalHash32,
|
||||||
prevLineHash32,
|
// prevLineHash32,
|
||||||
rawBytes
|
// rawBytes
|
||||||
);
|
// );
|
||||||
|
//
|
||||||
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
|
// byte[] hash32 = BchCryptoVerifier.sha256(preimage);
|
||||||
|
//
|
||||||
// если у тебя подпись должна быть по preimage — меняй тут
|
// // если у тебя подпись должна быть по preimage — меняй тут
|
||||||
byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
|
// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
|
||||||
|
//
|
||||||
return new BchBlockEntry(
|
// return new BchBlockEntry(
|
||||||
globalNumber,
|
// globalNumber,
|
||||||
ts,
|
// ts,
|
||||||
lineIndex,
|
// lineIndex,
|
||||||
lineBlockNumber,
|
// lineBlockNumber,
|
||||||
bodyBytes,
|
// bodyBytes,
|
||||||
signature64,
|
// signature64,
|
||||||
hash32
|
// hash32
|
||||||
).toBytes();
|
// ).toBytes();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// JSON BUILD
|
// // JSON BUILD
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static String buildAddBlockJson(String requestId,
|
// private static String buildAddBlockJson(String requestId,
|
||||||
String blockchainName,
|
// String blockchainName,
|
||||||
int globalNumber,
|
// int globalNumber,
|
||||||
String prevGlobalHashHex,
|
// String prevGlobalHashHex,
|
||||||
String blockBytesB64) {
|
// String blockBytesB64) {
|
||||||
return """
|
// return """
|
||||||
{
|
// {
|
||||||
"op": "AddBlock",
|
// "op": "AddBlock",
|
||||||
"requestId": "%s",
|
// "requestId": "%s",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"login": "%s",
|
// "login": "%s",
|
||||||
"blockchainName": "%s",
|
// "blockchainName": "%s",
|
||||||
"globalNumber": %d,
|
// "globalNumber": %d,
|
||||||
"prevGlobalHash": "%s",
|
// "prevGlobalHash": "%s",
|
||||||
"blockBytesB64": "%s"
|
// "blockBytesB64": "%s"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""".formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
// HELPERS
|
// // HELPERS
|
||||||
// =================================================================================
|
// // =================================================================================
|
||||||
|
//
|
||||||
private static int extractStatus(String json) {
|
// private static int extractStatus(String json) {
|
||||||
try {
|
// try {
|
||||||
JsonNode root = JSON.readTree(json);
|
// JsonNode root = JSON.readTree(json);
|
||||||
if (root.has("status")) return root.get("status").asInt();
|
// if (root.has("status")) return root.get("status").asInt();
|
||||||
} catch (Exception ignore) {}
|
// } catch (Exception ignore) {}
|
||||||
return -1;
|
// return -1;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String extractPayloadString(String json, String field) {
|
// private static String extractPayloadString(String json, String field) {
|
||||||
try {
|
// try {
|
||||||
JsonNode root = JSON.readTree(json);
|
// JsonNode root = JSON.readTree(json);
|
||||||
JsonNode payload = root.get("payload");
|
// JsonNode payload = root.get("payload");
|
||||||
if (payload != null && payload.has(field)) {
|
// if (payload != null && payload.has(field)) {
|
||||||
return payload.get(field).asText();
|
// return payload.get(field).asText();
|
||||||
}
|
// }
|
||||||
} catch (Exception ignore) {}
|
// } catch (Exception ignore) {}
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String base64(byte[] bytes) {
|
// private static String base64(byte[] bytes) {
|
||||||
return Base64.getEncoder().encodeToString(bytes);
|
// return Base64.getEncoder().encodeToString(bytes);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static byte[] hexToBytes32(String hex) {
|
// private static byte[] hexToBytes32(String hex) {
|
||||||
if (hex == null) throw new IllegalArgumentException("hex is null");
|
// if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||||
String s = hex.trim();
|
// String s = hex.trim();
|
||||||
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
||||||
byte[] out = new byte[32];
|
// byte[] out = new byte[32];
|
||||||
for (int i = 0; i < 32; i++) {
|
// for (int i = 0; i < 32; i++) {
|
||||||
int hi = Character.digit(s.charAt(i * 2), 16);
|
// int hi = Character.digit(s.charAt(i * 2), 16);
|
||||||
int lo = Character.digit(s.charAt(i * 2 + 1), 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));
|
// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
|
||||||
out[i] = (byte) ((hi << 4) | lo);
|
// out[i] = (byte) ((hi << 4) | lo);
|
||||||
}
|
// }
|
||||||
return out;
|
// return out;
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,109 +1,109 @@
|
|||||||
package Test;
|
//package Test;
|
||||||
|
//
|
||||||
import java.net.URI;
|
//import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
//import java.net.http.HttpClient;
|
||||||
import java.net.http.WebSocket;
|
//import java.net.http.WebSocket;
|
||||||
import java.net.http.WebSocket.Listener;
|
//import java.net.http.WebSocket.Listener;
|
||||||
import java.util.concurrent.CompletableFuture;
|
//import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
//import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.CountDownLatch;
|
//import java.util.concurrent.CountDownLatch;
|
||||||
|
//
|
||||||
public class Test_SessionRefreshClient {
|
//public class Test_SessionRefreshClient {
|
||||||
|
//
|
||||||
// Адрес сервера
|
// // Адрес сервера
|
||||||
private static final String WS_URI = "ws://localhost:7070/ws";
|
// private static final String WS_URI = "ws://localhost:7070/ws";
|
||||||
|
//
|
||||||
// ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ====
|
// // ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ====
|
||||||
private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId
|
// private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId
|
||||||
private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd
|
// private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd
|
||||||
// =============================================
|
// // =============================================
|
||||||
|
//
|
||||||
public static void main(String[] args) throws Exception {
|
// public static void main(String[] args) throws Exception {
|
||||||
System.out.println("Подключаемся к " + WS_URI);
|
// System.out.println("Подключаемся к " + WS_URI);
|
||||||
|
//
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
// CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
//
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
// HttpClient client = HttpClient.newHttpClient();
|
||||||
|
//
|
||||||
ClientListener listener = new ClientListener(latch);
|
// ClientListener listener = new ClientListener(latch);
|
||||||
|
//
|
||||||
client.newWebSocketBuilder()
|
// client.newWebSocketBuilder()
|
||||||
.buildAsync(URI.create(WS_URI), listener)
|
// .buildAsync(URI.create(WS_URI), listener)
|
||||||
.join();
|
// .join();
|
||||||
|
//
|
||||||
latch.await();
|
// latch.await();
|
||||||
System.out.println("Тест RefreshSession завершён, выходим.");
|
// System.out.println("Тест RefreshSession завершён, выходим.");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static String buildRefreshSessionJson() {
|
// private static String buildRefreshSessionJson() {
|
||||||
return """
|
// return """
|
||||||
{
|
// {
|
||||||
"op": "RefreshSession",
|
// "op": "RefreshSession",
|
||||||
"requestId": "test-session-refresh-1",
|
// "requestId": "test-session-refresh-1",
|
||||||
"payload": {
|
// "payload": {
|
||||||
"sessionId": %d,
|
// "sessionId": %d,
|
||||||
"sessionPwd": "%s"
|
// "sessionPwd": "%s"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
""".formatted(SESSION_ID, SESSION_PWD);
|
// """.formatted(SESSION_ID, SESSION_PWD);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
private static class ClientListener implements Listener {
|
// private static class ClientListener implements Listener {
|
||||||
|
//
|
||||||
private final CountDownLatch latch;
|
// private final CountDownLatch latch;
|
||||||
|
//
|
||||||
ClientListener(CountDownLatch latch) {
|
// ClientListener(CountDownLatch latch) {
|
||||||
this.latch = latch;
|
// this.latch = latch;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onOpen(WebSocket webSocket) {
|
// public void onOpen(WebSocket webSocket) {
|
||||||
System.out.println("✅ WebSocket подключен");
|
// System.out.println("✅ WebSocket подключен");
|
||||||
|
//
|
||||||
webSocket.request(1); // разрешаем принимать одно сообщение
|
// webSocket.request(1); // разрешаем принимать одно сообщение
|
||||||
|
//
|
||||||
// сразу отправляем запрос RefreshSession
|
// // сразу отправляем запрос RefreshSession
|
||||||
String json = buildRefreshSessionJson();
|
// String json = buildRefreshSessionJson();
|
||||||
System.out.println();
|
// System.out.println();
|
||||||
System.out.println("📤 Отправляем RefreshSession:");
|
// System.out.println("📤 Отправляем RefreshSession:");
|
||||||
System.out.println(json);
|
// System.out.println(json);
|
||||||
webSocket.sendText(json, true);
|
// webSocket.sendText(json, true);
|
||||||
|
//
|
||||||
Listener.super.onOpen(webSocket);
|
// Listener.super.onOpen(webSocket);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onText(WebSocket webSocket,
|
// public CompletionStage<?> onText(WebSocket webSocket,
|
||||||
CharSequence data,
|
// CharSequence data,
|
||||||
boolean last) {
|
// boolean last) {
|
||||||
System.out.println("📥 Ответ от сервера:");
|
// System.out.println("📥 Ответ от сервера:");
|
||||||
System.out.println(data.toString());
|
// System.out.println(data.toString());
|
||||||
System.out.println("-----------------------------------------------------");
|
// System.out.println("-----------------------------------------------------");
|
||||||
|
//
|
||||||
// После одного ответа просто закрываем соединение
|
// // После одного ответа просто закрываем соединение
|
||||||
System.out.println("✅ Получен ответ на RefreshSession, закрываем соединение");
|
// System.out.println("✅ Получен ответ на RefreshSession, закрываем соединение");
|
||||||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done");
|
// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done");
|
||||||
|
//
|
||||||
// запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся)
|
// // запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся)
|
||||||
webSocket.request(1);
|
// webSocket.request(1);
|
||||||
|
//
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
// public void onError(WebSocket webSocket, Throwable error) {
|
||||||
System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
|
// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
|
||||||
error.printStackTrace(System.out);
|
// error.printStackTrace(System.out);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public CompletionStage<?> onClose(WebSocket webSocket,
|
// public CompletionStage<?> onClose(WebSocket webSocket,
|
||||||
int statusCode,
|
// int statusCode,
|
||||||
String reason) {
|
// String reason) {
|
||||||
System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||||||
latch.countDown();
|
// latch.countDown();
|
||||||
return CompletableFuture.completedFuture(null);
|
// return CompletableFuture.completedFuture(null);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
package Test;
|
//package Test;
|
||||||
|
//
|
||||||
public class test1 {
|
//public class test1 {
|
||||||
|
//
|
||||||
}
|
//}
|
||||||
@ -7,100 +7,89 @@ import java.time.Duration;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
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 {
|
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) {
|
public static void main(String[] args) {
|
||||||
// чтобы тест можно было запускать вообще без JUnit
|
// чтобы тест можно было запускать вообще без JUnit
|
||||||
ItRunContext.initIfNeeded();
|
int failed = run();
|
||||||
new IT_01_AddUser().addUser_shouldReturn200_orAlreadyExists();
|
System.exit(failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
|
||||||
|
public static int run() {
|
||||||
|
return TestLog.runOne("IT_01_AddUser", IT_01_AddUser::testBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addUser_shouldReturn200_orAlreadyExists() {
|
void addUser_shouldReturn200_orAlreadyExists() {
|
||||||
|
// JUnit-режим: пусть падает через assert/fail как обычно
|
||||||
|
testBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testBody() {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
|
TestLog.title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
|
||||||
System.out.println("Используем:");
|
TestLog.info("Используем:");
|
||||||
System.out.println(" login = " + TestConfig.LOGIN());
|
TestLog.info(" login = " + TestConfig.LOGIN());
|
||||||
System.out.println(" blockchainName = " + TestConfig.BCH_NAME());
|
TestLog.info(" blockchainName = " + TestConfig.BCH_NAME());
|
||||||
System.out.println("Ожидание:");
|
TestLog.info("Ожидание:");
|
||||||
System.out.println(" - 200 (создан)");
|
TestLog.info(" - 200 (создан)");
|
||||||
System.out.println(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
|
TestLog.info(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
|
||||||
|
|
||||||
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
|
|
||||||
String reqId = "it-adduser-1";
|
String reqId = "it-adduser-1";
|
||||||
String reqJson = JsonBuilders.addUser(reqId);
|
String reqJson = JsonBuilders.addUser(reqId);
|
||||||
|
|
||||||
System.out.println("📤 Отправляем AddUser запрос:");
|
TestLog.info("📤 Отправляем AddUser запрос:");
|
||||||
System.out.println(reqJson);
|
TestLog.info(reqJson);
|
||||||
line();
|
TestLog.line();
|
||||||
|
|
||||||
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
|
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
|
||||||
|
|
||||||
System.out.println("📥 Ответ сервера:");
|
TestLog.info("📥 Ответ сервера:");
|
||||||
System.out.println(resp);
|
TestLog.info(resp);
|
||||||
line();
|
TestLog.line();
|
||||||
|
|
||||||
int st = JsonParsers.status(resp);
|
int st = JsonParsers.status(resp);
|
||||||
System.out.println("ℹ️ status=" + st);
|
TestLog.info("ℹ️ status=" + st);
|
||||||
|
|
||||||
boolean created = (st == 200);
|
boolean created = (st == 200);
|
||||||
boolean already = (st == 409);
|
boolean already = (st == 409);
|
||||||
|
|
||||||
if (already) {
|
if (already) {
|
||||||
String code = JsonParsers.errorCode(resp);
|
String code = JsonParsers.errorCode(resp);
|
||||||
System.out.println("ℹ️ server_code=" + code);
|
TestLog.info("ℹ️ server_code=" + code);
|
||||||
|
|
||||||
try {
|
|
||||||
assertEquals("USER_ALREADY_EXISTS", code,
|
assertEquals("USER_ALREADY_EXISTS", code,
|
||||||
"Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp);
|
"Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp);
|
||||||
ok("409 получен корректно: USER_ALREADY_EXISTS");
|
|
||||||
} catch (AssertionError ae) {
|
TestLog.ok("409 получен корректно: USER_ALREADY_EXISTS");
|
||||||
boom("409 получен, но code не тот. " + ae.getMessage());
|
|
||||||
throw ae;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (created) {
|
if (created) {
|
||||||
ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)");
|
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)");
|
||||||
} else if (already) {
|
} else if (already) {
|
||||||
ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)");
|
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)");
|
||||||
} else {
|
} else {
|
||||||
boom("Неожиданный status=" + st + ", resp=" + resp);
|
TestLog.boom("Неожиданный status=" + st + ", resp=" + resp);
|
||||||
fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp);
|
fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (AssertionError | RuntimeException e) {
|
|
||||||
boom("ТЕСТ УПАЛ: AddUserIT. Причина: " + e.getMessage());
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,96 +9,60 @@ import java.util.List;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
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 {
|
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) {
|
public static void main(String[] args) {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
ensureUserExists();
|
int failed = run();
|
||||||
new IT_02_Sessions().sessions_flow_shouldCreateListRefreshCloseCorrectly();
|
System.exit(failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
|
||||||
|
public static int run() {
|
||||||
|
return TestLog.runOne("IT_02_Sessions", IT_02_Sessions::testBodyStandalone);
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void ensureUserExists() {
|
static void ensureUserExists() {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
|
TestLog.title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
|
||||||
|
|
||||||
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String reqId = "it-adduser-beforeall";
|
String reqId = "it-adduser-beforeall";
|
||||||
String reqJson = JsonBuilders.addUser(reqId);
|
String reqJson = JsonBuilders.addUser(reqId);
|
||||||
|
|
||||||
send("AddUser(BeforeAll)", reqJson);
|
TestLog.send("AddUser(BeforeAll)", reqJson);
|
||||||
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
|
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
|
||||||
recv("AddUser(BeforeAll)", resp);
|
TestLog.recv("AddUser(BeforeAll)", resp);
|
||||||
|
|
||||||
int st = JsonParsers.status(resp);
|
int st = JsonParsers.status(resp);
|
||||||
|
|
||||||
if (st == 200) {
|
if (st == 200) {
|
||||||
ok("BeforeAll: пользователь создан/добавлен (status=200)");
|
TestLog.ok("BeforeAll: пользователь создан/добавлен (status=200)");
|
||||||
} else if (st == 409) {
|
} else if (st == 409) {
|
||||||
String code = JsonParsers.errorCode(resp);
|
String code = JsonParsers.errorCode(resp);
|
||||||
if ("USER_ALREADY_EXISTS".equals(code)) {
|
if ("USER_ALREADY_EXISTS".equals(code)) {
|
||||||
ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
|
TestLog.ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
|
||||||
} else {
|
} else {
|
||||||
boom("BeforeAll: status=409, но code неожиданный: " + code);
|
TestLog.boom("BeforeAll: status=409, но code неожиданный: " + code);
|
||||||
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
|
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
boom("BeforeAll: предусловие не выполнено. status=" + st);
|
TestLog.boom("BeforeAll: предусловие не выполнено. status=" + st);
|
||||||
fail("User precondition failed. status=" + st + ", resp=" + resp);
|
fail("User precondition failed. status=" + st + ", resp=" + resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,160 +70,184 @@ public class IT_02_Sessions {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void sessions_flow_shouldCreateListRefreshCloseCorrectly() {
|
void sessions_flow_shouldCreateListRefreshCloseCorrectly() {
|
||||||
|
// JUnit-режим: пусть падает через assert/fail как обычно
|
||||||
|
testBodyJUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone-режим: тут мы сами вызываем предусловие ensureUserExists(),
|
||||||
|
* потому что @BeforeAll сработает только в JUnit.
|
||||||
|
*/
|
||||||
|
private static void testBodyStandalone() {
|
||||||
|
ensureUserExists();
|
||||||
|
testBodyJUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testBodyJUnit() {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
title("SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)");
|
TestLog.titleBlock("""
|
||||||
System.out.println("Используем:");
|
SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)
|
||||||
System.out.println(" login = " + TestConfig.LOGIN());
|
Используем:
|
||||||
System.out.println("Ожидание сценария:");
|
login = %s
|
||||||
System.out.println(" 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession");
|
Ожидание сценария:
|
||||||
System.out.println(" 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2");
|
1) Создаём SESSION1 через AuthChallenge + CreateAuthSession
|
||||||
System.out.println(" 3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) → должны быть SESSION1 и SESSION2");
|
2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2
|
||||||
System.out.println(" 4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2");
|
3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) → должны быть SESSION1 и SESSION2
|
||||||
System.out.println(" 5) Проверяем ListSessions (AUTH_IN_PROGRESS) → осталась только SESSION1");
|
4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2
|
||||||
System.out.println(" 6) Закрываем SESSION1 в AUTH_IN_PROGRESS");
|
5) Проверяем ListSessions (AUTH_IN_PROGRESS) → осталась только SESSION1
|
||||||
System.out.println(" 7) Проверяем ListSessions → пусто\n");
|
6) Закрываем SESSION1 в AUTH_IN_PROGRESS
|
||||||
|
7) Проверяем ListSessions → пусто
|
||||||
|
""".formatted(TestConfig.LOGIN()));
|
||||||
|
|
||||||
String s1Id, s1Pwd;
|
String s1Id, s1Pwd;
|
||||||
String s2Id, s2Pwd;
|
String s2Id, s2Pwd;
|
||||||
|
|
||||||
try {
|
// ===== helpers (локальные, чтобы не раздувать TestLog лишней логикой assert200) =====
|
||||||
stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)");
|
final java.util.function.BiConsumer<String, String> 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");
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
|
TestLog.stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-1";
|
String r1 = "it-auth-1";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge#1", req1);
|
TestLog.send("AuthChallenge#1", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge#1", resp1);
|
TestLog.recv("AuthChallenge#1", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge#1", resp1);
|
assert200.accept("AuthChallenge#1", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce, "AuthChallenge#1: nonce must not be null");
|
assertNotNull(nonce, "AuthChallenge#1: nonce must not be null");
|
||||||
ok("AuthChallenge#1: authNonce получен: " + nonce);
|
TestLog.ok("AuthChallenge#1: authNonce получен: " + nonce);
|
||||||
|
|
||||||
String r2 = "it-create-1";
|
String r2 = "it-create-1";
|
||||||
String storagePwd = TestConfig.fakeStoragePwd();
|
String storagePwd = TestConfig.fakeStoragePwd();
|
||||||
String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd);
|
String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd);
|
||||||
send("CreateAuthSession#1", req2);
|
TestLog.send("CreateAuthSession#1", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("CreateAuthSession#1", resp2);
|
TestLog.recv("CreateAuthSession#1", resp2);
|
||||||
|
|
||||||
assert200("CreateAuthSession#1", resp2);
|
assert200.accept("CreateAuthSession#1", resp2);
|
||||||
|
|
||||||
s1Id = JsonParsers.sessionId(resp2);
|
s1Id = JsonParsers.sessionId(resp2);
|
||||||
s1Pwd = JsonParsers.sessionPwd(resp2);
|
s1Pwd = JsonParsers.sessionPwd(resp2);
|
||||||
assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null");
|
assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null");
|
||||||
assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null");
|
assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null");
|
||||||
ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
|
TestLog.ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2");
|
TestLog.stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-2";
|
String r1 = "it-auth-2";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge#2", req1);
|
TestLog.send("AuthChallenge#2", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge#2", resp1);
|
TestLog.recv("AuthChallenge#2", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge#2", resp1);
|
assert200.accept("AuthChallenge#2", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
ok("AuthChallenge#2: authNonce получен: " + nonce);
|
TestLog.ok("AuthChallenge#2: authNonce получен: " + nonce);
|
||||||
|
|
||||||
String r2 = "it-create-2";
|
String r2 = "it-create-2";
|
||||||
String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd());
|
String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd());
|
||||||
send("CreateAuthSession#2", req2);
|
TestLog.send("CreateAuthSession#2", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("CreateAuthSession#2", resp2);
|
TestLog.recv("CreateAuthSession#2", resp2);
|
||||||
|
|
||||||
assert200("CreateAuthSession#2", resp2);
|
assert200.accept("CreateAuthSession#2", resp2);
|
||||||
|
|
||||||
s2Id = JsonParsers.sessionId(resp2);
|
s2Id = JsonParsers.sessionId(resp2);
|
||||||
s2Pwd = JsonParsers.sessionPwd(resp2);
|
s2Pwd = JsonParsers.sessionPwd(resp2);
|
||||||
assertNotNull(s2Id);
|
assertNotNull(s2Id);
|
||||||
assertNotNull(s2Pwd);
|
assertNotNull(s2Pwd);
|
||||||
ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
|
TestLog.ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
|
||||||
|
|
||||||
String r3 = "it-list-in-session2";
|
String r3 = "it-list-in-session2";
|
||||||
String req3 = JsonBuilders.listSessions(r3, 0L, "");
|
String req3 = JsonBuilders.listSessions(r3, 0L, "");
|
||||||
send("ListSessions(in SESSION2)", req3);
|
TestLog.send("ListSessions(in SESSION2)", req3);
|
||||||
String resp3 = c.request(r3, req3, Duration.ofSeconds(5));
|
String resp3 = c.request(r3, req3, Duration.ofSeconds(5));
|
||||||
recv("ListSessions(in SESSION2)", resp3);
|
TestLog.recv("ListSessions(in SESSION2)", resp3);
|
||||||
|
|
||||||
assert200("ListSessions(in SESSION2)", resp3);
|
assert200.accept("ListSessions(in SESSION2)", resp3);
|
||||||
List<String> ids = JsonParsers.sessionIds(resp3);
|
List<String> ids = JsonParsers.sessionIds(resp3);
|
||||||
ok("ListSessions(in SESSION2): sessions=" + ids);
|
TestLog.ok("ListSessions(in SESSION2): sessions=" + ids);
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id), "Must contain session1");
|
assertTrue(ids.contains(s1Id), "Must contain session1");
|
||||||
assertTrue(ids.contains(s2Id), "Must contain session2");
|
assertTrue(ids.contains(s2Id), "Must contain session2");
|
||||||
ok("Проверка OK: список содержит SESSION1 и SESSION2");
|
TestLog.ok("Проверка OK: список содержит SESSION1 и SESSION2");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2");
|
TestLog.stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-list";
|
String r1 = "it-auth-list";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge(list)", req1);
|
TestLog.send("AuthChallenge(list)", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge(list)", resp1);
|
TestLog.recv("AuthChallenge(list)", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge(list)", resp1);
|
assert200.accept("AuthChallenge(list)", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
ok("AuthChallenge(list): authNonce=" + nonce);
|
TestLog.ok("AuthChallenge(list): authNonce=" + nonce);
|
||||||
|
|
||||||
long timeMs = System.currentTimeMillis();
|
long timeMs = System.currentTimeMillis();
|
||||||
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
|
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
|
||||||
ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]");
|
TestLog.ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]");
|
||||||
|
|
||||||
String r2 = "it-list-auth-in-progress";
|
String r2 = "it-list-auth-in-progress";
|
||||||
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
||||||
send("ListSessions(AUTH_IN_PROGRESS)", req2);
|
TestLog.send("ListSessions(AUTH_IN_PROGRESS)", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("ListSessions(AUTH_IN_PROGRESS)", resp2);
|
TestLog.recv("ListSessions(AUTH_IN_PROGRESS)", resp2);
|
||||||
|
|
||||||
assert200("ListSessions(AUTH_IN_PROGRESS)", resp2);
|
assert200.accept("ListSessions(AUTH_IN_PROGRESS)", resp2);
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(resp2);
|
List<String> ids = JsonParsers.sessionIds(resp2);
|
||||||
ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids);
|
TestLog.ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids);
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id));
|
assertTrue(ids.contains(s1Id));
|
||||||
assertTrue(ids.contains(s2Id));
|
assertTrue(ids.contains(s2Id));
|
||||||
ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2");
|
TestLog.ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)");
|
TestLog.stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
|
|
||||||
String r1 = "it-refresh-s1";
|
String r1 = "it-refresh-s1";
|
||||||
String req1 = JsonBuilders.refreshSession(r1, s1Id, s1Pwd);
|
String req1 = JsonBuilders.refreshSession(r1, s1Id, s1Pwd);
|
||||||
send("RefreshSession(SESSION1)", req1);
|
TestLog.send("RefreshSession(SESSION1)", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("RefreshSession(SESSION1)", resp1);
|
TestLog.recv("RefreshSession(SESSION1)", resp1);
|
||||||
|
|
||||||
assert200("RefreshSession(SESSION1)", resp1);
|
assert200.accept("RefreshSession(SESSION1)", resp1);
|
||||||
assertNotNull(JsonParsers.storagePwd(resp1));
|
assertNotNull(JsonParsers.storagePwd(resp1));
|
||||||
ok("RefreshSession: storagePwd получен");
|
TestLog.ok("RefreshSession: storagePwd получен");
|
||||||
|
|
||||||
String r2 = "it-close-s2";
|
String r2 = "it-close-s2";
|
||||||
String req2 = JsonBuilders.closeActiveSession(r2, s2Id, 0L, "");
|
String req2 = JsonBuilders.closeActiveSession(r2, s2Id, 0L, "");
|
||||||
send("CloseActiveSession(SESSION2)", req2);
|
TestLog.send("CloseActiveSession(SESSION2)", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("CloseActiveSession(SESSION2)", resp2);
|
TestLog.recv("CloseActiveSession(SESSION2)", resp2);
|
||||||
|
|
||||||
assert200("CloseActiveSession(SESSION2)", resp2);
|
assert200.accept("CloseActiveSession(SESSION2)", resp2);
|
||||||
ok("SESSION2 закрыта");
|
TestLog.ok("SESSION2 закрыта");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1");
|
TestLog.stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-list2";
|
String r1 = "it-auth-list2";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge(list2)", req1);
|
TestLog.send("AuthChallenge(list2)", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge(list2)", resp1);
|
TestLog.recv("AuthChallenge(list2)", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge(list2)", resp1);
|
assert200.accept("AuthChallenge(list2)", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
|
|
||||||
@ -268,29 +256,29 @@ public class IT_02_Sessions {
|
|||||||
|
|
||||||
String r2 = "it-list-after-close-s2";
|
String r2 = "it-list-after-close-s2";
|
||||||
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
||||||
send("ListSessions(after close S2)", req2);
|
TestLog.send("ListSessions(after close S2)", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("ListSessions(after close S2)", resp2);
|
TestLog.recv("ListSessions(after close S2)", resp2);
|
||||||
|
|
||||||
assert200("ListSessions(after close S2)", resp2);
|
assert200.accept("ListSessions(after close S2)", resp2);
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(resp2);
|
List<String> ids = JsonParsers.sessionIds(resp2);
|
||||||
ok("ListSessions(after close S2): sessions=" + ids);
|
TestLog.ok("ListSessions(after close S2): sessions=" + ids);
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id));
|
assertTrue(ids.contains(s1Id));
|
||||||
assertFalse(ids.contains(s2Id));
|
assertFalse(ids.contains(s2Id));
|
||||||
ok("Проверка OK: осталась только SESSION1");
|
TestLog.ok("Проверка OK: осталась только SESSION1");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS");
|
TestLog.stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-close-s1";
|
String r1 = "it-auth-close-s1";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge(close S1)", req1);
|
TestLog.send("AuthChallenge(close S1)", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge(close S1)", resp1);
|
TestLog.recv("AuthChallenge(close S1)", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge(close S1)", resp1);
|
assert200.accept("AuthChallenge(close S1)", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
|
|
||||||
@ -299,23 +287,23 @@ public class IT_02_Sessions {
|
|||||||
|
|
||||||
String r2 = "it-close-s1";
|
String r2 = "it-close-s1";
|
||||||
String req2 = JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig);
|
String req2 = JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig);
|
||||||
send("CloseActiveSession(SESSION1)", req2);
|
TestLog.send("CloseActiveSession(SESSION1)", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("CloseActiveSession(SESSION1)", resp2);
|
TestLog.recv("CloseActiveSession(SESSION1)", resp2);
|
||||||
|
|
||||||
assert200("CloseActiveSession(SESSION1)", resp2);
|
assert200.accept("CloseActiveSession(SESSION1)", resp2);
|
||||||
ok("SESSION1 закрыта");
|
TestLog.ok("SESSION1 закрыта");
|
||||||
}
|
}
|
||||||
|
|
||||||
stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список");
|
TestLog.stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
||||||
String r1 = "it-auth-list-empty";
|
String r1 = "it-auth-list-empty";
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
String req1 = JsonBuilders.authChallenge(r1);
|
||||||
send("AuthChallenge(list empty)", req1);
|
TestLog.send("AuthChallenge(list empty)", req1);
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
||||||
recv("AuthChallenge(list empty)", resp1);
|
TestLog.recv("AuthChallenge(list empty)", resp1);
|
||||||
|
|
||||||
assert200("AuthChallenge(list empty)", resp1);
|
assert200.accept("AuthChallenge(list empty)", resp1);
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
String nonce = JsonParsers.authNonce(resp1);
|
||||||
assertNotNull(nonce);
|
assertNotNull(nonce);
|
||||||
|
|
||||||
@ -324,24 +312,19 @@ public class IT_02_Sessions {
|
|||||||
|
|
||||||
String r2 = "it-list-empty";
|
String r2 = "it-list-empty";
|
||||||
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
|
||||||
send("ListSessions(empty)", req2);
|
TestLog.send("ListSessions(empty)", req2);
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
||||||
recv("ListSessions(empty)", resp2);
|
TestLog.recv("ListSessions(empty)", resp2);
|
||||||
|
|
||||||
assert200("ListSessions(empty)", resp2);
|
assert200.accept("ListSessions(empty)", resp2);
|
||||||
|
|
||||||
List<String> ids = JsonParsers.sessionIds(resp2);
|
List<String> ids = JsonParsers.sessionIds(resp2);
|
||||||
ok("ListSessions(empty): sessions=" + ids);
|
TestLog.ok("ListSessions(empty): sessions=" + ids);
|
||||||
|
|
||||||
assertTrue(ids.isEmpty(), "Sessions must be empty");
|
assertTrue(ids.isEmpty(), "Sessions must be empty");
|
||||||
ok("Проверка OK: список пуст");
|
TestLog.ok("Проверка OK: список пуст");
|
||||||
}
|
}
|
||||||
|
|
||||||
ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
|
TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
|
||||||
|
|
||||||
} catch (AssertionError | RuntimeException e) {
|
|
||||||
boom("ТЕСТ УПАЛ: SessionsIT. Причина: " + e.getMessage());
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
62
src/test/java/test/it/IT_RunAllCleanMain.java
Normal file
62
src/test/java/test/it/IT_RunAllCleanMain.java
Normal file
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,82 +1,79 @@
|
|||||||
package test.it;
|
package test.it;
|
||||||
|
|
||||||
import test.it.utils.TestConfig;
|
|
||||||
import test.it.utils.TestColors;
|
|
||||||
import test.it.utils.ItRunContext;
|
import test.it.utils.ItRunContext;
|
||||||
|
import test.it.utils.TestLog;
|
||||||
import test.it.ws.IT_03_AddBlock_NoAuth;
|
import test.it.ws.IT_03_AddBlock_NoAuth;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
|
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
|
||||||
*
|
*
|
||||||
* Делает:
|
* Делает:
|
||||||
* 1) чистит папку data/
|
* 1) запускает тесты по очереди
|
||||||
* 2) запускает 3 теста по очереди (через их main)
|
* 2) печатает итоговый короткий отчёт
|
||||||
*
|
*
|
||||||
* Запуск из IDE:
|
* Запуск из IDE:
|
||||||
* Run 'main' этого класса
|
* Run 'main' этого класса
|
||||||
*
|
*
|
||||||
* Запуск из консоли:
|
* Запуск из консоли:
|
||||||
* ./gradlew testClasses
|
* ./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 проще всего)
|
* (Classpath зависит от твоего Gradle, но в IDE проще всего)
|
||||||
*/
|
*/
|
||||||
public class IT_RunAllMain {
|
public class IT_RunAllMain {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
try {
|
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
banner("ШАГ 0: очистка data/");
|
int failed = runAll();
|
||||||
cleanupDataDir(TestConfig.DATA_DIR);
|
|
||||||
|
|
||||||
banner("ШАГ 1: IT_01_AddUser");
|
// Удобно для CI: код выхода = число упавших тестов
|
||||||
IT_01_AddUser.main(new String[0]);
|
System.exit(failed);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void banner(String s) {
|
/**
|
||||||
System.out.println(TestColors.C + "\n============================================================" + TestColors.R);
|
* Основной метод, который возвращает число не пройденных тестов (0 если всё хорошо).
|
||||||
System.out.println(TestColors.C + s + TestColors.R);
|
* Его можно вызывать из других раннеров (например, из варианта с очисткой data/).
|
||||||
System.out.println(TestColors.C + "============================================================\n" + TestColors.R);
|
*/
|
||||||
|
public static int runAll() {
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void cleanupDataDir(String dirName) throws IOException {
|
return failed;
|
||||||
Path dir = Paths.get(dirName);
|
|
||||||
if (!Files.exists(dir)) {
|
|
||||||
System.out.println("ℹ️ 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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
System.out.println("✅ data очищена: " + dir.toAbsolutePath());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,9 +34,6 @@ public final class TestConfig {
|
|||||||
// Любая строка клиента (для логов)
|
// Любая строка клиента (для логов)
|
||||||
public static final String TEST_CLIENT_INFO = "it-tests";
|
public static final String TEST_CLIENT_INFO = "it-tests";
|
||||||
|
|
||||||
// Папка данных (которую будет чистить IT_RunAllMain)
|
|
||||||
public static final String DATA_DIR = "data";
|
|
||||||
|
|
||||||
/** login для прогона (по умолчанию DEFAULT_LOGIN, можно переопределить -Dit.login=...). */
|
/** login для прогона (по умолчанию DEFAULT_LOGIN, можно переопределить -Dit.login=...). */
|
||||||
public static String LOGIN() {
|
public static String LOGIN() {
|
||||||
return System.getProperty("it.login", DEFAULT_LOGIN);
|
return System.getProperty("it.login", DEFAULT_LOGIN);
|
||||||
|
|||||||
@ -1,32 +1,128 @@
|
|||||||
package test.it.utils;
|
package test.it.utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestLog — единое место для:
|
||||||
|
* - ANSI цветов
|
||||||
|
* - стандартных красивых сообщений (title/ok/boom/line/step/send/recv)
|
||||||
|
*
|
||||||
|
* Включение/выключение подробных логов:
|
||||||
|
* -Dit.verbose=false
|
||||||
|
*
|
||||||
|
* По умолчанию verbose=true (удобно для ручного прогона).
|
||||||
|
*/
|
||||||
public final class TestLog {
|
public final class TestLog {
|
||||||
private TestLog(){}
|
private TestLog() {}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// VERBOSE
|
||||||
|
// ============================
|
||||||
|
|
||||||
// включается так: ./gradlew test -Dit.verbose=true
|
// включается так: ./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) {
|
public static void info(String s) {
|
||||||
if (VERBOSE) System.out.println(s);
|
if (VERBOSE) System.out.println(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void section(String title) {
|
public static void line() {
|
||||||
if (!VERBOSE) return;
|
if (!VERBOSE) return;
|
||||||
System.out.println("\n\n==================================================");
|
System.out.println(C + "------------------------------------------------------------" + R);
|
||||||
System.out.println(title);
|
|
||||||
System.out.println("==================================================\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void req(String title, String json) {
|
/** Короткое заглавие. */
|
||||||
|
public static void title(String s) {
|
||||||
if (!VERBOSE) return;
|
if (!VERBOSE) return;
|
||||||
System.out.println("\n📤 " + title);
|
System.out.println(C + "\n============================================================" + R);
|
||||||
System.out.println(json);
|
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;
|
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(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
426
src/test/java/test/it/ws/AddBlockScenarioRunner.java
Normal file
426
src/test/java/test/it/ws/AddBlockScenarioRunner.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,23 +1,13 @@
|
|||||||
package test.it.ws;
|
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.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import test.it.utils.ItRunContext;
|
import test.it.utils.ItRunContext;
|
||||||
import test.it.utils.JsonBuilders;
|
import test.it.utils.JsonBuilders;
|
||||||
import test.it.utils.JsonParsers;
|
import test.it.utils.JsonParsers;
|
||||||
import test.it.utils.TestConfig;
|
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.time.Duration;
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
@ -38,592 +28,81 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
* - line 0: нулевой блок (HEADER) один на весь блокчейн (глобальный 0)
|
* - line 0: нулевой блок (HEADER) один на весь блокчейн (глобальный 0)
|
||||||
* - line 1 и line 2: первый блок каждой линии ссылается prevLineHash на hash(нулевого блока)
|
* - line 1 и line 2: первый блок каждой линии ссылается prevLineHash на hash(нулевого блока)
|
||||||
*
|
*
|
||||||
* В этом тесте мы ведём 2 массива:
|
* В этом тесте состояние ведёт AddBlockFlow:
|
||||||
* - lineLastNumber[line] — сколько блоков в линии (то есть последний lineNum)
|
* - lineLastNumber[line] — сколько блоков в линии (то есть последний lineNum)
|
||||||
* - lineLastHashHex[line] — hash последнего блока линии (HEX64)
|
* - lineLastHashHex[line] — hash последнего блока линии (HEX64)
|
||||||
*/
|
*/
|
||||||
public class IT_03_AddBlock_NoAuth {
|
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) {
|
public static void main(String[] args) {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
ensureUserExists();
|
ensureUserExists();
|
||||||
new IT_03_AddBlock_NoAuth().addBlock_shouldAppendHeaderThenTextThenReaction();
|
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
|
@BeforeAll
|
||||||
static void ensureUserExists() {
|
static void ensureUserExists() {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
|
// ВАЖНО:
|
||||||
|
// - requestId тут не важен, но пусть будет.
|
||||||
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
// - отдельная авторизация не нужна, но пользователь должен существовать.
|
||||||
String reqId = "it03-adduser-beforeall";
|
String reqJson = JsonBuilders.addUser("it03-adduser-beforeall");
|
||||||
String reqJson = JsonBuilders.addUser(reqId);
|
|
||||||
|
|
||||||
send("AddUser(BeforeAll)", reqJson);
|
|
||||||
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
|
|
||||||
recv("AddUser(BeforeAll)", resp);
|
|
||||||
|
|
||||||
|
String resp = WsJsonOneShot.request(reqJson, Duration.ofSeconds(5));
|
||||||
int st = JsonParsers.status(resp);
|
int st = JsonParsers.status(resp);
|
||||||
|
|
||||||
if (st == 200) {
|
if (st == 200) {
|
||||||
ok("BeforeAll: пользователь создан/добавлен (status=200)");
|
// ok
|
||||||
} else if (st == 409) {
|
return;
|
||||||
|
}
|
||||||
|
if (st == 409) {
|
||||||
String code = JsonParsers.errorCode(resp);
|
String code = JsonParsers.errorCode(resp);
|
||||||
if ("USER_ALREADY_EXISTS".equals(code)) {
|
if ("USER_ALREADY_EXISTS".equals(code)) return;
|
||||||
ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
|
|
||||||
} else {
|
|
||||||
boom("BeforeAll: status=409, но code неожиданный: " + code);
|
|
||||||
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
|
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
boom("BeforeAll: предусловие не выполнено. status=" + st);
|
|
||||||
fail("User precondition failed. status=" + st + ", resp=" + resp);
|
fail("User precondition failed. status=" + st + ", resp=" + resp);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void addBlock_shouldAppendHeaderThenTextThenReaction() {
|
void addBlock_shouldAppendHeaderThenTextThenReaction() {
|
||||||
ItRunContext.initIfNeeded();
|
ItRunContext.initIfNeeded();
|
||||||
|
|
||||||
title("AddBlockIT: HEADER(0) + TEXT(1,2,3) + REACT(4->text1) без auth");
|
// таймаут на каждый one-shot запрос
|
||||||
System.out.println("Используем:");
|
Duration t = Duration.ofSeconds(8);
|
||||||
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)) {
|
// 1) состояние + сборка + отправка
|
||||||
|
AddBlockFlow flow = new AddBlockFlow();
|
||||||
// ============================
|
|
||||||
// Локальное состояние теста
|
|
||||||
// ============================
|
|
||||||
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)
|
// ШАГ 0: ВАЖНО — первым всегда HEADER global=0
|
||||||
// =========================================================
|
// =========================================================
|
||||||
stepTitle("ШАГ 1: AddBlock HEADER (global=0, line=0, lineNum=0)");
|
flow.sendHeader0(t);
|
||||||
|
|
||||||
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 уже есть
|
// ШАГ 1..3: TEXT (line=1)
|
||||||
// =========================================================
|
// =========================================================
|
||||||
assertNotNull(headerHash32, "internal: headerHash32 must be set after header step");
|
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);
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// ШАГ 2: TEXT#1 (global=1, line=1, lineNum=1)
|
// ШАГ 4: REACT#1 (line=2) -> на TEXT#1 (global=1, hash=text1)
|
||||||
// prevLineHash для первого блока линии = hash(нулевого блока)
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
stepTitle("ШАГ 2: AddBlock TEXT#1 (global=1, line=1, lineNum=1)");
|
flow.sendNextReaction(
|
||||||
|
|
||||||
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)
|
1, // reactionCode (пример: 1 = like)
|
||||||
TestConfig.BCH_NAME(),
|
TestConfig.BCH_NAME(), // toBlockchainName
|
||||||
1, // toBlockGlobalNumber = 1 (TEXT#1)
|
1, // toBlockGlobalNumber = 1 (TEXT#1)
|
||||||
text1.hash32 // toBlockHash32 = hash(TEXT#1)
|
text1.hash32, // toBlockHash32 = hash(TEXT#1)
|
||||||
|
t
|
||||||
);
|
);
|
||||||
|
|
||||||
String reqId5 = "it03-add-react-1";
|
// Мини-контроль итогов (если захочешь — красиво залогируем через твой TestLog)
|
||||||
String reqJson5 = buildAddBlockJson(reqId5, TestConfig.BCH_NAME(), 4, globalLastHashHex, base64(react1.fullBytes));
|
assertEquals(4, flow.globalLastNumber(), "После 1 header + 3 text + 1 react globalLastNumber должен быть 4");
|
||||||
|
assertEquals(3, flow.lineLastNumber(AddBlockFlow.LINE_TEXT), "В line=1 должно быть 3 блока");
|
||||||
send("AddBlock(" + reqId5 + ")", reqJson5);
|
assertEquals(1, flow.lineLastNumber(AddBlockFlow.LINE_REACT), "В line=2 должен быть 1 блок");
|
||||||
String resp5 = client.request(reqId5, reqJson5, Duration.ofSeconds(8));
|
assertNotNull(flow.globalLastHashHex());
|
||||||
recv("AddBlock(" + reqId5 + ")", resp5);
|
assertEquals(64, flow.globalLastHashHex().length());
|
||||||
|
|
||||||
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 байта, как ты утвердил
|
|
||||||
);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
93
src/test/java/test/it/ws/WsJsonRoundtripClient.java
Normal file
93
src/test/java/test/it/ws/WsJsonRoundtripClient.java
Normal file
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user