Всё ещё не работает проверка линий.
Переделал первые два теста! Третий (АддБлокс) ещё не работает
This commit is contained in:
AidarKC 2025-12-29 13:16:00 +03:00
parent 795341dd8d
commit 3f374f48e1
15 changed files with 2694 additions and 2572 deletions

View File

@ -1,9 +1,9 @@
import shine.db.DatabaseInitializer;
public class CreateNewDatabase {
public static void main(String[] args) {
// Просто прокидываем управление в DatabaseInitializer
DatabaseInitializer.createNewDB(args);
}
}
//import shine.db.DatabaseInitializer;
//
//public class CreateNewDatabase {
//
// public static void main(String[] args) {
// // Просто прокидываем управление в DatabaseInitializer
// DatabaseInitializer.createNewDB(args);
// }
//}

View File

@ -1,126 +1,126 @@
package Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocket.Listener;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public class TestJsonWsClient2 {
public static void main(String[] args) throws Exception {
String uri = "ws://localhost:7070/ws";
String jsonRequestRefreshSession = """
{
"op": "RefreshSession",
"requestId": "test-1",
"payload": {
"sessionId": 123,
"sessionPwd": "test-password"
}
}
""";
String jsonRequestAddUser = """
{
"op": "AddUser",
"requestId": "test-add-1",
"payload": {
"login": "anya1111",
"loginId": 100211,
"bchId": 4222,
"pubkey0": "PUB0",
"pubkey1": "PUB1",
"bchLimit": 1000000
}
}
""";
String jsonRequestAuthChallenge = """
{
"op": "AuthChallenge",
"requestId": "test-auth-1",
"payload": {
"login": "anya1111"
}
}
""";
// Что тестируем сейчас:
String jsonRequest = jsonRequestAuthChallenge;
// String jsonRequest = jsonRequestRefreshSession;
// String jsonRequest = jsonRequestAddUser;
System.out.println("Подключаемся к " + uri);
CountDownLatch latch = new CountDownLatch(1);
HttpClient client = HttpClient.newHttpClient();
WebSocket webSocket = client.newWebSocketBuilder()
.buildAsync(URI.create(uri), new Listener() {
// 0 ещё ничего не получили
// 1 получили 1-й ответ, отправили повторно
// 2 получили 2-й ответ, закрываемся
private int responsesCount = 0;
@Override
public void onOpen(WebSocket webSocket) {
System.out.println("✅ WebSocket подключен");
System.out.println("📤 Отправляем JSON-запрос (1 раз):");
System.out.println(jsonRequest);
webSocket.sendText(jsonRequest, true);
Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data,
boolean last) {
String message = data.toString();
responsesCount++;
System.out.println("📥 Получен TEXT-ответ #" + responsesCount + " от сервера:");
System.out.println(message);
if (responsesCount == 1) {
// После первого ответа отправляем тот же запрос ещё раз
System.out.println("📤 Отправляем JSON-запрос второй раз:");
System.out.println(jsonRequest);
webSocket.sendText(jsonRequest, true);
} else {
// После второго ответа закрываем соединение
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done");
latch.countDown();
}
return Listener.super.onText(webSocket, data, last);
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
error.printStackTrace(System.out);
latch.countDown();
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
latch.countDown();
return Listener.super.onClose(webSocket, statusCode, reason);
}
}).join();
// Ждём, пока получим ответ/ошибку/закрытие
latch.await();
System.out.println("Тест завершён, выходим.");
}
}
//package Test;
//
//import java.net.URI;
//import java.net.http.HttpClient;
//import java.net.http.WebSocket;
//import java.net.http.WebSocket.Listener;
//import java.util.concurrent.CompletionStage;
//import java.util.concurrent.CountDownLatch;
//
//public class TestJsonWsClient2 {
//
// public static void main(String[] args) throws Exception {
// String uri = "ws://localhost:7070/ws";
//
// String jsonRequestRefreshSession = """
// {
// "op": "RefreshSession",
// "requestId": "test-1",
// "payload": {
// "sessionId": 123,
// "sessionPwd": "test-password"
// }
// }
// """;
//
// String jsonRequestAddUser = """
// {
// "op": "AddUser",
// "requestId": "test-add-1",
// "payload": {
// "login": "anya1111",
// "loginId": 100211,
// "bchId": 4222,
// "pubkey0": "PUB0",
// "pubkey1": "PUB1",
// "bchLimit": 1000000
// }
// }
// """;
//
// String jsonRequestAuthChallenge = """
// {
// "op": "AuthChallenge",
// "requestId": "test-auth-1",
// "payload": {
// "login": "anya1111"
// }
// }
// """;
//
// // Что тестируем сейчас:
// String jsonRequest = jsonRequestAuthChallenge;
//// String jsonRequest = jsonRequestRefreshSession;
//// String jsonRequest = jsonRequestAddUser;
//
// System.out.println("Подключаемся к " + uri);
//
// CountDownLatch latch = new CountDownLatch(1);
//
// HttpClient client = HttpClient.newHttpClient();
//
// WebSocket webSocket = client.newWebSocketBuilder()
// .buildAsync(URI.create(uri), new Listener() {
//
// // 0 ещё ничего не получили
// // 1 получили 1-й ответ, отправили повторно
// // 2 получили 2-й ответ, закрываемся
// private int responsesCount = 0;
//
// @Override
// public void onOpen(WebSocket webSocket) {
// System.out.println("✅ WebSocket подключен");
//
// System.out.println("📤 Отправляем JSON-запрос (1 раз):");
// System.out.println(jsonRequest);
//
// webSocket.sendText(jsonRequest, true);
// Listener.super.onOpen(webSocket);
// }
//
// @Override
// public CompletionStage<?> onText(WebSocket webSocket,
// CharSequence data,
// boolean last) {
// String message = data.toString();
// responsesCount++;
//
// System.out.println("📥 Получен TEXT-ответ #" + responsesCount + " от сервера:");
// System.out.println(message);
//
// if (responsesCount == 1) {
// // После первого ответа отправляем тот же запрос ещё раз
// System.out.println("📤 Отправляем JSON-запрос второй раз:");
// System.out.println(jsonRequest);
// webSocket.sendText(jsonRequest, true);
// } else {
// // После второго ответа закрываем соединение
// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done");
// latch.countDown();
// }
//
// return Listener.super.onText(webSocket, data, last);
// }
//
// @Override
// public void onError(WebSocket webSocket, Throwable error) {
// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
// error.printStackTrace(System.out);
// latch.countDown();
// }
//
// @Override
// public CompletionStage<?> onClose(WebSocket webSocket,
// int statusCode,
// String reason) {
// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
// latch.countDown();
// return Listener.super.onClose(webSocket, statusCode, reason);
// }
// }).join();
//
// // Ждём, пока получим ответ/ошибку/закрытие
// latch.await();
// System.out.println("Тест завершён, выходим.");
// }
//}

View File

