Тесты почти переделал
This commit is contained in:
AidarKC 2026-01-08 14:12:16 +03:00
parent e2b89da2fa
commit 8e19486cf5
18 changed files with 609 additions and 1839 deletions

View File

@ -1,94 +1,59 @@
package test.it;
import org.junit.jupiter.api.Test;
import test.it.utils.*;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.fail;
/**
* IT_01_AddUser
*
* Можно запускать:
* 1) как JUnit тест (через Suite или выборочно)
* 2) вручную как standalone:
* - main()
* - или через IT_RunAllMain / IT_RunAllCleanMain
*
* Главная цель:
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
* - и иметь main() для запуска одного теста
* Создаёт 3 пользователей: TestUser1/2/3 (200 OK или 409 USER_ALREADY_EXISTS).
*/
public class IT_01_AddUser {
public static void main(String[] args) {
// чтобы тест можно было запускать вообще без JUnit
int failed = run();
String summary = run();
System.out.println(summary);
}
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
public static int run() {
return TestLog.runOne("IT_01_AddUser", IT_01_AddUser::testBody);
public static String run() {
TestResult r = new TestResult("IT_01_AddUser");
Duration t = Duration.ofSeconds(5);
try (WsSession ws = WsSession.open()) {
r.ok("AddUser USER1: " + TestConfig.LOGIN());
checkAddUser200or409(r, ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t));
r.ok("AddUser USER2: " + TestConfig.LOGIN2());
checkAddUser200or409(r, ws.call("AddUser#USER2", JsonBuilders.addUser(TestConfig.LOGIN2()), t));
r.ok("AddUser USER3: " + TestConfig.LOGIN3());
checkAddUser200or409(r, ws.call("AddUser#USER3", JsonBuilders.addUser(TestConfig.LOGIN3()), t));
} catch (Throwable e) {
r.fail("IT_01_AddUser упал: " + e.getMessage());
}
// @Test
void addUser_shouldReturn200_orAlreadyExists() {
// JUnit-режим: пусть падает через assert/fail как обычно
testBody();
return r.summaryLine();
}
private static void testBody() {
ItRunContext.initIfNeeded();
TestLog.title("AddUserIT: проверка добавления пользователя (200 OK) или 'уже существует' (409 USER_ALREADY_EXISTS)");
TestLog.info("Используем:");
TestLog.info(" login = " + TestConfig.LOGIN());
TestLog.info(" blockchainName = " + TestConfig.BCH_NAME());
TestLog.info("Ожидание:");
TestLog.info(" - 200 (создан)");
TestLog.info(" - или 409 + payload.code=USER_ALREADY_EXISTS\n");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "it-adduser-1";
String reqJson = JsonBuilders.addUser(reqId);
TestLog.info("📤 Отправляем AddUser запрос:");
TestLog.info(reqJson);
TestLog.line();
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
TestLog.info("📥 Ответ сервера:");
TestLog.info(resp);
TestLog.line();
private static void checkAddUser200or409(TestResult r, String resp) {
int st = JsonParsers.status(resp);
TestLog.info(" status=" + st);
boolean created = (st == 200);
boolean already = (st == 409);
if (already) {
if (st == 200) {
r.ok("AddUser: status=200 (создан)");
return;
}
if (st == 409) {
String code = JsonParsers.errorCode(resp);
TestLog.info(" server_code=" + code);
assertEquals("USER_ALREADY_EXISTS", code,
"Expected code=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp);
TestLog.ok("409 получен корректно: USER_ALREADY_EXISTS");
if ("USER_ALREADY_EXISTS".equals(code)) {
r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)");
return;
}
if (created) {
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser создан/добавлен (status=200)");
} else if (already) {
TestLog.ok("ТЕСТ ПРОЙДЕН: AddUser уже есть в системе (status=409, USER_ALREADY_EXISTS)");
} else {
TestLog.boom("Неожиданный status=" + st + ", resp=" + resp);
fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp);
}
r.fail("AddUser: status=409 но code=" + code + ", resp=" + resp);
fail("AddUser unexpected 409 code=" + code);
}
r.fail("AddUser: неожиданный status=" + st + ", resp=" + resp);
fail("AddUser unexpected status=" + st);
}
}

View File

@ -1,7 +1,5 @@
package test.it;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import test.it.utils.*;
import java.time.Duration;
@ -12,318 +10,151 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* IT_02_Sessions
*
* Можно запускать:
* 1) как JUnit тест (через Suite или выборочно)
* 2) вручную как standalone:
* - main()
* - или через IT_RunAllMain / IT_RunAllCleanMain
*
* Главная цель:
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
* - и иметь main() для запуска одного теста
* Цель:
* - проверить создание/листинг/refresh/close
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
*/
public class IT_02_Sessions {
private static final String LOGIN = TestConfig.LOGIN();
public static void main(String[] args) {
ItRunContext.initIfNeeded();
int failed = run();
TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
System.out.println(IT_01_AddUser.run());
String summary = run();
System.out.println(summary);
}
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
public static int run() {
return TestLog.runOne("IT_02_Sessions", IT_02_Sessions::testBodyStandalone);
}
public static String run() {
TestResult r = new TestResult("IT_02_Sessions");
@BeforeAll
static void ensureUserExists() {
ItRunContext.initIfNeeded();
TestLog.title("SessionsIT (BeforeAll): предусловие — пользователь должен существовать (AddUser: 200 или 409)");
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String reqId = "it-adduser-beforeall";
String reqJson = JsonBuilders.addUser(reqId);
TestLog.send("AddUser(BeforeAll)", reqJson);
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
TestLog.recv("AddUser(BeforeAll)", resp);
int st = JsonParsers.status(resp);
if (st == 200) {
TestLog.ok("BeforeAll: пользователь создан/добавлен (status=200)");
} else if (st == 409) {
String code = JsonParsers.errorCode(resp);
if ("USER_ALREADY_EXISTS".equals(code)) {
TestLog.ok("BeforeAll: пользователь уже есть (status=409, USER_ALREADY_EXISTS)");
} else {
TestLog.boom("BeforeAll: status=409, но code неожиданный: " + code);
fail("User precondition failed. status=409, code=" + code + ", resp=" + resp);
}
} else {
TestLog.boom("BeforeAll: предусловие не выполнено. status=" + st);
fail("User precondition failed. status=" + st + ", resp=" + resp);
}
}
}
// @Test
void sessions_flow_shouldCreateListRefreshCloseCorrectly() {
// JUnit-режим: пусть падает через assert/fail как обычно
testBodyJUnit();
}
/**
* Standalone-режим: тут мы сами вызываем предусловие ensureUserExists(),
* потому что @BeforeAll сработает только в JUnit.
*/
private static void testBodyStandalone() {
ensureUserExists();
testBodyJUnit();
}
private static void testBodyJUnit() {
ItRunContext.initIfNeeded();
TestLog.titleBlock("""
SessionsIT: полный сценарий сессий (создать 2, проверить list, refresh/close, проверить очистку)
Используем:
login = %s
Ожидание сценария:
1) Создаём SESSION1 через AuthChallenge + CreateAuthSession
2) Создаём SESSION2 и делаем ListSessions внутри неё (AUTH_STATUS_USER) должны быть SESSION1 и SESSION2
3) Делаем ListSessions в AUTH_IN_PROGRESS (подпись по nonce) должны быть SESSION1 и SESSION2
4) Refresh SESSION1 (входим в AUTH_STATUS_USER) и Close SESSION2
5) Проверяем ListSessions (AUTH_IN_PROGRESS) осталась только SESSION1
6) Закрываем SESSION1 в AUTH_IN_PROGRESS
7) Проверяем ListSessions пусто
""".formatted(TestConfig.LOGIN()));
Duration t = Duration.ofSeconds(5);
String s1Id, s1Pwd;
String s2Id, s2Pwd;
String s3Id, s3Pwd;
// ===== helpers (локальные, чтобы не раздувать TestLog лишней логикой assert200) =====
final java.util.function.BiConsumer<String, String> assert200 = (op, resp) -> {
int st = JsonParsers.status(resp);
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
TestLog.ok(op + ": status=200");
};
try {
// 1) Создаём 3 сессии (каждая отдельным соединением, чтобы не зависеть от состояния WS)
Session s1 = createSession(LOGIN, t, r, "S1");
s1Id = s1.sessionId; s1Pwd = s1.sessionPwd;
// ======================================================================
Session s2 = createSession(LOGIN, t, r, "S2");
s2Id = s2.sessionId; s2Pwd = s2.sessionPwd;
TestLog.stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-1";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge#1", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge#1", resp1);
Session s3 = createSession(LOGIN, t, r, "S3");
s3Id = s3.sessionId; s3Pwd = s3.sessionPwd;
assert200.accept("AuthChallenge#1", resp1);
String nonce = JsonParsers.authNonce(resp1);
assertNotNull(nonce, "AuthChallenge#1: nonce must not be null");
TestLog.ok("AuthChallenge#1: authNonce получен: " + nonce);
String r2 = "it-create-1";
String storagePwd = TestConfig.fakeStoragePwd();
String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd);
TestLog.send("CreateAuthSession#1", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("CreateAuthSession#1", resp2);
assert200.accept("CreateAuthSession#1", resp2);
s1Id = JsonParsers.sessionId(resp2);
s1Pwd = JsonParsers.sessionPwd(resp2);
assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null");
assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null");
TestLog.ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
}
TestLog.stepTitle("ШАГ 2: создать SESSION2 и ListSessions внутри неё (AUTH_STATUS_USER) → должны быть SESSION1+SESSION2");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-2";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge#2", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge#2", resp1);
assert200.accept("AuthChallenge#2", resp1);
String nonce = JsonParsers.authNonce(resp1);
assertNotNull(nonce);
TestLog.ok("AuthChallenge#2: authNonce получен: " + nonce);
String r2 = "it-create-2";
String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd());
TestLog.send("CreateAuthSession#2", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("CreateAuthSession#2", resp2);
assert200.accept("CreateAuthSession#2", resp2);
s2Id = JsonParsers.sessionId(resp2);
s2Pwd = JsonParsers.sessionPwd(resp2);
assertNotNull(s2Id);
assertNotNull(s2Pwd);
TestLog.ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
String r3 = "it-list-in-session2";
String req3 = JsonBuilders.listSessions(r3, 0L, "");
TestLog.send("ListSessions(in SESSION2)", req3);
String resp3 = c.request(r3, req3, Duration.ofSeconds(5));
TestLog.recv("ListSessions(in SESSION2)", resp3);
assert200.accept("ListSessions(in SESSION2)", resp3);
List<String> ids = JsonParsers.sessionIds(resp3);
TestLog.ok("ListSessions(in SESSION2): sessions=" + ids);
assertTrue(ids.contains(s1Id), "Must contain session1");
assertTrue(ids.contains(s2Id), "Must contain session2");
TestLog.ok("Проверка OK: список содержит SESSION1 и SESSION2");
}
TestLog.stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-list";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge(list)", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge(list)", resp1);
assert200.accept("AuthChallenge(list)", resp1);
String nonce = JsonParsers.authNonce(resp1);
assertNotNull(nonce);
TestLog.ok("AuthChallenge(list): authNonce=" + nonce);
// 2) ListSessions в AUTH_IN_PROGRESS должны быть S1,S2,S3
try (WsSession ws = WsSession.open()) {
String nonceResp = ws.call("AuthChallenge(list)", JsonBuilders.authChallenge(LOGIN), t);
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(list) must be 200");
String nonce = JsonParsers.authNonce(nonceResp);
assertNotNull(nonce, "authNonce must not be null");
long timeMs = System.currentTimeMillis();
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
TestLog.ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]");
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
String r2 = "it-list-auth-in-progress";
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
TestLog.send("ListSessions(AUTH_IN_PROGRESS)", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("ListSessions(AUTH_IN_PROGRESS)", resp2);
String listResp = ws.call("ListSessions(AUTH_IN_PROGRESS)", JsonBuilders.listSessions(timeMs, sig), t);
assertEquals(200, JsonParsers.status(listResp), "ListSessions must be 200");
assert200.accept("ListSessions(AUTH_IN_PROGRESS)", resp2);
List<String> ids = JsonParsers.sessionIds(listResp);
r.ok("ListSessions(AUTH_IN_PROGRESS): " + ids);
List<String> ids = JsonParsers.sessionIds(resp2);
TestLog.ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids);
assertTrue(ids.contains(s1Id), "Must contain S1");
assertTrue(ids.contains(s2Id), "Must contain S2");
assertTrue(ids.contains(s3Id), "Must contain S3");
r.ok("Проверка OK: список содержит S1,S2,S3");
}
// 3) RefreshSession(S1) -> после refresh в этом же соединении делаем ListSessions(AUTH_STATUS_USER) (timeMs=0)
try (WsSession ws = WsSession.open()) {
String refreshResp = ws.call("RefreshSession(S1)", JsonBuilders.refreshSession(s1Id, s1Pwd), t);
assertEquals(200, JsonParsers.status(refreshResp), "RefreshSession(S1) must be 200");
assertNotNull(JsonParsers.storagePwd(refreshResp), "storagePwd must not be null");
r.ok("RefreshSession(S1): OK");
String listInUserResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
assertEquals(200, JsonParsers.status(listInUserResp), "ListSessions(AUTH_STATUS_USER) must be 200");
List<String> ids = JsonParsers.sessionIds(listInUserResp);
r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
assertTrue(ids.contains(s1Id));
assertTrue(ids.contains(s2Id));
TestLog.ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2");
assertTrue(ids.contains(s3Id));
r.ok("Проверка OK: AUTH_STATUS_USER список содержит S1,S2,S3");
}
TestLog.stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
// 4) Проверяем CloseActiveSession, но так, чтобы итогом всё равно осталось 3 сессии:
// создаём TEMP, закрываем TEMP, убеждаемся что S1,S2,S3 остались.
Session temp = createSession(LOGIN, t, r, "TEMP");
String tempId = temp.sessionId;
String r1 = "it-refresh-s1";
String req1 = JsonBuilders.refreshSession(r1, s1Id, s1Pwd);
TestLog.send("RefreshSession(SESSION1)", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("RefreshSession(SESSION1)", resp1);
assert200.accept("RefreshSession(SESSION1)", resp1);
assertNotNull(JsonParsers.storagePwd(resp1));
TestLog.ok("RefreshSession: storagePwd получен");
String r2 = "it-close-s2";
String req2 = JsonBuilders.closeActiveSession(r2, s2Id, 0L, "");
TestLog.send("CloseActiveSession(SESSION2)", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("CloseActiveSession(SESSION2)", resp2);
assert200.accept("CloseActiveSession(SESSION2)", resp2);
TestLog.ok("SESSION2 закрыта");
}
TestLog.stepTitle("ШАГ 5: ListSessions(AUTH_IN_PROGRESS) → должна остаться только SESSION1");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-list2";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge(list2)", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge(list2)", resp1);
assert200.accept("AuthChallenge(list2)", resp1);
String nonce = JsonParsers.authNonce(resp1);
try (WsSession ws = WsSession.open()) {
String nonceResp = ws.call("AuthChallenge(close TEMP)", JsonBuilders.authChallenge(LOGIN), t);
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(close TEMP) must be 200");
String nonce = JsonParsers.authNonce(nonceResp);
assertNotNull(nonce);
long timeMs = System.currentTimeMillis();
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
String r2 = "it-list-after-close-s2";
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
TestLog.send("ListSessions(after close S2)", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("ListSessions(after close S2)", resp2);
String closeResp = ws.call("CloseActiveSession(TEMP)", JsonBuilders.closeActiveSession(tempId, timeMs, sig), t);
assertEquals(200, JsonParsers.status(closeResp), "CloseActiveSession(TEMP) must be 200");
r.ok("CloseActiveSession(TEMP): OK");
}
assert200.accept("ListSessions(after close S2)", resp2);
// 5) Финальная проверка: снова ListSessions(AUTH_IN_PROGRESS) => S1,S2,S3 должны остаться, TEMP нет
try (WsSession ws = WsSession.open()) {
String nonceResp = ws.call("AuthChallenge(final list)", JsonBuilders.authChallenge(LOGIN), t);
assertEquals(200, JsonParsers.status(nonceResp));
String nonce = JsonParsers.authNonce(nonceResp);
assertNotNull(nonce);
List<String> ids = JsonParsers.sessionIds(resp2);
TestLog.ok("ListSessions(after close S2): sessions=" + ids);
long timeMs = System.currentTimeMillis();
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
String listResp = ws.call("ListSessions(final AUTH_IN_PROGRESS)", JsonBuilders.listSessions(timeMs, sig), t);
assertEquals(200, JsonParsers.status(listResp));
List<String> ids = JsonParsers.sessionIds(listResp);
r.ok("Final ListSessions: " + ids);
assertTrue(ids.contains(s1Id));
assertFalse(ids.contains(s2Id));
TestLog.ok("Проверка OK: осталась только SESSION1");
assertTrue(ids.contains(s2Id));
assertTrue(ids.contains(s3Id));
assertFalse(ids.contains(tempId));
r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)");
}
TestLog.stepTitle("ШАГ 6: Close SESSION1 в AUTH_IN_PROGRESS");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-close-s1";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge(close S1)", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge(close S1)", resp1);
assert200.accept("AuthChallenge(close S1)", resp1);
String nonce = JsonParsers.authNonce(resp1);
assertNotNull(nonce);
long timeMs = System.currentTimeMillis();
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
String r2 = "it-close-s1";
String req2 = JsonBuilders.closeActiveSession(r2, s1Id, timeMs, sig);
TestLog.send("CloseActiveSession(SESSION1)", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("CloseActiveSession(SESSION1)", resp2);
assert200.accept("CloseActiveSession(SESSION1)", resp2);
TestLog.ok("SESSION1 закрыта");
} catch (Throwable e) {
r.fail("IT_02_Sessions упал: " + e.getMessage());
}
TestLog.stepTitle("ШАГ 7: ListSessions(AUTH_IN_PROGRESS) → ожидаем пустой список");
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
String r1 = "it-auth-list-empty";
String req1 = JsonBuilders.authChallenge(r1);
TestLog.send("AuthChallenge(list empty)", req1);
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
TestLog.recv("AuthChallenge(list empty)", resp1);
assert200.accept("AuthChallenge(list empty)", resp1);
String nonce = JsonParsers.authNonce(resp1);
assertNotNull(nonce);
long timeMs = System.currentTimeMillis();
String sig = JsonBuilders.signAuthorificated(nonce, timeMs);
String r2 = "it-list-empty";
String req2 = JsonBuilders.listSessions(r2, timeMs, sig);
TestLog.send("ListSessions(empty)", req2);
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
TestLog.recv("ListSessions(empty)", resp2);
assert200.accept("ListSessions(empty)", resp2);
List<String> ids = JsonParsers.sessionIds(resp2);
TestLog.ok("ListSessions(empty): sessions=" + ids);
assertTrue(ids.isEmpty(), "Sessions must be empty");
TestLog.ok("Проверка OK: список пуст");
return r.summaryLine();
}
TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
private static Session createSession(String login, Duration t, TestResult r, String label) {
try (WsSession ws = WsSession.open()) {
String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
String nonce = JsonParsers.authNonce(nonceResp);
assertNotNull(nonce, "authNonce must not be null for " + label);
String createResp = ws.call("CreateAuthSession(" + label + ")", JsonBuilders.createAuthSession(login, nonce, TestConfig.fakeStoragePwd()), t);
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
String sid = JsonParsers.sessionId(createResp);
String spw = JsonParsers.sessionPwd(createResp);
assertNotNull(sid, "sessionId must not be null");
assertNotNull(spw, "sessionPwd must not be null");
r.ok("Создана сессия " + label + ": sessionId=" + sid);
return new Session(sid, spw);
}
}
private record Session(String sessionId, String sessionPwd) {}
}

