Старая версия. Вроде там тесты доделал что бы работали
This commit is contained in:
AidarKC 2025-12-28 18:39:13 +03:00
parent 809d897da6
commit 1526392ca5
10 changed files with 399 additions and 85 deletions

View File

@ -51,6 +51,43 @@ dependencies {
implementation project(':shine-server-net-server') // Хэндлеры для обработки сетевых запросов
//
//
// // -------------------- ТЕСТЫ --------------------
// testImplementation platform('org.junit:junit-bom:5.11.4')
// testImplementation 'org.junit.jupiter:junit-jupiter'
// testImplementation 'org.junit.platform:junit-platform-suite-api'
// testRuntimeOnly 'org.junit.platform:junit-platform-suite-engine'
//
// testRuntimeOnly "org.junit.platform:junit-platform-launcher"
//
// // Suite (чтобы запустить классы в заданном порядке)
// testImplementation "org.junit.platform:junit-platform-suite:1.10.2"
//
// // Нужно, чтобы TestExecutionListener (RussianSummaryListener) корректно подхватывался
// testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//}
// -------------------- ТЕСТЫ (JUnit 5) --------------------
// Один BOM на всё семейство JUnit (Jupiter + Platform модули)
testImplementation platform("org.junit:junit-bom:5.10.2")
// JUnit Jupiter (Test, BeforeAll, Assertions)
testImplementation "org.junit.jupiter:junit-jupiter"
// JUnit Platform Suite (@Suite, @SelectClasses)
testImplementation "org.junit.platform:junit-platform-suite-api"
testRuntimeOnly "org.junit.platform:junit-platform-suite-engine"
// Нужно для компиляции RussianSummaryListener (org.junit.platform.launcher.*)
// и чтобы JUnit мог подхватить listener при запуске
testImplementation "org.junit.platform:junit-platform-launcher"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}
test {
useJUnitPlatform()
}
application {
@ -79,12 +116,3 @@ shadowJar {
)
}
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
showStandardStreams = true
}
}

16
src/test/concat_to_file.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
OUTFILE="all_files.txt"
# очищаем или создаём файл
: > "$OUTFILE"
# собрать только *.java файлы и вывести их содержимое в файл
find . -type f -name "*.java" | sort | while read -r f; do
cat "$f" >> "$OUTFILE"
echo >> "$OUTFILE" # пустая строка-разделитель
done
echo "Готово! Все .java файлы собраны в $OUTFILE"

View File

@ -0,0 +1,24 @@
package test.it;
import org.junit.platform.suite.api.SelectClasses;
import org.junit.platform.suite.api.Suite;
import test.it.ws.*;
/**
* Сьют, который запускает IT тесты строго в заданном порядке.
*
* Запуск:
* ./gradlew test --tests test.it.IT_00_Suite
*
* Важно:
* - ItRunContext.initOnce() будет вызван слушателем RussianSummaryListener ДО тестов.
*/
@Suite
@SelectClasses({
IT_01_AddUser.class,
IT_02_Sessions.class,
IT_03_AddBlock_NoAuth.class
})
public class IT_00_Suite {
// пусто
}

View File

