Сделал три теста на общий формат
This commit is contained in:
AidarKC 2025-12-25 17:16:15 +03:00
parent 25aa57dc5e
commit eeb8ee9069
4 changed files with 339 additions and 293 deletions

View File

@ -10,7 +10,7 @@ import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
public class AddUserIT {
public class IT_01_AddUser {
// ANSI цвета (работает в большинстве терминалов)
private static final String R = "\u001B[0m";

View File

@ -12,7 +12,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class SessionsIT {
public class IT_02_Sessions {
// ANSI цвета
private static final String R = "\u001B[0m";

View File

@ -0,0 +1,337 @@
package test.it;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import test.it.utils.JsonBuilders;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.WsTestClient;
import utils.crypto.Ed25519Util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Duration;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.*;
/**
* IT_03_AddBlock_NoAuth
*
* Интеграционный тест добавления блоков в персональный блокчейн без отдельной авторизации,
* в формате твоих IT-тестов (ANSI, шаги, WsTestClient, JsonBuilders/JsonParsers).
*
* Сценарий:
* 1) AddBlock: HEADER (global=0, prevGlobalHash=ZERO64) -> ожидаем 200
* - забираем payload.serverLastGlobalHash
* 2) AddBlock: TEXT (global=1, prevGlobalHash=serverLastGlobalHash) -> ожидаем 200
*
* Примечание:
* - lastLineHash пока равен lastGlobalHash (как ты говорил).
*/
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 Y = "\u001B[33m";
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 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 warn(String s) {
System.out.println(Y + "⚠️ " + s + R);
}
private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R);
}
private static void send(String op, String json) {
System.out.println("📤 [" + op + "] Request JSON:");
System.out.println(json);
line();
}
private static void recv(String op, String json) {
System.out.println("📥 [" + op + "] Response JSON:");
System.out.println(json);
line();
}
private static void assert200(String op, String resp) {
int st = JsonParsers.status(resp);
try {
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
ok(op + ": status=200");
} catch (AssertionError ae) {
boom(op + ": ожидали 200, но получили " + st);
throw ae;
}
}
@BeforeAll
static void ensureUserExists() {
title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "it03-adduser-beforeall";
String reqJson = JsonBuilders.addUser(reqId);
send("AddUser(BeforeAll)", reqJson);
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
recv("AddUser(BeforeAll)", resp);
int st = JsonParsers.status(resp);
if (st == 200) {
ok("BeforeAll: пользователь создан/добавлен (status=200)");
} else if (st == 409) {
String code = JsonParsers.errorCode(resp);
if ("USER_ALREADY_EXISTS".equals(code)) {
ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
} else {
boom("BeforeAll: status=409, но code неожиданный: " + code);
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
}
} else {
boom("BeforeAll: предусловие не выполнено. status=" + st);
fail("User precondition failed. status=" + st + ", resp=" + resp);
}
}
}
@Test
void addBlock_shouldAppendHeaderThenText() {
title("AddBlockIT: добавить HEADER(0) и затем TEXT(1) без auth — с проверкой serverLastGlobalHash");
System.out.println("Ожидание:");
System.out.println(" 1) AddBlock HEADER (global=0, prev=ZERO64) -> 200");
System.out.println(" - в ответе payload.serverLastGlobalHash (64 hex)");
System.out.println(" 2) AddBlock TEXT (global=1, prev=serverLastGlobalHash) -> 200\n");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
// =================================================================================
// ШАГ 1: HEADER (global=0)
// =================================================================================
stepTitle("ШАГ 1: AddBlock HEADER (global=0)");
byte[] headerFull = buildHeaderBlockFullBytes(
0, // globalNumber
(short) 0, // lineIndex
0, // lineBlockNumber
ZERO32, // prevGlobalHash32
ZERO32 // prevLineHash32 (пока равно prevGlobal)
);
String reqId1 = "it03-add-header";
String reqJson1 = buildAddBlockJson(reqId1, TestConfig.TEST_BCH_NAME, 0, ZERO64, base64(headerFull));
send("AddBlock#HEADER", reqJson1);
String resp1 = client.request(reqId1, reqJson1, Duration.ofSeconds(8));
recv("AddBlock#HEADER", resp1);
assert200("AddBlock#HEADER", resp1);
String serverLastGlobalHash = extractPayloadString(resp1, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash, "HEADER: payload.serverLastGlobalHash must not be null");
assertFalse(serverLastGlobalHash.isBlank(), "HEADER: payload.serverLastGlobalHash must not be blank");
assertEquals(64, serverLastGlobalHash.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars");
ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash);
// =================================================================================
// ШАГ 2: TEXT (global=1)
// =================================================================================
stepTitle("ШАГ 2: AddBlock TEXT (global=1)");
byte[] prevGlobal32 = hexToBytes32(serverLastGlobalHash);
byte[] prevLine32 = prevGlobal32; // пока lineHash = globalHash
byte[] textFull = buildTextBlockFullBytes(
1, // globalNumber
(short) 0, // lineIndex
1, // lineBlockNumber
prevGlobal32,
prevLine32,
"Hello from IT_03 test"
);
String reqId2 = "it03-add-text";
String reqJson2 = buildAddBlockJson(reqId2, TestConfig.TEST_BCH_NAME, 1, serverLastGlobalHash, base64(textFull));
send("AddBlock#TEXT", reqJson2);
String resp2 = client.request(reqId2, reqJson2, Duration.ofSeconds(8));
recv("AddBlock#TEXT", resp2);
assert200("AddBlock#TEXT", resp2);
ok("ТЕСТ ПРОЙДЕН: AddBlock HEADER(0) + TEXT(1) успешно добавлены");
} catch (AssertionError | RuntimeException e) {
boom("ТЕСТ УПАЛ: AddBlockIT. Причина: " + e.getMessage());
throw e;
}
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
// HeaderBody формата type=0 ver=1:
// [type][ver][tag "SHiNE001"][loginLen][login]
HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildTextBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
// recordSize = RAW header + body (без подписи/хэша это внутренняя "raw"-часть записи)
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();
byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.TEST_LOGIN,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
// В этом тесте подпись делаем ключом логина (как у тебя было)
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY);
return new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
}
// =================================================================================
// JSON BUILD
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
// =================================================================================
// HELPERS
// =================================================================================
private static String extractPayloadString(String json, String field) {
try {
// JsonParsers у тебя уже есть, но тут проще и не ломать совместимость:
// Если захочешь можем добавить в JsonParsers отдельный метод payloadString(...)
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);
}
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;
}
}