@ -1,317 +1,317 @@
package Test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import utils.crypto.Ed25519Util;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import blockchain.BchCryptoVerifier;
import blockchain.BchBlockEntry;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public class Test_AddBlock_new_NoAuth {
private static final String WS_URI = "ws://localhost:7070/ws";
private static final ObjectMapper JSON = new ObjectMapper();
private static final String TEST_LOGIN = "anya24";
// По твоему правилу: blockchainName = login + 4 цифры
private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
private static final byte[] LOGIN_PRIV_KEY;
private static final byte[] LOGIN_PUB_KEY;
static {
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
}
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
HttpClient client = HttpClient.newHttpClient();
client.newWebSocketBuilder()
.buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
private int step = 0;
// Эти значения обновим ПО ОТВЕТУ сервера на header
private String lastGlobalHashHex = ZERO64;
private String lastLineHashHex = ZERO64;
@Override
public void onOpen(WebSocket ws) {
System.out.println("✅ WS connected: " + WS_URI);
ws.request(1);
// 1) HEADER (global=0, line=0, lineNumber=0)
byte[] headerFull = buildHeaderBlockFullBytes(
/*global*/0,
/*lineIndex*/(short)0,
/*lineBlock*/0,
/*prevGlobal*/ZERO32,
/*prevLine*/ZERO32
);
String json = buildAddBlockJson(
"test-add-header",
TEST_BCH_NAME,
0,
ZERO64, // prevGlobalHash для первого блока нули
base64(headerFull)
);
System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
ws.sendText(json, true);
}
@Override
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
String msg = data.toString();
System.out.println("\n📥 RECV:\n" + msg);
System.out.println("-----------------------------------------------------");
try {
int status = extractStatus(msg);
if (step == 0) {
if (status != 200) {
System.out.println("❌ HEADER rejected, status=" + status);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
return CompletableFuture.completedFuture(null);
}
// Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
System.out.println("❌ No serverLastGlobalHash in response");
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
return CompletableFuture.completedFuture(null);
}
if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
// fallback: пусть будет как global (если сервер так хранит)
serverLastLineHash = serverLastGlobalHash;
}
lastGlobalHashHex = serverLastGlobalHash;
lastLineHashHex = serverLastLineHash;
byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
byte[] prevLine32 = hexToBytes32(lastLineHashHex);
// 2) TEXT (global=1, line=0, lineNumber=1)
byte[] textFull = buildTextBlockFullBytes(
/*global*/1,
/*lineIndex*/(short)0,
/*lineBlock*/1,
prevGlobal32,
prevLine32,
"Hello from test client"
);
String json2 = buildAddBlockJson(
"test-add-text",
TEST_BCH_NAME,
1,
lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
base64(textFull)
);
System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
step = 1;
ws.sendText(json2, true);
} else if (step == 1) {
if (status != 200) {
System.out.println("❌ TEXT rejected, status=" + status);
} else {
System.out.println("✅ Done. Closing.");
}
ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
}
} catch (Exception e) {
e.printStackTrace(System.out);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
}
ws.request(1);
return CompletableFuture.completedFuture(null);
}
@Override
public void onError(WebSocket ws, Throwable error) {
System.out.println("❌ WS error: " + error.getMessage());
error.printStackTrace(System.out);
latch.countDown();
}
@Override
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
latch.countDown();
return CompletableFuture.completedFuture(null);
}
}).join();
latch.await();
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
HeaderBody body = new HeaderBody(
TEST_LOGIN
);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildTextBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
int recordSize =
BchBlockEntry.RAW_HEADER_SIZE +
bodyBytes.length +
BchBlockEntry.SIGNATURE_LEN +
BchBlockEntry.HASH_LEN;
byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length)
.order(ByteOrder.BIG_ENDIAN)
.putInt(recordSize)
.putInt(globalNumber)
.putLong(ts)
.putShort(lineIndex)
.putInt(lineBlockNumber)
.put(bodyBytes)
.array();
byte[] preimage = BchCryptoVerifier.buildPreimage(
TEST_LOGIN,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
// если у тебя подпись должна быть по preimage меняй тут
byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
return new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
}
// =================================================================================
// JSON BUILD
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"login": "%s",
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
// =================================================================================
// HELPERS
// =================================================================================
private static int extractStatus(String json) {
try {
JsonNode root = JSON.readTree(json);
if (root.has("status")) return root.get("status").asInt();
} catch (Exception ignore) {}
return -1;
}
private static String extractPayloadString(String json, String field) {
try {
JsonNode root = JSON.readTree(json);
JsonNode payload = root.get("payload");
if (payload != null && payload.has(field)) {
return payload.get(field).asText();
}
} catch (Exception ignore) {}
return null;
}
private static String base64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
private static byte[] hexToBytes32(String hex) {
if (hex == null) throw new IllegalArgumentException("hex is null");
String s = hex.trim();
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
}
//package Test;
//
//import com.fasterxml.jackson.databind.JsonNode;
//import com.fasterxml.jackson.databind.ObjectMapper;
//import utils.crypto.Ed25519Util;
//import blockchain.body.HeaderBody;
//import blockchain.body.TextBody;
//import blockchain.BchCryptoVerifier;
//import blockchain.BchBlockEntry;
//
//import java.net.URI;
//import java.net.http.HttpClient;
//import java.net.http.WebSocket;
//import java.nio.ByteBuffer;
//import java.nio.ByteOrder;
//import java.util.Base64;
//import java.util.concurrent.CompletableFuture;
//import java.util.concurrent.CompletionStage;
//import java.util.concurrent.CountDownLatch;
//
//public class Test_AddBlock_new_NoAuth {
//
// private static final String WS_URI = "ws://localhost:7070/ws";
// private static final ObjectMapper JSON = new ObjectMapper();
//
// private static final String TEST_LOGIN = "anya24";
// // По твоему правилу: blockchainName = login + 4 цифры
// private static final String TEST_BCH_NAME = TEST_LOGIN + "0001";
//
// private static final byte[] LOGIN_PRIV_KEY;
// private static final byte[] LOGIN_PUB_KEY;
//
// static {
// LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
// LOGIN_PUB_KEY = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
// }
//
// private static final byte[] ZERO32 = new byte[32];
// private static final String ZERO64 = "0".repeat(64);
//
// public static void main(String[] args) throws Exception {
// CountDownLatch latch = new CountDownLatch(1);
// HttpClient client = HttpClient.newHttpClient();
//
// client.newWebSocketBuilder()
// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() {
//
// private int step = 0;
//
// // Эти значения обновим ПО ОТВЕТУ сервера на header
// private String lastGlobalHashHex = ZERO64;
// private String lastLineHashHex = ZERO64;
//
// @Override
// public void onOpen(WebSocket ws) {
// System.out.println("✅ WS connected: " + WS_URI);
// ws.request(1);
//
// // 1) HEADER (global=0, line=0, lineNumber=0)
// byte[] headerFull = buildHeaderBlockFullBytes(
// /*global*/0,
// /*lineIndex*/(short)0,
// /*lineBlock*/0,
// /*prevGlobal*/ZERO32,
// /*prevLine*/ZERO32
// );
//
// String json = buildAddBlockJson(
// "test-add-header",
// TEST_BCH_NAME,
// 0,
// ZERO64, // prevGlobalHash для первого блока нули
// base64(headerFull)
// );
//
// System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
// ws.sendText(json, true);
// }
//
// @Override
// public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
// String msg = data.toString();
// System.out.println("\n📥 RECV:\n" + msg);
// System.out.println("-----------------------------------------------------");
//
// try {
// int status = extractStatus(msg);
//
// if (step == 0) {
// if (status != 200) {
// System.out.println("❌ HEADER rejected, status=" + status);
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
// return CompletableFuture.completedFuture(null);
// }
//
// // Берём ИМЕННО ТОТ хэш, который сервер сохранил в state
// String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
// String serverLastLineHash = extractPayloadString(msg, "serverLastLineHash");
//
// if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
// System.out.println("❌ No serverLastGlobalHash in response");
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
// return CompletableFuture.completedFuture(null);
// }
// if (serverLastLineHash == null || serverLastLineHash.isBlank()) {
// // fallback: пусть будет как global (если сервер так хранит)
// serverLastLineHash = serverLastGlobalHash;
// }
//
// lastGlobalHashHex = serverLastGlobalHash;
// lastLineHashHex = serverLastLineHash;
//
// byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
// byte[] prevLine32 = hexToBytes32(lastLineHashHex);
//
// // 2) TEXT (global=1, line=0, lineNumber=1)
// byte[] textFull = buildTextBlockFullBytes(
// /*global*/1,
// /*lineIndex*/(short)0,
// /*lineBlock*/1,
// prevGlobal32,
// prevLine32,
// "Hello from test client"
// );
//
// String json2 = buildAddBlockJson(
// "test-add-text",
// TEST_BCH_NAME,
// 1,
// lastGlobalHashHex, // prevGlobalHash = хэш header'а из ответа сервера
// base64(textFull)
// );
//
// System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
// step = 1;
// ws.sendText(json2, true);
//
// } else if (step == 1) {
// if (status != 200) {
// System.out.println("❌ TEXT rejected, status=" + status);
// } else {
// System.out.println("✅ Done. Closing.");
// }
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
// }
//
// } catch (Exception e) {
// e.printStackTrace(System.out);
// ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
// }
//
// ws.request(1);
// return CompletableFuture.completedFuture(null);
// }
//
// @Override
// public void onError(WebSocket ws, Throwable error) {
// System.out.println("❌ WS error: " + error.getMessage());
// error.printStackTrace(System.out);
// latch.countDown();
// }
//
// @Override
// public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
// System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
// latch.countDown();
// return CompletableFuture.completedFuture(null);
// }
// }).join();
//
// latch.await();
// }
//
// // =================================================================================
// // BUILD BLOCKS
// // =================================================================================
//
// private static byte[] buildHeaderBlockFullBytes(int globalNumber,
// short lineIndex,
// int lineBlockNumber,
// byte[] prevGlobalHash32,
// byte[] prevLineHash32) {
//
// HeaderBody body = new HeaderBody(
// TEST_LOGIN
// );
// byte[] bodyBytes = body.toBytes();
//
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
// }
//
// private static byte[] buildTextBlockFullBytes(int globalNumber,
// short lineIndex,
// int lineBlockNumber,
// byte[] prevGlobalHash32,
// byte[] prevLineHash32,
// String text) {
//
// TextBody body = new TextBody(text);
// byte[] bodyBytes = body.toBytes();
//
// return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
// }
//
// private static byte[] buildSignedBlockFullBytes(int globalNumber,
// short lineIndex,
// int lineBlockNumber,
// byte[] bodyBytes,
// byte[] prevGlobalHash32,
// byte[] prevLineHash32) {
//
// long ts = System.currentTimeMillis() / 1000L;
//
// int recordSize =
// BchBlockEntry.RAW_HEADER_SIZE +
// bodyBytes.length +
// BchBlockEntry.SIGNATURE_LEN +
// BchBlockEntry.HASH_LEN;
//
// byte[] rawBytes = ByteBuffer.allocate(BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length)
// .order(ByteOrder.BIG_ENDIAN)
// .putInt(recordSize)
// .putInt(globalNumber)
// .putLong(ts)
// .putShort(lineIndex)
// .putInt(lineBlockNumber)
// .put(bodyBytes)
// .array();
//
// byte[] preimage = BchCryptoVerifier.buildPreimage(
// TEST_LOGIN,
// prevGlobalHash32,
// prevLineHash32,
// rawBytes
// );
//
// byte[] hash32 = BchCryptoVerifier.sha256(preimage);
//
// // если у тебя подпись должна быть по preimage меняй тут
// byte[] signature64 = Ed25519Util.sign(hash32, LOGIN_PRIV_KEY);
//
// return new BchBlockEntry(
// globalNumber,
// ts,
// lineIndex,
// lineBlockNumber,
// bodyBytes,
// signature64,
// hash32
// ).toBytes();
// }
//
// // =================================================================================
// // JSON BUILD
// // =================================================================================
//
// private static String buildAddBlockJson(String requestId,
// String blockchainName,
// int globalNumber,
// String prevGlobalHashHex,
// String blockBytesB64) {
// return """
// {
// "op": "AddBlock",
// "requestId": "%s",
// "payload": {
// "login": "%s",
// "blockchainName": "%s",
// "globalNumber": %d,
// "prevGlobalHash": "%s",
// "blockBytesB64": "%s"
// }
// }
// """.formatted(requestId, TEST_LOGIN, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
// }
//
// // =================================================================================
// // HELPERS
// // =================================================================================
//
// private static int extractStatus(String json) {
// try {
// JsonNode root = JSON.readTree(json);
// if (root.has("status")) return root.get("status").asInt();
// } catch (Exception ignore) {}
// return -1;
// }
//
// private static String extractPayloadString(String json, String field) {
// try {
// JsonNode root = JSON.readTree(json);
// JsonNode payload = root.get("payload");
// if (payload != null && payload.has(field)) {
// return payload.get(field).asText();
// }
// } catch (Exception ignore) {}
// return null;
// }
//
// private static String base64(byte[] bytes) {
// return Base64.getEncoder().encodeToString(bytes);
// }
//
// private static byte[] hexToBytes32(String hex) {
// if (hex == null) throw new IllegalArgumentException("hex is null");
// String s = hex.trim();
// if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
// byte[] out = new byte[32];
// for (int i = 0; i < 32; i++) {
// int hi = Character.digit(s.charAt(i * 2), 16);
// int lo = Character.digit(s.charAt(i * 2 + 1), 16);
// if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
// out[i] = (byte) ((hi << 4) | lo);
// }
// return out;
// }
//}

