08 01 25
Тесты почти переделал
This commit is contained in:
parent
e2b89da2fa
commit
8e19486cf5
@ -1,94 +1,59 @@
|
|||||||
package test.it;
|
package test.it;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import test.it.utils.*;
|
import test.it.utils.*;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IT_01_AddUser
|
* IT_01_AddUser
|
||||||
*
|
* Создаёт 3 пользователей: TestUser1/2/3 (200 OK или 409 USER_ALREADY_EXISTS).
|
||||||
* Можно запускать:
|
|
||||||
* 1) как JUnit тест (через Suite или выборочно)
|
|
||||||
* 2) вручную как standalone:
|
|
||||||
* - main()
|
|
||||||
* - или через IT_RunAllMain / IT_RunAllCleanMain
|
|
||||||
*
|
|
||||||
* Главная цель:
|
|
||||||
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
|
|
||||||
* - и иметь main() для запуска одного теста
|
|
||||||
*/
|
*/
|
||||||
public class IT_01_AddUser {
|
public class IT_01_AddUser {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// чтобы тест можно было запускать вообще без JUnit
|
String summary = run();
|
||||||
int failed = run();
|
System.out.println(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
|
public static String run() {
|
||||||
public static int run() {
|
TestResult r = new TestResult("IT_01_AddUser");
|
||||||
return TestLog.runOne("IT_01_AddUser", IT_01_AddUser::testBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Test
|
Duration t = Duration.ofSeconds(5);
|
||||||
void addUser_shouldReturn200_orAlreadyExists() {
|
|
||||||
// JUnit-режим: пусть падает через assert/fail как обычно
|
|
||||||
testBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testBody() {
|
try (WsSession ws = WsSession.open()) {
|
||||||
ItRunContext.initIfNeeded();
|
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)");
|
r.ok("AddUser USER2: " + TestConfig.LOGIN2());
|
||||||
TestLog.info("Используем:");
|
checkAddUser200or409(r, ws.call("AddUser#USER2", JsonBuilders.addUser(TestConfig.LOGIN2()), t));
|
||||||
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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
package test.it;
|
package test.it;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import test.it.utils.*;
|
import test.it.utils.*;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@ -12,318 +10,151 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
/**
|
/**
|
||||||
* IT_02_Sessions
|
* IT_02_Sessions
|
||||||
*
|
*
|
||||||
* Можно запускать:
|
* Цель:
|
||||||
* 1) как JUnit тест (через Suite или выборочно)
|
* - проверить создание/листинг/refresh/close
|
||||||
* 2) вручную как standalone:
|
* - и после завершения оставить в БД 3 активных сессии (S1,S2,S3)
|
||||||
* - main()
|
|
||||||
* - или через IT_RunAllMain / IT_RunAllCleanMain
|
|
||||||
*
|
|
||||||
* Главная цель:
|
|
||||||
* - иметь метод run() -> возвращает число не пройденных тестов (0 или 1)
|
|
||||||
* - и иметь main() для запуска одного теста
|
|
||||||
*/
|
*/
|
||||||
public class IT_02_Sessions {
|
public class IT_02_Sessions {
|
||||||
|
|
||||||
|
private static final String LOGIN = TestConfig.LOGIN();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
ItRunContext.initIfNeeded();
|
TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
|
||||||
int failed = run();
|
System.out.println(IT_01_AddUser.run());
|
||||||
|
String summary = run();
|
||||||
|
System.out.println(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Запуск одного теста (standalone). Возвращает 0 если ок, 1 если упал. */
|
public static String run() {
|
||||||
public static int run() {
|
TestResult r = new TestResult("IT_02_Sessions");
|
||||||
return TestLog.runOne("IT_02_Sessions", IT_02_Sessions::testBodyStandalone);
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeAll
|
Duration t = Duration.ofSeconds(5);
|
||||||
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()));
|
|
||||||
|
|
||||||
String s1Id, s1Pwd;
|
String s1Id, s1Pwd;
|
||||||
String s2Id, s2Pwd;
|
String s2Id, s2Pwd;
|
||||||
|
String s3Id, s3Pwd;
|
||||||
|
|
||||||
// ===== helpers (локальные, чтобы не раздувать TestLog лишней логикой assert200) =====
|
try {
|
||||||
final java.util.function.BiConsumer<String, String> assert200 = (op, resp) -> {
|
// 1) Создаём 3 сессии (каждая — отдельным соединением, чтобы не зависеть от состояния WS)
|
||||||
int st = JsonParsers.status(resp);
|
Session s1 = createSession(LOGIN, t, r, "S1");
|
||||||
assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
|
s1Id = s1.sessionId; s1Pwd = s1.sessionPwd;
|
||||||
TestLog.ok(op + ": status=200");
|
|
||||||
};
|
|
||||||
|
|
||||||
// ======================================================================
|
Session s2 = createSession(LOGIN, t, r, "S2");
|
||||||
|
s2Id = s2.sessionId; s2Pwd = s2.sessionPwd;
|
||||||
|
|
||||||
TestLog.stepTitle("ШАГ 1: создать SESSION1 (AuthChallenge -> CreateAuthSession)");
|
Session s3 = createSession(LOGIN, t, r, "S3");
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
s3Id = s3.sessionId; s3Pwd = s3.sessionPwd;
|
||||||
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);
|
|
||||||
|
|
||||||
assert200.accept("AuthChallenge#1", resp1);
|
// 2) ListSessions в AUTH_IN_PROGRESS — должны быть S1,S2,S3
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
try (WsSession ws = WsSession.open()) {
|
||||||
assertNotNull(nonce, "AuthChallenge#1: nonce must not be null");
|
String nonceResp = ws.call("AuthChallenge(list)", JsonBuilders.authChallenge(LOGIN), t);
|
||||||
TestLog.ok("AuthChallenge#1: authNonce получен: " + nonce);
|
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";
|
long timeMs = System.currentTimeMillis();
|
||||||
String storagePwd = TestConfig.fakeStoragePwd();
|
String sig = JsonBuilders.signAuthorificated(nonce, timeMs, TestConfig.getDevicePrivatKey(LOGIN));
|
||||||
String req2 = JsonBuilders.createAuthSession(r2, nonce, storagePwd);
|
|
||||||
TestLog.send("CreateAuthSession#1", req2);
|
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
|
||||||
TestLog.recv("CreateAuthSession#1", resp2);
|
|
||||||
|
|
||||||
assert200.accept("CreateAuthSession#1", resp2);
|
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);
|
List<String> ids = JsonParsers.sessionIds(listResp);
|
||||||
s1Pwd = JsonParsers.sessionPwd(resp2);
|
r.ok("ListSessions(AUTH_IN_PROGRESS): " + ids);
|
||||||
assertNotNull(s1Id, "CreateAuthSession#1: sessionId must not be null");
|
|
||||||
assertNotNull(s1Pwd, "CreateAuthSession#1: sessionPwd must not be null");
|
assertTrue(ids.contains(s1Id), "Must contain S1");
|
||||||
TestLog.ok("SESSION1 получена: sessionId=" + s1Id + ", sessionPwd=[получен]");
|
assertTrue(ids.contains(s2Id), "Must contain S2");
|
||||||
|
assertTrue(ids.contains(s3Id), "Must contain S3");
|
||||||
|
r.ok("Проверка OK: список содержит S1,S2,S3");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) RefreshSession(S1) -> после refresh в этом же соединении делаем ListSessions(AUTH_STATUS_USER) (timeMs=0)
|
||||||
|
try (WsSession ws = WsSession.open()) {
|
||||||
|
String refreshResp = ws.call("RefreshSession(S1)", JsonBuilders.refreshSession(s1Id, s1Pwd), t);
|
||||||
|
assertEquals(200, JsonParsers.status(refreshResp), "RefreshSession(S1) must be 200");
|
||||||
|
assertNotNull(JsonParsers.storagePwd(refreshResp), "storagePwd must not be null");
|
||||||
|
r.ok("RefreshSession(S1): OK");
|
||||||
|
|
||||||
|
String listInUserResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
|
||||||
|
assertEquals(200, JsonParsers.status(listInUserResp), "ListSessions(AUTH_STATUS_USER) must be 200");
|
||||||
|
|
||||||
|
List<String> ids = JsonParsers.sessionIds(listInUserResp);
|
||||||
|
r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
|
||||||
|
|
||||||
|
assertTrue(ids.contains(s1Id));
|
||||||
|
assertTrue(ids.contains(s2Id));
|
||||||
|
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<String> 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");
|
return r.summaryLine();
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
|
||||||
String r1 = "it-auth-2";
|
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
|
||||||
TestLog.send("AuthChallenge#2", req1);
|
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
|
||||||
TestLog.recv("AuthChallenge#2", resp1);
|
|
||||||
|
|
||||||
assert200.accept("AuthChallenge#2", resp1);
|
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
|
||||||
assertNotNull(nonce);
|
|
||||||
TestLog.ok("AuthChallenge#2: authNonce получен: " + nonce);
|
|
||||||
|
|
||||||
String r2 = "it-create-2";
|
|
||||||
String req2 = JsonBuilders.createAuthSession(r2, nonce, TestConfig.fakeStoragePwd());
|
|
||||||
TestLog.send("CreateAuthSession#2", req2);
|
|
||||||
String resp2 = c.request(r2, req2, Duration.ofSeconds(5));
|
|
||||||
TestLog.recv("CreateAuthSession#2", resp2);
|
|
||||||
|
|
||||||
assert200.accept("CreateAuthSession#2", resp2);
|
|
||||||
|
|
||||||
s2Id = JsonParsers.sessionId(resp2);
|
|
||||||
s2Pwd = JsonParsers.sessionPwd(resp2);
|
|
||||||
assertNotNull(s2Id);
|
|
||||||
assertNotNull(s2Pwd);
|
|
||||||
TestLog.ok("SESSION2 получена: sessionId=" + s2Id + ", sessionPwd=[получен]");
|
|
||||||
|
|
||||||
String r3 = "it-list-in-session2";
|
|
||||||
String req3 = JsonBuilders.listSessions(r3, 0L, "");
|
|
||||||
TestLog.send("ListSessions(in SESSION2)", req3);
|
|
||||||
String resp3 = c.request(r3, req3, Duration.ofSeconds(5));
|
|
||||||
TestLog.recv("ListSessions(in SESSION2)", resp3);
|
|
||||||
|
|
||||||
assert200.accept("ListSessions(in SESSION2)", resp3);
|
|
||||||
List<String> ids = JsonParsers.sessionIds(resp3);
|
|
||||||
TestLog.ok("ListSessions(in SESSION2): sessions=" + ids);
|
|
||||||
|
|
||||||
assertTrue(ids.contains(s1Id), "Must contain session1");
|
|
||||||
assertTrue(ids.contains(s2Id), "Must contain session2");
|
|
||||||
TestLog.ok("Проверка OK: список содержит SESSION1 и SESSION2");
|
|
||||||
}
|
|
||||||
|
|
||||||
TestLog.stepTitle("ШАГ 3: ListSessions в AUTH_IN_PROGRESS (nonce+signature) → должны быть SESSION1+SESSION2");
|
|
||||||
try (WsTestClient c = new WsTestClient(TestConfig.WS_URI)) {
|
|
||||||
String r1 = "it-auth-list";
|
|
||||||
String req1 = JsonBuilders.authChallenge(r1);
|
|
||||||
TestLog.send("AuthChallenge(list)", req1);
|
|
||||||
String resp1 = c.request(r1, req1, Duration.ofSeconds(5));
|
|
||||||
TestLog.recv("AuthChallenge(list)", resp1);
|
|
||||||
|
|
||||||
assert200.accept("AuthChallenge(list)", resp1);
|
|
||||||
String nonce = JsonParsers.authNonce(resp1);
|
|
||||||
assertNotNull(nonce);
|
|
||||||
TestLog.ok("AuthChallenge(list): authNonce=" + nonce);
|
|
||||||
|
|
||||||
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<String> 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<String> 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<String> ids = JsonParsers.sessionIds(resp2);
|
|
||||||
TestLog.ok("ListSessions(empty): sessions=" + ids);
|
|
||||||
|
|
||||||
assertTrue(ids.isEmpty(), "Sessions must be empty");
|
|
||||||
TestLog.ok("Проверка OK: список пуст");
|
|
||||||
}
|
|
||||||
|
|
||||||
TestLog.ok("ТЕСТ ПРОЙДЕН ЦЕЛИКОМ: SessionsIT (весь сценарий сессий выполнен успешно)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Session createSession(String login, Duration t, TestResult r, String label) {
|
||||||
|
try (WsSession ws = WsSession.open()) {
|
||||||
|
String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
|
||||||
|
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
|
||||||
|
String nonce = JsonParsers.authNonce(nonceResp);
|
||||||
|
assertNotNull(nonce, "authNonce must not be null for " + label);
|
||||||
|
|
||||||
|
String createResp = ws.call("CreateAuthSession(" + label + ")", JsonBuilders.createAuthSession(login, nonce, TestConfig.fakeStoragePwd()), t);
|
||||||
|
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
|
||||||
|
|
||||||
|
String sid = JsonParsers.sessionId(createResp);
|
||||||
|
String spw = JsonParsers.sessionPwd(createResp);
|
||||||
|
|
||||||
|
assertNotNull(sid, "sessionId must not be null");
|
||||||
|
assertNotNull(spw, "sessionPwd must not be null");
|
||||||
|
|
||||||
|
r.ok("Создана сессия " + label + ": sessionId=" + sid);
|
||||||
|
return new Session(sid, spw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Session(String sessionId, String sessionPwd) {}
|
||||||
}
|
}
|
||||||
@ -5,11 +5,12 @@ import blockchain.body.HeaderBody;
|
|||||||
import blockchain.body.ReactionBody;
|
import blockchain.body.ReactionBody;
|
||||||
import blockchain.body.TextBody;
|
import blockchain.body.TextBody;
|
||||||
import blockchain.body.UserParamBody;
|
import blockchain.body.UserParamBody;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import test.it.addBlockUtils.AddBlockSender;
|
import test.it.addBlockUtils.AddBlockSender;
|
||||||
import test.it.addBlockUtils.ChainState;
|
import test.it.addBlockUtils.ChainState;
|
||||||
import test.it.utils.*;
|
import test.it.utils.TestConfig;
|
||||||
import utils.crypto.Ed25519Util;
|
import test.it.utils.TestLog;
|
||||||
|
import test.it.utils.TestResult;
|
||||||
|
import test.it.utils.WsSession;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
@ -18,239 +19,89 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
/**
|
/**
|
||||||
* IT_03_AddBlock_NoAuth
|
* IT_03_AddBlock_NoAuth
|
||||||
*
|
*
|
||||||
* ОБЪЕДИНЕНО: прежний IT_03 + прежний IT_04 в одном тесте,
|
* ВАЖНО:
|
||||||
* чтобы "четвёртый" сценарий гарантированно запускался сразу после "третьего".
|
* - пользователей НЕ создаём (их создаёт IT_01)
|
||||||
*
|
* - ключи берём только из TestConfig по login
|
||||||
* Сценарий:
|
|
||||||
* 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), но локально.
|
|
||||||
*/
|
*/
|
||||||
public class IT_03_AddBlock_NoAuth {
|
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) {
|
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() {
|
public static String run() {
|
||||||
return TestLog.runOne("IT_03_AddBlock_NoAuth", IT_03_AddBlock_NoAuth::testBody);
|
TestResult r = new TestResult("IT_03_AddBlock_NoAuth");
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeAll
|
String u1 = TestConfig.LOGIN();
|
||||||
static void ensureUserExists() {
|
String u2 = TestConfig.LOGIN2();
|
||||||
ItRunContext.initIfNeeded();
|
|
||||||
// можно оставить пустым, как у тебя
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testBody() {
|
String bch1 = TestConfig.getBlockchainName(u1);
|
||||||
ItRunContext.initIfNeeded();
|
String bch2 = TestConfig.getBlockchainName(u2);
|
||||||
ensureUserExists();
|
|
||||||
|
|
||||||
Duration t = Duration.ofSeconds(1);
|
Duration t = Duration.ofSeconds(1);
|
||||||
|
|
||||||
// =========================================================
|
try (WsSession ws = WsSession.open()) {
|
||||||
// USER2 keys (детерминированно из login, как твой ItRunContext)
|
|
||||||
// =========================================================
|
|
||||||
byte[] user2LoginPriv = Ed25519Util.generatePrivateKeyFromString(USER2_LOGIN);
|
|
||||||
|
|
||||||
// =========================================================
|
if (TestConfig.DEBUG()) TestLog.titleBlock("IT_03: USER1=" + u1 + " bch=" + bch1 + " | USER2=" + u2 + " bch=" + bch2);
|
||||||
// 3) USER1 блоки (под message_stats + edits)
|
|
||||||
// =========================================================
|
// USER1
|
||||||
if (TestConfig.DEBUG()) {
|
ChainState st1 = new ChainState();
|
||||||
TestLog.titleBlock("""
|
AddBlockSender sender1 = new AddBlockSender(ws, st1, u1, bch1, TestConfig.getBlockchainPrivatKey(u1));
|
||||||
IT_03_AddBlock_NoAuth (combined): USER1 + USER2 сценарии
|
|
||||||
USER1 login = %s
|
sender1.send(new HeaderBody(u1), t);
|
||||||
USER1 blockchainName= %s
|
assertTrue(st1.hasHeader());
|
||||||
USER2 login = %s
|
|
||||||
USER2 blockchainName= %s
|
sender1.send(new TextBody(TextBody.SUB_NEW, "Hello #1 (NEW) from IT_03 test"), t);
|
||||||
""".formatted(TestConfig.LOGIN(), TestConfig.BCH_NAME(), USER2_LOGIN, USER2_BCH));
|
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();
|
return r.summaryLine();
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,6 @@ package test.it;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import test.it.utils.*;
|
import test.it.utils.*;
|
||||||
import utils.config.ShineSignatureConstants;
|
import utils.config.ShineSignatureConstants;
|
||||||
import utils.crypto.Ed25519Util;
|
import utils.crypto.Ed25519Util;
|
||||||
@ -16,143 +15,99 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
/**
|
/**
|
||||||
* IT_04_UserParams_NoAuth
|
* IT_04_UserParams_NoAuth
|
||||||
*
|
*
|
||||||
* Сценарий:
|
* ВАЖНО:
|
||||||
* 1) UpsertUserParam: сохранить param1
|
* - пользователей НЕ создаём (их создаёт IT_01)
|
||||||
* 2) GetUserParam: получить param1 и проверить поля
|
|
||||||
* 3) UpsertUserParam: сохранить param2
|
|
||||||
* 4) UpsertUserParam: обновить param1 (time_ms больше)
|
|
||||||
* 5) ListUserParams: получить список и проверить:
|
|
||||||
* - есть param1 (обновлённое значение/time)
|
|
||||||
* - есть param2
|
|
||||||
*
|
|
||||||
* Примечание по безопасности (на будущее):
|
|
||||||
* - сейчас (MVP) чтение/запись параметров без ограничений по сессии.
|
|
||||||
* - позже можно добавить: доступ только владельцу или доверенным, через active_session/ACL.
|
|
||||||
*/
|
*/
|
||||||
public class IT_04_UserParams_NoAuth {
|
public class IT_04_UserParams_NoAuth {
|
||||||
|
|
||||||
private static final ObjectMapper M = new ObjectMapper();
|
private static final ObjectMapper M = new ObjectMapper();
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
int failed = run();
|
TestLog.info("Standalone: этот тест требует заранее созданных пользователей -> сначала запускаю IT_01_AddUser");
|
||||||
// System.exit(failed);
|
System.out.println(IT_01_AddUser.run());
|
||||||
|
String summary = run();
|
||||||
|
System.out.println(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int run() {
|
public static String run() {
|
||||||
return TestLog.runOne("IT_04_UserParams_NoAuth", IT_04_UserParams_NoAuth::testBody);
|
TestResult r = new TestResult("IT_04_UserParams_NoAuth");
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void init() {
|
|
||||||
ItRunContext.initIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testBody() {
|
|
||||||
ItRunContext.initIfNeeded();
|
|
||||||
|
|
||||||
Duration timeout = Duration.ofSeconds(5);
|
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 login = TestConfig.LOGIN();
|
||||||
final String deviceKeyB64 = TestConfig.DEVICE_PUBKEY_B64();
|
final String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||||
final byte[] devicePrivKey = TestConfig.DEVICE_PRIV_KEY(); // важно: подпись именно device-ключом
|
final byte[] devicePrivKey = TestConfig.getDevicePrivatKey(login);
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
try {
|
||||||
// 1) сохранить param1
|
// 1) сохранить param1
|
||||||
// ---------------------------------------------------------
|
final String p1 = "profile:name";
|
||||||
final String p1 = "profile:name";
|
final String v1 = "Anna";
|
||||||
final String v1 = "Anna";
|
final long t1 = System.currentTimeMillis();
|
||||||
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");
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
// 3) сохранить param2
|
||||||
// 2) получить param1 и проверить
|
final String p2 = "profile:city";
|
||||||
// ---------------------------------------------------------
|
final String v2 = "Amsterdam";
|
||||||
NetParam got1 = getUserParam_200(login, p1, timeout);
|
final long t2 = t1 + 10;
|
||||||
|
upsertUserParam_OK(r, login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
|
||||||
|
|
||||||
assertEquals(login, got1.login);
|
// 4) обновить param1
|
||||||
assertEquals(p1, got1.param);
|
final String v1b = "Anna Updated";
|
||||||
assertEquals(t1, got1.timeMs);
|
final long t1b = t2 + 10;
|
||||||
assertEquals(v1, got1.value);
|
upsertUserParam_OK(r, login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
|
||||||
assertEquals(deviceKeyB64, got1.deviceKeyB64);
|
|
||||||
assertNotNull(got1.signatureB64);
|
|
||||||
assertFalse(got1.signatureB64.isBlank());
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
NetParam got1b = getUserParam_200(r, login, p1, timeout);
|
||||||
// 3) сохранить param2
|
assertEquals(t1b, got1b.timeMs);
|
||||||
// ---------------------------------------------------------
|
assertEquals(v1b, got1b.value);
|
||||||
final String p2 = "profile:city";
|
r.ok("GetUserParam(updated param1) OK");
|
||||||
final String v2 = "Amsterdam";
|
|
||||||
final long t2 = t1 + 10;
|
|
||||||
|
|
||||||
upsertUserParam_OK(login, p2, t2, v2, deviceKeyB64, devicePrivKey, timeout);
|
// 5) list всех параметров
|
||||||
|
NetParamList list = listUserParams_200(r, login, timeout);
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
NetParam lp1 = list.find(p1);
|
||||||
// 4) обновить param1 более новым временем
|
NetParam lp2 = list.find(p2);
|
||||||
// ---------------------------------------------------------
|
|
||||||
final String v1b = "Anna Updated";
|
|
||||||
final long t1b = t2 + 10;
|
|
||||||
|
|
||||||
upsertUserParam_OK(login, p1, t1b, v1b, deviceKeyB64, devicePrivKey, timeout);
|
assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1);
|
||||||
|
assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2);
|
||||||
|
|
||||||
// доп.проверка: GetUserParam теперь должен вернуть обновлённое
|
assertEquals(t1b, lp1.timeMs);
|
||||||
NetParam got1b = getUserParam_200(login, p1, timeout);
|
assertEquals(v1b, lp1.value);
|
||||||
assertEquals(t1b, got1b.timeMs);
|
|
||||||
assertEquals(v1b, got1b.value);
|
|
||||||
|
|
||||||
// ---------------------------------------------------------
|
assertEquals(t2, lp2.timeMs);
|
||||||
// 5) list всех параметров и проверка состава
|
assertEquals(v2, lp2.value);
|
||||||
// ---------------------------------------------------------
|
|
||||||
NetParamList list = listUserParams_200(login, timeout);
|
|
||||||
|
|
||||||
NetParam lp1 = list.find(p1);
|
assertEquals(deviceKeyB64, lp1.deviceKeyB64);
|
||||||
NetParam lp2 = list.find(p2);
|
assertEquals(deviceKeyB64, lp2.deviceKeyB64);
|
||||||
|
assertNotNull(lp1.signatureB64);
|
||||||
|
assertNotNull(lp2.signatureB64);
|
||||||
|
|
||||||
assertNotNull(lp1, "ListUserParams должен содержать param1=" + p1);
|
r.ok("ListUserParams OK");
|
||||||
assertNotNull(lp2, "ListUserParams должен содержать param2=" + p2);
|
|
||||||
|
|
||||||
assertEquals(t1b, lp1.timeMs, "param1 должен быть обновлённым");
|
} catch (Throwable e) {
|
||||||
assertEquals(v1b, lp1.value, "param1 должен иметь обновлённое значение");
|
r.fail("IT_04 упал: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(t2, lp2.timeMs);
|
return r.summaryLine();
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
// WS helpers: Upsert/Get/List
|
// WS helpers: Upsert/Get/List
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
|
||||||
private static void upsertUserParam_OK(String login,
|
private static void upsertUserParam_OK(TestResult r, String login, String param, long timeMs, String value, String deviceKeyB64, byte[] devicePrivKey, Duration timeout) {
|
||||||
String param,
|
|
||||||
long timeMs,
|
|
||||||
String value,
|
|
||||||
String deviceKeyB64,
|
|
||||||
byte[] devicePrivKey,
|
|
||||||
Duration timeout) {
|
|
||||||
|
|
||||||
String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
|
String signatureB64 = signUserParam(devicePrivKey, login, param, timeMs, value);
|
||||||
|
|
||||||
String reqId = "it-upsert-" + param.replace(':', '_');
|
|
||||||
|
|
||||||
String reqJson = """
|
String reqJson = """
|
||||||
{
|
{
|
||||||
"op": "UpsertUserParam",
|
"op": "UpsertUserParam",
|
||||||
@ -166,29 +121,16 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
"signature": "%s"
|
"signature": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(TestIds.next("upsert"), login, param, timeMs, jsonEscape(value), deviceKeyB64, signatureB64);
|
||||||
reqId,
|
|
||||||
login,
|
|
||||||
param,
|
|
||||||
timeMs,
|
|
||||||
jsonEscape(value),
|
|
||||||
deviceKeyB64,
|
|
||||||
signatureB64
|
|
||||||
);
|
|
||||||
|
|
||||||
try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) {
|
try (WsSession ws = WsSession.open()) {
|
||||||
TestLog.send("UpsertUserParam", reqJson);
|
String resp = ws.call("UpsertUserParam(" + param + ")", reqJson, timeout);
|
||||||
String resp = client.request(reqId, reqJson, timeout);
|
assertEquals(200, JsonParsers.status(resp), "UpsertUserParam expected 200, resp=" + resp);
|
||||||
TestLog.recv("UpsertUserParam", resp);
|
r.ok("UpsertUserParam(" + param + "): OK");
|
||||||
|
|
||||||
int st = JsonParsers.status(resp);
|
|
||||||
assertEquals(200, st, "UpsertUserParam expected 200, resp=" + resp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NetParam getUserParam_200(String login, String param, Duration timeout) {
|
private static NetParam getUserParam_200(TestResult r, String login, String param, Duration timeout) {
|
||||||
String reqId = "it-get-" + param.replace(':', '_');
|
|
||||||
|
|
||||||
String reqJson = """
|
String reqJson = """
|
||||||
{
|
{
|
||||||
"op": "GetUserParam",
|
"op": "GetUserParam",
|
||||||
@ -198,41 +140,29 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
"param": "%s"
|
"param": "%s"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(reqId, login, param);
|
""".formatted(TestIds.next("getparam"), 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);
|
|
||||||
|
|
||||||
|
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);
|
return parseParamFromResponsePayload(resp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NetParamList listUserParams_200(String login, Duration timeout) {
|
private static NetParamList listUserParams_200(TestResult r, String login, Duration timeout) {
|
||||||
String reqId = "it-list-params";
|
|
||||||
|
|
||||||
String reqJson = """
|
String reqJson = """
|
||||||
{
|
{
|
||||||
"op": "ListUserParams",
|
"op": "ListUserParams",
|
||||||
"requestId": "%s",
|
"requestId": "%s",
|
||||||
"payload": {
|
"payload": { "login": "%s" }
|
||||||
"login": "%s"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
""".formatted(reqId, login);
|
""".formatted(TestIds.next("listparams"), 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);
|
|
||||||
|
|
||||||
|
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);
|
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,
|
private static String signUserParam(byte[] devicePrivKey, String login, String param, long timeMs, String value) {
|
||||||
String login,
|
String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX + login + param + timeMs + value;
|
||||||
String param,
|
|
||||||
long timeMs,
|
|
||||||
String value) {
|
|
||||||
|
|
||||||
String signText =
|
|
||||||
ShineSignatureConstants.USER_PARAMETER_PREFIX +
|
|
||||||
login + param + timeMs + value;
|
|
||||||
|
|
||||||
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
// Важно: Ed25519Util.sign(...) ожидает (dataHash OR data?) — у тебя в проекте это уже устаканено.
|
|
||||||
// В хэндлере verify(...) делается на signBytes напрямую, значит подписывать нужно signBytes.
|
|
||||||
byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
|
byte[] sig64 = Ed25519Util.sign(signBytes, devicePrivKey);
|
||||||
return Base64.getEncoder().encodeToString(sig64);
|
return Base64.getEncoder().encodeToString(sig64);
|
||||||
}
|
}
|
||||||
@ -328,60 +247,7 @@ public class IT_04_UserParams_NoAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
// AddUser helper (как у тебя)
|
// DTOs
|
||||||
// =================================================================================
|
|
||||||
|
|
||||||
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
|
|
||||||
// =================================================================================
|
// =================================================================================
|
||||||
|
|
||||||
private static final class NetParam {
|
private static final class NetParam {
|
||||||
|
|||||||
@ -5,16 +5,13 @@ import server.ws.WsServer;
|
|||||||
public class IT_RunAllCleanStartWsMain {
|
public class IT_RunAllCleanStartWsMain {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// 1) Гасим всё на 7070 (если ничего нет — не падаем)
|
|
||||||
runBash("kill -9 $(lsof -t -i:7070) 2>/dev/null || true");
|
runBash("kill -9 $(lsof -t -i:7070) 2>/dev/null || true");
|
||||||
|
|
||||||
// 2) Чистим data/
|
|
||||||
IT_CleanAllDate.main(new String[0]);
|
IT_CleanAllDate.main(new String[0]);
|
||||||
|
|
||||||
// 3) Стартуем WS сервер в отдельном потоке (daemon, чтобы JVM могла завершиться)
|
|
||||||
Thread wsThread = new Thread(() -> {
|
Thread wsThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
WsServer.main(new String[0]); // внутри join() -> поток будет висеть
|
WsServer.main(new String[0]);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
t.printStackTrace(System.out);
|
t.printStackTrace(System.out);
|
||||||
}
|
}
|
||||||
@ -22,24 +19,16 @@ public class IT_RunAllCleanStartWsMain {
|
|||||||
wsThread.setDaemon(true);
|
wsThread.setDaemon(true);
|
||||||
wsThread.start();
|
wsThread.start();
|
||||||
|
|
||||||
// 4) Ждём, чтобы успел стартануть
|
|
||||||
sleepMs(1000);
|
sleepMs(1000);
|
||||||
|
|
||||||
// 5) Запускаем все IT тесты (без System.exit внутри)
|
|
||||||
int failed = IT_RunAllMain.runAll();
|
int failed = IT_RunAllMain.runAll();
|
||||||
|
|
||||||
// 6) Завершаем процесс с кодом ошибок
|
|
||||||
System.exit(failed);
|
System.exit(failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void runBash(String cmd) {
|
private static void runBash(String cmd) {
|
||||||
try {
|
try {
|
||||||
Process p = new ProcessBuilder("bash", "-lc", cmd)
|
Process p = new ProcessBuilder("bash", "-lc", cmd).inheritIO().start();
|
||||||
.inheritIO()
|
p.waitFor();
|
||||||
.start();
|
|
||||||
int code = p.waitFor();
|
|
||||||
// тут не ругаемся: команда может быть "пустой" (ничего не слушает порт)
|
|
||||||
// а мы уже добавили "|| true"
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("WARN: bash command failed: " + e);
|
System.out.println("WARN: bash command failed: " + e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,38 @@
|
|||||||
package test.it;
|
package test.it;
|
||||||
|
|
||||||
import test.it.utils.ItRunContext;
|
|
||||||
import test.it.utils.TestLog;
|
import test.it.utils.TestLog;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ручной запуск всех IT тестов БЕЗ JUnit / Suite.
|
* Ручной запуск всех IT тестов БЕЗ JUnit.
|
||||||
|
* Печатает итоги по каждому тесту отдельной строкой.
|
||||||
*/
|
*/
|
||||||
public class IT_RunAllMain {
|
public class IT_RunAllMain {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
ItRunContext.initIfNeeded();
|
|
||||||
|
|
||||||
int failed = runAll();
|
int failed = runAll();
|
||||||
|
System.exit(failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int runAll() {
|
public static int runAll() {
|
||||||
|
|
||||||
final int total = 4; // было 3
|
List<String> summaries = new ArrayList<>();
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
int passed = 0;
|
|
||||||
|
|
||||||
TestLog.title("IT RUN: запуск всех тестов подряд (без очистки data/)");
|
TestLog.title("IT RUN: запуск всех тестов подряд");
|
||||||
|
|
||||||
TestLog.stepTitle("RUN: IT_01_AddUser");
|
String s1 = IT_01_AddUser.run(); summaries.add(s1); if (s1.contains("FAIL:")) failed++;
|
||||||
int f1 = IT_01_AddUser.run();
|
String s2 = IT_02_Sessions.run(); summaries.add(s2); if (s2.contains("FAIL:")) failed++;
|
||||||
failed += f1; passed += (f1 == 0 ? 1 : 0);
|
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");
|
TestLog.title("IT RUN RESULT (per test)");
|
||||||
int f2 = IT_02_Sessions.run();
|
for (String s : summaries) System.out.println(s);
|
||||||
failed += f2; passed += (f2 == 0 ? 1 : 0);
|
|
||||||
|
|
||||||
TestLog.stepTitle("RUN: IT_03_AddBlock_NoAuth (combined 3+4)");
|
if (failed == 0) TestLog.ok("✅ ВСЕ IT ТЕСТЫ УСПЕШНО ЗАВЕРШЕНЫ");
|
||||||
int f3 = IT_03_AddBlock_NoAuth.run();
|
else TestLog.boom("❌ IT ПРОГОН УПАЛ: failed=" + failed + " из " + summaries.size());
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return failed;
|
return failed;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,11 @@ package test.it.addBlockUtils;
|
|||||||
import blockchain.BchBlockEntry;
|
import blockchain.BchBlockEntry;
|
||||||
import blockchain.BchCryptoVerifier;
|
import blockchain.BchCryptoVerifier;
|
||||||
import blockchain.body.BodyRecord;
|
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.TestLog;
|
||||||
|
import test.it.utils.WsSession;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@ -14,58 +18,46 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AddBlockSender — "одна кнопка":
|
* AddBlockSender — отправка AddBlock поверх одного WsSession:
|
||||||
* - принимает ГОТОВЫЙ Body (HeaderBody/TextBody/ReactionBody/ConnectionBody/UserParamsBody и т.п.)
|
* - берёт номера/prev-hash из ChainState
|
||||||
* - сам берёт номера/prev-hash из ChainState
|
|
||||||
* - строит raw/hash/signature
|
* - строит raw/hash/signature
|
||||||
* - собирает BchBlockEntry (старый, без изменений)
|
|
||||||
* - отправляет AddBlock
|
* - отправляет AddBlock
|
||||||
* - проверяет serverLastGlobalHash == localHash
|
* - проверяет serverLastGlobalHash == localHash
|
||||||
* - обновляет ChainState
|
* - обновляет ChainState
|
||||||
*
|
|
||||||
* ИЗМЕНЕНО:
|
|
||||||
* - sender больше НЕ завязан на TestConfig.LOGIN()/BCH_NAME()/ключи
|
|
||||||
* - теперь он работает от параметров конкретного пользователя:
|
|
||||||
* login, blockchainName, loginPrivKey
|
|
||||||
*/
|
*/
|
||||||
public final class AddBlockSender {
|
public final class AddBlockSender {
|
||||||
|
|
||||||
private static final byte[] ZERO32 = new byte[32];
|
private static final byte[] ZERO32 = new byte[32];
|
||||||
private static final String ZERO64 = "0".repeat(64);
|
private static final String ZERO64 = "0".repeat(64);
|
||||||
|
|
||||||
|
private final WsSession ws;
|
||||||
private final ChainState state;
|
private final ChainState state;
|
||||||
|
|
||||||
private final String login;
|
private final String login;
|
||||||
private final String blockchainName;
|
private final String blockchainName;
|
||||||
private final byte[] loginPrivKey;
|
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.state = state;
|
||||||
this.login = login;
|
this.login = login;
|
||||||
this.blockchainName = blockchainName;
|
this.blockchainName = blockchainName;
|
||||||
this.loginPrivKey = (loginPrivKey == null ? null : loginPrivKey.clone());
|
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");
|
if (this.loginPrivKey == null) throw new IllegalArgumentException("loginPrivKey == null");
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChainState state() { return state; }
|
public ChainState state() { return state; }
|
||||||
|
|
||||||
/**
|
|
||||||
* Отправить следующий блок по body.expectedLineIndex().
|
|
||||||
*/
|
|
||||||
public void send(BodyRecord body, Duration timeout) {
|
public void send(BodyRecord body, Duration timeout) {
|
||||||
if (body == null) throw new IllegalArgumentException("body == null");
|
if (body == null) throw new IllegalArgumentException("body == null");
|
||||||
|
|
||||||
short lineIndex = body.expectedLineIndex();
|
short lineIndex = body.expectedLineIndex();
|
||||||
|
|
||||||
// header должен быть первым
|
|
||||||
if (lineIndex == 0) {
|
if (lineIndex == 0) {
|
||||||
if (state.globalLastNumber() != -1) {
|
if (state.globalLastNumber() != -1) throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
|
||||||
throw new IllegalStateException("HEADER должен быть первым: globalLastNumber уже " + state.globalLastNumber());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!state.hasHeader()) {
|
if (!state.hasHeader()) throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
|
||||||
throw new IllegalStateException("Нельзя слать line=" + lineIndex + " до HEADER (нет headerHash32)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int globalNumber = state.nextGlobalNumber();
|
int globalNumber = state.nextGlobalNumber();
|
||||||
@ -77,7 +69,6 @@ public final class AddBlockSender {
|
|||||||
long ts = System.currentTimeMillis() / 1000L;
|
long ts = System.currentTimeMillis() / 1000L;
|
||||||
byte[] bodyBytes = body.toBytes();
|
byte[] bodyBytes = body.toBytes();
|
||||||
|
|
||||||
// RAW bytes
|
|
||||||
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
|
int recordSize = BchBlockEntry.RAW_HEADER_SIZE + bodyBytes.length;
|
||||||
|
|
||||||
byte[] rawBytes = ByteBuffer.allocate(recordSize)
|
byte[] rawBytes = ByteBuffer.allocate(recordSize)
|
||||||
@ -90,41 +81,18 @@ public final class AddBlockSender {
|
|||||||
.put(bodyBytes)
|
.put(bodyBytes)
|
||||||
.array();
|
.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[] hash32 = BchCryptoVerifier.sha256(preimage);
|
||||||
byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
|
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 prevGlobalHashHex = (globalNumber == 0) ? ZERO64 : state.globalLastHashHex();
|
||||||
|
|
||||||
String req = buildAddBlockJson(
|
String reqJson = buildAddBlockJson(blockchainName, globalNumber, prevGlobalHashHex, base64(entry.toBytes()));
|
||||||
blockchainName,
|
String op = "AddBlock(user=" + login + ", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
|
||||||
globalNumber,
|
|
||||||
prevGlobalHashHex,
|
|
||||||
base64(entry.toBytes())
|
|
||||||
);
|
|
||||||
|
|
||||||
String op = "AddBlock (user=" + login + ", bch=" + blockchainName +
|
String resp = ws.call(op, reqJson, timeout);
|
||||||
", global=" + globalNumber + ", line=" + lineIndex + ", lineNum=" + lineNumber + ")";
|
|
||||||
|
|
||||||
String resp = WsJsonOneShot.request(op, req, timeout);
|
|
||||||
|
|
||||||
assert200(op, resp);
|
assert200(op, resp);
|
||||||
|
|
||||||
@ -134,27 +102,20 @@ public final class AddBlockSender {
|
|||||||
|
|
||||||
String localHashHex = bytesToHex64(hash32);
|
String localHashHex = bytesToHex64(hash32);
|
||||||
|
|
||||||
if (test.it.utils.TestConfig.DEBUG()) {
|
if (TestConfig.DEBUG()) {
|
||||||
TestLog.ok(op + ": localHash=" + localHashHex);
|
TestLog.info(op + ": localHash=" + localHashHex);
|
||||||
TestLog.ok(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
|
TestLog.info(op + ": serverLastGlobalHash=" + serverLastGlobalHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash");
|
assertEquals(localHashHex, serverLastGlobalHash, op + ": serverLastGlobalHash must match local hash");
|
||||||
|
|
||||||
// обновляем ChainState
|
|
||||||
state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32);
|
state.applyAppendedBlock(globalNumber, lineIndex, lineNumber, hash32);
|
||||||
|
|
||||||
if (test.it.utils.TestConfig.DEBUG()) {
|
if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
|
||||||
TestLog.ok(op + ": state updated");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- json helpers --------------------
|
private static String buildAddBlockJson(String blockchainName, int globalNumber, String prevGlobalHashHex, String blockBytesB64) {
|
||||||
|
String requestId = TestIds.next("addblock");
|
||||||
private static String buildAddBlockJson(String blockchainName,
|
|
||||||
int globalNumber,
|
|
||||||
String prevGlobalHashHex,
|
|
||||||
String blockBytesB64) {
|
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"op": "AddBlock",
|
"op": "AddBlock",
|
||||||
@ -166,13 +127,13 @@ public final class AddBlockSender {
|
|||||||
"blockBytesB64": "%s"
|
"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) {
|
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);
|
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) {
|
private static String base64(byte[] bytes) {
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
package test.it.addBlockUtils;
|
|
||||||
|
|
||||||
// старый класс
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.WebSocket;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.concurrent.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WsJsonRoundtripClient
|
|
||||||
*
|
|
||||||
* Один запрос = одно соединение:
|
|
||||||
* - открыл WS
|
|
||||||
* - отправил JSON (text frame)
|
|
||||||
* - дождался первого ответа TEXT
|
|
||||||
* - закрыл WS
|
|
||||||
*
|
|
||||||
* Здесь requestId НЕ используется вообще (ни для ожидания, ни для логов).
|
|
||||||
* Просто возвращаем первый пришедший ответ как строку JSON.
|
|
||||||
*/
|
|
||||||
public final class WsJsonRoundtripClient {
|
|
||||||
|
|
||||||
private WsJsonRoundtripClient() {}
|
|
||||||
|
|
||||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
|
||||||
|
|
||||||
public static String sendOnce(String wsUri, String requestJson, Duration timeout) {
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
|
||||||
|
|
||||||
CompletableFuture<String> firstMessage = new CompletableFuture<>();
|
|
||||||
|
|
||||||
WebSocket ws = client.newWebSocketBuilder()
|
|
||||||
.connectTimeout(timeout)
|
|
||||||
.buildAsync(URI.create(wsUri), new WebSocket.Listener() {
|
|
||||||
|
|
||||||
private final StringBuilder buf = new StringBuilder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket webSocket) {
|
|
||||||
webSocket.request(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
|
||||||
buf.append(data);
|
|
||||||
if (last) {
|
|
||||||
String msg = buf.toString();
|
|
||||||
buf.setLength(0);
|
|
||||||
if (!firstMessage.isDone()) firstMessage.complete(msg);
|
|
||||||
}
|
|
||||||
webSocket.request(1);
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(WebSocket webSocket, Throwable error) {
|
|
||||||
if (!firstMessage.isDone()) firstMessage.completeExceptionally(error);
|
|
||||||
}
|
|
||||||
}).join();
|
|
||||||
|
|
||||||
// отправляем
|
|
||||||
ws.sendText(requestJson, true).join();
|
|
||||||
|
|
||||||
// ждём
|
|
||||||
String resp;
|
|
||||||
try {
|
|
||||||
resp = firstMessage.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
|
||||||
} catch (Exception e) {
|
|
||||||
try { ws.abort(); } catch (Exception ignored) {}
|
|
||||||
throw new RuntimeException("Timeout/Fail waiting response (single-shot WS). uri=" + wsUri, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// закрываем
|
|
||||||
try {
|
|
||||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS).join();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Утилита: прочитать status из ответа (если нужно быстро проверить). */
|
|
||||||
public static int status(String json) {
|
|
||||||
try {
|
|
||||||
JsonNode root = MAPPER.readTree(json);
|
|
||||||
return root.has("status") ? root.get("status").asInt() : -1;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,40 +5,17 @@ import utils.crypto.Ed25519Util;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/** Builder'ы JSON запросов. Внутри автоматически генерим requestId. */
|
||||||
public final class JsonBuilders {
|
public final class JsonBuilders {
|
||||||
private JsonBuilders(){}
|
private JsonBuilders() {}
|
||||||
|
|
||||||
// =========================
|
// ---------------- AddUser ----------------
|
||||||
// AddUser USER1 (как было)
|
|
||||||
// =========================
|
|
||||||
public static String addUser(String requestId) {
|
|
||||||
return addUserAny(
|
|
||||||
requestId,
|
|
||||||
TestConfig.LOGIN(),
|
|
||||||
TestConfig.BCH_NAME(),
|
|
||||||
TestConfig.LOGIN_PUBKEY_B64(),
|
|
||||||
TestConfig.DEVICE_PUBKEY_B64()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
public static String addUser(String login) {
|
||||||
// AddUser USER2 (новое)
|
String requestId = TestIds.next("adduser");
|
||||||
// =========================
|
String blockchainName = TestConfig.getBlockchainName(login);
|
||||||
public static String addUser2(String requestId) {
|
String loginKeyB64 = TestConfig.blockchainPublicKeyB64(login); // loginKey = blockchain pub
|
||||||
return addUserAny(
|
String deviceKeyB64 = TestConfig.devicePublicKeyB64(login);
|
||||||
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) {
|
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"op": "AddUser",
|
"op": "AddUser",
|
||||||
@ -51,30 +28,30 @@ public final class JsonBuilders {
|
|||||||
"bchLimit": %d
|
"bchLimit": %d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(requestId, login, blockchainName, loginKeyB64, deviceKeyB64, TestConfig.TEST_BCH_LIMIT);
|
||||||
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 """
|
return """
|
||||||
{
|
{
|
||||||
"op": "AuthChallenge",
|
"op": "AuthChallenge",
|
||||||
"requestId": "%s",
|
"requestId": "%s",
|
||||||
"payload": { "login": "%s" }
|
"payload": { "login": "%s" }
|
||||||
}
|
}
|
||||||
""".formatted(requestId, TestConfig.LOGIN());
|
""".formatted(requestId, login);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String createAuthSession(String requestId, String authNonce, String storagePwd) {
|
// ---------------- CreateAuthSession ----------------
|
||||||
long timeMs = System.currentTimeMillis();
|
|
||||||
String sigB64 = signAuthorificated(authNonce, timeMs, TestConfig.DEVICE_PRIV_KEY());
|
|
||||||
|
|
||||||
|
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 """
|
return """
|
||||||
{
|
{
|
||||||
"op": "CreateAuthSession",
|
"op": "CreateAuthSession",
|
||||||
@ -89,7 +66,10 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, storagePwd, timeMs, sigB64, TestConfig.TEST_CLIENT_INFO);
|
""".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 = "";
|
if (signatureB64 == null) signatureB64 = "";
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
@ -103,7 +83,10 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, timeMs, signatureB64);
|
""".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 """
|
return """
|
||||||
{
|
{
|
||||||
"op": "RefreshSession",
|
"op": "RefreshSession",
|
||||||
@ -117,7 +100,10 @@ public final class JsonBuilders {
|
|||||||
""".formatted(requestId, sessionId, sessionPwd, TestConfig.TEST_CLIENT_INFO);
|
""".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 = "";
|
if (signatureB64 == null) signatureB64 = "";
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
@ -143,9 +129,4 @@ public final class JsonBuilders {
|
|||||||
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
byte[] sig = Ed25519Util.sign(preimage, devicePrivKey);
|
||||||
return Base64.getEncoder().encodeToString(sig);
|
return Base64.getEncoder().encodeToString(sig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// старый метод оставим для совместимости
|
|
||||||
public static String signAuthorificated(String authNonce, long timeMs) {
|
|
||||||
return signAuthorificated(authNonce, timeMs, TestConfig.DEVICE_PRIV_KEY());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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";
|
|
||||||
}
|
|
||||||
@ -1,94 +1,121 @@
|
|||||||
package test.it.utils;
|
package test.it.utils;
|
||||||
|
|
||||||
|
import utils.crypto.Ed25519Util;
|
||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Конфиг для IT тестов.
|
* TestConfig — конфиг IT тестов:
|
||||||
*
|
* - 3 пользователя (TestUser1/2/3)
|
||||||
* ДОБАВЛЕНО:
|
* - ключи по login через map (device/solana/blockchain)
|
||||||
* - Второй пользователь (LOGIN2) + его blockchainName и ключи.
|
* - blockchainName = login + "001"
|
||||||
*
|
*
|
||||||
* Важно:
|
* Важно:
|
||||||
* - Имена/ключи вычисляются детерминированно из логина (см. ItRunContext).
|
* - privateKey = Ed25519Util.generatePrivateKeyFromString(login) (sha256, 32 bytes)
|
||||||
|
* - publicKey = Ed25519Util.derivePublicKey(privateKey)
|
||||||
|
* - пока device/solana/blockchain ключи одинаковые (один seed на login)
|
||||||
*/
|
*/
|
||||||
public final class TestConfig {
|
public final class TestConfig {
|
||||||
|
|
||||||
private TestConfig() {}
|
private TestConfig() {}
|
||||||
|
|
||||||
// Твой WS URI
|
|
||||||
public static final String WS_URI = "ws://localhost:7070/ws";
|
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 long TEST_BCH_LIMIT = 50_000_000L;
|
||||||
|
|
||||||
// Любая строка клиента (для логов)
|
|
||||||
public static final String TEST_CLIENT_INFO = "it-tests";
|
public static final String TEST_CLIENT_INFO = "it-tests";
|
||||||
|
|
||||||
/** DEBUG-режим: подробные логи (по умолчанию true, как у тебя). */
|
|
||||||
public static boolean DEBUG() {
|
public static boolean DEBUG() {
|
||||||
return Boolean.parseBoolean(System.getProperty("it.debug", "true"));
|
return Boolean.parseBoolean(System.getProperty("it.debug", "true"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
// 3 users
|
||||||
// USER #1
|
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.login1", DEFAULT_LOGIN1); }
|
||||||
public static String LOGIN() {
|
public static String LOGIN2() { return System.getProperty("it.login2", DEFAULT_LOGIN2); }
|
||||||
return System.getProperty("it.login", DEFAULT_LOGIN);
|
public static String LOGIN3() { return System.getProperty("it.login3", DEFAULT_LOGIN3); }
|
||||||
}
|
|
||||||
|
|
||||||
/** Суффикс для имени блокчейна (user1). */
|
|
||||||
public static String BCH_SUFFIX_3() {
|
public static String BCH_SUFFIX_3() {
|
||||||
return System.getProperty("it.bchSuffix", DEFAULT_BCH_SUFFIX_3);
|
return System.getProperty("it.bchSuffix", DEFAULT_BCH_SUFFIX_3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** blockchainName по правилу: login + суффикс (user1). */
|
public static String getBlockchainName(String login) {
|
||||||
public static String BCH_NAME() {
|
if (login == null) throw new IllegalArgumentException("login is null");
|
||||||
return LOGIN() + BCH_SUFFIX_3();
|
return login + BCH_SUFFIX_3();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] LOGIN_PRIV_KEY() { return ItRunContext.login1PrivKey(); }
|
// ============ key maps ============
|
||||||
public static byte[] LOGIN_PUB_KEY() { return ItRunContext.login1PubKey(); }
|
private static final Map<String, byte[]> devicePriv = new ConcurrentHashMap<>();
|
||||||
public static byte[] DEVICE_PRIV_KEY(){ return ItRunContext.device1PrivKey(); }
|
private static final Map<String, byte[]> devicePub = new ConcurrentHashMap<>();
|
||||||
public static byte[] DEVICE_PUB_KEY() { return ItRunContext.device1PubKey(); }
|
|
||||||
|
|
||||||
public static String LOGIN_PUBKEY_B64() { return Base64.getEncoder().encodeToString(LOGIN_PUB_KEY()); }
|
private static final Map<String, byte[]> solanaPriv = new ConcurrentHashMap<>();
|
||||||
public static String DEVICE_PUBKEY_B64() { return Base64.getEncoder().encodeToString(DEVICE_PUB_KEY()); }
|
private static final Map<String, byte[]> solanaPub = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// =========================
|
private static final Map<String, byte[]> bchPriv = new ConcurrentHashMap<>();
|
||||||
// USER #2
|
private static final Map<String, byte[]> bchPub = new ConcurrentHashMap<>();
|
||||||
// =========================
|
|
||||||
|
|
||||||
/** login второго пользователя. Можно переопределить -Dit.login2=... */
|
static {
|
||||||
public static String LOGIN2() {
|
initUserKeys(LOGIN());
|
||||||
return System.getProperty("it.login2", DEFAULT_LOGIN2);
|
initUserKeys(LOGIN2());
|
||||||
|
initUserKeys(LOGIN3());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** blockchainName второго: login2 + тот же суффикс. */
|
private static void initUserKeys(String login) {
|
||||||
public static String BCH_NAME2() {
|
byte[] priv = Ed25519Util.generatePrivateKeyFromString(login); // sha256(login) => 32 bytes
|
||||||
return LOGIN2() + BCH_SUFFIX_3();
|
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(); }
|
// ============ requested getters (with your names) ============
|
||||||
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(); }
|
|
||||||
|
|
||||||
public static String LOGIN2_PUBKEY_B64() { return Base64.getEncoder().encodeToString(LOGIN2_PUB_KEY()); }
|
public static byte[] getDevicePrivatKey(String login) { return cloneOrThrow(devicePriv.get(login), "devicePriv", login); }
|
||||||
public static String DEVICE2_PUBKEY_B64() { return Base64.getEncoder().encodeToString(DEVICE2_PUB_KEY()); }
|
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() {
|
public static String fakeStoragePwd() {
|
||||||
return "pwd-" + System.nanoTime();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
15
src/test/java/test/it/utils/TestIds.java
Normal file
15
src/test/java/test/it/utils/TestIds.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package test.it.utils;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
/** Генератор уникальных requestId для IT тестов (в пределах одной JVM). */
|
||||||
|
public final class TestIds {
|
||||||
|
private static final AtomicLong SEQ = new AtomicLong(0);
|
||||||
|
|
||||||
|
private TestIds() {}
|
||||||
|
|
||||||
|
public static String next(String prefix) {
|
||||||
|
long n = SEQ.incrementAndGet();
|
||||||
|
return "it-" + (prefix == null ? "req" : prefix) + "-" + n;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,38 +3,30 @@ package test.it.utils;
|
|||||||
/**
|
/**
|
||||||
* TestLog — единое место для:
|
* TestLog — единое место для:
|
||||||
* - ANSI цветов
|
* - ANSI цветов
|
||||||
* - стандартных красивых сообщений (title/line/step/send/recv)
|
* - стандартных сообщений (title/step/send/recv)
|
||||||
|
* - PASS/FAIL строк и окраски
|
||||||
*
|
*
|
||||||
* РЕЖИМЫ:
|
* Режим:
|
||||||
* - it.debug=false (по умолчанию):
|
* - it.debug=false: печатаем минимум (без JSON)
|
||||||
* печатаем ТОЛЬКО итог: PASS/FAIL по каждому тесту
|
* - it.debug=true: печатаем JSON отправка/ответ + заголовки шагов
|
||||||
* - it.debug=true:
|
|
||||||
* печатаем всё: ожидания, отправка/ответ (JSON), промежуточные проверки
|
|
||||||
*/
|
*/
|
||||||
public final class TestLog {
|
public final class TestLog {
|
||||||
private TestLog() {}
|
private TestLog() {}
|
||||||
|
|
||||||
// ============================
|
|
||||||
// DEBUG SWITCH
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
public static final boolean DEBUG = TestConfig.DEBUG();
|
public static final boolean DEBUG = TestConfig.DEBUG();
|
||||||
|
|
||||||
// ============================
|
// ANSI COLORS (ТОЛЬКО ТУТ)
|
||||||
// ANSI COLORS
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
public static final String R = "\u001B[0m";
|
public static final String R = "\u001B[0m";
|
||||||
public static final String G = "\u001B[32m";
|
public static final String G = "\u001B[32m";
|
||||||
public static final String Y = "\u001B[33m";
|
public static final String Y = "\u001B[33m";
|
||||||
public static final String RED = "\u001B[31m";
|
public static final String RED = "\u001B[31m";
|
||||||
public static final String C = "\u001B[36m";
|
public static final String C = "\u001B[36m";
|
||||||
|
|
||||||
// ============================
|
public static String green(String s) { return G + s + R; }
|
||||||
// BASIC OUTPUT
|
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) {
|
public static void info(String s) {
|
||||||
if (DEBUG) System.out.println(s);
|
if (DEBUG) System.out.println(s);
|
||||||
}
|
}
|
||||||
@ -44,7 +36,6 @@ public final class TestLog {
|
|||||||
System.out.println(C + "------------------------------------------------------------" + R);
|
System.out.println(C + "------------------------------------------------------------" + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Короткое заглавие (только DEBUG). */
|
|
||||||
public static void title(String s) {
|
public static void title(String s) {
|
||||||
if (!DEBUG) return;
|
if (!DEBUG) return;
|
||||||
System.out.println(C + "\n============================================================" + R);
|
System.out.println(C + "\n============================================================" + R);
|
||||||
@ -52,7 +43,6 @@ public final class TestLog {
|
|||||||
System.out.println(C + "============================================================\n" + R);
|
System.out.println(C + "============================================================\n" + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Длинное заглавие (только DEBUG). */
|
|
||||||
public static void titleBlock(String multiLineText) {
|
public static void titleBlock(String multiLineText) {
|
||||||
if (!DEBUG) return;
|
if (!DEBUG) return;
|
||||||
System.out.println(C + "\n============================================================" + R);
|
System.out.println(C + "\n============================================================" + R);
|
||||||
@ -60,29 +50,23 @@ public final class TestLog {
|
|||||||
System.out.println(C + "============================================================\n" + R);
|
System.out.println(C + "============================================================\n" + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Заголовок шага (только DEBUG). */
|
|
||||||
public static void stepTitle(String s) {
|
public static void stepTitle(String s) {
|
||||||
if (!DEBUG) return;
|
if (!DEBUG) return;
|
||||||
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
|
System.out.println(C + "\n-------------------- " + s + " --------------------" + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Промежуточное ОК (только DEBUG). */
|
/** OK (печатаем ВСЕГДА, чтобы было видно зелёное прохождение шагов). */
|
||||||
public static void ok(String s) {
|
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);
|
System.out.println(G + "✅ " + s + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** WARN (только DEBUG). */
|
||||||
public static void warn(String s) {
|
public static void warn(String s) {
|
||||||
if (!DEBUG) return;
|
if (!DEBUG) return;
|
||||||
System.out.println(Y + "⚠️ " + s + R);
|
System.out.println(Y + "⚠️ " + s + R);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FAIL (печатается ВСЕГДА). */
|
/** FAIL (печатаем ВСЕГДА). */
|
||||||
public static void boom(String s) {
|
public static void boom(String s) {
|
||||||
System.out.println(RED + "****************************************************************" + R);
|
System.out.println(RED + "****************************************************************" + R);
|
||||||
System.out.println(RED + "❌ " + s + R);
|
System.out.println(RED + "❌ " + s + R);
|
||||||
@ -102,28 +86,4 @@ public final class TestLog {
|
|||||||
System.out.println(json);
|
System.out.println(json);
|
||||||
line();
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
46
src/test/java/test/it/utils/TestResult.java
Normal file
46
src/test/java/test/it/utils/TestResult.java
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package test.it.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestResult — накопитель результатов внутри одного теста:
|
||||||
|
* - ok(...) печатает зелёным
|
||||||
|
* - fail(...) печатает красным и добавляет в итоговую строку
|
||||||
|
* - summaryLine() возвращает одну строку: PASS/FAIL + детали
|
||||||
|
*/
|
||||||
|
public final class TestResult {
|
||||||
|
|
||||||
|
private final String testName;
|
||||||
|
private final List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
public TestResult(String testName) {
|
||||||
|
this.testName = testName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ok(String msg) {
|
||||||
|
TestLog.ok(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fail(String msg) {
|
||||||
|
errors.add(msg);
|
||||||
|
TestLog.boom(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOk() {
|
||||||
|
return errors.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String summaryLine() {
|
||||||
|
if (errors.isEmpty()) {
|
||||||
|
return TestLog.green("PASS: " + testName + " — OK");
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(TestLog.red("FAIL: ")).append(testName).append(" — ").append(errors.size()).append(" ошибок: ");
|
||||||
|
for (int i = 0; i < errors.size(); i++) {
|
||||||
|
if (i > 0) sb.append(" | ");
|
||||||
|
sb.append(errors.get(i));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/test/java/test/it/utils/WsSession.java
Normal file
58
src/test/java/test/it/utils/WsSession.java
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package test.it.utils;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WsSession — одно WS соединение на много запросов.
|
||||||
|
*
|
||||||
|
* Использование в тесте:
|
||||||
|
* try (WsSession ws = WsSession.open()) {
|
||||||
|
* String resp = ws.call("AuthChallenge", JsonBuilders.authChallenge(login), t);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public final class WsSession implements AutoCloseable {
|
||||||
|
|
||||||
|
private static final ObjectMapper M = new ObjectMapper();
|
||||||
|
|
||||||
|
private final WsTestClient client;
|
||||||
|
|
||||||
|
private WsSession(WsTestClient client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WsSession open() {
|
||||||
|
return new WsSession(new WsTestClient(TestConfig.WS_URI));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Отправить JSON (в котором уже есть requestId) и получить JSON ответ строкой. */
|
||||||
|
public String call(String op, String requestJson, Duration timeout) {
|
||||||
|
String requestId = extractRequestId(requestJson);
|
||||||
|
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestJson must contain requestId: " + requestJson);
|
||||||
|
|
||||||
|
if (TestConfig.DEBUG()) TestLog.send(op, requestJson);
|
||||||
|
|
||||||
|
String resp = client.request(requestId, requestJson, timeout);
|
||||||
|
|
||||||
|
if (TestConfig.DEBUG()) TestLog.recv(op, resp);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractRequestId(String json) {
|
||||||
|
try {
|
||||||
|
JsonNode root = M.readTree(json);
|
||||||
|
JsonNode id = root.get("requestId");
|
||||||
|
return (id == null || id.isNull()) ? null : id.asText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user