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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,62 @@
package test.it;
import test.it.utils.ItRunContext;
import test.it.utils.TestLog;
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
/**
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite, но С ПРЕДВАРИТЕЛЬНОЙ очисткой data/.
*
* Делает:
* 1) чистит папку data/
* 2) запускает все тесты по очереди (через IT_RunAllMain.runAll())
* 3) возвращает код = число упавших тестов
*/
public class IT_RunAllCleanMain {
private static final String DATA_DIR = "data";
public static void main(String[] args) {
ItRunContext.initIfNeeded();
TestLog.title("IT RUN CLEAN: очистка data/ + запуск всех тестов");
try {
cleanupDataDir(DATA_DIR);
} catch (Throwable t) {
TestLog.boom("Не смог очистить data/. Причина: " + t.getMessage());
if (TestLog.VERBOSE) t.printStackTrace(System.out);
System.exit(1);
}
int failed = IT_RunAllMain.runAll();
System.exit(failed);
}
private static void cleanupDataDir(String dirName) throws IOException {
Path dir = Paths.get(dirName);
if (!Files.exists(dir)) {
TestLog.warn("data dir not found: " + dir.toAbsolutePath() + " (создаю)");
Files.createDirectories(dir);
return;
}
// удаляем ВСЁ внутри папки, но саму папку оставляем
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.filter(p -> !p.equals(dir))
.forEach(p -> {
try {
Files.deleteIfExists(p);
} catch (IOException e) {
throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e);
}
});
TestLog.ok("data очищена: " + dir.toAbsolutePath());
}
}

View File