File diff suppressed because it is too large Load Diff

View File

@ -1,109 +1,109 @@
package Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocket.Listener;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public class Test_SessionRefreshClient {
// Адрес сервера
private static final String WS_URI = "ws://localhost:7070/ws";
// ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ====
private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId
private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd
// =============================================
public static void main(String[] args) throws Exception {
System.out.println("Подключаемся к " + WS_URI);
CountDownLatch latch = new CountDownLatch(1);
HttpClient client = HttpClient.newHttpClient();
ClientListener listener = new ClientListener(latch);
client.newWebSocketBuilder()
.buildAsync(URI.create(WS_URI), listener)
.join();
latch.await();
System.out.println("Тест RefreshSession завершён, выходим.");
}
private static String buildRefreshSessionJson() {
return """
{
"op": "RefreshSession",
"requestId": "test-session-refresh-1",
"payload": {
"sessionId": %d,
"sessionPwd": "%s"
}
}
""".formatted(SESSION_ID, SESSION_PWD);
}
private static class ClientListener implements Listener {
private final CountDownLatch latch;
ClientListener(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void onOpen(WebSocket webSocket) {
System.out.println("✅ WebSocket подключен");
webSocket.request(1); // разрешаем принимать одно сообщение
// сразу отправляем запрос RefreshSession
String json = buildRefreshSessionJson();
System.out.println();
System.out.println("📤 Отправляем RefreshSession:");
System.out.println(json);
webSocket.sendText(json, true);
Listener.super.onOpen(webSocket);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data,
boolean last) {
System.out.println("📥 Ответ от сервера:");
System.out.println(data.toString());
System.out.println("-----------------------------------------------------");
// После одного ответа просто закрываем соединение
System.out.println("✅ Получен ответ на RefreshSession, закрываем соединение");
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done");
// запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся)
webSocket.request(1);
return CompletableFuture.completedFuture(null);
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
error.printStackTrace(System.out);
latch.countDown();
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
latch.countDown();
return CompletableFuture.completedFuture(null);
}
}
}
//package Test;
//
//import java.net.URI;
//import java.net.http.HttpClient;
//import java.net.http.WebSocket;
//import java.net.http.WebSocket.Listener;
//import java.util.concurrent.CompletableFuture;
//import java.util.concurrent.CompletionStage;
//import java.util.concurrent.CountDownLatch;
//
//public class Test_SessionRefreshClient {
//
// // Адрес сервера
// private static final String WS_URI = "ws://localhost:7070/ws";
//
// // ==== ЗДЕСЬ ПОДСТАВИШЬ СВОИ ДАННЫЕ СЕССИИ ====
// private static final long SESSION_ID = 7599553208996461137L; // TODO: подставь реальный sessionId
// private static final String SESSION_PWD = "11b3508f37ae7b41816f42031b90"; // TODO: подставь реальный sessionPwd
// // =============================================
//
// public static void main(String[] args) throws Exception {
// System.out.println("Подключаемся к " + WS_URI);
//
// CountDownLatch latch = new CountDownLatch(1);
//
// HttpClient client = HttpClient.newHttpClient();
//
// ClientListener listener = new ClientListener(latch);
//
// client.newWebSocketBuilder()
// .buildAsync(URI.create(WS_URI), listener)
// .join();
//
// latch.await();
// System.out.println("Тест RefreshSession завершён, выходим.");
// }
//
// private static String buildRefreshSessionJson() {
// return """
// {
// "op": "RefreshSession",
// "requestId": "test-session-refresh-1",
// "payload": {
// "sessionId": %d,
// "sessionPwd": "%s"
// }
// }
// """.formatted(SESSION_ID, SESSION_PWD);
// }
//
// private static class ClientListener implements Listener {
//
// private final CountDownLatch latch;
//
// ClientListener(CountDownLatch latch) {
// this.latch = latch;
// }
//
// @Override
// public void onOpen(WebSocket webSocket) {
// System.out.println("✅ WebSocket подключен");
//
// webSocket.request(1); // разрешаем принимать одно сообщение
//
// // сразу отправляем запрос RefreshSession
// String json = buildRefreshSessionJson();
// System.out.println();
// System.out.println("📤 Отправляем RefreshSession:");
// System.out.println(json);
// webSocket.sendText(json, true);
//
// Listener.super.onOpen(webSocket);
// }
//
// @Override
// public CompletionStage<?> onText(WebSocket webSocket,
// CharSequence data,
// boolean last) {
// System.out.println("📥 Ответ от сервера:");
// System.out.println(data.toString());
// System.out.println("-----------------------------------------------------");
//
// // После одного ответа просто закрываем соединение
// System.out.println("✅ Получен ответ на RefreshSession, закрываем соединение");
// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session refresh test done");
//
// // запрашиваем следующее сообщение на всякий случай (хотя уже закрываемся)
// webSocket.request(1);
//
// return CompletableFuture.completedFuture(null);
// }
//
// @Override
// public void onError(WebSocket webSocket, Throwable error) {
// System.out.println("❌ Ошибка WebSocket-клиента: " + error.getMessage());
// error.printStackTrace(System.out);
// latch.countDown();
// }
//
// @Override
// public CompletionStage<?> onClose(WebSocket webSocket,
// int statusCode,
// String reason) {
// System.out.println("🔚 Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
// latch.countDown();
// return CompletableFuture.completedFuture(null);
// }
// }
//}

View File

@ -1,5 +1,5 @@
package Test;
public class test1 {
}
//package Test;
//
//public class test1 {
//
//}

View File

@ -7,100 +7,89 @@ import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
/**
* IT_01_AddUser
*
* Можно запускать:
* 1) как JUnit тест (через Suite или выборочно)
* 2) вручную как standalone:
* - main()
* - или через IT_RunAllMain / IT_RunAllCleanMain
*
* Главная цель:
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
* - и иметь main() для запуска одного теста
*/
public class IT_01_AddUser {
// ANSI цвета
private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m";
private static final String Y = "\u001B[33m";
private static final String RED = "\u001B[31m";
private static final String C = "\u001B[36m";
private static void line() {
System.out.println(C + "------------------------------------------------------------" + R);
}
private static void title(String s) {
System.out.println(C + "\n============================================================" + R);
System.out.println(C + s + R);
System.out.println(C + "============================================================\n" + R);
}
private static void ok(String s) {
System.out.println(G + "" + s + R);
}
private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R);
}
public static void main(String[] args) {
// чтобы тест можно было запускать вообще без JUnit
ItRunContext.initIfNeeded();
new IT_01_AddUser().addUser_shouldReturn200_orAlreadyExists();
int failed = run();
System.exit(failed);
}
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
public static int run() {
return TestLog.runOne("IT_01_AddUser", IT_01_AddUser::testBody);
}
@Test
void addUser_shouldReturn200_orAlreadyExists() {
// JUnit-режим: пусть падает через assert/fail как обычно
testBody();
}
private static void testBody() {
ItRunContext.initIfNeeded();
title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println(" blockchainName = " + TestConfig.BCH_NAME());
System.out.println("Ожидание:");
System.out.println(" - 200 (создан)");
System.out.println(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
TestLog.title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
TestLog.info("Используем:");
TestLog.info(" login = " + TestConfig.LOGIN());
TestLog.info(" blockchainName = " + TestConfig.BCH_NAME());
TestLog.info("Ожидание:");
TestLog.info(" - 200 (создан)");
TestLog.info(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "it-adduser-1";
String reqJson = JsonBuilders.addUser(reqId);
System.out.println("📤 Отправляем AddUser запрос:");
System.out.println(reqJson);
line();
TestLog.info("📤 Отправляем AddUser запрос:");
TestLog.info(reqJson);
TestLog.line();
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
System.out.println("📥 Ответ сервера:");
System.out.println(resp);
line();
TestLog.info("📥 Ответ сервера:");
TestLog.info(resp);
TestLog.line();
int st = JsonParsers.status(resp);
System.out.println(" status=" + st);
TestLog.info(" status=" + st);
boolean created = (st == 200);
boolean already = (st == 409);
if (already) {
String code = JsonParsers.errorCode(resp);
System.out.println(" server_code=" + code);
TestLog.info(" server_code=" + code);
try {
assertEquals("USER_ALREADY_EXISTS", code,
"Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp);
ok("409 получен корректно: USER_ALREADY_EXISTS");
} catch (AssertionError ae) {
boom("409 получен, но code не тот. " + ae.getMessage());
throw ae;
}
TestLog.ok("409 получен корректно: USER_ALREADY_EXISTS");
}
if (created) {
ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)");
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)");
} else if (already) {
ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)");
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)");
} else {
boom("Неожиданный status=" + st + ", resp=" + resp);
TestLog.boom("Неожиданный status=" + st + ", resp=" + resp);
fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp);
}
} catch (AssertionError | RuntimeException e) {
boom("ТЕСТ УПАЛ: AddUserIT. Причина: " + e.getMessage());
throw e;
}
}
}

