Всё ещё не работает проверка линий. Залил старые тесты какие есть - каке есть
This commit is contained in:
AidarKC 2025-12-29 12:13:12 +03:00
parent c523816cdf
commit 795341dd8d
8 changed files with 543 additions and 248 deletions

View File

@ -1,10 +1,7 @@
package test.it; package test.it;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import test.it.utils.JsonBuilders; import test.it.utils.*;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.WsTestClient;
import java.time.Duration; import java.time.Duration;
@ -12,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.*;
public class IT_01_AddUser { public class IT_01_AddUser {
// ANSI цвета (работает в большинстве терминалов) // ANSI цвета
private static final String R = "\u001B[0m"; private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m"; private static final String G = "\u001B[32m";
private static final String Y = "\u001B[33m"; private static final String Y = "\u001B[33m";
@ -33,23 +30,29 @@ public class IT_01_AddUser {
System.out.println(G + "" + s + R); System.out.println(G + "" + s + R);
} }
private static void warn(String s) {
System.out.println(Y + "⚠️ " + s + R);
}
private static void boom(String s) { private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R); System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R); System.out.println(RED + "" + s + R);
System.out.println(RED + "****************************************************************" + R); System.out.println(RED + "****************************************************************" + R);
} }
public static void main(String[] args) {
// чтобы тест можно было запускать вообще без JUnit
ItRunContext.initIfNeeded();
new IT_01_AddUser().addUser_shouldReturn200_orAlreadyExists();
}
@Test @Test
void addUser_shouldReturn200_orAlreadyExists() { void addUser_shouldReturn200_orAlreadyExists() {
ItRunContext.initIfNeeded();
title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)"); title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println(" blockchainName = " + TestConfig.BCH_NAME());
System.out.println("Ожидание:"); System.out.println("Ожидание:");
System.out.println(" - сервер принимает AddUser и возвращает:"); System.out.println(" - 200 (создан)");
System.out.println(" * 200 (пользователь создан/добавлен)"); System.out.println(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
System.out.println(" * либо 409 + payload.code=USER_ALREADY_EXISTS (если уже есть)\n");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
@ -73,12 +76,9 @@ public class IT_01_AddUser {
boolean already = (st == 409); boolean already = (st == 409);
if (already) { if (already) {
// ВАЖНО: payload.code (не errorCode)
String code = JsonParsers.errorCode(resp); String code = JsonParsers.errorCode(resp);
System.out.println(" server_code=" + code); System.out.println(" server_code=" + code);
// Если 409 пришёл, требуем понятный code
try { 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);
@ -99,7 +99,6 @@ public class IT_01_AddUser {
} }
} catch (AssertionError | RuntimeException e) { } catch (AssertionError | RuntimeException e) {
// чтобы красным было видно даже если Gradle/IDE печатает стек отдельно
boom("ТЕСТ УПАЛ: AddUserIT. Причина: " + e.getMessage()); boom("ТЕСТ УПАЛ: AddUserIT. Причина: " + e.getMessage());
throw e; throw e;
} }

View File

@ -2,10 +2,7 @@ package test.it;
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.JsonBuilders; import test.it.utils.*;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.WsTestClient;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
@ -39,10 +36,6 @@ public class IT_02_Sessions {
System.out.println(G + "" + s + R); System.out.println(G + "" + s + R);
} }
private static void warn(String s) {
System.out.println(Y + "⚠️ " + s + R);
}
private static void boom(String s) { private static void boom(String s) {
System.out.println(RED + "****************************************************************" + R); System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R); System.out.println(RED + "" + s + R);
@ -72,8 +65,16 @@ public class IT_02_Sessions {
} }
} }
public static void main(String[] args) {
ItRunContext.initIfNeeded();
ensureUserExists();
new IT_02_Sessions().sessions_flow_shouldCreateListRefreshCloseCorrectly();
}
@BeforeAll @BeforeAll
static void ensureUserExists() { static void ensureUserExists() {
ItRunContext.initIfNeeded();
title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)"); title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
@ -86,7 +87,6 @@ public class IT_02_Sessions {
int st = JsonParsers.status(resp); int st = JsonParsers.status(resp);
// 200 или "уже есть" ок
if (st == 200) { if (st == 200) {
ok("BeforeAll: пользователь создан/добавлен (status=200)"); ok("BeforeAll: пользователь создан/добавлен (status=200)");
} else if (st == 409) { } else if (st == 409) {
@ -106,7 +106,11 @@ public class IT_02_Sessions {
@Test @Test
void sessions_flow_shouldCreateListRefreshCloseCorrectly() { void sessions_flow_shouldCreateListRefreshCloseCorrectly() {
ItRunContext.initIfNeeded();
title("SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)"); title("SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println("Ожидание сценария:"); System.out.println("Ожидание сценария:");
System.out.println(" 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession"); System.out.println(" 1) Создаём SESSION1 через AuthChallenge + CreateAuthSession");
System.out.println(" 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2"); System.out.println(" 2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1 и SESSION2");
@ -120,7 +124,6 @@ public class IT_02_Sessions {
String s2Id, s2Pwd; String s2Id, s2Pwd;
try { try {
// --- create session1 ---
stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)"); 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";
@ -150,7 +153,6 @@ public class IT_02_Sessions {
ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]"); ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
} }
// --- create session2 and list inside (AUTH_STATUS_USER) ---
stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2"); 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";
@ -178,7 +180,6 @@ public class IT_02_Sessions {
assertNotNull(s2Pwd); assertNotNull(s2Pwd);
ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]"); ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
// list inside session2 (у тебя это AUTH_STATUS_USER без подписи)
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); send("ListSessions(in SESSION2)", req3);
@ -194,7 +195,6 @@ public class IT_02_Sessions {
ok("Проверка OK: список содержит SESSION1 и SESSION2"); ok("Проверка OK: список содержит SESSION1 и SESSION2");
} }
// --- list in AUTH_IN_PROGRESS (подпись по nonce) ---
stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2"); 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";
@ -228,7 +228,6 @@ public class IT_02_Sessions {
ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2"); ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2");
} }
// --- refresh session1 and close session2 (from session1) ---
stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)"); stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
@ -252,7 +251,6 @@ public class IT_02_Sessions {
ok("SESSION2 закрыта"); ok("SESSION2 закрыта");
} }
// --- verify only session1 remains (AUTH_IN_PROGRESS list) ---
stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1"); 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";
@ -284,7 +282,6 @@ public class IT_02_Sessions {
ok("Проверка OK: осталась только SESSION1"); ok("Проверка OK: осталась только SESSION1");
} }
// --- close session1 in AUTH_IN_PROGRESS ---
stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS"); 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";
@ -310,7 +307,6 @@ public class IT_02_Sessions {
ok("SESSION1 закрыта"); ok("SESSION1 закрыта");
} }
// --- verify empty list ---
stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список"); 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";