View File

@ -1,291 +0,0 @@
package test.it.ws;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import test.it.utils.TestConfig;
import utils.crypto.Ed25519Util;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Base64;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
public class Test_AddBlock_NoAuth {
private static final ObjectMapper JSON = new ObjectMapper();
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
public static void main(String[] args) throws Exception {
CountDownLatch latch = new CountDownLatch(1);
HttpClient client = HttpClient.newHttpClient();
client.newWebSocketBuilder()
.buildAsync(URI.create(TestConfig.WS_URI), new WebSocket.Listener() {
private int step = 0;
private String lastGlobalHashHex = ZERO64;
@Override
public void onOpen(WebSocket ws) {
System.out.println("✅ WS connected: " + TestConfig.WS_URI);
ws.request(1);
// 1) HEADER block: global=0, line=0, lineNumber=0
byte[] headerFull = buildHeaderBlockFullBytes(
0,
(short) 0,
0,
ZERO32,
ZERO32
);
String json = buildAddBlockJson(
"test-add-header",
TestConfig.TEST_BCH_NAME,
0,
ZERO64,
base64(headerFull)
);
System.out.println("\n📤 SEND #1 (HEADER):\n" + json);
ws.sendText(json, true);
}
@Override
public CompletionStage<?> onText(WebSocket ws, CharSequence data, boolean last) {
String msg = data.toString();
System.out.println("\n📥 RECV:\n" + msg);
System.out.println("-----------------------------------------------------");
try {
int status = extractStatus(msg);
if (step == 0) {
if (status != 200) {
System.out.println("❌ HEADER rejected, status=" + status);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "fail");
return CompletableFuture.completedFuture(null);
}
String serverLastGlobalHash = extractPayloadString(msg, "serverLastGlobalHash");
if (serverLastGlobalHash == null || serverLastGlobalHash.isBlank()) {
System.out.println("❌ No serverLastGlobalHash in response");
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bad-response");
return CompletableFuture.completedFuture(null);
}
lastGlobalHashHex = serverLastGlobalHash;
byte[] prevGlobal32 = hexToBytes32(lastGlobalHashHex);
byte[] prevLine32 = prevGlobal32;
// 2) TEXT block: global=1, line=0, lineNumber=1
byte[] textFull = buildTextBlockFullBytes(
1,
(short) 0,
1,
prevGlobal32,
prevLine32,
"Hello from test client"
);
String json2 = buildAddBlockJson(
"test-add-text",
TestConfig.TEST_BCH_NAME,
1,
lastGlobalHashHex,
base64(textFull)
);
System.out.println("\n📤 SEND #2 (TEXT):\n" + json2);
step = 1;
ws.sendText(json2, true);
} else if (step == 1) {
if (status != 200) {
System.out.println("❌ TEXT rejected, status=" + status);
} else {
System.out.println("✅ Done. Closing.");
}
ws.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
}
} catch (Exception e) {
e.printStackTrace(System.out);
ws.sendClose(WebSocket.NORMAL_CLOSURE, "exception");
}
ws.request(1);
return CompletableFuture.completedFuture(null);
}
@Override
public void onError(WebSocket ws, Throwable error) {
System.out.println("❌ WS error: " + error.getMessage());
error.printStackTrace(System.out);
latch.countDown();
}
@Override
public CompletionStage<?> onClose(WebSocket ws, int statusCode, String reason) {
System.out.println("🔚 WS closed. code=" + statusCode + " reason=" + reason);
latch.countDown();
return CompletableFuture.completedFuture(null);
}
}).join();
latch.await();
}
// =================================================================================
// BUILD BLOCKS
// =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
// В твоём текущем коде HeaderBody формата type=0 ver=1:
// [type][ver][tag "SHiNE001"][loginLen][login]
HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildTextBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] prevGlobalHash32,
byte[] prevLineHash32,
String text) {
TextBody body = new TextBody(text);
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
}
private static byte[] buildSignedBlockFullBytes(int globalNumber,
short lineIndex,
int lineBlockNumber,
byte[] bodyBytes,
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
long ts = System.currentTimeMillis() / 1000L;
// recordSize = только RAW = header + body
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();
byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.TEST_LOGIN,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY);
return new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineBlockNumber,
bodyBytes,
signature64,
hash32
).toBytes();
}
// =================================================================================
// JSON BUILD
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
// =================================================================================
// HELPERS
// =================================================================================
private static int extractStatus(String json) {
try {
JsonNode root = JSON.readTree(json);
if (root.has("status")) return root.get("status").asInt();
} catch (Exception ignore) {}
return -1;
}
private static String extractPayloadString(String json, String field) {
try {
JsonNode root = JSON.readTree(json);
JsonNode payload = root.get("payload");
if (payload != null && payload.has(field)) {
return payload.get(field).asText();
}
} catch (Exception ignore) {}
return null;
}
private static String base64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
private static byte[] hexToBytes32(String hex) {
if (hex == null) throw new IllegalArgumentException("hex is null");
String s = hex.trim();
if (s.length() != 64) throw new IllegalArgumentException("hex must be 64 chars, got " + s.length());
byte[] out = new byte[32];
for (int i = 0; i < 32; i++) {
int hi = Character.digit(s.charAt(i * 2), 16);
int lo = Character.digit(s.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex at pos " + (i * 2));
out[i] = (byte) ((hi << 4) | lo);
}
return out;
}
}