View File

@ -9,96 +9,60 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
/**
* IT_02_Sessions
*
* Можно запускать:
* 1) как JUnit тест (через Suite или выборочно)
* 2) вручную как standalone:
* - main()
* - или через IT_RunAllMain / IT_RunAllCleanMain
*
* Главная цель:
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
* - и иметь main() для запуска одного теста
*/
public class IT_02_Sessions {
// ANSI цвета
private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m";
private static final String Y = "\u001B[33m";
private static final String RED = "\u001B[31m";
private static final String C = "\u001B[36m";
private static void line() {
System.out.println(C + "------------------------------------------------------------" + R);
}
private static void title(String s) {
System.out.println(C + "\n============================================================" + R);
System.out.println(C + s + R);
System.out.println(C + "============================================================\n" + R);
}
private static void stepTitle(String s) {
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
}
private static void ok(String s) {
System.out.println(G + "" + s + R);
}
private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R);
}
private static void send(String op, String json) {
System.out.println("📤 [" + op + "] Request JSON:");
System.out.println(json);
line();
}
private static void recv(String op, String json) {
System.out.println("📥 [" + op + "] Response JSON:");
System.out.println(json);
line();
}
private static void assert200(String op, String resp) {
int st = JsonParsers.status(resp);
try {
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
ok(op + ": status=200");
} catch (AssertionError ae) {
boom(op + ": ожидали 200, но получили " + st);
throw ae;
}
}
public static void main(String[] args) {
ItRunContext.initIfNeeded();
ensureUserExists();
new IT_02_Sessions().sessions_flow_shouldCreateListRefreshCloseCorrectly();
int failed = run();
System.exit(failed);
}
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
public static int run() {
return TestLog.runOne("IT_02_Sessions", IT_02_Sessions::testBodyStandalone);
}
@BeforeAll
static void ensureUserExists() {
ItRunContext.initIfNeeded();
title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
TestLog.title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "it-adduser-beforeall";
String reqJson = JsonBuilders.addUser(reqId);
send("AddUser(BeforeAll)", reqJson);
TestLog.send("AddUser(BeforeAll)", reqJson);
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
recv("AddUser(BeforeAll)", resp);
TestLog.recv("AddUser(BeforeAll)", resp);
int st = JsonParsers.status(resp);
if (st == 200) {
ok("BeforeAll: пользователь создан/добавлен (status=200)");
TestLog.ok("BeforeAll: пользователь создан/добавлен (status=200)");
} else if (st == 409) {
String code = JsonParsers.errorCode(resp);
if ("USER_ALREADY_EXISTS".equals(code)) {
ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
TestLog.ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
} else {
boom("BeforeAll: status=409, но code неожиданный: " + code);
TestLog.boom("BeforeAll: status=409, но code неожиданный: " + code);
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
}
} else {
boom("BeforeAll: предусловие не выполнено. status=" + st);
TestLog.boom("BeforeAll: предусловие не выполнено. status=" + st);
fail("User precondition failed. status=" + st + ", resp=" + resp);
}
}
@ -106,160 +70,184 @@ public class IT_02_Sessions {
@Test
void sessions_flow_shouldCreateListRefreshCloseCorrectly() {
// JUnit-режим: пусть падает через assert/fail как обычно
testBodyJUnit();
}
/**
* Standalone-режим: тут мы сами вызываем предусловие ensureUserExists(),
* потому что @BeforeAll сработает только в JUnit.
*/
private static void testBodyStandalone() {
ensureUserExists();
testBodyJUnit();
}
private static void testBodyJUnit() {
ItRunContext.initIfNeeded();
title("SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println("Ожидание сценария:");
System.out.println(" 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession");
System.out.println(" 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2");
System.out.println(" 3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) → должны быть SESSION1 и SESSION2");
System.out.println(" 4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2");
System.out.println(" 5) Проверяем ListSessions (AUTH_IN_PROGRESS) → осталась только SESSION1");
System.out.println(" 6) Закрываем SESSION1 в AUTH_IN_PROGRESS");
System.out.println(" 7) Проверяем ListSessions → пусто\n");
TestLog.titleBlock("""
SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)
Используем:
login = %s
Ожидание сценария:
1) Создаём SESSION1 через AuthChallenge + CreateAuthSession
2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) должны быть SESSION1 и SESSION2
3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) должны быть SESSION1 и SESSION2
4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2
5) Проверяем ListSessions (AUTH_IN_PROGRESS) осталась только SESSION1
6) Закрываем SESSION1 в AUTH_IN_PROGRESS
7) Проверяем ListSessions пусто
""".formatted(TestConfig.LOGIN()));
String s1Id, s1Pwd;
String s2Id, s2Pwd;
try {
stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)");
// ===== helpers (локальные, чтобы не раздувать TestLog лишней логикой assert200) =====
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)) {
String r1 = "it-auth-1";
String req1 = JsonBuilders.authChallenge(r1);
send("AuthChallenge#1", req1);
TestLog.send("AuthChallenge#1", req1);
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);
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 storagePwd = TestConfig.fakeStoragePwd();
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));
recv("CreateAuthSession#1", resp2);
TestLog.recv("CreateAuthSession#1", resp2);
assert200("CreateAuthSession#1", resp2);
assert200.accept("CreateAuthSession#1", resp2);
s1Id = JsonParsers.sessionId(resp2);
s1Pwd = JsonParsers.sessionPwd(resp2);
assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null");
assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null");
ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
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)) {
String r1 = "it-auth-2";
String req1 = JsonBuilders.authChallenge(r1);
send("AuthChallenge#2", req1);
TestLog.send("AuthChallenge#2", req1);
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);
assertNotNull(nonce);
ok("AuthChallenge#2: authNonce получен: " + nonce);
TestLog.ok("AuthChallenge#2: authNonce получен: " + nonce);
String r2 = "it-create-2";
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));
recv("CreateAuthSession#2", resp2);
TestLog.recv("CreateAuthSession#2", resp2);
assert200("CreateAuthSession#2", resp2);
assert200.accept("CreateAuthSession#2", resp2);
s2Id = JsonParsers.sessionId(resp2);
s2Pwd = JsonParsers.sessionPwd(resp2);
assertNotNull(s2Id);
assertNotNull(s2Pwd);
ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
TestLog.ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
String r3 = "it-list-in-session2";
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));
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);
ok("ListSessions(in SESSION2): sessions=" + ids);
TestLog.ok("ListSessions(in SESSION2): sessions=" + ids);
assertTrue(ids.contains(s1Id), "Must contain session1");
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)) {
String r1 = "it-auth-list";
String req1 = JsonBuilders.authChallenge(r1);
send("AuthChallenge(list)", req1);
TestLog.send("AuthChallenge(list)", req1);
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);
assertNotNull(nonce);
ok("AuthChallenge(list): authNonce=" + nonce);
TestLog.ok("AuthChallenge(list): authNonce=" + nonce);
long timeMs = System.currentTimeMillis();
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 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));
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);
ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids);
TestLog.ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids);
assertTrue(ids.contains(s1Id));
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)) {
String r1 = "it-refresh-s1";
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));
recv("RefreshSession(SESSION1)", resp1);
TestLog.recv("RefreshSession(SESSION1)", resp1);
assert200("RefreshSession(SESSION1)", resp1);
assert200.accept("RefreshSession(SESSION1)", resp1);
assertNotNull(JsonParsers.storagePwd(resp1));
ok("RefreshSession: storagePwd получен");
TestLog.ok("RefreshSession: storagePwd получен");
String r2 = "it-close-s2";
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));
recv("CloseActiveSession(SESSION2)", resp2);
TestLog.recv("CloseActiveSession(SESSION2)", resp2);
assert200("CloseActiveSession(SESSION2)", resp2);
ok("SESSION2 закрыта");
assert200.accept("CloseActiveSession(SESSION2)", resp2);
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)) {
String r1 = "it-auth-list2";
String req1 = JsonBuilders.authChallenge(r1);
send("AuthChallenge(list2)", req1);
TestLog.send("AuthChallenge(list2)", req1);
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);
assertNotNull(nonce);
@ -268,29 +256,29 @@ public class IT_02_Sessions {
String r2 = "it-list-after-close-s2";
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));
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);
ok("ListSessions(after close S2): sessions=" + ids);
TestLog.ok("ListSessions(after close S2): sessions=" + ids);
assertTrue(ids.contains(s1Id));
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)) {
String r1 = "it-auth-close-s1";
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));
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);
assertNotNull(nonce);
@ -299,23 +287,23 @@ public class IT_02_Sessions {
String r2 = "it-close-s1";
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));
recv("CloseActiveSession(SESSION1)", resp2);
TestLog.recv("CloseActiveSession(SESSION1)", resp2);
assert200("CloseActiveSession(SESSION1)", resp2);
ok("SESSION1 закрыта");
assert200.accept("CloseActiveSession(SESSION1)", resp2);
TestLog.ok("SESSION1 закрыта");
}
stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список");
TestLog.stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-list-empty";
String req1 = JsonBuilders.authChallenge(r1);
send("AuthChallenge(list empty)", req1);
TestLog.send("AuthChallenge(list empty)", req1);
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);
assertNotNull(nonce);
@ -324,24 +312,19 @@ public class IT_02_Sessions {
String r2 = "it-list-empty";
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));
recv("ListSessions(empty)", resp2);
TestLog.recv("ListSessions(empty)", resp2);
assert200("ListSessions(empty)", resp2);
assert200.accept("ListSessions(empty)", 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");
ok("Проверка OK: список пуст");
TestLog.ok("Проверка OK: список пуст");
}
ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
} catch (AssertionError | RuntimeException e) {
boom("ТЕСТ УПАЛ: SessionsIT. Причина: " + e.getMessage());
throw e;
}
TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
}
}