View File

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

View File

@ -2,17 +2,16 @@ package test.it.utils;
import utils.crypto.Ed25519Util; import utils.crypto.Ed25519Util;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
/** /**
* Глобальный контекст интеграционного прогона (один запуск Gradle test). * Глобальный контекст IT прогона (одна JVM).
* *
* ВАЖНО: * ТЕПЕРЬ:
* - инициализируется ровно один раз на весь процесс JVM; * - login берётся из TestConfig.LOGIN()
* - хранит случайный login + blockchainName + ключи, чтобы все IT тесты работали в одной "сессии данных". * - blockchainName = TestConfig.BCH_NAME()
* - ключи генерятся ДЕТЕРМИНИРОВАННО из login (как ты хотела)
*
* ПЛЮС:
* - тесты можно запускать по одному initIfNeeded() вызовется автоматически.
*/ */
public final class ItRunContext { public final class ItRunContext {
@ -30,25 +29,16 @@ public final class ItRunContext {
private ItRunContext() {} private ItRunContext() {}
public static void initOnce() { /** Инициализировать, если ещё не инициализировано. */
public static void initIfNeeded() {
if (inited) return; if (inited) return;
synchronized (LOCK) { synchronized (LOCK) {
if (inited) return; if (inited) return;
// 1) Генерим читаемый суффикс по времени + случайности, чтобы не было коллизий. login = TestConfig.LOGIN();
String ts = LocalDateTime.now() blockchainName = TestConfig.BCH_NAME();
.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss", Locale.ROOT));
String rnd = randomBase32(6).toLowerCase(Locale.ROOT); // 1) Генерация ключей ИЗ login
// login должен быть валидным по твоим правилам (латиница + цифры + _)
// Пример: it_20251226_173012_ab12cd
login = "it_" + ts + "_" + rnd;
// 2) blockchainName по правилу: login + 4 цифры
blockchainName = login + TestConfig.BCH_SUFFIX_3;
// 3) Генерация ключей ИЗ login (как ты попросила)
// loginKey: приватный ключ = SHA-256(login) // loginKey: приватный ключ = SHA-256(login)
loginPrivKey = Ed25519Util.generatePrivateKeyFromString(login); loginPrivKey = Ed25519Util.generatePrivateKeyFromString(login);
loginPubKey = Ed25519Util.derivePublicKey(loginPrivKey); loginPubKey = Ed25519Util.derivePublicKey(loginPrivKey);
@ -61,7 +51,7 @@ public final class ItRunContext {
inited = true; inited = true;
System.out.println(TestColors.C + "\n============================================================" + TestColors.R); System.out.println(TestColors.C + "\n============================================================" + TestColors.R);
System.out.println(TestColors.C + "IT ПРОГОН: сгенерированы случайные данные" + TestColors.R); System.out.println(TestColors.C + "IT CONTEXT INIT: фиксированные данные из TestConfig" + TestColors.R);
System.out.println(TestColors.C + "============================================================" + TestColors.R); System.out.println(TestColors.C + "============================================================" + TestColors.R);
System.out.println("login = " + login); System.out.println("login = " + login);
System.out.println("blockchainName = " + blockchainName); System.out.println("blockchainName = " + blockchainName);
@ -72,51 +62,35 @@ public final class ItRunContext {
} }
public static String login() { public static String login() {
ensureInit(); initIfNeeded();
return login; return login;
} }
public static String blockchainName() { public static String blockchainName() {
ensureInit(); initIfNeeded();
return blockchainName; return blockchainName;
} }
public static byte[] loginPrivKey() { public static byte[] loginPrivKey() {
ensureInit(); initIfNeeded();
return loginPrivKey.clone(); return loginPrivKey.clone();
} }
public static byte[] loginPubKey() { public static byte[] loginPubKey() {
ensureInit(); initIfNeeded();
return loginPubKey.clone(); return loginPubKey.clone();
} }
public static byte[] devicePrivKey() { public static byte[] devicePrivKey() {
ensureInit(); initIfNeeded();
return devicePrivKey.clone(); return devicePrivKey.clone();
} }
public static byte[] devicePubKey() { public static byte[] devicePubKey() {
ensureInit(); initIfNeeded();
return devicePubKey.clone(); return devicePubKey.clone();
} }
private static void ensureInit() {
if (!inited) {
throw new IllegalStateException("ItRunContext ещё не инициализирован. Он должен быть вызван до тестов (через RussianSummaryListener).");
}
}
private static String randomBase32(int len) {
final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
SecureRandom r = new SecureRandom();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append(alphabet.charAt(r.nextInt(alphabet.length())));
}
return sb.toString();
}
private static String bytesToHexShort(byte[] b) { private static String bytesToHexShort(byte[] b) {
if (b == null) return "null"; if (b == null) return "null";
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@ -23,8 +23,8 @@ public final class JsonBuilders {
} }
""".formatted( """.formatted(
requestId, requestId,
TestConfig.TEST_LOGIN(), TestConfig.LOGIN(),
TestConfig.TEST_BCH_NAME(), TestConfig.BCH_NAME(),
TestConfig.LOGIN_PUBKEY_B64(), TestConfig.LOGIN_PUBKEY_B64(),
TestConfig.DEVICE_PUBKEY_B64(), TestConfig.DEVICE_PUBKEY_B64(),
TestConfig.TEST_BCH_LIMIT TestConfig.TEST_BCH_LIMIT
@ -38,7 +38,7 @@ public final class JsonBuilders {
"requestId": "%s", "requestId": "%s",
"payload": { "login": "%s" } "payload": { "login": "%s" }
} }
""".formatted(requestId, TestConfig.TEST_LOGIN()); """.formatted(requestId, TestConfig.LOGIN());
} }
public static String createAuthSession(String requestId, String authNonce, String storagePwd) { public static String createAuthSession(String requestId, String authNonce, String storagePwd) {

View File

@ -1,90 +0,0 @@
package test.it.utils;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.engine.TestExecutionResult;
import java.util.ArrayList;
import java.util.List;
/**
* Слушатель JUnit, который в конце прогона печатает "главный отчёт" по-русски.
*
* Подключается через src/test/resources/junit-platform.properties
*/
public class RussianSummaryListener implements TestExecutionListener {
private final List<String> failed = new ArrayList<>();
private int total = 0;
private int passed = 0;
private int skipped = 0;
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
// Инициализируем данные прогона прямо тут, чтобы гарантированно до тестов
ItRunContext.initOnce();
}
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
if (!testIdentifier.isTest()) return;
total++;
switch (testExecutionResult.getStatus()) {
case SUCCESSFUL -> passed++;
case ABORTED -> skipped++;
case FAILED -> {
String name = prettyName(testIdentifier);
String msg = testExecutionResult.getThrowable()
.map(t -> t.getClass().getSimpleName() + ": " + safeMsg(t.getMessage()))
.orElse("Причина неизвестна");
failed.add(name + "" + msg);
}
}
}
@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
System.out.println(TestColors.C + "\n============================================================" + TestColors.R);
System.out.println(TestColors.C + "ГЛАВНЫЙ ОТЧЁТ IT ПРОГОНА" + TestColors.R);
System.out.println(TestColors.C + "============================================================" + TestColors.R);
System.out.println("Всего тестов: " + total);
System.out.println(TestColors.G + "Прошло: " + passed + TestColors.R);
System.out.println(TestColors.Y + "Пропущено: " + skipped + TestColors.R);
System.out.println(TestColors.RED + "Упало: " + failed.size() + TestColors.R);
if (failed.isEmpty()) {
System.out.println(TestColors.G + "\n✅ ВСЕ ТЕСТЫ ПРОШЛИ УСПЕШНО" + TestColors.R);
} else {
System.out.println(TestColors.RED + "\n❌ СПИСОК УПАВШИХ ТЕСТОВ:" + TestColors.R);
for (int i = 0; i < failed.size(); i++) {
System.out.println(TestColors.RED + (i + 1) + ") " + failed.get(i) + TestColors.R);
}
}
System.out.println(TestColors.C + "------------------------------------------------------------" + TestColors.R);
System.out.println("login = " + ItRunContext.login());
System.out.println("blockchainName = " + ItRunContext.blockchainName());
System.out.println(TestColors.C + "============================================================\n" + TestColors.R);
}
private static String prettyName(TestIdentifier id) {
// displayName обычно типа: "addUser_shouldReturn200_orAlreadyExists()"
// Добавим класс, если удастся вытащить из legacyId (обычно там есть полное имя)
String dn = id.getDisplayName();
String legacy = id.getLegacyReportingName(); // часто содержит "test.it.IT_01_AddUser"
if (legacy != null && !legacy.isBlank()) {
return legacy + " :: " + (dn == null ? "test" : dn);
}
return dn == null ? "test" : dn;
}
private static String safeMsg(String s) {
if (s == null) return "";
return s.replace("\n", " ").replace("\r", " ");
}
}

View File

@ -5,8 +5,15 @@ import java.util.Base64;
/** /**
* Конфиг для IT тестов. * Конфиг для IT тестов.
* *
* ЛОГИКА:
* - login по умолчанию берём из DEFAULT_LOGIN
* - можно переопределить запуском:
* -Dit.login=anya24
* -Dit.bchSuffix=001
*
* ВАЖНО: * ВАЖНО:
* - login/blockchainName/ключи берём из ItRunContext (случайные на каждый прогон). * - ключи/имя блокчейна вычисляются из login (через ItRunContext).
* - тесты можно запускать по отдельности, ItRunContext сам инициализируется при первом обращении.
*/ */
public final class TestConfig { public final class TestConfig {
@ -15,8 +22,11 @@ public final class TestConfig {
// Твой WS URI // Твой WS URI
public static final String WS_URI = "ws://localhost:7070/ws"; public static final String WS_URI = "ws://localhost:7070/ws";
// ======= По умолчанию (можно поменять под свою среду) =======
public static final String DEFAULT_LOGIN = "anya24";
// Суффикс блокчейна по твоему правилу: login + 3 цифры // Суффикс блокчейна по твоему правилу: login + 3 цифры
public static final String BCH_SUFFIX_3 = "001"; public static final String DEFAULT_BCH_SUFFIX_3 = "001";
// Лимит блокчейна для AddUser // Лимит блокчейна для AddUser
public static final long TEST_BCH_LIMIT = 50_000_000L; public static final long TEST_BCH_LIMIT = 50_000_000L;
@ -24,14 +34,26 @@ public final class TestConfig {
// Любая строка клиента (для логов) // Любая строка клиента (для логов)
public static final String TEST_CLIENT_INFO = "it-tests"; public static final String TEST_CLIENT_INFO = "it-tests";
public static String TEST_LOGIN() { // Папка данных (которую будет чистить IT_RunAllMain)
return ItRunContext.login(); public static final String DATA_DIR = "data";
/** login для прогона (по умолчанию DEFAULT_LOGIN, можно переопределить -Dit.login=...). */
public static String LOGIN() {
return System.getProperty("it.login", DEFAULT_LOGIN);
} }
public static String TEST_BCH_NAME() { /** Суффикс для имени блокчейна (по умолчанию DEFAULT_BCH_SUFFIX_3, можно переопределить -Dit.bchSuffix=...). */
return ItRunContext.blockchainName(); public static String BCH_SUFFIX_3() {
return System.getProperty("it.bchSuffix", DEFAULT_BCH_SUFFIX_3);
} }
/** blockchainName по правилу: login + суффикс. */
public static String BCH_NAME() {
return LOGIN() + BCH_SUFFIX_3();
}
// ======= Ключи (берём из ItRunContext) =======
public static byte[] LOGIN_PRIV_KEY() { public static byte[] LOGIN_PRIV_KEY() {
return ItRunContext.loginPrivKey(); return ItRunContext.loginPrivKey();
} }

View File

@ -3,9 +3,11 @@ package test.it.ws;
import blockchain.BchBlockEntry; import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier; import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody; import blockchain.body.HeaderBody;
import blockchain.body.ReactionBody;
import blockchain.body.TextBody; 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.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;
@ -22,30 +24,45 @@ import static org.junit.jupiter.api.Assertions.*;
/** /**
* IT_03_AddBlock_NoAuth * IT_03_AddBlock_NoAuth
* *
* Интеграционный тест добавления блоков в персональный блокчейн без отдельной авторизации, * Интеграционный тест добавления блоков в персональный блокчейн без отдельной авторизации.
* в формате твоих IT-тестов (ANSI, шаги, WsTestClient).
* *
* Сценарий: * Сценарий (как ты попросил):
* 1) AddBlock: HEADER (global=0, prevGlobalHash=ZERO64) -> ожидаем 200 * 1) AddBlock: HEADER (global=0, line=0, lineNum=0, prevGlobalHash=ZERO64) -> 200
* - забираем payload.serverLastGlobalHash * 2) AddBlock: TEXT#1 (global=1, line=1, lineNum=1, prevGlobalHash=hash(0)) -> 200
* 2) AddBlock: TEXT (global=1, prevGlobalHash=serverLastGlobalHash) -> ожидаем 200 * 3) AddBlock: TEXT#2 (global=2, line=1, lineNum=2, prevGlobalHash=hash(1)) -> 200
* 4) AddBlock: TEXT#3 (global=3, line=1, lineNum=3, prevGlobalHash=hash(2)) -> 200
* 5) AddBlock: REACT#1 (global=4, line=2, lineNum=1, prevGlobalHash=hash(3)) -> 200
* - реакция на TEXT#1 (toBchName, toGlobal=1, toHash=hash(TEXT#1))
* *
* Примечание: * Важно по линиям (твоя договорённость):
* - lastLineHash пока равен lastGlobalHash. * - line 0: нулевой блок (HEADER) один на весь блокчейн (глобальный 0)
* - подпись блока делаем ключом логина (loginPrivKey). * - line 1 и line 2: первый блок каждой линии ссылается prevLineHash на hash(нулевого блока)
*
* В этом тесте мы ведём 2 массива:
* - lineLastNumber[line] сколько блоков в линии (то есть последний lineNum)
* - lineLastHashHex[line] hash последнего блока линии (HEX64)
*/ */
public class IT_03_AddBlock_NoAuth { public class IT_03_AddBlock_NoAuth {
// ANSI цвета // ANSI цвета
private static final String R = "\u001B[0m"; private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m"; 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 RED = "\u001B[31m";
private static final String C = "\u001B[36m"; private static final String C = "\u001B[36m";
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);
private static final short LINE_HEADER = 0;
private static final short LINE_TEXT = 1;
private static final short LINE_REACT = 2;
public static void main(String[] args) {
ItRunContext.initIfNeeded();
ensureUserExists();
new IT_03_AddBlock_NoAuth().addBlock_shouldAppendHeaderThenTextThenReaction();
}
private static void line() { private static void line() {
System.out.println(C + "------------------------------------------------------------" + R); System.out.println(C + "------------------------------------------------------------" + R);
} }
@ -95,6 +112,8 @@ public class IT_03_AddBlock_NoAuth {
@BeforeAll @BeforeAll
static void ensureUserExists() { static void ensureUserExists() {
ItRunContext.initIfNeeded();
title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)"); title("AddBlockIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
@ -125,67 +144,261 @@ public class IT_03_AddBlock_NoAuth {
} }
@Test @Test
void addBlock_shouldAppendHeaderThenText() { void addBlock_shouldAppendHeaderThenTextThenReaction() {
title("AddBlockIT: добавить HEADER(0) и затем TEXT(1) без auth — с проверкой serverLastGlobalHash"); ItRunContext.initIfNeeded();
title("AddBlockIT: HEADER(0) + TEXT(1,2,3) + REACT(4->text1) без auth");
System.out.println("Используем:");
System.out.println(" login = " + TestConfig.LOGIN());
System.out.println(" blockchainName = " + TestConfig.BCH_NAME());
System.out.println("Ожидание:"); System.out.println("Ожидание:");
System.out.println(" 1) AddBlock HEADER (global=0, prev=ZERO64) -> 200"); System.out.println(" 1) HEADER (global=0, line=0, lineNum=0, prevGlobal=ZERO64) -> 200");
System.out.println(" - в ответе payload.serverLastGlobalHash (64 hex)"); System.out.println(" 2) TEXT#1 (global=1, line=1, lineNum=1, prevGlobal=hash0, prevLine=hash0) -> 200");
System.out.println(" 2) AddBlock TEXT (global=1, prev=serverLastGlobalHash) -> 200\n"); 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)) { try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
// -------------------- ШАГ 1: HEADER (global=0) -------------------- // ============================
stepTitle("ШАГ 1: AddBlock HEADER (global=0)"); // Локальное состояние теста
// ============================
int[] lineLastNumber = new int[8];
String[] lineLastHashHex = new String[8];
for (int i = 0; i < 8; i++) lineLastHashHex[i] = "";
byte[] headerFull = buildHeaderBlockFullBytes( int globalLastNumber = -1;
String globalLastHashHex = ZERO64;
byte[] headerHash32 = null; // понадобится как prevLineHash для первых блоков линий 1/2
// =========================================================
// ШАГ 1: HEADER (global=0, line=0, lineNum=0)
// =========================================================
stepTitle("ШАГ 1: AddBlock HEADER (global=0, line=0, lineNum=0)");
BuiltBlock header = buildHeaderBlock(
0, 0,
(short) 0, LINE_HEADER,
0, 0,
ZERO32, ZERO32, // prevGlobalHash32
ZERO32 ZERO32 // prevLineHash32
); );
String reqId1 = "it03-add-header"; String reqId1 = "it03-add-header";
String reqJson1 = buildAddBlockJson(reqId1, TestConfig.TEST_BCH_NAME(), 0, ZERO64, base64(headerFull)); String reqJson1 = buildAddBlockJson(reqId1, TestConfig.BCH_NAME(), 0, ZERO64, base64(header.fullBytes));
send("AddBlock#HEADER", reqJson1); send("AddBlock(" + reqId1 + ")", reqJson1);
String resp1 = client.request(reqId1, reqJson1, Duration.ofSeconds(8)); String resp1 = client.request(reqId1, reqJson1, Duration.ofSeconds(8));
recv("AddBlock#HEADER", resp1); recv("AddBlock(" + reqId1 + ")", resp1);
assert200("AddBlock#HEADER", resp1); assert200("AddBlock(" + reqId1 + ")", resp1);
String serverLastGlobalHash = extractPayloadString(resp1, "serverLastGlobalHash"); String serverLastGlobalHash0 = extractPayloadString(resp1, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash, "HEADER: payload.serverLastGlobalHash must not be null"); assertNotNull(serverLastGlobalHash0, "HEADER: payload.serverLastGlobalHash must not be null");
assertFalse(serverLastGlobalHash.isBlank(), "HEADER: payload.serverLastGlobalHash must not be blank"); assertEquals(64, serverLastGlobalHash0.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars");
assertEquals(64, serverLastGlobalHash.trim().length(), "HEADER: serverLastGlobalHash must be 64 hex chars");
ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash); String localHash0 = bytesToHex64(header.hash32);
assertEquals(localHash0, serverLastGlobalHash0, "HEADER: serverLastGlobalHash должен совпасть с локальным hash");
// -------------------- ШАГ 2: TEXT (global=1) -------------------- // обновляем локальное состояние
stepTitle("ШАГ 2: AddBlock TEXT (global=1)"); headerHash32 = header.hash32;
globalLastNumber = 0;
globalLastHashHex = localHash0;
byte[] prevGlobal32 = hexToBytes32(serverLastGlobalHash); lineLastNumber[0] = 0;
byte[] prevLine32 = prevGlobal32; lineLastHashHex[0] = localHash0;
byte[] textFull = buildTextBlockFullBytes( ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash0);
// =========================================================
// Общая проверка: headerHash32 уже есть
// =========================================================
assertNotNull(headerHash32, "internal: headerHash32 must be set after header step");
// =========================================================
// ШАГ 2: TEXT#1 (global=1, line=1, lineNum=1)
// prevLineHash для первого блока линии = hash(нулевого блока)
// =========================================================
stepTitle("ШАГ 2: AddBlock TEXT#1 (global=1, line=1, lineNum=1)");
int text1LineNum = nextLineNum(lineLastNumber, LINE_TEXT);
byte[] prevLineHashText1 = prevLineHash32(lineLastNumber, lineLastHashHex, headerHash32, LINE_TEXT);
BuiltBlock text1 = buildTextBlock(
1, 1,
(short) 0, LINE_TEXT,
1, text1LineNum,
prevGlobal32, hexToBytes32(globalLastHashHex), // prevGlobalHash32
prevLine32, prevLineHashText1, // prevLineHash32
"Hello from IT_03 test" "Hello #1 from IT_03 test"
); );
String reqId2 = "it03-add-text"; String reqId2 = "it03-add-text-1";
String reqJson2 = buildAddBlockJson(reqId2, TestConfig.TEST_BCH_NAME(), 1, serverLastGlobalHash, base64(textFull)); String reqJson2 = buildAddBlockJson(reqId2, TestConfig.BCH_NAME(), 1, globalLastHashHex, base64(text1.fullBytes));
send("AddBlock#TEXT", reqJson2); send("AddBlock(" + reqId2 + ")", reqJson2);
String resp2 = client.request(reqId2, reqJson2, Duration.ofSeconds(8)); String resp2 = client.request(reqId2, reqJson2, Duration.ofSeconds(8));
recv("AddBlock#TEXT", resp2); recv("AddBlock(" + reqId2 + ")", resp2);
assert200("AddBlock#TEXT", resp2); assert200("AddBlock(" + reqId2 + ")", resp2);
ok("ТЕСТ ПРОЙДЕН: AddBlock HEADER(0) + TEXT(1) успешно добавлены"); 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)
TestConfig.BCH_NAME(),
1, // toBlockGlobalNumber = 1 (TEXT#1)
text1.hash32 // toBlockHash32 = hash(TEXT#1)
);
String reqId5 = "it03-add-react-1";
String reqJson5 = buildAddBlockJson(reqId5, TestConfig.BCH_NAME(), 4, globalLastHashHex, base64(react1.fullBytes));
send("AddBlock(" + reqId5 + ")", reqJson5);
String resp5 = client.request(reqId5, reqJson5, Duration.ofSeconds(8));
recv("AddBlock(" + reqId5 + ")", resp5);
assert200("AddBlock(" + reqId5 + ")", resp5);
String serverLastGlobalHash4 = extractPayloadString(resp5, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash4, "REACT#1: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash4.trim().length(), "REACT#1: serverLastGlobalHash must be 64 hex chars");
String localHash4 = bytesToHex64(react1.hash32);
assertEquals(localHash4, serverLastGlobalHash4, "REACT#1: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = 4;
globalLastHashHex = localHash4;
lineLastNumber[LINE_REACT] = react1LineNum;
lineLastHashHex[LINE_REACT] = localHash4;
ok("REACT#1 принят. hash4=" + serverLastGlobalHash4);
// =========================================================
// Итоговый контроль массивов линий
// =========================================================
ok("ИТОГ по линиям:");
ok(" line0: lastNum=" + lineLastNumber[0] + ", lastHash=" + lineLastHashHex[0]);
ok(" line1: lastNum=" + lineLastNumber[1] + ", lastHash=" + lineLastHashHex[1]);
ok(" line2: lastNum=" + lineLastNumber[2] + ", lastHash=" + lineLastHashHex[2]);
ok("ТЕСТ ПРОЙДЕН: HEADER + 3xTEXT(line1) + 1xREACT(line2) успешно добавлены и согласованы по globalHash/lineHash");
} catch (AssertionError | RuntimeException e) { } catch (AssertionError | RuntimeException e) {
boom("ТЕСТ УПАЛ: AddBlockIT. Причина: " + e.getMessage()); boom("ТЕСТ УПАЛ: AddBlockIT. Причина: " + e.getMessage());
@ -193,28 +406,81 @@ public class IT_03_AddBlock_NoAuth {
} }
} }
// =================================================================================
// 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 // BUILD BLOCKS
// ================================================================================= // =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber, /** Небольшой холдер, чтобы тест мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */
short lineIndex, private static final class BuiltBlock {
int lineBlockNumber, final byte[] fullBytes;
byte[] prevGlobalHash32, final byte[] hash32;
byte[] prevLineHash32) {
HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN()); 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(); 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 BuiltBlock buildTextBlock(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();
@ -222,12 +488,34 @@ public class IT_03_AddBlock_NoAuth {
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32); return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
} }
private static byte[] buildSignedBlockFullBytes(int globalNumber, private static BuiltBlock buildReactionBlock(int globalNumber,
short lineIndex, short lineIndex,
int lineBlockNumber, int lineBlockNumber,
byte[] bodyBytes, byte[] prevGlobalHash32,
byte[] prevGlobalHash32, byte[] prevLineHash32,
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; long ts = System.currentTimeMillis() / 1000L;
@ -243,8 +531,11 @@ public class IT_03_AddBlock_NoAuth {
.put(bodyBytes) .put(bodyBytes)
.array(); .array();
// Ключевой момент: preimage должен совпасть с серверным правилом.
// Сервер НЕ получает prevLineHash по сети он берёт его из своего состояния линии.
// Поэтому в тесте мы обязаны передавать сюда ровно тот же prevLineHash32 (см. prevLineHash32()).
byte[] preimage = BchCryptoVerifier.buildPreimage( byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.TEST_LOGIN(), TestConfig.LOGIN(),
prevGlobalHash32, prevGlobalHash32,
prevLineHash32, prevLineHash32,
rawBytes rawBytes
@ -252,10 +543,9 @@ public class IT_03_AddBlock_NoAuth {
byte[] hash32 = BchCryptoVerifier.sha256(preimage); byte[] hash32 = BchCryptoVerifier.sha256(preimage);
// Подпись делаем ключом логина
byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY()); byte[] signature64 = Ed25519Util.sign(hash32, TestConfig.LOGIN_PRIV_KEY());
return new BchBlockEntry( byte[] full = new BchBlockEntry(
globalNumber, globalNumber,
ts, ts,
lineIndex, lineIndex,
@ -264,8 +554,14 @@ public class IT_03_AddBlock_NoAuth {
signature64, signature64,
hash32 hash32
).toBytes(); ).toBytes();
return new BuiltBlock(full, hash32);
} }
// =================================================================================
// JSON HELPERS
// =================================================================================
private static String buildAddBlockJson(String requestId, private static String buildAddBlockJson(String requestId,
String blockchainName, String blockchainName,
int globalNumber, int globalNumber,
@ -301,6 +597,10 @@ public class IT_03_AddBlock_NoAuth {
return Base64.getEncoder().encodeToString(bytes); return Base64.getEncoder().encodeToString(bytes);
} }
// =================================================================================
// HEX HELPERS
// =================================================================================
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();
@ -314,4 +614,16 @@ public class IT_03_AddBlock_NoAuth {
} }
return out; 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);
}
} }