View File

@ -5,11 +5,12 @@ import blockchain.body.HeaderBody;
import blockchain.body.ReactionBody;
import blockchain.body.TextBody;
import blockchain.body.UserParamBody;
import org.junit.jupiter.api.BeforeAll;
import test.it.addBlockUtils.AddBlockSender;
import test.it.addBlockUtils.ChainState;
import test.it.utils.*;
import utils.crypto.Ed25519Util;
import test.it.utils.TestConfig;
import test.it.utils.TestLog;
import test.it.utils.TestResult;
import test.it.utils.WsSession;
import java.time.Duration;
@ -18,92 +19,43 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* IT_03_AddBlock_NoAuth
*
* ОБЪЕДИНЕНО: прежний IT_03 + прежний IT_04 в одном тесте,
* чтобы "четвёртый" сценарий гарантированно запускался сразу после "третьего".
*
* Сценарий:
* 1) (УБРАНО) AddUser(USER1) создаётся раньше в первом тесте
* 2) (УБРАНО) AddUser(USER2) создаётся раньше в первом тесте
*
* 3) USER1: HEADER + 3 NEW + 2 REPLY + 2 REACT + 3 EDIT (добавили)
* - редактируем два ранее написанных сообщения
* - одно сообщение редактируем два раза
*
* 4) USER2: HEADER + UserParams(name+address) + Connection(FRIEND -> USER1)
* 5) USER1: UserParams(name+surname) + Connection(FRIEND -> USER2) + Connection(FOLLOW -> USER2)
* 6) USER2: Connection(UNFRIEND -> USER1)
*
* Важно:
* - у каждого пользователя СВОЙ ChainState
* - AddBlockSender создаём с новой сигнатурой:
* new AddBlockSender(state, login, blockchainName, loginPrivKey)
* - USER2 ключи делаем детерминированно из login (как в ItRunContext), но локально.
* ВАЖНО:
* - пользователей НЕ создаём (их создаёт IT_01)
* - ключи берём только из TestConfig по login
*/
public class IT_03_AddBlock_NoAuth {
// ===== USER2 (константы прямо тут, чтобы не ломать твой TestConfig) =====
private static final String USER2_LOGIN = "Anya2";
private static final String BCH_SUFFIX_3 = "001";
private static final String USER2_BCH = USER2_LOGIN + BCH_SUFFIX_3;
public static void main(String[] args) {
int failed = run();
TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
System.out.println(IT_01_AddUser.run());
String summary = run();
System.out.println(summary);
}
public static int run() {
return TestLog.runOne("IT_03_AddBlock_NoAuth", IT_03_AddBlock_NoAuth::testBody);
}
public static String run() {
TestResult r = new TestResult("IT_03_AddBlock_NoAuth");
@BeforeAll
static void ensureUserExists() {
ItRunContext.initIfNeeded();
// можно оставить пустым, как у тебя
}
String u1 = TestConfig.LOGIN();
String u2 = TestConfig.LOGIN2();
private static void testBody() {
ItRunContext.initIfNeeded();
ensureUserExists();
String bch1 = TestConfig.getBlockchainName(u1);
String bch2 = TestConfig.getBlockchainName(u2);
Duration t = Duration.ofSeconds(1);
// =========================================================
// USER2 keys (детерминированно из login, как твой ItRunContext)
// =========================================================
byte[] user2LoginPriv = Ed25519Util.generatePrivateKeyFromString(USER2_LOGIN);
try (WsSession ws = WsSession.open()) {
// =========================================================
// 3) USER1 блоки (под message_stats + edits)
// =========================================================
if (TestConfig.DEBUG()) {
TestLog.titleBlock("""
IT_03_AddBlock_NoAuth (combined): USER1 + USER2 сценарии
USER1 login = %s
USER1 blockchainName= %s
USER2 login = %s
USER2 blockchainName= %s
""".formatted(TestConfig.LOGIN(), TestConfig.BCH_NAME(), USER2_LOGIN, USER2_BCH));
}
if (TestConfig.DEBUG()) TestLog.titleBlock("IT_03: USER1=" + u1 + " bch=" + bch1 + " | USER2=" + u2 + " bch=" + bch2);
// USER1
ChainState st1 = new ChainState();
AddBlockSender sender1 = new AddBlockSender(
st1,
TestConfig.LOGIN(),
TestConfig.BCH_NAME(),
TestConfig.LOGIN_PRIV_KEY()
);
AddBlockSender sender1 = new AddBlockSender(ws, st1, u1, bch1, TestConfig.getBlockchainPrivatKey(u1));
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: HEADER");
sender1.send(new HeaderBody(TestConfig.LOGIN()), t);
sender1.send(new HeaderBody(u1), t);
assertTrue(st1.hasHeader());
// 3 NEW
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#1 (NEW) <- будет LIKE + REPLY");
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #1 (NEW) from IT_03 test"), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#2 (NEW) <- будет ONLY LIKE + 2 EDIT");
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #2 (NEW) from IT_03 test"), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#3 (NEW) <- будет ONLY REPLY + 1 EDIT");
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #3 (NEW) from IT_03 test"), t);
byte[] text1Hash = st1.getGlobalHash32(1);
@ -113,144 +65,43 @@ public class IT_03_AddBlock_NoAuth {
assertNotNull(text2Hash);
assertNotNull(text3Hash);
// 2 REPLY
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#4 (REPLY -> TEXT#1) (делает TEXT#1: replies+1)");
sender1.send(new TextBody(
TextBody.SUB_REPLY,
"Reply to TEXT#1",
TestConfig.BCH_NAME(),
1,
text1Hash
), t);
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#1", bch1, 1, text1Hash), t);
sender1.send(new TextBody(TextBody.SUB_REPLY, "Reply to TEXT#3", bch1, 3, text3Hash), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#5 (REPLY -> TEXT#3) (делает TEXT#3: replies+1)");
sender1.send(new TextBody(
TextBody.SUB_REPLY,
"Reply to TEXT#3",
TestConfig.BCH_NAME(),
3,
text3Hash
), t);
sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 1, text1Hash), t);
sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 2, text2Hash), t);
// 2 LIKE
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: REACT#1 (LIKE -> TEXT#1) (делает TEXT#1: likes+1)");
sender1.send(new ReactionBody(
ReactionBody.SUB_LIKE,
TestConfig.BCH_NAME(),
1,
text1Hash
), t);
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #2 (EDIT#1) from IT_03 test", bch1, 2, text2Hash), t);
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #2 (EDIT#2) from IT_03 test", bch1, 2, text2Hash), t);
sender1.send(new TextBody(TextBody.SUB_EDIT, "Hello #3 (EDIT#1) from IT_03 test", bch1, 3, text3Hash), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: REACT#2 (LIKE -> TEXT#2) (делает TEXT#2: likes+1)");
sender1.send(new ReactionBody(
ReactionBody.SUB_LIKE,
TestConfig.BCH_NAME(),
2,
text2Hash
), t);
// 3 EDIT (два сообщения исправляем, одно два раза)
// ВАЖНО: subType EDIT берём из TextBody.SUB_EDIT (единая константа = 10)
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#6 (EDIT -> TEXT#2) (исправление #1)");
sender1.send(new TextBody(
TextBody.SUB_EDIT,
"Hello #2 (EDIT#1) from IT_03 test",
TestConfig.BCH_NAME(),
2,
text2Hash
), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#7 (EDIT -> TEXT#2) (исправление #2)");
sender1.send(new TextBody(
TextBody.SUB_EDIT,
"Hello #2 (EDIT#2) from IT_03 test",
TestConfig.BCH_NAME(),
2,
text2Hash
), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: TEXT#8 (EDIT -> TEXT#3) (исправление #1)");
sender1.send(new TextBody(
TextBody.SUB_EDIT,
"Hello #3 (EDIT#1) from IT_03 test",
TestConfig.BCH_NAME(),
3,
text3Hash
), t);
assertEquals(10, st1.globalLastNumber(), "USER1: после EDIT должно быть 11 блоков: globalLastNumber=10");
assertEquals(8, st1.lineLastNumber((short) 1), "USER1: line=1 должно быть 8 TEXT блоков (3 new + 2 reply + 3 edit)");
assertEquals(10, st1.globalLastNumber(), "USER1: globalLastNumber должен быть 10 (11 блоков)");
assertEquals(8, st1.lineLastNumber((short) 1), "USER1: line=1 должно быть 8 TEXT блоков");
assertEquals(2, st1.lineLastNumber((short) 2), "USER1: line=2 должно быть 2 REACTION блока");
// =========================================================
// 4) USER2: HEADER + PARAMS + FRIEND->USER1
// =========================================================
// USER2
ChainState st2 = new ChainState();
AddBlockSender sender2 = new AddBlockSender(
st2,
USER2_LOGIN,
USER2_BCH,
user2LoginPriv
);
AddBlockSender sender2 = new AddBlockSender(ws, st2, u2, bch2, TestConfig.getBlockchainPrivatKey(u2));
if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: HEADER");
sender2.send(new HeaderBody(USER2_LOGIN), t);
sender2.send(new HeaderBody(u2), t);
assertTrue(st2.hasHeader());
if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: UserParams (name + address)");
sender2.send(new UserParamBody(
"Anya",
"Amsterdam, Example street 10"
), t);
sender2.send(new UserParamBody("Anya", "Amsterdam, Example street 10"), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: Connection (FRIEND -> USER1)");
sender2.send(new ConnectionBody(
ConnectionBody.SUB_FRIEND,
TestConfig.LOGIN(), // to_login (USER1)
TestConfig.BCH_NAME(), // toBch (USER1 chain)
0,
new byte[32]
), t);
sender2.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u1, bch1, 0, new byte[32]), t);
// =========================================================
// 5) USER1: params + взаимность + подписка (без нового HEADER!)
// =========================================================
// ВАЖНО: мы НЕ создаём новый ChainState для USER1, и НЕ шлём header заново.
// Мы продолжаем тем же sender1 и st1, иначе будет пытаться начать цепочку заново.
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: UserParams (name + surname)");
sender1.send(new UserParamBody(
"Anna",
"Gareeva"
), t);
sender1.send(new UserParamBody("Anna", "Gareeva"), t);
sender1.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u2, bch2, 0, new byte[32]), t);
sender1.send(new ConnectionBody(ConnectionBody.SUB_FOLLOW, u2, bch2, 0, new byte[32]), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: Connection (FRIEND -> USER2)");
sender1.send(new ConnectionBody(
ConnectionBody.SUB_FRIEND,
USER2_LOGIN,
USER2_BCH,
0,
new byte[32]
), t);
sender2.send(new ConnectionBody(ConnectionBody.SUB_UNFRIEND, u1, bch1, 0, new byte[32]), t);
if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: Connection (FOLLOW -> USER2)");
sender1.send(new ConnectionBody(
ConnectionBody.SUB_FOLLOW,
USER2_LOGIN,
USER2_BCH,
0,
new byte[32]
), t);
r.ok("IT_03 сценарий блоков выполнен");
// 6) USER2: Connection (UNFRIEND -> USER1) USER2 больше не друг USER1
if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: Connection (UNFRIEND -> USER1)");
sender2.send(new ConnectionBody(
ConnectionBody.SUB_UNFRIEND,
TestConfig.LOGIN(), // to_login (USER1)
TestConfig.BCH_NAME(), // toBch (USER1 chain)
0,
new byte[32]
), t);
} catch (Throwable e) {
r.fail("IT_03 упал: " + e.getMessage());
}
TestLog.pass("IT_03_AddBlock_NoAuth (combined): OK");
return r.summaryLine();
}
}