@ -0,0 +1,128 @@
package test.it.utils;
import utils.crypto.Ed25519Util;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
/**
* Глобальный контекст интеграционного прогона (один запуск Gradle test).
*
* ВАЖНО:
* - инициализируется ровно один раз на весь процесс JVM;
* - хранит случайный login + blockchainName + ключи, чтобы все IT тесты работали в одной "сессии данных".
*/
public final class ItRunContext {
private static final Object LOCK = new Object();
private static volatile boolean inited = false;
private static String login;
private static String blockchainName;
private static byte[] loginPrivKey;
private static byte[] loginPubKey;
private static byte[] devicePrivKey;
private static byte[] devicePubKey;
private ItRunContext() {}
public static void initOnce() {
if (inited) return;
synchronized (LOCK) {
if (inited) return;
// 1) Генерим читаемый суффикс по времени + случайности, чтобы не было коллизий.
String ts = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss", Locale.ROOT));
String rnd = randomBase32(6).toLowerCase(Locale.ROOT);
// 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)
loginPrivKey = Ed25519Util.generatePrivateKeyFromString(login);
loginPubKey = Ed25519Util.derivePublicKey(loginPrivKey);
// deviceKey: приватный ключ = SHA-256(login + "#device")
String deviceSeedStr = login + "#device";
devicePrivKey = Ed25519Util.generatePrivateKeyFromString(deviceSeedStr);
devicePubKey = Ed25519Util.derivePublicKey(devicePrivKey);
inited = true;
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("login = " + login);
System.out.println("blockchainName = " + blockchainName);
System.out.println("loginPubKey = " + bytesToHexShort(loginPubKey));
System.out.println("devicePubKey = " + bytesToHexShort(devicePubKey));
System.out.println(TestColors.C + "------------------------------------------------------------\n" + TestColors.R);
}
}
public static String login() {
ensureInit();
return login;
}
public static String blockchainName() {
ensureInit();
return blockchainName;
}
public static byte[] loginPrivKey() {
ensureInit();
return loginPrivKey.clone();
}
public static byte[] loginPubKey() {
ensureInit();
return loginPubKey.clone();
}
public static byte[] devicePrivKey() {
ensureInit();
return devicePrivKey.clone();
}
public static byte[] devicePubKey() {
ensureInit();
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) {
if (b == null) return "null";
StringBuilder sb = new StringBuilder();
int n = Math.min(b.length, 10);
for (int i = 0; i < n; i++) sb.append(String.format("%02x", b[i]));
if (b.length > n) sb.append("...");
return sb.toString();
}
}

View File

@ -23,10 +23,10 @@ public final class JsonBuilders {
}
""".formatted(
requestId,
TestConfig.TEST_LOGIN,
TestConfig.TEST_BCH_NAME,
TestConfig.LOGIN_PUBKEY_B64,
TestConfig.DEVICE_PUBKEY_B64,
TestConfig.TEST_LOGIN(),
TestConfig.TEST_BCH_NAME(),
TestConfig.LOGIN_PUBKEY_B64(),
TestConfig.DEVICE_PUBKEY_B64(),
TestConfig.TEST_BCH_LIMIT
);
}
@ -38,7 +38,7 @@ public final class JsonBuilders {
"requestId": "%s",
"payload": { "login": "%s" }
}
""".formatted(requestId, TestConfig.TEST_LOGIN);
""".formatted(requestId, TestConfig.TEST_LOGIN());
}
public static String createAuthSession(String requestId, String authNonce, String storagePwd) {
@ -102,10 +102,15 @@ public final class JsonBuilders {
""".formatted(requestId, sessionId, timeMs, signatureB64);
}
/**
* Подпись для режима AUTH_IN_PROGRESS:
* preimage = "AUTHORIFICATED:" + timeMs + authNonce
* подписываем devicePrivKey (как в твоём протоколе).
*/
public static String signAuthorificated(String authNonce, long timeMs) {
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
byte[] sig = Ed25519Util.sign(preimage, TestConfig.DEVICE_PRIV_KEY);
byte[] sig = Ed25519Util.sign(preimage, TestConfig.DEVICE_PRIV_KEY());
return Base64.getEncoder().encodeToString(sig);
}
}

View File

@ -0,0 +1,90 @@
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

@ -0,0 +1,12 @@
package test.it.utils;
/** ANSI цвета для красивого вывода в терминал. */
public final class TestColors {
private TestColors() {}
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";
}

View File