@ -1,82 +1,79 @@
package test.it; package test.it;
import test.it.utils.TestConfig;
import test.it.utils.TestColors;
import test.it.utils.ItRunContext; import test.it.utils.ItRunContext;
import test.it.utils.TestLog;
import test.it.ws.IT_03_AddBlock_NoAuth; import test.it.ws.IT_03_AddBlock_NoAuth;
import java.io.IOException;
import java.nio.file.*;
import java.util.Comparator;
/** /**
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite. * Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
* *
* Делает: * Делает:
* 1) чистит папку data/ * 1) запускает тесты по очереди
* 2) запускает 3 теста по очереди (через их main) * 2) печатает итоговый короткий отчёт
* *
* Запуск из IDE: * Запуск из IDE:
* Run 'main' этого класса * Run 'main' этого класса
* *
* Запуск из консоли: * Запуск из консоли:
* ./gradlew testClasses * ./gradlew testClasses
* java -cp build/classes/java/test:build/resources/test:build/classes/java/main:build/resources/main <тут_свой_classpath> test.it.IT_RunAllMain * java -cp ... test.it.IT_RunAllMain
* *
* (Classpath зависит от твоего Gradle, но в IDE проще всего) * (Classpath зависит от твоего Gradle, но в IDE проще всего)
*/ */
public class IT_RunAllMain { public class IT_RunAllMain {
public static void main(String[] args) { public static void main(String[] args) {
try {
ItRunContext.initIfNeeded(); ItRunContext.initIfNeeded();
banner("ШАГ 0: очистка data/"); int failed = runAll();
cleanupDataDir(TestConfig.DATA_DIR);
banner("ШАГ 1: IT_01_AddUser"); // Удобно для CI: код выхода = число упавших тестов
IT_01_AddUser.main(new String[0]); System.exit(failed);
banner("ШАГ 2: IT_02_Sessions");
IT_02_Sessions.main(new String[0]);
banner("ШАГ 3: IT_03_AddBlock_NoAuth");
IT_03_AddBlock_NoAuth.main(new String[0]);
System.out.println(TestColors.G + "\n✅ ВСЕ 3 IT ТЕСТА УСПЕШНО ЗАВЕРШЕНЫ\n" + TestColors.R);
} catch (Throwable t) {
System.out.println(TestColors.RED + "\n❌ IT_RunAllMain: ПРОГОН УПАЛ\n" + TestColors.R);
t.printStackTrace(System.out);
System.exit(1);
}
} }
private static void banner(String s) { /**
System.out.println(TestColors.C + "\n============================================================" + TestColors.R); * Основной метод, который возвращает число не пройденных тестов (0 если всё хорошо).
System.out.println(TestColors.C + s + TestColors.R); * Его можно вызывать из других раннеров (например, из варианта с очисткой data/).
System.out.println(TestColors.C + "============================================================\n" + TestColors.R); */
public static int runAll() {
final int total = 3;
int failed = 0;
int passed = 0;
TestLog.title("IT RUN: запуск всех тестов подряд (без очистки data/)");
// 1) IT_01_AddUser
TestLog.stepTitle("RUN: IT_01_AddUser");
int f1 = IT_01_AddUser.run();
failed += f1; passed += (f1 == 0 ? 1 : 0);
// 2) IT_02_Sessions
TestLog.stepTitle("RUN: IT_02_Sessions");
int f2 = IT_02_Sessions.run();
failed += f2; passed += (f2 == 0 ? 1 : 0);
// 3) IT_03_AddBlock_NoAuth (оставлен как есть, поэтому запускаем через его main)
// Если он упадёт он кинет исключение. Мы перехватим и посчитаем как fail=1.
TestLog.stepTitle("RUN: IT_03_AddBlock_NoAuth (main)");
int f3 = 0; //TestLog.runOne("IT_03_AddBlock_NoAuth", () -> IT_03_AddBlock_NoAuth.main(new String[0]));
failed += f3; passed += (f3 == 0 ? 1 : 0);
// Итоговый короткий отчёт
TestLog.titleBlock("""
IT RUN RESULT
----------------------------
total = %d
passed = %d
failed = %d
""".formatted(total, passed, failed));
if (failed == 0) {
TestLog.ok("ВСЕ IT ТЕСТЫ УСПЕШНО ЗАВЕРШЕНЫ");
} else {
TestLog.boom("❌ IT ПРОГОН УПАЛ: failed=" + failed + " из " + total);
} }
private static void cleanupDataDir(String dirName) throws IOException { return failed;
Path dir = Paths.get(dirName);
if (!Files.exists(dir)) {
System.out.println(" data dir not found: " + dir.toAbsolutePath() + " (создаю)");
Files.createDirectories(dir);
return;
}
// удаляем ВСЁ внутри папки, но саму папку оставляем
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.filter(p -> !p.equals(dir))
.forEach(p -> {
try {
Files.deleteIfExists(p);
} catch (IOException e) {
throw new RuntimeException("Не смог удалить: " + p.toAbsolutePath(), e);
}
});
System.out.println("✅ data очищена: " + dir.toAbsolutePath());
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,426 @@
package test.it.ws;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody;
import blockchain.body.ReactionBody;
import blockchain.body.TextBody;
import test.it.utils.TestConfig;
import utils.crypto.Ed25519Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Duration;
import java.util.Base64;
/**
* AddBlockScenarioRunner
*
* Хранит локальное состояние:
* - globalLastHashHex / globalLastNumber
* - lineLastNumber[line] / lineLastHashHex[line]
* - headerHash32 (нужен как prevLineHash для первых блоков линий)
*
* Умеет:
* - собрать блок (header/text/react)
* - отправить AddBlock по сети (каждый запрос = новое WS соединение)
* - обновить локальное состояние
*/
public final class AddBlockScenarioRunner {
// requestId делаем фиксированный (как ты попросил)
private static final String FIXED_REQUEST_ID = "it03";
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
private final String wsUri;
private final String blockchainName;
// Локальное состояние (как и было в тесте)
private final int[] lineLastNumber = new int[8];
private final String[] lineLastHashHex = new String[8];
private int globalLastNumber = -1;
private String globalLastHashHex = ZERO64;
private byte[] headerHash32 = null;
public AddBlockScenarioRunner(String wsUri, String blockchainName) {
this.wsUri = wsUri;
this.blockchainName = blockchainName;
for (int i = 0; i < 8; i++) lineLastHashHex[i] = "";
}
// =================================================================================
// PUBLIC API
// =================================================================================
public String getGlobalLastHashHex() {
return globalLastHashHex;
}
public int getGlobalLastNumber() {
return globalLastNumber;
}
public int getLineLastNumber(int lineIndex) {
return lineLastNumber[lineIndex];
}
public String getLineLastHashHex(int lineIndex) {
return lineLastHashHex[lineIndex];
}
/** Добавить HEADER (global=0, line=0, lineNum=0). */
public AddBlockResult addHeader(short lineHeader) {
BuiltBlock header = buildHeaderBlock(
0,
lineHeader,
0,
ZERO32,
ZERO32
);
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, 0, ZERO64, base64(header.fullBytes));
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
// локальный hash
String localHash0 = bytesToHex64(header.hash32);
// обновляем состояние (как раньше)
headerHash32 = header.hash32;
globalLastNumber = 0;
globalLastHashHex = localHash0;
lineLastNumber[0] = 0;
lineLastHashHex[0] = localHash0;
return new AddBlockResult(reqJson, resp, localHash0);
}
/** Добавить TEXT в lineText, следующим lineNum, global=globalNumber. */
public AddBlockResult addText(int globalNumber, short lineText, String text) {
int lineNum = nextLineNum(lineText);
byte[] prevLineHash = prevLineHash32(lineText);
BuiltBlock b = buildTextBlock(
globalNumber,
lineText,
lineNum,
hexToBytes32(globalLastHashHex),
prevLineHash,
text
);
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes));
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
String localHash = bytesToHex64(b.hash32);
// обновляем состояние
globalLastNumber = globalNumber;
globalLastHashHex = localHash;
lineLastNumber[lineText] = lineNum;
lineLastHashHex[lineText] = localHash;
return new AddBlockResult(reqJson, resp, localHash, b.hash32);
}
/** Добавить REACT в lineReact, следующим lineNum, global=globalNumber, ссылка на (toGlobal,toHash32). */
public AddBlockResult addReaction(int globalNumber,
short lineReact,
int reactionCode,
String toBlockchainName,
int toBlockGlobalNumber,
byte[] toBlockHash32) {
int lineNum = nextLineNum(lineReact);
byte[] prevLineHash = prevLineHash32(lineReact);
BuiltBlock b = buildReactionBlock(
globalNumber,
lineReact,
lineNum,
hexToBytes32(globalLastHashHex),
prevLineHash,
reactionCode,
toBlockchainName,
toBlockGlobalNumber,
toBlockHash32
);
String reqJson = buildAddBlockJson(FIXED_REQUEST_ID, blockchainName, globalNumber, globalLastHashHex, base64(b.fullBytes));
String resp = WsJsonRoundtripClient.sendOnce(wsUri, reqJson, Duration.ofSeconds(8));
String localHash = bytesToHex64(b.hash32);
// обновляем состояние
globalLastNumber = globalNumber;
globalLastHashHex = localHash;
lineLastNumber[lineReact] = lineNum;
lineLastHashHex[lineReact] = localHash;
return new AddBlockResult(reqJson, resp, localHash, b.hash32);
}
// =================================================================================
// RESULT HOLDER
// =================================================================================
public static final class AddBlockResult {
public final String requestJson;
public final String responseJson;
/** локально вычисленный hash (HEX64) именно для этого блока */
public final String localHashHex;
/** локальный hash32 (если надо ссылаться на блок дальше) */
public final byte[] localHash32;
public AddBlockResult(String requestJson, String responseJson, String localHashHex) {
this(requestJson, responseJson, localHashHex, null);
}
public AddBlockResult(String requestJson, String responseJson, String localHashHex, byte[] localHash32) {
this.requestJson = requestJson;
this.responseJson = responseJson;
this.localHashHex = localHashHex;
this.localHash32 = localHash32;
}
}
// =================================================================================
// LINE HELPERS
// =================================================================================
/** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 тут только 0. */
private int nextLineNum(short lineIndex) {
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
if (lineIndex == 0) return 0;
return lineLastNumber[lineIndex] + 1;
}
/**
* prevLineHash32 по твоему правилу:
* - для первого блока линии (lineLastNumber[line]==0): prevLineHash = hash(нулевого блока)
* - иначе: prevLineHash = hash последнего блока этой линии
*
* Важно: для line0 здесь не используем (header имеет prevLine=ZERO32).
*/
private byte[] prevLineHash32(short lineIndex) {
if (lineIndex < 0 || lineIndex > 7) throw new IllegalArgumentException("lineIndex must be 0..7");
if (lineIndex == 0) return ZERO32;
if (lineLastNumber[lineIndex] == 0) {
// первый блок линии -> от нулевого блока
if (headerHash32 == null || headerHash32.length != 32) {
throw new IllegalStateException("headerHash32 is not set but required for first block of line " + lineIndex);
}
return headerHash32;
}
String lastHex = lineLastHashHex[lineIndex];
if (lastHex == null || lastHex.isBlank()) {
throw new IllegalStateException("lineLastHashHex[" + lineIndex + "] is blank but lineLastNumber>0");
}
return hexToBytes32(lastHex);
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
/** Небольшой холдер, чтобы сценарий мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */
private static final class BuiltBlock {
final byte[] fullBytes;
final byte[] hash32;
BuiltBlock(byte[] fullBytes, byte[] hash32) {
this.fullBytes = fullBytes;
this.hash32 = hash32;
}
}
private static BuiltBlock buildHeaderBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
HeaderBody body = new HeaderBody(TestConfig.LOGIN());
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildTextBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
// ВАЖНО:
// У тебя сервер ругается: "Body is in wrong lineIndex expected=1 actual=0 (type=1 ver=1)".
// Это значит, что lineIndex хранится ВНУТРИ bodyBytes.
// Ниже безопасный патч: предполагаем формат "type(1) + ver(1) + lineIndex(2)" и проставляем lineIndex.
bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex);
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildReactionBlock(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
int reactionCode,
String toBlockchainName,
int toBlockGlobalNumber,
byte[] toBlockHash32) {
ReactionBody body = new ReactionBody(
reactionCode,
toBlockchainName,
toBlockGlobalNumber,
toBlockHash32 // [32] сырые 32 байта, как ты утвердил
);
byte[] bodyBytes = body.toBytes();
// Аналогично TextBody если внутри есть lineIndex, проставляем.
bodyBytes = patchBodyLineIndexIfPresent(bodyBytes, lineIndex);
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static BuiltBlock buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
byte[] rawBytes = ByteBuffer.allocate(recordSize)
.order(ByteOrder.BIG_ENDIAN)
.putInt(recordSize)
.putInt(globalNumber)
.putLong(ts)
.putShort(lineIndex)
.putInt(lineBlockNumber)
.put(bodyBytes)
.array();
// Ключевой момент: preimage должен совпасть с серверным правилом.
// Сервер НЕ получает prevLineHash по сети он берёт его из своего состояния линии.
// Поэтому мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()).
byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.LOGIN(),
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY());
byte[] full = new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
return new BuiltBlock(full, hash32);
}
/**
* Патч lineIndex внутри bodyBytes.
*
* Предположение (по твоей ошибке type=1 ver=1):
* bodyBytes[0] = type
* bodyBytes[1] = ver
* bodyBytes[2..3] = lineIndex (big-endian short)
*
* Если формат другой скажешь, поменяю оффсет/проверки.
*/
private static byte[] patchBodyLineIndexIfPresent(byte[] bodyBytes, short lineIndex) {
if (bodyBytes == null) return null;
if (bodyBytes.length < 4) return bodyBytes;
// Патчим только для line>0 (для header line=0 и так норм).
if (lineIndex <= 0) return bodyBytes;
ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN).putShort(2, lineIndex);
return bodyBytes;
}
// =================================================================================
// JSON HELPERS
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
private static String base64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
// =================================================================================
// HEX HELPERS
// =================================================================================
private static byte[] hexToBytes32(String hex) {
if (hex == null) throw new IllegalArgumentException("hex is null");
String s = hex.trim();
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
private static String bytesToHex64(byte[] b32) {
if (b32 == null || b32.length != 32) throw new IllegalArgumentException("b32 must be 32 bytes");
char[] out = new char[64];
final char[] HEX = "0123456789abcdef".toCharArray();
for (int i = 0; i < 32; i++) {
int v = b32[i] & 0xFF;
out[i * 2] = HEX[v >>> 4];
out[i * 2 + 1] = HEX[v & 0x0F];
}
return new String(out);
}
}