View File

@ -2,7 +2,6 @@ package test.it;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeAll;
import test.it.utils.*;
import utils.config.ShineSignatureConstants;
import utils.crypto.Ed25519Util;
@ -16,71 +15,38 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* IT_04_UserParams_NoAuth
*
* Сценарий:
* 1) UpsertUserParam: сохранить param1
* 2) GetUserParam: получить param1 и проверить поля
* 3) UpsertUserParam: сохранить param2
* 4) UpsertUserParam: обновить param1 (time_ms больше)
* 5) ListUserParams: получить список и проверить:
* - есть param1 (обновлённое значение/time)
* - есть param2
*
* Примечание по безопасности (на будущее):
* - сейчас (MVP) чтение/запись параметров без ограничений по сессии.
* - позже можно добавить: доступ только владельцу или доверенным, через active_session/ACL.
* ВАЖНО:
* - пользователей НЕ создаём (их создаёт IT_01)
*/
public class IT_04_UserParams_NoAuth {
private static final ObjectMapper M = new ObjectMapper();
public static void main(String[] args) {
int failed = run();
// System.exit(failed);
TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
System.out.println(IT_01_AddUser.run());
String summary = run();
System.out.println(summary);
}
public static int run() {
return TestLog.runOne("IT_04_UserParams_NoAuth", IT_04_UserParams_NoAuth::testBody);
}
@BeforeAll
static void init() {
ItRunContext.initIfNeeded();
}
private static void testBody() {
ItRunContext.initIfNeeded();
public static String run() {
TestResult r = new TestResult("IT_04_UserParams_NoAuth");
Duration timeout = Duration.ofSeconds(5);
// ---------------------------------------------------------
// ensure user exists (как в твоих тестах: 200 или 409)
// ---------------------------------------------------------
addUserOr409AlreadyExists(
"USER1",
TestConfig.LOGIN(),
TestConfig.BCH_NAME(),
TestConfig.LOGIN_PUBKEY_B64(),
TestConfig.DEVICE_PUBKEY_B64()
);
final String login = TestConfig.LOGIN();
final String deviceKeyB64 = TestConfig.DEVICE_PUBKEY_B64();
final byte[] devicePrivKey = TestConfig.DEVICE_PRIV_KEY(); // важно: подпись именно device-ключом
final String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
final byte[] devicePrivKey = TestConfig.getDevicePrivatKey(login);
// ---------------------------------------------------------
try {
// 1) сохранить param1
// ---------------------------------------------------------
final String p1 = "profile:name";
final String v1 = "Anna";
final long t1 = System.currentTimeMillis();
upsertUserParam_OK(r, login, p1, t1, v1, deviceKeyB64, devicePrivKey, timeout);
upsertUserParam_OK(login, p1, t1, v1, deviceKeyB64, devicePrivKey, timeout);
// ---------------------------------------------------------
// 2) получить param1 и проверить
// ---------------------------------------------------------
NetParam got1 = getUserParam_200(login, p1, timeout);
NetParam got1 = getUserParam_200(r, login, p1, timeout);
assertEquals(login, got1.login);
assertEquals(p1, got1.param);
assertEquals(t1, got1.timeMs);
@ -88,33 +54,26 @@ public class IT_04_UserParams_NoAuth {
assertEquals(deviceKeyB64, got1.deviceKeyB64);
assertNotNull(got1.signatureB64);
assertFalse(got1.signatureB64.isBlank());
r.ok("GetUserParam(param1) OK");
// ---------------------------------------------------------
// 3) сохранить param2
// ---------------------------------------------------------
final String p2 = "profile:city";
final String v2 = "Amsterdam";
final long t2 = t1 + 10;
upsertUserParam_OK(r, login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
upsertUserParam_OK(login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
// ---------------------------------------------------------
// 4) обновить param1 более новым временем
// ---------------------------------------------------------
// 4) обновить param1
final String v1b = "Anna Updated";
final long t1b = t2 + 10;
upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
upsertUserParam_OK(login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
// доп.проверка: GetUserParam теперь должен вернуть обновлённое
NetParam got1b = getUserParam_200(login, p1, timeout);
NetParam got1b = getUserParam_200(r, login, p1, timeout);
assertEquals(t1b, got1b.timeMs);
assertEquals(v1b, got1b.value);
r.ok("GetUserParam(updated param1) OK");
// ---------------------------------------------------------
// 5) list всех параметров и проверка состава
// ---------------------------------------------------------
NetParamList list = listUserParams_200(login, timeout);
// 5) list всех параметров
NetParamList list = listUserParams_200(r, login, timeout);
NetParam lp1 = list.find(p1);
NetParam lp2 = list.find(p2);
@ -122,37 +81,33 @@ public class IT_04_UserParams_NoAuth {
assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1);
assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2);
assertEquals(t1b, lp1.timeMs, "param1 должен быть обновлённым");
assertEquals(v1b, lp1.value, "param1 должен иметь обновлённое значение");
assertEquals(t1b, lp1.timeMs);
assertEquals(v1b, lp1.value);
assertEquals(t2, lp2.timeMs);
assertEquals(v2, lp2.value);
// и у обоих должны возвращаться все поля из БД (как ты просил)
assertEquals(deviceKeyB64, lp1.deviceKeyB64);
assertEquals(deviceKeyB64, lp2.deviceKeyB64);
assertNotNull(lp1.signatureB64);
assertNotNull(lp2.signatureB64);
TestLog.pass("IT_04_UserParams_NoAuth: OK");
r.ok("ListUserParams OK");
} catch (Throwable e) {
r.fail("IT_04 упал: " + e.getMessage());
}
return r.summaryLine();
}
// =================================================================================
// WS helpers: Upsert/Get/List
// =================================================================================
private static void upsertUserParam_OK(String login,
String param,
long timeMs,
String value,
String deviceKeyB64,
byte[] devicePrivKey,
Duration timeout) {
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String deviceKeyB64, byte[] devicePrivKey, Duration timeout) {
String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
String reqId = "it-upsert-" + param.replace(':', '_');
String reqJson = """
{
"op": "UpsertUserParam",
@ -166,29 +121,16 @@ public class IT_04_UserParams_NoAuth {
"signature": "%s"
}
}
""".formatted(
reqId,
login,
param,
timeMs,
jsonEscape(value),
deviceKeyB64,
signatureB64
);
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), deviceKeyB64, signatureB64);
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
TestLog.send("UpsertUserParam", reqJson);
String resp = client.request(reqId, reqJson, timeout);
TestLog.recv("UpsertUserParam", resp);
int st = JsonParsers.status(resp);
assertEquals(200, st, "UpsertUserParam expected 200, resp=" + resp);
try (WsSession ws = WsSession.open()) {
String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
assertEquals(200, JsonParsers.status(resp), "UpsertUserParam expected 200, resp=" + resp);
r.ok("UpsertUserParam(" + param + "): OK");
}
}
private static NetParam getUserParam_200(String login, String param, Duration timeout) {
String reqId = "it-get-" + param.replace(':', '_');
private static NetParam getUserParam_200(TestResult r, String login, String param, Duration timeout) {
String reqJson = """
{
"op": "GetUserParam",
@ -198,41 +140,29 @@ public class IT_04_UserParams_NoAuth {
"param": "%s"
}
}
""".formatted(reqId, login, param);
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
TestLog.send("GetUserParam", reqJson);
String resp = client.request(reqId, reqJson, timeout);
TestLog.recv("GetUserParam", resp);
int st = JsonParsers.status(resp);
assertEquals(200, st, "GetUserParam expected 200, resp=" + resp);
""".formatted(TestIds.next("getparam"), login, param);
try (WsSession ws = WsSession.open()) {
String resp = ws.call("GetUserParam(" + param + ")", reqJson, timeout);
assertEquals(200, JsonParsers.status(resp), "GetUserParam expected 200, resp=" + resp);
r.ok("GetUserParam(" + param + "): OK");
return parseParamFromResponsePayload(resp);
}
}
private static NetParamList listUserParams_200(String login, Duration timeout) {
String reqId = "it-list-params";
private static NetParamList listUserParams_200(TestResult r, String login, Duration timeout) {
String reqJson = """
{
"op": "ListUserParams",
"requestId": "%s",
"payload": {
"login": "%s"
"payload": { "login": "%s" }
}
}
""".formatted(reqId, login);
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
TestLog.send("ListUserParams", reqJson);
String resp = client.request(reqId, reqJson, timeout);
TestLog.recv("ListUserParams", resp);
int st = JsonParsers.status(resp);
assertEquals(200, st, "ListUserParams expected 200, resp=" + resp);
""".formatted(TestIds.next("listparams"), login);
try (WsSession ws = WsSession.open()) {
String resp = ws.call("ListUserParams", reqJson, timeout);
assertEquals(200, JsonParsers.status(resp), "ListUserParams expected 200, resp=" + resp);
r.ok("ListUserParams: OK");
return parseParamListFromResponsePayload(resp);
}
}
@ -301,23 +231,12 @@ public class IT_04_UserParams_NoAuth {
}
// =================================================================================
// Signature + JSON string helpers
// Signature + JSON helpers
// =================================================================================
private static String signUserParam(byte[] devicePrivKey,
String login,
String param,
long timeMs,
String value) {
String signText =
ShineSignatureConstants.USER_PARAMETER_PREFIX +
login + param + timeMs + value;
private static String signUserParam(byte[] devicePrivKey, String login, String param, long timeMs, String value) {
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
// Важно: Ed25519Util.sign(...) ожидает (dataHash OR data?) у тебя в проекте это уже устаканено.
// В хэндлере verify(...) делается на signBytes напрямую, значит подписывать нужно signBytes.
byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
return Base64.getEncoder().encodeToString(sig64);
}
@ -328,60 +247,7 @@ public class IT_04_UserParams_NoAuth {
}
// =================================================================================
// AddUser helper (как у тебя)
// =================================================================================
private static void addUserOr409AlreadyExists(String label,
String login,
String blockchainName,
String loginPubKeyB64,
String devicePubKeyB64) {
TestLog.title(label + ": AddUser (200 OK) или 409 USER_ALREADY_EXISTS");
String reqId = "it-adduser-" + label.toLowerCase();
String reqJson = """
{
"op": "AddUser",
"requestId": "%s",
"payload": {
"login": "%s",
"blockchainName": "%s",
"loginKey": "%s",
"deviceKey": "%s",
"bchLimit": %d
}
}
""".formatted(
reqId,
login,
blockchainName,
loginPubKeyB64,
devicePubKeyB64,
TestConfig.TEST_BCH_LIMIT
);
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
TestLog.send("AddUser(" + label + ")", reqJson);
String resp = client.request(reqId, reqJson, Duration.ofSeconds(5));
TestLog.recv("AddUser(" + label + ")", resp);
int st = JsonParsers.status(resp);
if (st == 200) {
TestLog.ok(label + ": создан/добавлен (status=200)");
} else if (st == 409) {
String code = JsonParsers.errorCode(resp);
assertEquals("USER_ALREADY_EXISTS", code, label + ": expected USER_ALREADY_EXISTS, resp=" + resp);
TestLog.ok(label + ": уже есть (status=409, USER_ALREADY_EXISTS)");
} else {
fail(label + ": неожиданный status=" + st + ", resp=" + resp);
}
}
}
// =================================================================================
// Small DTOs
// DTOs
// =================================================================================
private static final class NetParam {

View File

@ -5,16 +5,13 @@ import server.ws.WsServer;
public class IT_RunAllCleanStartWsMain {
public static void main(String[] args) {
// 1) Гасим всё на 7070 (если ничего нет не падаем)
runBash("kill -9 $(lsof -t -i:7070) 2>/dev/null || true");
// 2) Чистим data/
IT_CleanAllDate.main(new String[0]);
// 3) Стартуем WS сервер в отдельном потоке (daemon, чтобы JVM могла завершиться)
Thread wsThread = new Thread(() -> {
try {
WsServer.main(new String[0]); // внутри join() -> поток будет висеть
WsServer.main(new String[0]);
} catch (Throwable t) {
t.printStackTrace(System.out);
}
@ -22,24 +19,16 @@ public class IT_RunAllCleanStartWsMain {
wsThread.setDaemon(true);
wsThread.start();
// 4) Ждём, чтобы успел стартануть
sleepMs(1000);
// 5) Запускаем все IT тесты (без System.exit внутри)
int failed = IT_RunAllMain.runAll();
// 6) Завершаем процесс с кодом ошибок
System.exit(failed);
}
private static void runBash(String cmd) {
try {
Process p = new ProcessBuilder("bash", "-lc", cmd)
.inheritIO()
.start();
int code = p.waitFor();
// тут не ругаемся: команда может быть "пустой" (ничего не слушает порт)
// а мы уже добавили "|| true"
Process p = new ProcessBuilder("bash", "-lc", cmd).inheritIO().start();
p.waitFor();
} catch (Exception e) {
System.out.println("WARN: bash command failed: " + e);
}

View File

@ -1,56 +1,38 @@
package test.it;
import test.it.utils.ItRunContext;
import test.it.utils.TestLog;
import java.util.ArrayList;
import java.util.List;
/**
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
* Ручной запуск всех IT тестов БЕЗ JUnit.
* Печатает итоги по каждому тесту отдельной строкой.
*/
public class IT_RunAllMain {
public static void main(String[] args) {
ItRunContext.initIfNeeded();
int failed = runAll();
System.exit(failed);
}
public static int runAll() {
final int total = 4; // было 3
List<String> summaries = new ArrayList<>();
int failed = 0;
int passed = 0;
TestLog.title("IT RUN: запуск всех тестов подряд (без очистки data/)");
TestLog.title("IT RUN: запуск всех тестов подряд");
TestLog.stepTitle("RUN: IT_01_AddUser");
int f1 = IT_01_AddUser.run();
failed += f1; passed += (f1 == 0 ? 1 : 0);
String s1 = IT_01_AddUser.run(); summaries.add(s1); if (s1.contains("FAIL:")) failed++;
String s2 = IT_02_Sessions.run(); summaries.add(s2); if (s2.contains("FAIL:")) failed++;
String s3 = IT_03_AddBlock_NoAuth.run(); summaries.add(s3); if (s3.contains("FAIL:")) failed++;
String s4 = IT_04_UserParams_NoAuth.run(); summaries.add(s4); if (s4.contains("FAIL:")) failed++;
TestLog.stepTitle("RUN: IT_02_Sessions");
int f2 = IT_02_Sessions.run();
failed += f2; passed += (f2 == 0 ? 1 : 0);
TestLog.title("IT RUN RESULT (per test)");
for (String s : summaries) System.out.println(s);
TestLog.stepTitle("RUN: IT_03_AddBlock_NoAuth (combined 3+4)");
int f3 = IT_03_AddBlock_NoAuth.run();
failed += f3; passed += (f3 == 0 ? 1 : 0);
TestLog.stepTitle("RUN: IT_04_UserParams_NoAuth");
int f4 = IT_04_UserParams_NoAuth.run();
failed += f4; passed += (f4 == 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);
}
if (failed == 0) TestLog.ok("ВСЕ IT ТЕСТЫ УСПЕШНО ЗАВЕРШЕНЫ");
else TestLog.boom("❌ IT ПРОГОН УПАЛ: failed=" + failed + " из " + summaries.size());
return failed;
}

View File

@ -1,448 +0,0 @@
package test.it.addBlockUtils;
// старый класс
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.HeaderBody;
import blockchain.body.ReactionBody;
import blockchain.body.TextBody;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.TestLog;
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.*;
/**
* AddBlockFlow
*
* Держит локальное состояние цепочки:
* - last globalNumber / last globalHash
* - last lineNum / last lineHash для каждой линии
*
* И умеет:
* - собрать следующий блок (HEADER / TEXT / REACTION)
* - отправить AddBlock в сервер (через WsJsonOneShot)
* - проверить serverLastGlobalHash == localHash
* - обновить локальное состояние
*
* Важно:
* - Этот класс НЕ занимается красивыми логами. Только логика + проверки.
*
* ДОБАВЛЕНО:
* - При it.debug=true печатаем:
* * какой блок шлём (global/line/lineNum)
* * локальный hash
* * serverLastGlobalHash
* * итоги проверок
*/
public final class AddBlockFlow {
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
// линии как у тебя
public static final short LINE_HEADER = 0;
public static final short LINE_TEXT = 1;
public static final short LINE_REACT = 2;
// локальное состояние
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 AddBlockFlow() {
for (int i = 0; i < 8; i++) lineLastHashHex[i] = "";
}
// =================================================================================
// PUBLIC API
// =================================================================================
/** Шлём HEADER (global=0, line=0, lineNum=0). Должно быть ПЕРВЫМ. */
public void sendHeader0(Duration timeout) {
assertEquals(-1, globalLastNumber, "HEADER должен идти первым: globalLastNumber сейчас уже " + globalLastNumber);
BuiltBlock header = buildHeaderBlock(
0,
LINE_HEADER,
0,
ZERO32,
ZERO32
);
String req = buildAddBlockJson(TestConfig.BCH_NAME(), 0, ZERO64, base64(header.fullBytes));
String resp = WsJsonOneShot.request("AddBlock#HEADER", req, timeout);
assert200("AddBlock(HEADER)", resp);
String serverLastGlobalHash0 = extractPayloadString(resp, "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);
if (TestConfig.DEBUG()) {
TestLog.ok("HEADER: локальный hash=" + localHash0);
TestLog.ok("HEADER: serverLastGlobalHash=" + serverLastGlobalHash0);
}
assertEquals(localHash0, serverLastGlobalHash0, "HEADER: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем локальное состояние
headerHash32 = header.hash32;
globalLastNumber = 0;
globalLastHashHex = localHash0;
lineLastNumber[LINE_HEADER] = 0;
lineLastHashHex[LINE_HEADER] = localHash0;
if (TestConfig.DEBUG()) {
TestLog.ok("HEADER: проверка OK, состояние обновлено (globalLastNumber=0)");
}
}
/** Шлём следующий TEXT блок в line=1. */
public BuiltBlock sendNextText(String text, Duration timeout) {
assertNotNull(headerHash32, "TEXT нельзя слать до HEADER (headerHash32 == null)");
int nextGlobal = globalLastNumber + 1;
int lineNum = nextLineNum(LINE_TEXT);
byte[] prevLineHash = prevLineHash32(LINE_TEXT);
BuiltBlock b = buildTextBlock(
nextGlobal,
LINE_TEXT,
lineNum,
hexToBytes32(globalLastHashHex),
prevLineHash,
text
);
String req = buildAddBlockJson(TestConfig.BCH_NAME(), nextGlobal, globalLastHashHex, base64(b.fullBytes));
String op = "AddBlock#TEXT (global=" + nextGlobal + ", line=1, lineNum=" + lineNum + ")";
String resp = WsJsonOneShot.request(op, req, timeout);
assert200("AddBlock(TEXT)", resp);
String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash, "TEXT: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash.trim().length(), "TEXT: serverLastGlobalHash must be 64 hex chars");
String localHash = bytesToHex64(b.hash32);
if (TestConfig.DEBUG()) {
TestLog.ok("TEXT: локальный hash=" + localHash);
TestLog.ok("TEXT: serverLastGlobalHash=" + serverLastGlobalHash);
}
assertEquals(localHash, serverLastGlobalHash, "TEXT: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = nextGlobal;
globalLastHashHex = localHash;
lineLastNumber[LINE_TEXT] = lineNum;
lineLastHashHex[LINE_TEXT] = localHash;
if (TestConfig.DEBUG()) {
TestLog.ok("TEXT: проверка OK, состояние обновлено (globalLastNumber=" + globalLastNumber + ")");
}
return b;
}
/** Шлём следующий REACTION блок в line=2, ссылаясь на конкретный блок. */
public BuiltBlock sendNextReaction(short reactionCode,
String toBlockchainName,
int toBlockGlobalNumber,
byte[] toBlockHash32,
Duration timeout) {
assertNotNull(headerHash32, "REACTION нельзя слать до HEADER (headerHash32 == null)");
assertNotNull(toBlockHash32, "toBlockHash32 is null");
assertEquals(32, toBlockHash32.length, "toBlockHash32 must be 32 bytes");
int nextGlobal = globalLastNumber + 1;
int lineNum = nextLineNum(LINE_REACT);
byte[] prevLineHash = prevLineHash32(LINE_REACT);
BuiltBlock b = buildReactionBlock(
nextGlobal,
LINE_REACT,
lineNum,
hexToBytes32(globalLastHashHex),
prevLineHash,
reactionCode,
toBlockchainName,
toBlockGlobalNumber,
toBlockHash32
);
String req = buildAddBlockJson(TestConfig.BCH_NAME(), nextGlobal, globalLastHashHex, base64(b.fullBytes));
String op = "AddBlock#REACT (global=" + nextGlobal + ", line=2, lineNum=" + lineNum + ")";
String resp = WsJsonOneShot.request(op, req, timeout);
assert200("AddBlock(REACT)", resp);
String serverLastGlobalHash = extractPayloadString(resp, "serverLastGlobalHash");
assertNotNull(serverLastGlobalHash, "REACT: payload.serverLastGlobalHash must not be null");
assertEquals(64, serverLastGlobalHash.trim().length(), "REACT: serverLastGlobalHash must be 64 hex chars");
String localHash = bytesToHex64(b.hash32);
if (TestConfig.DEBUG()) {
TestLog.ok("REACT: локальный hash=" + localHash);
TestLog.ok("REACT: serverLastGlobalHash=" + serverLastGlobalHash);
}
assertEquals(localHash, serverLastGlobalHash, "REACT: serverLastGlobalHash должен совпасть с локальным hash");
// обновляем состояние
globalLastNumber = nextGlobal;
globalLastHashHex = localHash;
lineLastNumber[LINE_REACT] = lineNum;
lineLastHashHex[LINE_REACT] = localHash;
if (TestConfig.DEBUG()) {
TestLog.ok("REACT: проверка OK, состояние обновлено (globalLastNumber=" + globalLastNumber + ")");
}
return b;
}
// getters для итогов/логов (если надо)
public int globalLastNumber() { return globalLastNumber; }
public String globalLastHashHex() { return globalLastHashHex; }
public int lineLastNumber(short line) { return lineLastNumber[line]; }
public String lineLastHashHex(short line) { return lineLastHashHex[line]; }
// =================================================================================
// INTERNALS: line helpers
// =================================================================================
/** Следующий lineNum: если в линии было N блоков, новый будет N+1 (для line>0). Для line0 здесь не используется. */
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);
}
// =================================================================================
// INTERNALS: build blocks
// =================================================================================
/** Небольшой холдер, чтобы flow мог использовать hash32 как prevGlobal/prevLine и как toBlockHash. */
public static final class BuiltBlock {
public final byte[] fullBytes;
public final byte[] hash32;
public 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,
short 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.
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);
}
// =================================================================================
// INTERNALS: json helpers
// =================================================================================
private static String buildAddBlockJson(String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
return """
{
"op": "AddBlock",
"requestId": "%s",
"payload": {
"blockchainName": "%s",
"globalNumber": %d,
"prevGlobalHash": "%s",
"blockBytesB64": "%s"
}
}
""".formatted(WsJsonOneShot.FIXED_REQUEST_ID, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
private static void assert200(String op, String resp) {
int st = JsonParsers.status(resp);
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
if (TestConfig.DEBUG()) {
TestLog.ok(op + ": status=200");
}
}
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);
}
// =================================================================================
// INTERNALS: 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

@ -3,7 +3,11 @@ package test.it.addBlockUtils;
import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.body.BodyRecord;
import test.it.utils.JsonParsers;
import test.it.utils.TestConfig;
import test.it.utils.TestIds;
import test.it.utils.TestLog;
import test.it.utils.WsSession;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@ -14,58 +18,46 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
* AddBlockSender "одна кнопка":
* - принимает ГОТОВЫЙ Body (HeaderBody/TextBody/ReactionBody/ConnectionBody/UserParamsBody и т.п.)
* - сам берёт номера/prev-hash из ChainState
* AddBlockSender отправка AddBlock поверх одного WsSession:
* - берёт номера/prev-hash из ChainState
* - строит raw/hash/signature
* - собирает BchBlockEntry (старый, без изменений)
* - отправляет AddBlock
* - проверяет serverLastGlobalHash == localHash
* - обновляет ChainState
*
* ИЗМЕНЕНО:
* - sender больше НЕ завязан на TestConfig.LOGIN()/BCH_NAME()/ключи
* - теперь он работает от параметров конкретного пользователя:
* login, blockchainName, loginPrivKey
*/
public final class AddBlockSender {
private static final byte[] ZERO32 = new byte[32];
private static final String ZERO64 = "0".repeat(64);
private final WsSession ws;
private final ChainState state;
private final String login;
private final String blockchainName;
private final byte[] loginPrivKey;
public AddBlockSender(ChainState state, String login, String blockchainName, byte[] loginPrivKey) {
public AddBlockSender(WsSession ws, ChainState state, String login, String blockchainName, byte[] loginPrivKey) {
this.ws = ws;
this.state = state;
this.login = login;
this.blockchainName = blockchainName;
this.loginPrivKey = (loginPrivKey == null ? null : loginPrivKey.clone());
if (this.ws == null) throw new IllegalArgumentException("ws == null");
if (this.loginPrivKey == null) throw new IllegalArgumentException("loginPrivKey == null");
}
public ChainState state() { return state; }
/**
* Отправить следующий блок по body.expectedLineIndex().
*/
public void send(BodyRecord body, Duration timeout) {
if (body == null) throw new IllegalArgumentException("body == null");
short lineIndex = body.expectedLineIndex();
// header должен быть первым
if (lineIndex == 0) {
if (state.globalLastNumber() != -1) {
throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
}
if (state.globalLastNumber() != -1) throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
} else {
if (!state.hasHeader()) {
throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
}
if (!state.hasHeader()) throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
}
int globalNumber = state.nextGlobalNumber();
@ -77,7 +69,6 @@ public final class AddBlockSender {
long ts = System.currentTimeMillis() / 1000L;
byte[] bodyBytes = body.toBytes();
// RAW bytes
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
byte[] rawBytes = ByteBuffer.allocate(recordSize)
@ -90,41 +81,18 @@ public final class AddBlockSender {
.put(bodyBytes)
.array();
// preimage -> sha256 -> signature
byte[] preimage = BchCryptoVerifier.buildPreimage(
login,
prevGlobalHash32,
prevLineHash32,
rawBytes
);
byte[] preimage = BchCryptoVerifier.buildPreimage(login, prevGlobalHash32, prevLineHash32, rawBytes);
byte[] hash32 = BchCryptoVerifier.sha256(preimage);
byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
// Собираем полный блок (BchBlockEntry не меняем)
BchBlockEntry entry = new BchBlockEntry(
globalNumber,
ts,
lineIndex,
lineNumber,
bodyBytes,
signature64,
hash32
);
BchBlockEntry entry = new BchBlockEntry(globalNumber, ts, lineIndex, lineNumber, bodyBytes, signature64, hash32);
// JSON AddBlock
String prevGlobalHashHex = (globalNumber == 0) ? ZERO64 : state.globalLastHashHex();
String req = buildAddBlockJson(
blockchainName,
globalNumber,
prevGlobalHashHex,
base64(entry.toBytes())
);
String reqJson = buildAddBlockJson(blockchainName, globalNumber, prevGlobalHashHex, base64(entry.toBytes()));
String op = "AddBlock(user=" + login + ", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
String op = "AddBlock (user=" + login + ", bch=" + blockchainName +
", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
String resp = WsJsonOneShot.request(op, req, timeout);
String resp = ws.call(op, reqJson, timeout);
assert200(op, resp);
@ -134,27 +102,20 @@ public final class AddBlockSender {
String localHashHex = bytesToHex64(hash32);
if (test.it.utils.TestConfig.DEBUG()) {
TestLog.ok(op + ": localHash=" + localHashHex);
TestLog.ok(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
if (TestConfig.DEBUG()) {
TestLog.info(op + ": localHash=" + localHashHex);
TestLog.info(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
}
assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash");
// обновляем ChainState
state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32);
if (test.it.utils.TestConfig.DEBUG()) {
TestLog.ok(op + ": state updated");
}
if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
}
// -------------------- json helpers --------------------
private static String buildAddBlockJson(String blockchainName,
int globalNumber,
String prevGlobalHashHex,
String blockBytesB64) {
private static String buildAddBlockJson(String blockchainName, int globalNumber, String prevGlobalHashHex, String blockBytesB64) {
String requestId = TestIds.next("addblock");
return """
{
"op": "AddBlock",
@ -166,13 +127,13 @@ public final class AddBlockSender {
"blockBytesB64": "%s"
}
}
""".formatted(WsJsonOneShot.FIXED_REQUEST_ID, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
""".formatted(requestId, blockchainName, globalNumber, prevGlobalHashHex, blockBytesB64);
}
private static void assert200(String op, String resp) {
int st = test.it.utils.JsonParsers.status(resp);
int st = JsonParsers.status(resp);
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
if (test.it.utils.TestConfig.DEBUG()) TestLog.ok(op + ": status=200");
TestLog.ok(op + ": status=200");
}
private static String base64(byte[] bytes) {

View File

@ -1,92 +0,0 @@
package test.it.addBlockUtils;
// старый класс
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import test.it.utils.TestConfig;
import test.it.utils.TestLog;
import test.it.utils.WsTestClient;
import java.time.Duration;
/**
* WsJsonOneShot
*
* Утилита "отправил JSON -> получил JSON", строго:
* - на каждый request создаём НОВОЕ WS соединение
* - отправляем
* - ждём ответ
* - закрываем соединение
*
* Важно:
* - requestId тут не важен для человека, но важен для WsTestClient, чтобы сопоставить ответ.
* - поэтому ставим ВСЕГДА один и тот же requestId (FIXED_REQUEST_ID).
* - requestId НЕ логируем.
*/
public final class WsJsonOneShot {
private static final ObjectMapper MAPPER = new ObjectMapper();
/** Всегда один и тот же requestId. */
public static final String FIXED_REQUEST_ID = "it";
private WsJsonOneShot() {}
/**
* Старый API (без имени операции) оставляем для совместимости.
*/
public static String request(String json, Duration timeout) {
return request("WS", json, timeout);
}
/**
* Отправить JSON строкой и вернуть JSON ответ строкой.
* Соединение создаётся и закрывается ВНУТРИ.
*
* Если включён it.debug=true печатаем request/response.
*/
public static String request(String op, String json, Duration timeout) {
String patched = forceRequestId(json, FIXED_REQUEST_ID);
if (TestConfig.DEBUG()) {
TestLog.send(op, prettyOrRaw(patched));
}
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
String resp = client.request(FIXED_REQUEST_ID, patched, timeout);
if (TestConfig.DEBUG()) {
TestLog.recv(op, prettyOrRaw(resp));
}
return resp;
}
}
/**
* Гарантируем, что requestId есть и равен FIXED_REQUEST_ID.
* Если JSON кривой вернём как есть (тогда упадёт выше по логике, и это нормально для теста).
*/
private static String forceRequestId(String json, String requestId) {
try {
JsonNode root = MAPPER.readTree(json);
if (!(root instanceof ObjectNode obj)) return json;
obj.put("requestId", requestId);
return MAPPER.writeValueAsString(obj);
} catch (Exception ignore) {
return json;
}
}
private static String prettyOrRaw(String json) {
try {
JsonNode n = MAPPER.readTree(json);
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(n);
} catch (Exception ignore) {
return json;
}
}
}

View File

@ -1,95 +0,0 @@
package test.it.addBlockUtils;
// старый класс
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;
}
}
}

View File

@ -1,115 +0,0 @@
package test.it.utils;
import utils.crypto.Ed25519Util;
/**
* Глобальный контекст IT прогона (одна JVM).
*
* БЫЛО:
* - один пользователь (login/device)
*
* СТАЛО:
* - два пользователя (login1/device1 и login2/device2)
* - ключи детерминированы из логинов
*/
public final class ItRunContext {
private static final Object LOCK = new Object();
private static volatile boolean inited = false;
private static String login1;
private static String bchName1;
private static String login2;
private static String bchName2;
private static byte[] login1PrivKey;
private static byte[] login1PubKey;
private static byte[] device1PrivKey;
private static byte[] device1PubKey;
private static byte[] login2PrivKey;
private static byte[] login2PubKey;
private static byte[] device2PrivKey;
private static byte[] device2PubKey;
private ItRunContext() {}
public static void initIfNeeded() {
if (inited) return;
synchronized (LOCK) {
if (inited) return;
// USER1
login1 = TestConfig.LOGIN();
bchName1 = TestConfig.BCH_NAME();
login1PrivKey = Ed25519Util.generatePrivateKeyFromString(login1);
login1PubKey = Ed25519Util.derivePublicKey(login1PrivKey);
String deviceSeed1 = login1 + "#device";
device1PrivKey = Ed25519Util.generatePrivateKeyFromString(deviceSeed1);
device1PubKey = Ed25519Util.derivePublicKey(device1PrivKey);
// USER2
login2 = TestConfig.LOGIN2();
bchName2 = TestConfig.BCH_NAME2();
login2PrivKey = Ed25519Util.generatePrivateKeyFromString(login2);
login2PubKey = Ed25519Util.derivePublicKey(login2PrivKey);
String deviceSeed2 = login2 + "#device";
device2PrivKey = Ed25519Util.generatePrivateKeyFromString(deviceSeed2);
device2PubKey = Ed25519Util.derivePublicKey(device2PrivKey);
inited = true;
System.out.println(TestColors.C + "\n============================================================" + TestColors.R);
System.out.println(TestColors.C + "IT CONTEXT INIT: 2 users" + TestColors.R);
System.out.println(TestColors.C + "============================================================" + TestColors.R);
System.out.println("USER1 login = " + login1);
System.out.println("USER1 blockchainName = " + bchName1);
System.out.println("USER1 loginPubKey = " + bytesToHexShort(login1PubKey));
System.out.println("USER1 devicePubKey = " + bytesToHexShort(device1PubKey));
System.out.println(TestColors.C + "------------------------------------------------------------" + TestColors.R);
System.out.println("USER2 login = " + login2);
System.out.println("USER2 blockchainName = " + bchName2);
System.out.println("USER2 loginPubKey = " + bytesToHexShort(login2PubKey));
System.out.println("USER2 devicePubKey = " + bytesToHexShort(device2PubKey));
System.out.println(TestColors.C + "------------------------------------------------------------\n" + TestColors.R);
}
}
// =========================
// USER1 getters
// =========================
public static String login1() { initIfNeeded(); return login1; }
public static String bchName1(){ initIfNeeded(); return bchName1; }
public static byte[] login1PrivKey() { initIfNeeded(); return login1PrivKey.clone(); }
public static byte[] login1PubKey() { initIfNeeded(); return login1PubKey.clone(); }
public static byte[] device1PrivKey(){ initIfNeeded(); return device1PrivKey.clone(); }
public static byte[] device1PubKey() { initIfNeeded(); return device1PubKey.clone(); }
// =========================
// USER2 getters
// =========================
public static String login2() { initIfNeeded(); return login2; }
public static String bchName2(){ initIfNeeded(); return bchName2; }
public static byte[] login2PrivKey() { initIfNeeded(); return login2PrivKey.clone(); }
public static byte[] login2PubKey() { initIfNeeded(); return login2PubKey.clone(); }
public static byte[] device2PrivKey(){ initIfNeeded(); return device2PrivKey.clone(); }
public static byte[] device2PubKey() { initIfNeeded(); return device2PubKey.clone(); }
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

@ -5,40 +5,17 @@ import utils.crypto.Ed25519Util;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/** Builder'ы JSON запросов. Внутри автоматически генерим requestId. */
public final class JsonBuilders {
private JsonBuilders() {}
// =========================
// AddUser USER1 (как было)
// =========================
public static String addUser(String requestId) {
return addUserAny(
requestId,
TestConfig.LOGIN(),
TestConfig.BCH_NAME(),
TestConfig.LOGIN_PUBKEY_B64(),
TestConfig.DEVICE_PUBKEY_B64()
);
}
// ---------------- AddUser ----------------
// =========================
// AddUser USER2 (новое)
// =========================
public static String addUser2(String requestId) {
return addUserAny(
requestId,
TestConfig.LOGIN2(),
TestConfig.BCH_NAME2(),
TestConfig.LOGIN2_PUBKEY_B64(),
TestConfig.DEVICE2_PUBKEY_B64()
);
}
private static String addUserAny(String requestId,
String login,
String blockchainName,
String loginKeyB64,
String deviceKeyB64) {
public static String addUser(String login) {
String requestId = TestIds.next("adduser");
String blockchainName = TestConfig.getBlockchainName(login);
String loginKeyB64 = TestConfig.blockchainPublicKeyB64(login); // loginKey = blockchain pub
String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
return """
{
"op": "AddUser",
@ -51,30 +28,30 @@ public final class JsonBuilders {
"bchLimit": %d
}
}
""".formatted(
requestId,
login,
blockchainName,
loginKeyB64,
deviceKeyB64,
TestConfig.TEST_BCH_LIMIT
);
""".formatted(requestId, login, blockchainName, loginKeyB64, deviceKeyB64, TestConfig.TEST_BCH_LIMIT);
}
public static String authChallenge(String requestId) {
// ---------------- AuthChallenge ----------------
public static String authChallenge(String login) {
String requestId = TestIds.next("auth");
return """
{
"op": "AuthChallenge",
"requestId": "%s",
"payload": { "login": "%s" }
}
""".formatted(requestId, TestConfig.LOGIN());
""".formatted(requestId, login);
}
public static String createAuthSession(String requestId, String authNonce, String storagePwd) {
long timeMs = System.currentTimeMillis();
String sigB64 = signAuthorificated(authNonce, timeMs, TestConfig.DEVICE_PRIV_KEY());
// ---------------- CreateAuthSession ----------------
public static String createAuthSession(String login, String authNonce, String storagePwd) {
long timeMs = System.currentTimeMillis();
byte[] devicePriv = TestConfig.getDevicePrivatKey(login);
String sigB64 = signAuthorificated(authNonce, timeMs, devicePriv);
String requestId = TestIds.next("create");
return """
{
"op": "CreateAuthSession",
@ -89,7 +66,10 @@ public final class JsonBuilders {
""".formatted(requestId, storagePwd, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
}
public static String listSessions(String requestId, long timeMs, String signatureB64) {
// ---------------- ListSessions ----------------
public static String listSessions(long timeMs, String signatureB64) {
String requestId = TestIds.next("list");
if (signatureB64 == null) signatureB64 = "";
return """
{
@ -103,7 +83,10 @@ public final class JsonBuilders {
""".formatted(requestId, timeMs, signatureB64);
}
public static String refreshSession(String requestId, String sessionId, String sessionPwd) {
// ---------------- RefreshSession ----------------
public static String refreshSession(String sessionId, String sessionPwd) {
String requestId = TestIds.next("refresh");
return """
{
"op": "RefreshSession",
@ -117,7 +100,10 @@ public final class JsonBuilders {
""".formatted(requestId, sessionId, sessionPwd, TestConfig.TEST_CLIENT_INFO);
}
public static String closeActiveSession(String requestId, String sessionId, long timeMs, String signatureB64) {
// ---------------- CloseActiveSession ----------------
public static String closeActiveSession(String sessionId, long timeMs, String signatureB64) {
String requestId = TestIds.next("close");
if (signatureB64 == null) signatureB64 = "";
return """
{
@ -143,9 +129,4 @@ public final class JsonBuilders {
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
return Base64.getEncoder().encodeToString(sig);
}
// старый метод оставим для совместимости
public static String signAuthorificated(String authNonce, long timeMs) {
return signAuthorificated(authNonce, timeMs, TestConfig.DEVICE_PRIV_KEY());
}
}

View File

@ -1,12 +0,0 @@
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,94 +1,121 @@
package test.it.utils;
import utils.crypto.Ed25519Util;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Конфиг для IT тестов.
*
* ДОБАВЛЕНО:
* - Второй пользователь (LOGIN2) + его blockchainName и ключи.
* TestConfig конфиг IT тестов:
* - 3 пользователя (TestUser1/2/3)
* - ключи по login через map (device/solana/blockchain)
* - blockchainName = login + "001"
*
* Важно:
* - Имена/ключи вычисляются детерминированно из логина (см. ItRunContext).
* - privateKey = Ed25519Util.generatePrivateKeyFromString(login) (sha256, 32 bytes)
* - publicKey = Ed25519Util.derivePublicKey(privateKey)
* - пока device/solana/blockchain ключи одинаковые (один seed на login)
*/
public final class TestConfig {
private TestConfig() {}
// Твой WS URI
public static final String WS_URI = "ws://localhost:7070/ws";
// ======= Пользователь #1 (по умолчанию) =======
public static final String DEFAULT_LOGIN = "Anya";
// ======= Пользователь #2 (новый) =======
public static final String DEFAULT_LOGIN2 = "Anya2";
// Суффикс блокчейна по твоему правилу: login + 3 цифры
public static final String DEFAULT_BCH_SUFFIX_3 = "001";
// Лимит блокчейна для AddUser
public static final long TEST_BCH_LIMIT = 50_000_000L;
// Любая строка клиента (для логов)
public static final String TEST_CLIENT_INFO = "it-tests";
/** DEBUG-режим: подробные логи (по умолчанию true, как у тебя). */
public static boolean DEBUG() {
return Boolean.parseBoolean(System.getProperty("it.debug", "true"));
}
// =========================
// USER #1
// =========================
// 3 users
public static final String DEFAULT_LOGIN1 = "TestUser1";
public static final String DEFAULT_LOGIN2 = "TestUser2";
public static final String DEFAULT_LOGIN3 = "TestUser3";
public static final String DEFAULT_BCH_SUFFIX_3 = "001";
/** login для прогона (user1). */
public static String LOGIN() {
return System.getProperty("it.login", DEFAULT_LOGIN);
}
public static String LOGIN() { return System.getProperty("it.login1", DEFAULT_LOGIN1); }
public static String LOGIN2() { return System.getProperty("it.login2", DEFAULT_LOGIN2); }
public static String LOGIN3() { return System.getProperty("it.login3", DEFAULT_LOGIN3); }
/** Суффикс для имени блокчейна (user1). */
public static String BCH_SUFFIX_3() {
return System.getProperty("it.bchSuffix", DEFAULT_BCH_SUFFIX_3);
}
/** blockchainName по правилу: login + суффикс (user1). */
public static String BCH_NAME() {
return LOGIN() + BCH_SUFFIX_3();
public static String getBlockchainName(String login) {
if (login == null) throw new IllegalArgumentException("login is null");
return login + BCH_SUFFIX_3();
}
public static byte[] LOGIN_PRIV_KEY() { return ItRunContext.login1PrivKey(); }
public static byte[] LOGIN_PUB_KEY() { return ItRunContext.login1PubKey(); }
public static byte[] DEVICE_PRIV_KEY(){ return ItRunContext.device1PrivKey(); }
public static byte[] DEVICE_PUB_KEY() { return ItRunContext.device1PubKey(); }
// ============ key maps ============
private static final Map<String, byte[]> devicePriv = new ConcurrentHashMap<>();
private static final Map<String, byte[]> devicePub = new ConcurrentHashMap<>();
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()); }
private static final Map<String, byte[]> solanaPriv = new ConcurrentHashMap<>();
private static final Map<String, byte[]> solanaPub = new ConcurrentHashMap<>();
// =========================
// USER #2
// =========================
private static final Map<String, byte[]> bchPriv = new ConcurrentHashMap<>();
private static final Map<String, byte[]> bchPub = new ConcurrentHashMap<>();
/** login второго пользователя. Можно переопределить -Dit.login2=... */
public static String LOGIN2() {
return System.getProperty("it.login2", DEFAULT_LOGIN2);
static {
initUserKeys(LOGIN());
initUserKeys(LOGIN2());
initUserKeys(LOGIN3());
}
/** blockchainName второго: login2 + тот же суффикс. */
public static String BCH_NAME2() {
return LOGIN2() + BCH_SUFFIX_3();
private static void initUserKeys(String login) {
byte[] priv = Ed25519Util.generatePrivateKeyFromString(login); // sha256(login) => 32 bytes
byte[] pub = Ed25519Util.derivePublicKey(priv);
// пока одинаковые
devicePriv.put(login, priv);
devicePub.put(login, pub);
solanaPriv.put(login, priv);
solanaPub.put(login, pub);
bchPriv.put(login, priv);
bchPub.put(login, pub);
}
public static byte[] LOGIN2_PRIV_KEY() { return ItRunContext.login2PrivKey(); }
public static byte[] LOGIN2_PUB_KEY() { return ItRunContext.login2PubKey(); }
public static byte[] DEVICE2_PRIV_KEY() { return ItRunContext.device2PrivKey(); }
public static byte[] DEVICE2_PUB_KEY() { return ItRunContext.device2PubKey(); }
// ============ requested getters (with your names) ============
public static String LOGIN2_PUBKEY_B64() { return Base64.getEncoder().encodeToString(LOGIN2_PUB_KEY()); }
public static String DEVICE2_PUBKEY_B64() { return Base64.getEncoder().encodeToString(DEVICE2_PUB_KEY()); }
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(devicePriv.get(login), "devicePriv", login); }
public static byte[] getDevicePublicKey(String login) { return cloneOrThrow(devicePub.get(login), "devicePub", login); }
/** Псевдо-пароль хранилища — достаточно для тестов. */
public static byte[] getSolanaPrivatKey(String login) { return cloneOrThrow(solanaPriv.get(login), "solanaPriv", login); }
public static byte[] getSolanaPublicKey(String login) { return cloneOrThrow(solanaPub.get(login), "solanaPub", login); }
public static byte[] getBlockchainPrivatKey(String login) { return cloneOrThrow(bchPriv.get(login), "bchPriv", login); }
public static byte[] getBlockchainPublicKey(String login) { return cloneOrThrow(bchPub.get(login), "bchPub", login); }
// ============ base64 helpers ============
public static String devicePublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getDevicePublicKey(login)); }
public static String blockchainPublicKeyB64(String login) { return Base64.getEncoder().encodeToString(getBlockchainPublicKey(login)); }
// ============ backward-compatible helpers for "user1" ============
public static String BCH_NAME() { return getBlockchainName(LOGIN()); }
public static String BCH_NAME2() { return getBlockchainName(LOGIN2()); }
public static String BCH_NAME3() { return getBlockchainName(LOGIN3()); }
/** loginKey для AddUser: по твоему решению = blockchain pubkey. */
public static String LOGIN_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN()); }
public static String LOGIN2_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN2()); }
public static String LOGIN3_PUBKEY_B64() { return blockchainPublicKeyB64(LOGIN3()); }
public static String DEVICE_PUBKEY_B64() { return devicePublicKeyB64(LOGIN()); }
public static String DEVICE2_PUBKEY_B64() { return devicePublicKeyB64(LOGIN2()); }
public static String DEVICE3_PUBKEY_B64() { return devicePublicKeyB64(LOGIN3()); }
// ============ misc ============
public static String fakeStoragePwd() {
return "pwd-" + System.nanoTime();
}
private static byte[] cloneOrThrow(byte[] v, String mapName, String login) {
if (login == null) throw new IllegalArgumentException("login is null");
if (v == null) throw new IllegalStateException("No key in " + mapName + " for login=" + login);
return v.clone();
}
}

View File

@ -0,0 +1,15 @@
package test.it.utils;
import java.util.concurrent.atomic.AtomicLong;
/** Генератор уникальных requestId для IT тестов (в пределах одной JVM). */
public final class TestIds {
private static final AtomicLong SEQ = new AtomicLong(0);
private TestIds() {}
public static String next(String prefix) {
long n = SEQ.incrementAndGet();
return "it-" + (prefix == null ? "req" : prefix) + "-" + n;
}
}

View File

@ -3,38 +3,30 @@ package test.it.utils;
/**
* TestLog единое место для:
* - ANSI цветов
* - стандартных красивых сообщений (title/line/step/send/recv)
* - стандартных сообщений (title/step/send/recv)
* - PASS/FAIL строк и окраски
*
* РЕЖИМЫ:
* - it.debug=false (по умолчанию):
* печатаем ТОЛЬКО итог: PASS/FAIL по каждому тесту
* - it.debug=true:
* печатаем всё: ожидания, отправка/ответ (JSON), промежуточные проверки
* Режим:
* - it.debug=false: печатаем минимум (без JSON)
* - it.debug=true: печатаем JSON отправка/ответ + заголовки шагов
*/
public final class TestLog {
private TestLog() {}
// ============================
// DEBUG SWITCH
// ============================
public static final boolean DEBUG = TestConfig.DEBUG();
// ============================
// ANSI COLORS
// ============================
// 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 String green(String s) { return G + s + R; }
public static String red(String s) { return RED + s + R; }
public static String cyan(String s) { return C + s + R; }
/** Дебаг-инфо (печатается только при DEBUG=true). */
/** Инфо (печатается только при DEBUG=true). */
public static void info(String s) {
if (DEBUG) System.out.println(s);
}
@ -44,7 +36,6 @@ public final class TestLog {
System.out.println(C + "------------------------------------------------------------" + R);
}
/** Короткое заглавие (только DEBUG). */
public static void title(String s) {
if (!DEBUG) return;
System.out.println(C + "\n============================================================" + R);
@ -52,7 +43,6 @@ public final class TestLog {
System.out.println(C + "============================================================\n" + R);
}
/** Длинное заглавие (только DEBUG). */
public static void titleBlock(String multiLineText) {
if (!DEBUG) return;
System.out.println(C + "\n============================================================" + R);
@ -60,29 +50,23 @@ public final class TestLog {
System.out.println(C + "============================================================\n" + R);
}
/** Заголовок шага (только DEBUG). */
public static void stepTitle(String s) {
if (!DEBUG) return;
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
}
/** Промежуточное ОК (только DEBUG). */
/** OK (печатаем ВСЕГДА, чтобы было видно зелёное прохождение шагов). */
public static void ok(String s) {
if (!DEBUG) return;
System.out.println(G + "" + s + R);
}
/** Итоговый PASS (печатается ВСЕГДА). */
public static void pass(String s) {
System.out.println(G + "" + s + R);
}
/** WARN (только DEBUG). */
public static void warn(String s) {
if (!DEBUG) return;
System.out.println(Y + "⚠️ " + s + R);
}
/** FAIL (печатается ВСЕГДА). */
/** FAIL (печатаем ВСЕГДА). */
public static void boom(String s) {
System.out.println(RED + "****************************************************************" + R);
System.out.println(RED + "" + s + R);
@ -102,28 +86,4 @@ public final class TestLog {
System.out.println(json);
line();
}
// ============================
// RUN HELPERS
// ============================
/**
* Запуск тестового тела (без JUnit).
* Возвращает 0 если ок, 1 если упал.
*
* Важно:
* - здесь мы НЕ глотаем ошибку: печатаем и возвращаем код
* - раннер суммирует количество упавших тестов
*/
public static int runOne(String testName, Runnable body) {
try {
body.run();
pass(testName + ": OK");
return 0;
} catch (Throwable t) {
boom(testName + ": FAIL. Причина: " + t.getMessage());
if (DEBUG) t.printStackTrace(System.out);
return 1;
}
}
}

View File

@ -0,0 +1,46 @@
package test.it.utils;
import java.util.ArrayList;
import java.util.List;
/**
* TestResult накопитель результатов внутри одного теста:
* - ok(...) печатает зелёным
* - fail(...) печатает красным и добавляет в итоговую строку
* - summaryLine() возвращает одну строку: PASS/FAIL + детали
*/
public final class TestResult {
private final String testName;
private final List<String> errors = new ArrayList<>();
public TestResult(String testName) {
this.testName = testName;
}
public void ok(String msg) {
TestLog.ok(msg);
}
public void fail(String msg) {
errors.add(msg);
TestLog.boom(msg);
}
public boolean isOk() {
return errors.isEmpty();
}
public String summaryLine() {
if (errors.isEmpty()) {
return TestLog.green("PASS: " + testName + " — OK");
}
StringBuilder sb = new StringBuilder();
sb.append(TestLog.red("FAIL: ")).append(testName).append("").append(errors.size()).append(" ошибок: ");
for (int i = 0; i < errors.size(); i++) {
if (i > 0) sb.append(" | ");
sb.append(errors.get(i));
}
return sb.toString();
}
}

View File

@ -0,0 +1,58 @@
package test.it.utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.Duration;
/**
* WsSession одно WS соединение на много запросов.
*
* Использование в тесте:
* try (WsSession ws = WsSession.open()) {
* String resp = ws.call("AuthChallenge", JsonBuilders.authChallenge(login), t);
* }
*/
public final class WsSession implements AutoCloseable {
private static final ObjectMapper M = new ObjectMapper();
private final WsTestClient client;
private WsSession(WsTestClient client) {
this.client = client;
}
public static WsSession open() {
return new WsSession(new WsTestClient(TestConfig.WS_URI));
}
/** Отправить JSON (в котором уже есть requestId) и получить JSON ответ строкой. */
public String call(String op, String requestJson, Duration timeout) {
String requestId = extractRequestId(requestJson);
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestJson must contain requestId: " + requestJson);
if (TestConfig.DEBUG()) TestLog.send(op, requestJson);
String resp = client.request(requestId, requestJson, timeout);
if (TestConfig.DEBUG()) TestLog.recv(op, resp);
return resp;
}
private static String extractRequestId(String json) {
try {
JsonNode root = M.readTree(json);
JsonNode id = root.get("requestId");
return (id == null || id.isNull()) ? null : id.asText();
} catch (Exception e) {
return null;
}
}
@Override
public void close() {
client.close();
}
}