@ -1,37 +1,63 @@
package test.it.utils;
import utils.crypto.Ed25519Util;
import java.util.Base64;
/**
* Конфиг для IT тестов.
*
* ВАЖНО:
* - login/blockchainName/ключи берём из ItRunContext (случайные на каждый прогон).
*/
public final class TestConfig {
private TestConfig(){}
private TestConfig() {}
// Твой WS URI
public static final String WS_URI = "ws://localhost:7070/ws";
public static final String TEST_LOGIN = "anya24";
public static final String TEST_BCH_NAME = TEST_LOGIN + "001";
public static final int TEST_BCH_LIMIT = 1_000_000;
public static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";
public static final byte[] LOGIN_PRIV_KEY;
public static final String LOGIN_PUBKEY_B64;
// Суффикс блокчейна по твоему правилу: login + 3 цифры
public static final String BCH_SUFFIX_3 = "001";
public static final byte[] DEVICE_PRIV_KEY;
public static final String DEVICE_PUBKEY_B64;
// Лимит блокчейна для AddUser
public static final long TEST_BCH_LIMIT = 50_000_000L;
static {
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
byte[] loginPub = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
LOGIN_PUBKEY_B64 = Ed25519Util.keyToBase64(loginPub);
// Любая строка клиента (для логов)
public static final String TEST_CLIENT_INFO = "it-tests";
DEVICE_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-device-" + TEST_LOGIN);
byte[] devicePub = Ed25519Util.derivePublicKey(DEVICE_PRIV_KEY);
DEVICE_PUBKEY_B64 = Ed25519Util.keyToBase64(devicePub);
public static String TEST_LOGIN() {
return ItRunContext.login();
}
public static String TEST_BCH_NAME() {
return ItRunContext.blockchainName();
}
public static byte[] LOGIN_PRIV_KEY() {
return ItRunContext.loginPrivKey();
}
public static byte[] LOGIN_PUB_KEY() {
return ItRunContext.loginPubKey();
}
public static byte[] DEVICE_PRIV_KEY() {
return ItRunContext.devicePrivKey();
}
public static byte[] DEVICE_PUB_KEY() {
return ItRunContext.devicePubKey();
}
public static String LOGIN_PUBKEY_B64() {
return Base64.getEncoder().encodeToString(LOGIN_PUB_KEY());
}
public static String DEVICE_PUBKEY_B64() {
return Base64.getEncoder().encodeToString(DEVICE_PUB_KEY());
}
/** Псевдо-пароль хранилища — достаточно для тестов. */
public static String fakeStoragePwd() {
byte[] data = new byte[32];
for (int i = 0; i < data.length; i++) data[i] = (byte) (i + 1);
return Base64.getEncoder().encodeToString(data);
return "pwd-" + System.nanoTime();
}
}

View File

@ -1,4 +1,4 @@
package test.it;
package test.it.ws;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.*;
* IT_03_AddBlock_NoAuth
*
* Интеграционный тест добавления блоков в персональный блокчейн без отдельной авторизации,
* в формате твоих IT-тестов (ANSI, шаги, WsTestClient, JsonBuilders/JsonParsers).
* в формате твоих IT-тестов (ANSI, шаги, WsTestClient).
*
* Сценарий:
* 1) AddBlock: HEADER (global=0, prevGlobalHash=ZERO64) -> ожидаем 200
@ -31,11 +31,12 @@ import static org.junit.jupiter.api.Assertions.*;
* 2) AddBlock: TEXT (global=1, prevGlobalHash=serverLastGlobalHash) -> ожидаем 200
*
* Примечание:
* - lastLineHash пока равен lastGlobalHash (как ты говорил).
* - lastLineHash пока равен lastGlobalHash.
* - подпись блока делаем ключом логина (loginPrivKey).
*/
public class IT_03_AddBlock_NoAuth {
// ANSI цвета (работает в большинстве терминалов)
// ANSI цвета
private static final String R = "\u001B[0m";
private static final String G = "\u001B[32m";
private static final String Y = "\u001B[33m";
@ -63,10 +64,6 @@ public class IT_03_AddBlock_NoAuth {
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);
@ -137,21 +134,19 @@ public class IT_03_AddBlock_NoAuth {
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
// =================================================================================
// ШАГ 1: HEADER (global=0)
// =================================================================================
// -------------------- ШАГ 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)
0,
(short) 0,
0,
ZERO32,
ZERO32
);
String reqId1 = "it03-add-header";
String reqJson1 = buildAddBlockJson(reqId1, TestConfig.TEST_BCH_NAME, 0, ZERO64, base64(headerFull));
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));
@ -166,25 +161,23 @@ public class IT_03_AddBlock_NoAuth {
ok("HEADER принят. serverLastGlobalHash=" + serverLastGlobalHash);
// =================================================================================
// ШАГ 2: TEXT (global=1)
// =================================================================================
// -------------------- ШАГ 2: TEXT (global=1) --------------------
stepTitle("ШАГ 2: AddBlock TEXT (global=1)");
byte[] prevGlobal32 = hexToBytes32(serverLastGlobalHash);
byte[] prevLine32 = prevGlobal32; // пока lineHash = globalHash
byte[] prevLine32 = prevGlobal32;
byte[] textFull = buildTextBlockFullBytes(
1, // globalNumber
(short) 0, // lineIndex
1, // lineBlockNumber
1,
(short) 0,
1,
prevGlobal32,
prevLine32,
"Hello from IT_03 test"
);
String reqId2 = "it03-add-text";
String reqJson2 = buildAddBlockJson(reqId2, TestConfig.TEST_BCH_NAME, 1, serverLastGlobalHash, base64(textFull));
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));
@ -201,7 +194,7 @@ public class IT_03_AddBlock_NoAuth {
}
// =================================================================================
// BUILD BLOCKS
// BUILD BLOCKS
// =================================================================================
private static byte[] buildHeaderBlockFullBytes(int globalNumber,
@ -210,9 +203,7 @@ public class IT_03_AddBlock_NoAuth {
byte[] prevGlobalHash32,
byte[] prevLineHash32) {
// HeaderBody формата type=0 ver=1:
// [type][ver][tag "SHiNE001"][loginLen][login]
HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN);
HeaderBody body = new HeaderBody(TestConfig.TEST_LOGIN());
byte[] bodyBytes = body.toBytes();
return buildSignedBlockFullBytes(globalNumber, lineIndex, lineBlockNumber, bodyBytes, prevGlobalHash32, prevLineHash32);
@ -240,7 +231,6 @@ public class IT_03_AddBlock_NoAuth {
long ts = System.currentTimeMillis() / 1000L;
// recordSize = RAW header + body (без подписи/хэша это внутренняя "raw"-часть записи)
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
byte[] rawBytes = ByteBuffer.allocate(recordSize)
@ -254,7 +244,7 @@ public class IT_03_AddBlock_NoAuth {
.array();
byte[] preimage = BchCryptoVerifier.buildPreimage(
TestConfig.TEST_LOGIN,
TestConfig.TEST_LOGIN(),
prevGlobalHash32,
prevLineHash32,
rawBytes
@ -262,8 +252,8 @@ public class IT_03_AddBlock_NoAuth {
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(
globalNumber,
@ -276,15 +266,11 @@ public class IT_03_AddBlock_NoAuth {
).toBytes();
}
// =================================================================================
// JSON BUILD
// =================================================================================
private static String buildAddBlockJson(String requestId,
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
@ -299,14 +285,8 @@ public class IT_03_AddBlock_NoAuth {
""".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");

View File

@ -0,0 +1,5 @@
# ???????????? ??? ?????????, ????? ? ????? ??????? ??? "??????? ?????" ??-??????
junit.platform.listeners.default=test.it.utils.RussianSummaryListener
# ?? ?????? ??????: ????????? ???????????? ?????????? ?? ?????? JUnit
junit.jupiter.execution.parallel.enabled=false