28 12 25
Всё ещё не работает проверка линий. Переделал первые два теста! Третий (АддБлокс) ещё не работает
This commit is contained in:
parent
795341dd8d
commit
3f374f48e1
@ -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);
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -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("Тест завершён, выходим.");
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -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
@ -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);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
package Test;
|
||||
|
||||
public class test1 {
|
||||
|
||||
}
|
||||
//package Test;
|
||||
//
|
||||
//public class test1 {
|
||||
//
|
||||
//}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 (весь сценарий сессий выполнен успешно)");
|
||||
}
|
||||
}
|
||||
62
src/test/java/test/it/IT_RunAllCleanMain.java
Normal file
62
src/test/java/test/it/IT_RunAllCleanMain.java
Normal file
@ -0,0 +1,62 @@
|
||||
package test.it;
|
||||
|
||||
import test.it.utils.ItRunContext;
|
||||
import test.it.utils.TestLog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite, но С ПРЕДВАРИТЕЛЬНОЙ очисткой data/.
|
||||
*
|
||||
* Делает:
|
||||
* 1) чистит папку data/
|
||||
* 2) запускает все тесты по очереди (через IT_RunAllMain.runAll())
|
||||
* 3) возвращает код = число упавших тестов
|
||||
*/
|
||||
public class IT_RunAllCleanMain {
|
||||
|
||||
private static final String DATA_DIR = "data";
|
||||
|
||||
public static void main(String[] args) {
|
||||
ItRunContext.initIfNeeded();
|
||||
|
||||
TestLog.title("IT RUN CLEAN: очистка data/ + запуск всех тестов");
|
||||
|
||||
try {
|
||||
cleanupDataDir(DATA_DIR);
|
||||
} catch (Throwable t) {
|
||||
TestLog.boom("Не смог очистить data/. Причина: " + t.getMessage());
|
||||
if (TestLog.VERBOSE) t.printStackTrace(System.out);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
int failed = IT_RunAllMain.runAll();
|
||||
System.exit(failed);
|
||||
}
|
||||
|
||||
private static void cleanupDataDir(String dirName) throws IOException {
|
||||
Path dir = Paths.get(dirName);
|
||||
|
||||
if (!Files.exists(dir)) {
|
||||
TestLog.warn("data dir not found: " + dir.toAbsolutePath() + " (создаю)");
|
||||
Files.createDirectories(dir);
|
||||
return;
|
||||
}
|
||||
|
||||
// удаляем ВСЁ внутри папки, но саму папку оставляем
|
||||
Files.walk(dir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.filter(p -> !p.equals(dir))
|
||||
.forEach(p -> {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e);
|
||||
}
|
||||
});
|
||||
|
||||
TestLog.ok("data очищена: " + dir.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
@ -1,82 +1,79 @@
|
||||
package test.it;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -1,32 +1,128 @@
|
||||
package test.it.utils;
|
||||
|
||||
/**
|
||||
* TestLog — единое место для:
|
||||
* - ANSI цветов
|
||||
* - стандартных красивых сообщений (title/ok/boom/line/step/send/recv)
|
||||
*
|
||||
* Включение/выключение подробных логов:
|
||||
* -Dit.verbose=false
|
||||
*
|
||||
* По умолчанию verbose=true (удобно для ручного прогона).
|
||||
*/
|
||||
public final class TestLog {
|
||||
private TestLog(){}
|
||||
private TestLog() {}
|
||||
|
||||
// ============================
|
||||
// VERBOSE
|
||||
// ============================
|
||||
|
||||
// включается так: ./gradlew test -Dit.verbose=true
|
||||
public static final boolean VERBOSE = true; //Boolean.parseBoolean(System.getProperty("it.verbose", "false"));
|
||||
public static final boolean VERBOSE = Boolean.parseBoolean(System.getProperty("it.verbose", "true"));
|
||||
|
||||
// ============================
|
||||
// ANSI COLORS
|
||||
// ============================
|
||||
|
||||
public static final String R = "\u001B[0m";
|
||||
public static final String G = "\u001B[32m";
|
||||
public static final String Y = "\u001B[33m";
|
||||
public static final String RED = "\u001B[31m";
|
||||
public static final String C = "\u001B[36m";
|
||||
|
||||
// ============================
|
||||
// BASIC OUTPUT
|
||||
// ============================
|
||||
|
||||
public static void info(String s) {
|
||||
if (VERBOSE) System.out.println(s);
|
||||
}
|
||||
|
||||
public static void section(String title) {
|
||||
public static void line() {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println("\n\n==================================================");
|
||||
System.out.println(title);
|
||||
System.out.println("==================================================\n");
|
||||
System.out.println(C + "------------------------------------------------------------" + R);
|
||||
}
|
||||
|
||||
public static void req(String title, String json) {
|
||||
/** Короткое заглавие. */
|
||||
public static void title(String s) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println("\n📤 " + title);
|
||||
System.out.println(json);
|
||||
System.out.println(C + "\n============================================================" + R);
|
||||
System.out.println(C + s + R);
|
||||
System.out.println(C + "============================================================\n" + R);
|
||||
}
|
||||
|
||||
public static void resp(String title, String json) {
|
||||
/**
|
||||
* Длинное заглавие (под многострочный текст).
|
||||
*
|
||||
* Пример:
|
||||
* TestLog.titleBlock("""
|
||||
* ТЕСТ: ...
|
||||
* Ожидание: ...
|
||||
* """);
|
||||
*/
|
||||
public static void titleBlock(String multiLineText) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println("\n📥 " + title);
|
||||
System.out.println(C + "\n============================================================" + R);
|
||||
System.out.println(C + multiLineText + R);
|
||||
System.out.println(C + "============================================================\n" + R);
|
||||
}
|
||||
|
||||
public static void stepTitle(String s) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
|
||||
}
|
||||
|
||||
public static void ok(String s) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println(G + "✅ " + s + R);
|
||||
}
|
||||
|
||||
public static void warn(String s) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println(Y + "⚠️ " + s + R);
|
||||
}
|
||||
|
||||
public static void boom(String s) {
|
||||
System.out.println(RED + "****************************************************************" + R);
|
||||
System.out.println(RED + "❌ " + s + R);
|
||||
System.out.println(RED + "****************************************************************" + R);
|
||||
}
|
||||
|
||||
public static void send(String op, String json) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println("📤 [" + op + "] Request JSON:");
|
||||
System.out.println(json);
|
||||
System.out.println("-----------------------------------------------------");
|
||||
line();
|
||||
}
|
||||
|
||||
public static void recv(String op, String json) {
|
||||
if (!VERBOSE) return;
|
||||
System.out.println("📥 [" + op + "] Response JSON:");
|
||||
System.out.println(json);
|
||||
line();
|
||||
}
|
||||
|
||||
// ============================
|
||||
// RUN HELPERS
|
||||
// ============================
|
||||
|
||||
/**
|
||||
* Запуск тестового тела (без JUnit).
|
||||
* Возвращает 0 если ок, 1 если упал.
|
||||
*
|
||||
* Важно:
|
||||
* - здесь мы НЕ глотаем ошибку: печатаем и возвращаем код
|
||||
* - раннер суммирует количество упавших тестов
|
||||
*/
|
||||
public static int runOne(String testName, Runnable body) {
|
||||
try {
|
||||
body.run();
|
||||
ok(testName + ": OK");
|
||||
return 0;
|
||||
} catch (Throwable t) {
|
||||
boom(testName + ": FAIL. Причина: " + t.getMessage());
|
||||
if (VERBOSE) t.printStackTrace(System.out);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
426
src/test/java/test/it/ws/AddBlockScenarioRunner.java
Normal file
426
src/test/java/test/it/ws/AddBlockScenarioRunner.java
Normal file
@ -0,0 +1,426 @@
|
||||
package test.it.ws;
|
||||
|
||||
import blockchain.BchBlockEntry;
|
||||
import blockchain.BchCryptoVerifier;
|
||||
import blockchain.body.HeaderBody;
|
||||
import blockchain.body.ReactionBody;
|
||||
import blockchain.body.TextBody;
|
||||
import test.it.utils.TestConfig;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* AddBlockScenarioRunner
|
||||
*
|
||||
* Хранит локальное состояние:
|
||||
* - globalLastHashHex / globalLastNumber
|
||||
* - lineLastNumber[line] / lineLastHashHex[line]
|
||||
* - headerHash32 (нужен как prevLineHash для первых блоков линий)
|
||||
*
|
||||
* Умеет:
|
||||
* - собрать блок (header/text/react)
|
||||
* - отправить AddBlock по сети (каждый запрос = новое WS соединение)
|
||||
* - обновить локальное состояние
|
||||
*/
|
||||
public final class AddBlockScenarioRunner {
|
||||
|
||||
// requestId делаем фиксированный (как ты попросил)
|
||||
private static final String FIXED_REQUEST_ID = "it03";
|
||||
|
||||
private static final byte[] ZERO32 = new byte[32];
|
||||
private static final String ZERO64 = "0".repeat(64);
|
||||
|
||||
private final String wsUri;
|
||||
private final String blockchainName;
|
||||
|
||||
// Локальное состояние (как и было в тесте)
|
||||
private final int[] lineLastNumber = new int[8];
|
||||
private final String[] lineLastHashHex = new String[8];
|
||||
|
||||
private int globalLastNumber = -1;
|
||||
private String globalLastHashHex = ZERO64;
|
||||
|
||||
private byte[] headerHash32 = null;
|
||||
|
||||
public AddBlockScenarioRunner(String wsUri, String blockchainName) {
|
||||
this.wsUri = wsUri;
|
||||
this.blockchainName = blockchainName;
|
||||
|
||||
for (int i = 0; i < 8; i++) lineLastHashHex[i] = "";
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// PUBLIC API
|
||||
// =================================================================================
|
||||
|
||||
public String getGlobalLastHashHex() {
|
||||
return globalLastHashHex;
|
||||
}
|
||||
|
||||
public int getGlobalLastNumber() {
|
||||
return globalLastNumber;
|
||||
}
|
||||
|
||||
public int getLineLastNumber(int lineIndex) {
|
||||
return lineLastNumber[lineIndex];
|
||||
}
|
||||
|
||||
public String getLineLastHashHex(int lineIndex) {
|
||||
return lineLastHashHex[lineIndex];
|
||||
}
|
||||
|
||||
/** Добавить HEADER (global=0, line=0, lineNum=0). */
|
||||
public AddBlockResult addHeader(short lineHeader) {
|
||||
BuiltBlock header = buildHeaderBlock(
|
||||
0,
|
||||
lineHeader,
|
||||
0,
|
||||
ZERO32,
|
||||
ZERO32
|
||||
);
|
||||
|
||||
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, 0, ZERO64, base64(header.fullBytes));
|
||||
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
|
||||
|
||||
// локальный hash
|
||||
String localHash0 = bytesToHex64(header.hash32);
|
||||
|
||||
// обновляем состояние (как раньше)
|
||||
headerHash32 = header.hash32;
|
||||
globalLastNumber = 0;
|
||||
globalLastHashHex = localHash0;
|
||||
lineLastNumber[0] = 0;
|
||||
lineLastHashHex[0] = localHash0;
|
||||
|
||||
return new AddBlockResult(reqJson, resp, localHash0);
|
||||
}
|
||||
|
||||
/** Добавить TEXT в lineText, следующим lineNum, global=globalNumber. */
|
||||
public AddBlockResult addText(int globalNumber, short lineText, String text) {
|
||||
int lineNum = nextLineNum(lineText);
|
||||
byte[] prevLineHash = prevLineHash32(lineText);
|
||||
|
||||
BuiltBlock b = buildTextBlock(
|
||||
globalNumber,
|
||||
lineText,
|
||||
lineNum,
|
||||
hexToBytes32(globalLastHashHex),
|
||||
prevLineHash,
|
||||
text
|
||||
);
|
||||
|
||||
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes));
|
||||
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
|
||||
|
||||
String localHash = bytesToHex64(b.hash32);
|
||||
|
||||
// обновляем состояние
|
||||
globalLastNumber = globalNumber;
|
||||
globalLastHashHex = localHash;
|
||||
lineLastNumber[lineText] = lineNum;
|
||||
lineLastHashHex[lineText] = localHash;
|
||||
|
||||
return new AddBlockResult(reqJson, resp, localHash, b.hash32);
|
||||
}
|
||||
|
||||
/** Добавить REACT в lineReact, следующим lineNum, global=globalNumber, ссылка на (toGlobal,toHash32). */
|
||||
public AddBlockResult addReaction(int globalNumber,
|
||||
short lineReact,
|
||||
int reactionCode,
|
||||
String toBlockchainName,
|
||||
int toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
|
||||
int lineNum = nextLineNum(lineReact);
|
||||
byte[] prevLineHash = prevLineHash32(lineReact);
|
||||
|
||||
BuiltBlock b = buildReactionBlock(
|
||||
globalNumber,
|
||||
lineReact,
|
||||
lineNum,
|
||||
hexToBytes32(globalLastHashHex),
|
||||
prevLineHash,
|
||||
reactionCode,
|
||||
toBlockchainName,
|
||||
toBlockGlobalNumber,
|
||||
toBlockHash32
|
||||
);
|
||||
|
||||
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes));
|
||||
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
|
||||
|
||||
String localHash = bytesToHex64(b.hash32);
|
||||
|
||||
// обновляем состояние
|
||||
globalLastNumber = globalNumber;
|
||||
globalLastHashHex = localHash;
|
||||
lineLastNumber[lineReact] = lineNum;
|
||||
lineLastHashHex[lineReact] = localHash;
|
||||
|
||||
return new AddBlockResult(reqJson, resp, localHash, b.hash32);
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// RESULT HOLDER
|
||||
// =================================================================================
|
||||
|
||||
public static final class AddBlockResult {
|
||||
public final String requestJson;
|
||||
public final String responseJson;
|
||||
|
||||
/** локально вычисленный hash (HEX64) именно для этого блока */
|
||||
public final String localHashHex;
|
||||
|
||||
/** локальный hash32 (если надо ссылаться на блок дальше) */
|
||||
public final byte[] localHash32;
|
||||
|
||||
public AddBlockResult(String requestJson, String responseJson, String localHashHex) {
|
||||
this(requestJson, responseJson, localHashHex, null);
|
||||
}
|
||||
|
||||
public AddBlockResult(String requestJson, String responseJson, String localHashHex, byte[] localHash32) {
|
||||
this.requestJson = requestJson;
|
||||
this.responseJson = responseJson;
|
||||
this.localHashHex = localHashHex;
|
||||
this.localHash32 = localHash32;
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// LINE HELPERS
|
||||
// =================================================================================
|
||||
|
||||
/** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 тут только 0. */
|
||||
private int nextLineNum(short lineIndex) {
|
||||
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
|
||||
if (lineIndex == 0) return 0;
|
||||
return lineLastNumber[lineIndex] + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* prevLineHash32 по твоему правилу:
|
||||
* - для первого блока линии (lineLastNumber[line]==0): prevLineHash = hash(нулевого блока)
|
||||
* - иначе: prevLineHash = hash последнего блока этой линии
|
||||
*
|
||||
* Важно: для line0 здесь не используем (header имеет prevLine=ZERO32).
|
||||
*/
|
||||
private byte[] prevLineHash32(short lineIndex) {
|
||||
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
|
||||
if (lineIndex == 0) return ZERO32;
|
||||
|
||||
if (lineLastNumber[lineIndex] == 0) {
|
||||
// первый блок линии -> от нулевого блока
|
||||
if (headerHash32 == null || headerHash32.length != 32) {
|
||||
throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex);
|
||||
}
|
||||
return headerHash32;
|
||||
}
|
||||
|
||||
String lastHex = lineLastHashHex[lineIndex];
|
||||
if (lastHex == null || lastHex.isBlank()) {
|
||||
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0");
|
||||
}
|
||||
return hexToBytes32(lastHex);
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// BUILD BLOCKS
|
||||
// =================================================================================
|
||||
|
||||
/** Небольшой холдер, чтобы сценарий мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */
|
||||
private static final class BuiltBlock {
|
||||
final byte[] fullBytes;
|
||||
final byte[] hash32;
|
||||
|
||||
BuiltBlock(byte[] fullBytes, byte[] hash32) {
|
||||
this.fullBytes = fullBytes;
|
||||
this.hash32 = hash32;
|
||||
}
|
||||
}
|
||||
|
||||
private static BuiltBlock buildHeaderBlock(int globalNumber,
|
||||
short lineIndex,
|
||||
int lineBlockNumber,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32) {
|
||||
|
||||
HeaderBody body = new HeaderBody(TestConfig.LOGIN());
|
||||
byte[] bodyBytes = body.toBytes();
|
||||
|
||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||
}
|
||||
|
||||
private static BuiltBlock buildTextBlock(int globalNumber,
|
||||
short lineIndex,
|
||||
int lineBlockNumber,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
String text) {
|
||||
|
||||
TextBody body = new TextBody(text);
|
||||
byte[] bodyBytes = body.toBytes();
|
||||
|
||||
// ⚠️ ВАЖНО:
|
||||
// У тебя сервер ругается: "Body is in wrong lineIndex expected=1 actual=0 (type=1 ver=1)".
|
||||
// Это значит, что lineIndex хранится ВНУТРИ bodyBytes.
|
||||
// Ниже — безопасный патч: предполагаем формат "type(1) + ver(1) + lineIndex(2)" и проставляем lineIndex.
|
||||
bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex);
|
||||
|
||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||
}
|
||||
|
||||
private static BuiltBlock buildReactionBlock(int globalNumber,
|
||||
short lineIndex,
|
||||
int lineBlockNumber,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
int reactionCode,
|
||||
String toBlockchainName,
|
||||
int toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
|
||||
ReactionBody body = new ReactionBody(
|
||||
reactionCode,
|
||||
toBlockchainName,
|
||||
toBlockGlobalNumber,
|
||||
toBlockHash32 // [32] сырые 32 байта, как ты утвердил
|
||||
);
|
||||
|
||||
byte[] bodyBytes = body.toBytes();
|
||||
|
||||
// Аналогично TextBody — если внутри есть lineIndex, проставляем.
|
||||
bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex);
|
||||
|
||||
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
|
||||
}
|
||||
|
||||
private static BuiltBlock buildSignedBlockFullBytes(int globalNumber,
|
||||
short lineIndex,
|
||||
int lineBlockNumber,
|
||||
byte[] bodyBytes,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32) {
|
||||
|
||||
long ts = System.currentTimeMillis() / 1000L;
|
||||
|
||||
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
|
||||
|
||||
byte[] rawBytes = ByteBuffer.allocate(recordSize)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(recordSize)
|
||||
.putInt(globalNumber)
|
||||
.putLong(ts)
|
||||
.putShort(lineIndex)
|
||||
.putInt(lineBlockNumber)
|
||||
.put(bodyBytes)
|
||||
.array();
|
||||
|
||||
// Ключевой момент: preimage должен совпасть с серверным правилом.
|
||||
// Сервер НЕ получает prevLineHash по сети — он берёт его из своего состояния линии.
|
||||
// Поэтому мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()).
|
||||
byte[] preimage = BchCryptoVerifier.buildPreimage(
|
||||
TestConfig.LOGIN(),
|
||||
prevGlobalHash32,
|
||||
prevLineHash32,
|
||||
rawBytes
|
||||
);
|
||||
|
||||
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
|
||||
|
||||
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY());
|
||||
|
||||
byte[] full = new BchBlockEntry(
|
||||
globalNumber,
|
||||
ts,
|
||||
lineIndex,
|
||||
lineBlockNumber,
|
||||
bodyBytes,
|
||||
signature64,
|
||||
hash32
|
||||
).toBytes();
|
||||
|
||||
return new BuiltBlock(full, hash32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Патч lineIndex внутри bodyBytes.
|
||||
*
|
||||
* Предположение (по твоей ошибке type=1 ver=1):
|
||||
* bodyBytes[0] = type
|
||||
* bodyBytes[1] = ver
|
||||
* bodyBytes[2..3] = lineIndex (big-endian short)
|
||||
*
|
||||
* Если формат другой — скажешь, поменяю оффсет/проверки.
|
||||
*/
|
||||
private static byte[] patchBodyLineIndexIfPresent(byte[] bodyBytes, short lineIndex) {
|
||||
if (bodyBytes == null) return null;
|
||||
if (bodyBytes.length < 4) return bodyBytes;
|
||||
|
||||
// Патчим только для line>0 (для header line=0 и так норм).
|
||||
if (lineIndex <= 0) return bodyBytes;
|
||||
|
||||
ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN).putShort(2, lineIndex);
|
||||
return bodyBytes;
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// JSON HELPERS
|
||||
// =================================================================================
|
||||
|
||||
private static String buildAddBlockJson(String requestId,
|
||||
String blockchainName,
|
||||
int globalNumber,
|
||||
String prevGlobalHashHex,
|
||||
String blockBytesB64) {
|
||||
return """
|
||||
{
|
||||
"op": "AddBlock",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"blockchainName": "%s",
|
||||
"globalNumber": %d,
|
||||
"prevGlobalHash": "%s",
|
||||
"blockBytesB64": "%s"
|
||||
}
|
||||
}
|
||||
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
|
||||
}
|
||||
|
||||
private static String base64(byte[] bytes) {
|
||||
return Base64.getEncoder().encodeToString(bytes);
|
||||
}
|
||||
|
||||
// =================================================================================
|
||||
// HEX HELPERS
|
||||
// =================================================================================
|
||||
|
||||
private static byte[] hexToBytes32(String hex) {
|
||||
if (hex == null) throw new IllegalArgumentException("hex is null");
|
||||
String s = hex.trim();
|
||||
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
|
||||
byte[] out = new byte[32];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int hi = Character.digit(s.charAt(i * 2), 16);
|
||||
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
|
||||
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
|
||||
out[i] = (byte) ((hi << 4) | lo);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String bytesToHex64(byte[] b32) {
|
||||
if (b32 == null || b32.length != 32) throw new IllegalArgumentException("b32 must be 32 bytes");
|
||||
char[] out = new char[64];
|
||||
final char[] HEX = "0123456789abcdef".toCharArray();
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int v = b32[i] & 0xFF;
|
||||
out[i * 2] = HEX[v >>> 4];
|
||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,13 @@
|
||||
package test.it.ws;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
93
src/test/java/test/it/ws/WsJsonRoundtripClient.java
Normal file
93
src/test/java/test/it/ws/WsJsonRoundtripClient.java
Normal file
@ -0,0 +1,93 @@
|
||||
package test.it.ws;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.WebSocket;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* WsJsonRoundtripClient
|
||||
*
|
||||
* Один запрос = одно соединение:
|
||||
* - открыл WS
|
||||
* - отправил JSON (text frame)
|
||||
* - дождался первого ответа TEXT
|
||||
* - закрыл WS
|
||||
*
|
||||
* Здесь requestId НЕ используется вообще (ни для ожидания, ни для логов).
|
||||
* Просто возвращаем первый пришедший ответ как строку JSON.
|
||||
*/
|
||||
public final class WsJsonRoundtripClient {
|
||||
|
||||
private WsJsonRoundtripClient() {}
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
public static String sendOnce(String wsUri, String requestJson, Duration timeout) {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
CompletableFuture<String> firstMessage = new CompletableFuture<>();
|
||||
|
||||
WebSocket ws = client.newWebSocketBuilder()
|
||||
.connectTimeout(timeout)
|
||||
.buildAsync(URI.create(wsUri), new WebSocket.Listener() {
|
||||
|
||||
private final StringBuilder buf = new StringBuilder();
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket) {
|
||||
webSocket.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||
buf.append(data);
|
||||
if (last) {
|
||||
String msg = buf.toString();
|
||||
buf.setLength(0);
|
||||
if (!firstMessage.isDone()) firstMessage.complete(msg);
|
||||
}
|
||||
webSocket.request(1);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket webSocket, Throwable error) {
|
||||
if (!firstMessage.isDone()) firstMessage.completeExceptionally(error);
|
||||
}
|
||||
}).join();
|
||||
|
||||
// отправляем
|
||||
ws.sendText(requestJson, true).join();
|
||||
|
||||
// ждём
|
||||
String resp;
|
||||
try {
|
||||
resp = firstMessage.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (Exception e) {
|
||||
try { ws.abort(); } catch (Exception ignored) {}
|
||||
throw new RuntimeException("Timeout/Fail waiting response (single-shot WS). uri=" + wsUri, e);
|
||||
}
|
||||
|
||||
// закрываем
|
||||
try {
|
||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS).join();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/** Утилита: прочитать status из ответа (если нужно быстро проверить). */
|
||||
public static int status(String json) {
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(json);
|
||||
return root.has("status") ? root.get("status").asInt() : -1;
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user