View 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());
}
}

View File

@ -1,82 +1,79 @@
package test.it;
import test.it.utils.TestConfig;
import test.it.utils.TestColors;
import test.it.utils.ItRunContext;
import test.it.utils.TestLog;
import test.it.ws.IT_03_AddBlock_NoAuth;
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
/**
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
*
* Делает:
* 1) чистит папку data/
* 2) запускает 3 теста по очереди (через их main)
* 1) запускает тесты по очереди
* 2) печатает итоговый короткий отчёт
*
* Запуск из IDE:
* Run 'main' этого класса
*
* Запуск из консоли:
* ./gradlew testClasses
* java -cp build/classes/java/test:build/resources/test:build/classes/java/main:build/resources/main <тут_свой_classpath> test.it.IT_RunAllMain
* java -cp ... test.it.IT_RunAllMain
*
* (Classpath зависит от твоего Gradle, но в IDE проще всего)
*/
public class IT_RunAllMain {
public static void main(String[] args) {
try {
ItRunContext.initIfNeeded();
banner("ШАГ 0: очистка data/");
cleanupDataDir(TestConfig.DATA_DIR);
int failed = runAll();
banner("ШАГ 1: IT_01_AddUser");
IT_01_AddUser.main(new String[0]);
banner("ШАГ 2: IT_02_Sessions");
IT_02_Sessions.main(new String[0]);
banner("ШАГ 3: IT_03_AddBlock_NoAuth");
IT_03_AddBlock_NoAuth.main(new String[0]);
System.out.println(TestColors.G + "\n✅ ВСЕ 3 IT ТЕСТА УСПЕШНО ЗАВЕРШЕНЫ\n" + TestColors.R);
} catch (Throwable t) {
System.out.println(TestColors.RED + "\n❌ IT_RunAllMain: ПРОГОН УПАЛ\n" + TestColors.R);
t.printStackTrace(System.out);
System.exit(1);
}
// Удобно для CI: код выхода = число упавших тестов
System.exit(failed);
}
private static void banner(String s) {
System.out.println(TestColors.C + "\n============================================================" + TestColors.R);
System.out.println(TestColors.C + s + TestColors.R);
System.out.println(TestColors.C + "============================================================\n" + TestColors.R);
/**
* Основной метод, который возвращает число не пройденных тестов (0 если всё хорошо).
* Его можно вызывать из других раннеров (например, из варианта с очисткой data/).
*/
public static int runAll() {
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 {
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());
return failed;
}
}

View File

@ -34,9 +34,6 @@ public final class TestConfig {
// Любая строка клиента (для логов)
public static final String TEST_CLIENT_INFO = "it-tests";
// Папка данных (которую будет чистить IT_RunAllMain)
public static final String DATA_DIR = "data";
/** login для прогона (по умолчанию DEFAULT_LOGIN, можно переопределить -Dit.login=...). */
public static String LOGIN() {
return System.getProperty("it.login", DEFAULT_LOGIN);

View File

@ -1,32 +1,128 @@
package test.it.utils;
/**
* TestLog единое место для:
* - ANSI цветов
* - стандартных красивых сообщений (title/ok/boom/line/step/send/recv)
*
* Включение/выключение подробных логов:
* -Dit.verbose=false
*
* По умолчанию verbose=true (удобно для ручного прогона).
*/
public final class TestLog {
private TestLog() {}
// ============================
// VERBOSE
// ============================
// включается так: ./gradlew test -Dit.verbose=true
public static final boolean VERBOSE = true; //Boolean.parseBoolean(System.getProperty("it.verbose", "false"));
public static final boolean VERBOSE = Boolean.parseBoolean(System.getProperty("it.verbose", "true"));
// ============================
// ANSI COLORS
// ============================
public static final String R = "\u001B[0m";
public static final String G = "\u001B[32m";
public static final String Y = "\u001B[33m";
public static final String RED = "\u001B[31m";
public static final String C = "\u001B[36m";
// ============================
// BASIC OUTPUT
// ============================
public static void info(String s) {
if (VERBOSE) System.out.println(s);
}
public static void section(String title) {
public static void line() {
if (!VERBOSE) return;
System.out.println("\n\n==================================================");
System.out.println(title);
System.out.println("==================================================\n");
System.out.println(C + "------------------------------------------------------------" + R);
}
public static void req(String title, String json) {
/** Короткое заглавие. */
public static void title(String s) {
if (!VERBOSE) return;
System.out.println("\n📤 " + title);
System.out.println(json);
System.out.println(C + "\n============================================================" + R);
System.out.println(C + s + R);
System.out.println(C + "============================================================\n" + R);
}
public static void resp(String title, String json) {
/**
* Длинное заглавие (под многострочный текст).
*
* Пример:
* TestLog.titleBlock("""
* ТЕСТ: ...
* Ожидание: ...
* """);
*/
public static void titleBlock(String multiLineText) {
if (!VERBOSE) return;
System.out.println("\n📥 " + title);
System.out.println(C + "\n============================================================" + R);
System.out.println(C + multiLineText + R);
System.out.println(C + "============================================================\n" + R);
}
public static void stepTitle(String s) {
if (!VERBOSE) return;
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
}
public static void ok(String s) {
if (!VERBOSE) return;
System.out.println(G + "" + s + R);
}
public static void warn(String s) {
if (!VERBOSE) return;
System.out.println(Y + "⚠️ " + s + R);
}
public static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R);
}
public static void send(String op, String json) {
if (!VERBOSE) return;
System.out.println("📤 [" + op + "] Request JSON:");
System.out.println(json);
System.out.println("-----------------------------------------------------");
line();
}
public static void recv(String op, String json) {
if (!VERBOSE) return;
System.out.println("📥 [" + op + "] Response JSON:");
System.out.println(json);
line();
}
// ============================
// RUN HELPERS
// ============================
/**
* Запуск тестового тела (без JUnit).
* Возвращает 0 если ок, 1 если упал.
*
* Важно:
* - здесь мы НЕ глотаем ошибку: печатаем и возвращаем код
* - раннер суммирует количество упавших тестов
*/
public static int runOne(String testName, Runnable body) {
try {
body.run();
ok(testName + ": OK");
return 0;
} catch (Throwable t) {
boom(testName + ": FAIL. Причина: " + t.getMessage());
if (VERBOSE) t.printStackTrace(System.out);
return 1;
}
}
}

View 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);
}
}

View File

@ -1,23 +1,13 @@
package test.it.ws;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody;
import blockchain.body.ReactionBody;
import blockchain.body.TextBody;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import test.it.utils.ItRunContext;
import test.it.utils.JsonBuilders;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.WsTestClient;
import utils.crypto.Ed25519Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Duration;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
@ -38,592 +28,81 @@ import static org.junit.jupiter.api.Assertions.*;
* - line 0: нулевой блок (HEADER) один на весь блокчейн (глобальный 0)
* - line 1 и line 2: первый блок каждой линии ссылается prevLineHash на hash(нулевого блока)
*
* В этом тесте мы ведём 2 массива:
* В этом тесте состояние ведёт AddBlockFlow:
* - lineLastNumber[line] сколько блоков в линии (то есть последний lineNum)
* - lineLastHashHex[line] hash последнего блока линии (HEX64)
*/
public class IT_03_AddBlock_NoAuth {
// ANSI цвета
private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m";
private static final String RED = "\u001B[31m";
private static final String C = "\u001B[36m";
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
private static final short LINE_HEADER = 0;
private static final short LINE_TEXT = 1;
private static final short LINE_REACT = 2;
public static void main(String[] args) {
ItRunContext.initIfNeeded();
ensureUserExists();
new IT_03_AddBlock_NoAuth().addBlock_shouldAppendHeaderThenTextThenReaction();
}
private static void line() {
System.out.println(C + "------------------------------------------------------------" + R);
}
private static void title(String s) {
System.out.println(C + "\n============================================================" + R);
System.out.println(C + s + R);
System.out.println(C + "============================================================\n" + R);
}
private static void stepTitle(String s) {
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
}
private static void ok(String s) {
System.out.println(G + "" + s + R);
}
private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R);
}
private static void send(String op, String json) {
System.out.println("📤 [" + op + "] Request JSON:");
System.out.println(json);
line();
}
private static void recv(String op, String json) {
System.out.println("📥 [" + op + "] Response JSON:");
System.out.println(json);
line();
}
private static void assert200(String op, String resp) {
int st = JsonParsers.status(resp);
try {
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
ok(op + ": status=200");
} catch (AssertionError ae) {
boom(op + ": ожидали 200, но получили " + st);
throw ae;
}
}
@BeforeAll
static void ensureUserExists() {
ItRunContext.initIfNeeded();
title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "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);
// ВАЖНО:
// - requestId тут не важен, но пусть будет.
// - отдельная авторизация не нужна, но пользователь должен существовать.
String reqJson = JsonBuilders.addUser("it03-adduser-beforeall");
String resp = WsJsonOneShot.request(reqJson, Duration.ofSeconds(5));
int st = JsonParsers.status(resp);
if (st == 200) {
ok("BeforeAll: пользователь создан/добавлен (status=200)");
} else if (st == 409) {
// ok
return;
}
if (st == 409) {
String code = JsonParsers.errorCode(resp);
if ("USER_ALREADY_EXISTS".equals(code)) {
ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
} else {
boom("BeforeAll: status=409, но code неожиданный: " + code);
if ("USER_ALREADY_EXISTS".equals(code)) return;
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
}
} else {
boom("BeforeAll: предусловие не выполнено. status=" + st);
fail("User precondition failed. status=" + st + ", resp=" + resp);
}
}
}
@Test
void addBlock_shouldAppendHeaderThenTextThenReaction() {
ItRunContext.initIfNeeded();
title("AddBlockIT: HEADER(0) + TEXT(1,2,3) + REACT(4->text1) без auth");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println(" blockchainName = " + TestConfig.BCH_NAME());
System.out.println("Ожидание:");
System.out.println(" 1) HEADER (global=0, line=0, lineNum=0, prevGlobal=ZERO64) -> 200");
System.out.println(" 2) TEXT#1 (global=1, line=1, lineNum=1, prevGlobal=hash0, prevLine=hash0) -> 200");
System.out.println(" 3) TEXT#2 (global=2, line=1, lineNum=2, prevGlobal=hash1, prevLine=hash1) -> 200");
System.out.println(" 4) TEXT#3 (global=3, line=1, lineNum=3, prevGlobal=hash2, prevLine=hash2) -> 200");
System.out.println(" 5) REACT#1 (global=4, line=2, lineNum=1, prevGlobal=hash3, prevLine=hash0) -> 200 (to TEXT#1)\n");
// таймаут на каждый one-shot запрос
Duration t = Duration.ofSeconds(8);
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
// ============================
// Локальное состояние теста
// ============================
int[] lineLastNumber = new int[8];
String[] lineLastHashHex = new String[8];
for (int i = 0; i < 8; i++) lineLastHashHex[i] = "";
int globalLastNumber = -1;
String globalLastHashHex = ZERO64;
byte[] headerHash32 = null; // понадобится как prevLineHash для первых блоков линий 1/2
// 1) состояние + сборка + отправка
AddBlockFlow flow = new AddBlockFlow();
// =========================================================
// ШАГ 1: HEADER (global=0, line=0, lineNum=0)
// ШАГ 0: ВАЖНО первым всегда HEADER global=0
// =========================================================
stepTitle("ШАГ 1: AddBlock HEADER (global=0, line=0, lineNum=0)");
BuiltBlock header = buildHeaderBlock(
0,
LINE_HEADER,
0,
ZERO32, // prevGlobalHash32
ZERO32 // prevLineHash32
);
String reqId1 = "it03-add-header";
String reqJson1 = buildAddBlockJson(reqId1, TestConfig.BCH_NAME(), 0, ZERO64, base64(header.fullBytes));
send("AddBlock(" + reqId1 + ")", reqJson1);
String resp1 = client.request(reqId1, reqJson1, Duration.ofSeconds(8));
recv("AddBlock(" + reqId1 + ")", resp1);
assert200("AddBlock(" + reqId1 + ")", resp1);
String serverLastGlobalHash0 = extractPayloadString(resp1, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash0, "HEADER: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash0.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars");
String localHash0 = bytesToHex64(header.hash32);
assertEquals(localHash0, serverLastGlobalHash0, "HEADER: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем локальное состояние
headerHash32 = header.hash32;
globalLastNumber = 0;
globalLastHashHex = localHash0;
lineLastNumber[0] = 0;
lineLastHashHex[0] = localHash0;
ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash0);
flow.sendHeader0(t);
// =========================================================
// Общая проверка: 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)
// prevLineHash для первого блока линии = hash(нулевого блока)
// ШАГ 4: REACT#1 (line=2) -> на TEXT#1 (global=1, hash=text1)
// =========================================================
stepTitle("ШАГ 2: AddBlock TEXT#1 (global=1, line=1, lineNum=1)");
int text1LineNum = nextLineNum(lineLastNumber, LINE_TEXT);
byte[] prevLineHashText1 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT);
BuiltBlock text1 = buildTextBlock(
1,
LINE_TEXT,
text1LineNum,
hexToBytes32(globalLastHashHex), // prevGlobalHash32
prevLineHashText1, // prevLineHash32
"Hello #1 from IT_03 test"
);
String reqId2 = "it03-add-text-1";
String reqJson2 = buildAddBlockJson(reqId2, TestConfig.BCH_NAME(), 1, globalLastHashHex, base64(text1.fullBytes));
send("AddBlock(" + reqId2 + ")", reqJson2);
String resp2 = client.request(reqId2, reqJson2, Duration.ofSeconds(8));
recv("AddBlock(" + reqId2 + ")", resp2);
assert200("AddBlock(" + reqId2 + ")", resp2);
String serverLastGlobalHash1 = extractPayloadString(resp2, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash1, "TEXT#1: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash1.trim().length(), "TEXT#1: serverLastGlobalHash must be 64 hex chars");
String localHash1 = bytesToHex64(text1.hash32);
assertEquals(localHash1, serverLastGlobalHash1, "TEXT#1: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = 1;
globalLastHashHex = localHash1;
lineLastNumber[LINE_TEXT] = text1LineNum;
lineLastHashHex[LINE_TEXT] = localHash1;
ok("TEXT#1 принят. hash1=" + serverLastGlobalHash1);
// =========================================================
// ШАГ 3: TEXT#2 (global=2, line=1, lineNum=2)
// prevLineHash для второго блока линии = hash(TEXT#1)
// =========================================================
stepTitle("ШАГ 3: AddBlock TEXT#2 (global=2, line=1, lineNum=2)");
int text2LineNum = nextLineNum(lineLastNumber, LINE_TEXT);
byte[] prevLineHashText2 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT);
BuiltBlock text2 = buildTextBlock(
2,
LINE_TEXT,
text2LineNum,
hexToBytes32(globalLastHashHex),
prevLineHashText2,
"Hello #2 from IT_03 test"
);
String reqId3 = "it03-add-text-2";
String reqJson3 = buildAddBlockJson(reqId3, TestConfig.BCH_NAME(), 2, globalLastHashHex, base64(text2.fullBytes));
send("AddBlock(" + reqId3 + ")", reqJson3);
String resp3 = client.request(reqId3, reqJson3, Duration.ofSeconds(8));
recv("AddBlock(" + reqId3 + ")", resp3);
assert200("AddBlock(" + reqId3 + ")", resp3);
String serverLastGlobalHash2 = extractPayloadString(resp3, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash2, "TEXT#2: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash2.trim().length(), "TEXT#2: serverLastGlobalHash must be 64 hex chars");
String localHash2 = bytesToHex64(text2.hash32);
assertEquals(localHash2, serverLastGlobalHash2, "TEXT#2: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = 2;
globalLastHashHex = localHash2;
lineLastNumber[LINE_TEXT] = text2LineNum;
lineLastHashHex[LINE_TEXT] = localHash2;
ok("TEXT#2 принят. hash2=" + serverLastGlobalHash2);
// =========================================================
// ШАГ 4: TEXT#3 (global=3, line=1, lineNum=3)
// prevLineHash = hash(TEXT#2)
// =========================================================
stepTitle("ШАГ 4: AddBlock TEXT#3 (global=3, line=1, lineNum=3)");
int text3LineNum = nextLineNum(lineLastNumber, LINE_TEXT);
byte[] prevLineHashText3 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT);
BuiltBlock text3 = buildTextBlock(
3,
LINE_TEXT,
text3LineNum,
hexToBytes32(globalLastHashHex),
prevLineHashText3,
"Hello #3 from IT_03 test"
);
String reqId4 = "it03-add-text-3";
String reqJson4 = buildAddBlockJson(reqId4, TestConfig.BCH_NAME(), 3, globalLastHashHex, base64(text3.fullBytes));
send("AddBlock(" + reqId4 + ")", reqJson4);
String resp4 = client.request(reqId4, reqJson4, Duration.ofSeconds(8));
recv("AddBlock(" + reqId4 + ")", resp4);
assert200("AddBlock(" + reqId4 + ")", resp4);
String serverLastGlobalHash3 = extractPayloadString(resp4, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash3, "TEXT#3: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash3.trim().length(), "TEXT#3: serverLastGlobalHash must be 64 hex chars");
String localHash3 = bytesToHex64(text3.hash32);
assertEquals(localHash3, serverLastGlobalHash3, "TEXT#3: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = 3;
globalLastHashHex = localHash3;
lineLastNumber[LINE_TEXT] = text3LineNum;
lineLastHashHex[LINE_TEXT] = localHash3;
ok("TEXT#3 принят. hash3=" + serverLastGlobalHash3);
// =========================================================
// ШАГ 5: REACT#1 (global=4, line=2, lineNum=1) -> на TEXT#1
// prevLineHash для первого блока line2 = hash(нулевого блока)
// =========================================================
stepTitle("ШАГ 5: AddBlock REACT#1 (global=4, line=2, lineNum=1) -> to TEXT#1");
int react1LineNum = nextLineNum(lineLastNumber, LINE_REACT);
byte[] prevLineHashReact1 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_REACT);
// ссылка на TEXT#1 (global=1, hash=text1)
String text1HashHex = lineHashAtOrThrow(text1, "text1.hash32");
BuiltBlock react1 = buildReactionBlock(
4,
LINE_REACT,
react1LineNum,
hexToBytes32(globalLastHashHex),
prevLineHashReact1,
flow.sendNextReaction(
1, // reactionCode (пример: 1 = like)
TestConfig.BCH_NAME(),
TestConfig.BCH_NAME(), // toBlockchainName
1, // toBlockGlobalNumber = 1 (TEXT#1)
text1.hash32 // toBlockHash32 = hash(TEXT#1)
text1.hash32, // toBlockHash32 = hash(TEXT#1)
t
);
String reqId5 = "it03-add-react-1";
String reqJson5 = buildAddBlockJson(reqId5, TestConfig.BCH_NAME(), 4, globalLastHashHex, base64(react1.fullBytes));
send("AddBlock(" + reqId5 + ")", reqJson5);
String resp5 = client.request(reqId5, reqJson5, Duration.ofSeconds(8));
recv("AddBlock(" + reqId5 + ")", resp5);
assert200("AddBlock(" + reqId5 + ")", resp5);
String serverLastGlobalHash4 = extractPayloadString(resp5, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash4, "REACT#1: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash4.trim().length(), "REACT#1: serverLastGlobalHash must be 64 hex chars");
String localHash4 = bytesToHex64(react1.hash32);
assertEquals(localHash4, serverLastGlobalHash4, "REACT#1: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = 4;
globalLastHashHex = localHash4;
lineLastNumber[LINE_REACT] = react1LineNum;
lineLastHashHex[LINE_REACT] = localHash4;
ok("REACT#1 принят. hash4=" + serverLastGlobalHash4);
// =========================================================
// Итоговый контроль массивов линий
// =========================================================
ok("ИТОГ по линиям:");
ok(" line0: lastNum=" + lineLastNumber[0] + ", lastHash=" + lineLastHashHex[0]);
ok(" line1: lastNum=" + lineLastNumber[1] + ", lastHash=" + lineLastHashHex[1]);
ok(" line2: lastNum=" + lineLastNumber[2] + ", lastHash=" + lineLastHashHex[2]);
ok("ТЕСТ ПРОЙДЕН: HEADER + 3xTEXT(line1) + 1xREACT(line2) успешно добавлены и согласованы по globalHash/lineHash");
} catch (AssertionError | RuntimeException e) {
boom("ТЕСТ УПАЛ: AddBlockIT. Причина: " + e.getMessage());
throw e;
}
}
// =================================================================================
// LINE HELPERS
// =================================================================================
/** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 в этом тесте только 0. */
private static int nextLineNum(int[] lineLastNumber, short lineIndex) {
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
if (lineIndex == 0) return 0; // у нас header фиксированно line0/num0
return lineLastNumber[lineIndex] + 1;
}
/**
* prevLineHash32 по твоему правилу:
* - для первого блока линии (lineLastNumber[line]==0): prevLineHash = hash(нулевого блока)
* - иначе: prevLineHash = hash последнего блока этой линии
*
* Важно: для line0 здесь не используем (header имеет prevLine=ZERO32).
*/
private static byte[] prevLineHash32(int[] lineLastNumber, String[] lineLastHashHex, byte[] headerHash32, short lineIndex) {
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
if (lineIndex == 0) return ZERO32;
if (lineLastNumber[lineIndex] == 0) {
// первый блок линии -> от нулевого блока
if (headerHash32 == null || headerHash32.length != 32) {
throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex);
}
return headerHash32;
}
String lastHex = lineLastHashHex[lineIndex];
if (lastHex == null || lastHex.isBlank()) {
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0");
}
return hexToBytes32(lastHex);
}
private static String lineHashAtOrThrow(BuiltBlock b, String name) {
if (b == null || b.hash32 == null || b.hash32.length != 32) throw new IllegalArgumentException(name + " must be 32 bytes");
return bytesToHex64(b.hash32);
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
/** Небольшой холдер, чтобы тест мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */
private static final class BuiltBlock {
final byte[] fullBytes;
final byte[] hash32;
BuiltBlock(byte[] fullBytes, byte[] hash32) {
this.fullBytes = fullBytes;
this.hash32 = hash32;
}
}
private static BuiltBlock buildHeaderBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
HeaderBody body = new HeaderBody(TestConfig.LOGIN());
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildTextBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildReactionBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
int reactionCode,
String toBlockchainName,
int toBlockGlobalNumber,
byte[] toBlockHash32) {
ReactionBody body = new ReactionBody(
reactionCode,
toBlockchainName,
toBlockGlobalNumber,
toBlockHash32 // [32] сырые 32 байта, как ты утвердил
);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
byte[] rawBytes = ByteBuffer.allocate(recordSize)
.order(ByteOrder.BIG_ENDIAN)
.putInt(recordSize)
.putInt(globalNumber)
.putLong(ts)
.putShort(lineIndex)
.putInt(lineBlockNumber)
.put(bodyBytes)
.array();
// Ключевой момент: preimage должен совпасть с серверным правилом.
// Сервер НЕ получает prevLineHash по сети он берёт его из своего состояния линии.
// Поэтому в тесте мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()).
byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.LOGIN(),
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY());
byte[] full = new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
return new BuiltBlock(full, hash32);
}
// =================================================================================
// JSON HELPERS
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
private static String extractPayloadString(String json, String field) {
try {
com.fasterxml.jackson.databind.JsonNode root =
new com.fasterxml.jackson.databind.ObjectMapper().readTree(json);
com.fasterxml.jackson.databind.JsonNode payload = root.get("payload");
if (payload != null && payload.has(field)) {
return payload.get(field).asText();
}
} catch (Exception ignore) {}
return null;
}
private static String base64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
// =================================================================================
// HEX HELPERS
// =================================================================================
private static byte[] hexToBytes32(String hex) {
if (hex == null) throw new IllegalArgumentException("hex is null");
String s = hex.trim();
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
private static String bytesToHex64(byte[] b32) {
if (b32 == null || b32.length != 32) throw new IllegalArgumentException("b32 must be 32 bytes");
char[] out = new char[64];
final char[] HEX = "0123456789abcdef".toCharArray();
for (int i = 0; i < 32; i++) {
int v = b32[i] & 0xFF;
out[i * 2] = HEX[v >>> 4];
out[i * 2 + 1] = HEX[v & 0x0F];
}
return new String(out);
// Мини-контроль итогов (если захочешь красиво залогируем через твой TestLog)
assertEquals(4, flow.globalLastNumber(), "После 1 header + 3 text + 1 react globalLastNumber должен быть 4");
assertEquals(3, flow.lineLastNumber(AddBlockFlow.LINE_TEXT), "В line=1 должно быть 3 блока");
assertEquals(1, flow.lineLastNumber(AddBlockFlow.LINE_REACT), "В line=2 должен быть 1 блок");
assertNotNull(flow.globalLastHashHex());
assertEquals(64, flow.globalLastHashHex().length());
}
}

View 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;
}
}
}