View File

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

View File

@ -0,0 +1,93 @@
package test.it.ws;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.concurrent.*;
/**
* WsJsonRoundtripClient
*
* Один запрос = одно соединение:
* - открыл WS
* - отправил JSON (text frame)
* - дождался первого ответа TEXT
* - закрыл WS
*
* Здесь requestId НЕ используется вообще (ни для ожидания, ни для логов).
* Просто возвращаем первый пришедший ответ как строку JSON.
*/
public final class WsJsonRoundtripClient {
private WsJsonRoundtripClient() {}
private static final ObjectMapper MAPPER = new ObjectMapper();
public static String sendOnce(String wsUri, String requestJson, Duration timeout) {
HttpClient client = HttpClient.newHttpClient();
CompletableFuture<String> firstMessage = new CompletableFuture<>();
WebSocket ws = client.newWebSocketBuilder()
.connectTimeout(timeout)
.buildAsync(URI.create(wsUri), new WebSocket.Listener() {
private final StringBuilder buf = new StringBuilder();
@Override
public void onOpen(WebSocket webSocket) {
webSocket.request(1);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
buf.append(data);
if (last) {
String msg = buf.toString();
buf.setLength(0);
if (!firstMessage.isDone()) firstMessage.complete(msg);
}
webSocket.request(1);
return CompletableFuture.completedFuture(null);
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
if (!firstMessage.isDone()) firstMessage.completeExceptionally(error);
}
}).join();
// отправляем
ws.sendText(requestJson, true).join();
// ждём
String resp;
try {
resp = firstMessage.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
try { ws.abort(); } catch (Exception ignored) {}
throw new RuntimeException("Timeout/Fail waiting response (single-shot WS). uri=" + wsUri, e);
}
// закрываем
try {
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS).join();
} catch (Exception ignored) {}
return resp;
}
/** Утилита: прочитать status из ответа (если нужно быстро проверить). */
public static int status(String json) {
try {
JsonNode root = MAPPER.readTree(json);
return root.has("status") ? root.get("status").asInt() : -1;
} catch (Exception e) {
return -1;
}
}
}