From 8e19486cf529d7563c88b31beb634b92b2af370b118f8869b50aab9bdcb22cf8 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Thu, 8 Jan 2026 14:12:16 +0300 Subject: [PATCH] =?UTF-8?q?08=2001=2025=20=D0=A2=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=87=D1=82=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/test/it/IT_01_AddUser.java | 109 ++--- src/test/java/test/it/IT_02_Sessions.java | 423 +++++------------ .../java/test/it/IT_03_AddBlock_NoAuth.java | 297 +++--------- .../java/test/it/IT_04_UserParams_NoAuth.java | 296 ++++-------- .../test/it/IT_RunAllCleanStartWsMain.java | 17 +- src/test/java/test/it/IT_RunAllMain.java | 50 +- .../test/it/addBlockUtils/AddBlockFlow.java | 448 ------------------ .../test/it/addBlockUtils/AddBlockSender.java | 91 +--- .../test/it/addBlockUtils/WsJsonOneShot.java | 92 ---- .../addBlockUtils/WsJsonRoundtripClient.java | 95 ---- src/test/java/test/it/utils/ItRunContext.java | 115 ----- src/test/java/test/it/utils/JsonBuilders.java | 85 ++-- src/test/java/test/it/utils/TestColors.java | 12 - src/test/java/test/it/utils/TestConfig.java | 133 +++--- src/test/java/test/it/utils/TestIds.java | 15 + src/test/java/test/it/utils/TestLog.java | 66 +-- src/test/java/test/it/utils/TestResult.java | 46 ++ src/test/java/test/it/utils/WsSession.java | 58 +++ 18 files changed, 609 insertions(+), 1839 deletions(-) delete mode 100644 src/test/java/test/it/addBlockUtils/AddBlockFlow.java delete mode 100644 src/test/java/test/it/addBlockUtils/WsJsonOneShot.java delete mode 100644 src/test/java/test/it/addBlockUtils/WsJsonRoundtripClient.java delete mode 100644 src/test/java/test/it/utils/ItRunContext.java delete mode 100644 src/test/java/test/it/utils/TestColors.java create mode 100644 src/test/java/test/it/utils/TestIds.java create mode 100644 src/test/java/test/it/utils/TestResult.java create mode 100644 src/test/java/test/it/utils/WsSession.java diff --git a/src/test/java/test/it/IT_01_AddUser.java b/src/test/java/test/it/IT_01_AddUser.java index 0b5b3c7..47d27ae 100644 --- a/src/test/java/test/it/IT_01_AddUser.java +++ b/src/test/java/test/it/IT_01_AddUser.java @@ -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"); -// @Test - void addUser_shouldReturn200_orAlreadyExists() { - // JUnit-режим: пусть падает через assert/fail как обычно - testBody(); - } + Duration t = Duration.ofSeconds(5); - private static void testBody() { - ItRunContext.initIfNeeded(); + try (WsSession ws = WsSession.open()) { + r.ok("AddUser USER1: " + TestConfig.LOGIN()); + checkAddUser200or409(r, ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t)); - 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(); - - int st = JsonParsers.status(resp); - TestLog.info("ℹ️ status=" + st); - - boolean created = (st == 200); - boolean already = (st == 409); - - if (already) { - 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 (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.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()); } + + return r.summaryLine(); + } + + private static void checkAddUser200or409(TestResult r, String resp) { + int st = JsonParsers.status(resp); + if (st == 200) { + r.ok("AddUser: status=200 (создан)"); + return; + } + if (st == 409) { + String code = JsonParsers.errorCode(resp); + if ("USER_ALREADY_EXISTS".equals(code)) { + r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)"); + return; + } + 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); } } \ No newline at end of file diff --git a/src/test/java/test/it/IT_02_Sessions.java b/src/test/java/test/it/IT_02_Sessions.java index 497bda0..b431765 100644 --- a/src/test/java/test/it/IT_02_Sessions.java +++ b/src/test/java/test/it/IT_02_Sessions.java @@ -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 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); + // 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"); - 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); + long timeMs = System.currentTimeMillis(); + String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN)); - assert200.accept("CreateAuthSession#1", resp2); + String listResp = ws.call("ListSessions(AUTH_IN_PROGRESS)", JsonBuilders.listSessions(timeMs, sig), t); + assertEquals(200, JsonParsers.status(listResp), "ListSessions must be 200"); - 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=[получен]"); + List ids = JsonParsers.sessionIds(listResp); + r.ok("ListSessions(AUTH_IN_PROGRESS): " + 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 ids = JsonParsers.sessionIds(listInUserResp); + r.ok("ListSessions(AUTH_STATUS_USER): " + ids); + + assertTrue(ids.contains(s1Id)); + assertTrue(ids.contains(s2Id)); + assertTrue(ids.contains(s3Id)); + r.ok("Проверка OK: AUTH_STATUS_USER список содержит S1,S2,S3"); + } + + // 4) Проверяем CloseActiveSession, но так, чтобы итогом всё равно осталось 3 сессии: + // создаём TEMP, закрываем TEMP, убеждаемся что S1,S2,S3 остались. + Session temp = createSession(LOGIN, t, r, "TEMP"); + String tempId = temp.sessionId; + + 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, TestConfig.getDevicePrivatKey(LOGIN)); + + 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"); + } + + // 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); + + 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 ids = JsonParsers.sessionIds(listResp); + r.ok("Final ListSessions: " + ids); + + assertTrue(ids.contains(s1Id)); + assertTrue(ids.contains(s2Id)); + assertTrue(ids.contains(s3Id)); + assertFalse(ids.contains(tempId)); + r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)"); + } + + } catch (Throwable e) { + r.fail("IT_02_Sessions упал: " + e.getMessage()); } - 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 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); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - TestLog.ok("Подпись для AUTH_IN_PROGRESS: timeMs=" + timeMs + ", signatureB64=[сгенерирована]"); - - 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); - - assert200.accept("ListSessions(AUTH_IN_PROGRESS)", resp2); - - List ids = JsonParsers.sessionIds(resp2); - TestLog.ok("ListSessions(AUTH_IN_PROGRESS): sessions=" + ids); - - assertTrue(ids.contains(s1Id)); - assertTrue(ids.contains(s2Id)); - TestLog.ok("Проверка OK: AUTH_IN_PROGRESS список содержит SESSION1 и SESSION2"); - } - - TestLog.stepTitle("ШАГ 4: Refresh SESSION1 (входим) и Close SESSION2 (из SESSION1)"); - try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) { - - 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); - assertNotNull(nonce); - - long timeMs = System.currentTimeMillis(); - String sig = JsonBuilders.signAuthorificated(nonce, timeMs); - - 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); - - assert200.accept("ListSessions(after close S2)", resp2); - - List ids = JsonParsers.sessionIds(resp2); - TestLog.ok("ListSessions(after close S2): sessions=" + ids); - - assertTrue(ids.contains(s1Id)); - assertFalse(ids.contains(s2Id)); - TestLog.ok("Проверка OK: осталась только SESSION1"); - } - - 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 закрыта"); - } - - 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 ids = JsonParsers.sessionIds(resp2); - TestLog.ok("ListSessions(empty): sessions=" + ids); - - assertTrue(ids.isEmpty(), "Sessions must be empty"); - TestLog.ok("Проверка OK: список пуст"); - } - - TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)"); + return r.summaryLine(); } + + 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) {} } \ No newline at end of file diff --git a/src/test/java/test/it/IT_03_AddBlock_NoAuth.java b/src/test/java/test/it/IT_03_AddBlock_NoAuth.java index d4ee562..1aed448 100644 --- a/src/test/java/test/it/IT_03_AddBlock_NoAuth.java +++ b/src/test/java/test/it/IT_03_AddBlock_NoAuth.java @@ -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,239 +19,89 @@ 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(ws, st1, u1, bch1, TestConfig.getBlockchainPrivatKey(u1)); + + sender1.send(new HeaderBody(u1), t); + assertTrue(st1.hasHeader()); + + sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #1 (NEW) from IT_03 test"), t); + sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #2 (NEW) from IT_03 test"), t); + sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #3 (NEW) from IT_03 test"), t); + + byte[] text1Hash = st1.getGlobalHash32(1); + byte[] text2Hash = st1.getGlobalHash32(2); + byte[] text3Hash = st1.getGlobalHash32(3); + assertNotNull(text1Hash); + assertNotNull(text2Hash); + assertNotNull(text3Hash); + + 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); + + sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 1, text1Hash), t); + sender1.send(new ReactionBody(ReactionBody.SUB_LIKE, bch1, 2, text2Hash), 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); + + 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 блока"); + + // USER2 + ChainState st2 = new ChainState(); + AddBlockSender sender2 = new AddBlockSender(ws, st2, u2, bch2, TestConfig.getBlockchainPrivatKey(u2)); + + sender2.send(new HeaderBody(u2), t); + assertTrue(st2.hasHeader()); + + sender2.send(new UserParamBody("Anya", "Amsterdam, Example street 10"), t); + + sender2.send(new ConnectionBody(ConnectionBody.SUB_FRIEND, u1, bch1, 0, new byte[32]), 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); + + sender2.send(new ConnectionBody(ConnectionBody.SUB_UNFRIEND, u1, bch1, 0, new byte[32]), t); + + r.ok("IT_03 сценарий блоков выполнен"); + + } catch (Throwable e) { + r.fail("IT_03 упал: " + e.getMessage()); } - ChainState st1 = new ChainState(); - AddBlockSender sender1 = new AddBlockSender( - st1, - TestConfig.LOGIN(), - TestConfig.BCH_NAME(), - TestConfig.LOGIN_PRIV_KEY() - ); - - if (TestConfig.DEBUG()) TestLog.stepTitle("USER1: HEADER"); - sender1.send(new HeaderBody(TestConfig.LOGIN()), 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); - byte[] text2Hash = st1.getGlobalHash32(2); - byte[] text3Hash = st1.getGlobalHash32(3); - assertNotNull(text1Hash); - 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); - - 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); - - // 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); - - 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(2, st1.lineLastNumber((short) 2), "USER1: line=2 должно быть 2 REACTION блока"); - - // ========================================================= - // 4) USER2: HEADER + PARAMS + FRIEND->USER1 - // ========================================================= - ChainState st2 = new ChainState(); - AddBlockSender sender2 = new AddBlockSender( - st2, - USER2_LOGIN, - USER2_BCH, - user2LoginPriv - ); - - if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: HEADER"); - sender2.send(new HeaderBody(USER2_LOGIN), t); - assertTrue(st2.hasHeader()); - - if (TestConfig.DEBUG()) TestLog.stepTitle("USER2: UserParams (name + address)"); - 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); - - // ========================================================= - // 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); - - 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); - - 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); - - // 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); - - TestLog.pass("IT_03_AddBlock_NoAuth (combined): OK"); + return r.summaryLine(); } } \ No newline at end of file diff --git a/src/test/java/test/it/IT_04_UserParams_NoAuth.java b/src/test/java/test/it/IT_04_UserParams_NoAuth.java index 35fc560..e0c113c 100644 --- a/src/test/java/test/it/IT_04_UserParams_NoAuth.java +++ b/src/test/java/test/it/IT_04_UserParams_NoAuth.java @@ -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,143 +15,99 @@ 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); - // --------------------------------------------------------- - // 1) сохранить param1 - // --------------------------------------------------------- - final String p1 = "profile:name"; - final String v1 = "Anna"; - final long t1 = System.currentTimeMillis(); + 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(r, login, p1, timeout); + assertEquals(login, got1.login); + assertEquals(p1, got1.param); + assertEquals(t1, got1.timeMs); + assertEquals(v1, got1.value); + assertEquals(deviceKeyB64, got1.deviceKeyB64); + assertNotNull(got1.signatureB64); + assertFalse(got1.signatureB64.isBlank()); + r.ok("GetUserParam(param1) OK"); - // --------------------------------------------------------- - // 2) получить param1 и проверить - // --------------------------------------------------------- - NetParam got1 = getUserParam_200(login, p1, timeout); + // 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); - assertEquals(login, got1.login); - assertEquals(p1, got1.param); - assertEquals(t1, got1.timeMs); - assertEquals(v1, got1.value); - assertEquals(deviceKeyB64, got1.deviceKeyB64); - assertNotNull(got1.signatureB64); - assertFalse(got1.signatureB64.isBlank()); + // 4) обновить param1 + final String v1b = "Anna Updated"; + final long t1b = t2 + 10; + upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout); - // --------------------------------------------------------- - // 3) сохранить param2 - // --------------------------------------------------------- - final String p2 = "profile:city"; - final String v2 = "Amsterdam"; - final long t2 = t1 + 10; + NetParam got1b = getUserParam_200(r, login, p1, timeout); + assertEquals(t1b, got1b.timeMs); + assertEquals(v1b, got1b.value); + r.ok("GetUserParam(updated param1) OK"); - upsertUserParam_OK(login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout); + // 5) list всех параметров + NetParamList list = listUserParams_200(r, login, timeout); - // --------------------------------------------------------- - // 4) обновить param1 более новым временем - // --------------------------------------------------------- - final String v1b = "Anna Updated"; - final long t1b = t2 + 10; + NetParam lp1 = list.find(p1); + NetParam lp2 = list.find(p2); - upsertUserParam_OK(login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout); + assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1); + assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2); - // доп.проверка: GetUserParam теперь должен вернуть обновлённое - NetParam got1b = getUserParam_200(login, p1, timeout); - assertEquals(t1b, got1b.timeMs); - assertEquals(v1b, got1b.value); + assertEquals(t1b, lp1.timeMs); + assertEquals(v1b, lp1.value); - // --------------------------------------------------------- - // 5) list всех параметров и проверка состава - // --------------------------------------------------------- - NetParamList list = listUserParams_200(login, timeout); + assertEquals(t2, lp2.timeMs); + assertEquals(v2, lp2.value); - NetParam lp1 = list.find(p1); - NetParam lp2 = list.find(p2); + assertEquals(deviceKeyB64, lp1.deviceKeyB64); + assertEquals(deviceKeyB64, lp2.deviceKeyB64); + assertNotNull(lp1.signatureB64); + assertNotNull(lp2.signatureB64); - assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1); - assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2); + r.ok("ListUserParams OK"); - assertEquals(t1b, lp1.timeMs, "param1 должен быть обновлённым"); - assertEquals(v1b, lp1.value, "param1 должен иметь обновлённое значение"); + } catch (Throwable e) { + r.fail("IT_04 упал: " + e.getMessage()); + } - 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"); + 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 { diff --git a/src/test/java/test/it/IT_RunAllCleanStartWsMain.java b/src/test/java/test/it/IT_RunAllCleanStartWsMain.java index bd9fc46..27c163d 100644 --- a/src/test/java/test/it/IT_RunAllCleanStartWsMain.java +++ b/src/test/java/test/it/IT_RunAllCleanStartWsMain.java @@ -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); } diff --git a/src/test/java/test/it/IT_RunAllMain.java b/src/test/java/test/it/IT_RunAllMain.java index d3a4fb5..e4df4c2 100644 --- a/src/test/java/test/it/IT_RunAllMain.java +++ b/src/test/java/test/it/IT_RunAllMain.java @@ -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 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; } diff --git a/src/test/java/test/it/addBlockUtils/AddBlockFlow.java b/src/test/java/test/it/addBlockUtils/AddBlockFlow.java deleted file mode 100644 index 98839c4..0000000 --- a/src/test/java/test/it/addBlockUtils/AddBlockFlow.java +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/src/test/java/test/it/addBlockUtils/AddBlockSender.java b/src/test/java/test/it/addBlockUtils/AddBlockSender.java index c3c3841..092a92b 100644 --- a/src/test/java/test/it/addBlockUtils/AddBlockSender.java +++ b/src/test/java/test/it/addBlockUtils/AddBlockSender.java @@ -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) { diff --git a/src/test/java/test/it/addBlockUtils/WsJsonOneShot.java b/src/test/java/test/it/addBlockUtils/WsJsonOneShot.java deleted file mode 100644 index c65ac5f..0000000 --- a/src/test/java/test/it/addBlockUtils/WsJsonOneShot.java +++ /dev/null @@ -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; - } - } -} \ No newline at end of file diff --git a/src/test/java/test/it/addBlockUtils/WsJsonRoundtripClient.java b/src/test/java/test/it/addBlockUtils/WsJsonRoundtripClient.java deleted file mode 100644 index 45d4faf..0000000 --- a/src/test/java/test/it/addBlockUtils/WsJsonRoundtripClient.java +++ /dev/null @@ -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 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; - } - } -} \ No newline at end of file diff --git a/src/test/java/test/it/utils/ItRunContext.java b/src/test/java/test/it/utils/ItRunContext.java deleted file mode 100644 index 503eaec..0000000 --- a/src/test/java/test/it/utils/ItRunContext.java +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/test/java/test/it/utils/JsonBuilders.java b/src/test/java/test/it/utils/JsonBuilders.java index 3e15683..bd3e1e7 100644 --- a/src/test/java/test/it/utils/JsonBuilders.java +++ b/src/test/java/test/it/utils/JsonBuilders.java @@ -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(){} + 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()); - } } \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestColors.java b/src/test/java/test/it/utils/TestColors.java deleted file mode 100644 index c7520cb..0000000 --- a/src/test/java/test/it/utils/TestColors.java +++ /dev/null @@ -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"; -} \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestConfig.java b/src/test/java/test/it/utils/TestConfig.java index 2567d9c..1ef3f08 100644 --- a/src/test/java/test/it/utils/TestConfig.java +++ b/src/test/java/test/it/utils/TestConfig.java @@ -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 devicePriv = new ConcurrentHashMap<>(); + private static final Map 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 solanaPriv = new ConcurrentHashMap<>(); + private static final Map solanaPub = new ConcurrentHashMap<>(); - // ========================= - // USER #2 - // ========================= + private static final Map bchPriv = new ConcurrentHashMap<>(); + private static final Map 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(); + } } \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestIds.java b/src/test/java/test/it/utils/TestIds.java new file mode 100644 index 0000000..2cf3ac9 --- /dev/null +++ b/src/test/java/test/it/utils/TestIds.java @@ -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; + } +} \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestLog.java b/src/test/java/test/it/utils/TestLog.java index 8f417b9..365ae83 100644 --- a/src/test/java/test/it/utils/TestLog.java +++ b/src/test/java/test/it/utils/TestLog.java @@ -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; - } - } } \ No newline at end of file diff --git a/src/test/java/test/it/utils/TestResult.java b/src/test/java/test/it/utils/TestResult.java new file mode 100644 index 0000000..e82a1ce --- /dev/null +++ b/src/test/java/test/it/utils/TestResult.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/src/test/java/test/it/utils/WsSession.java b/src/test/java/test/it/utils/WsSession.java new file mode 100644 index 0000000..23b49bc --- /dev/null +++ b/src/test/java/test/it/utils/WsSession.java @@ -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(); + } +} \ No newline at end of file