From c515d5287e55f6e9d14dbd8a33058b7150bb01d2525f49732a4df1712e726f6c Mon Sep 17 00:00:00 2001 From: AidarKC Date: Tue, 23 Dec 2025 13:51:20 +0300 Subject: [PATCH] =?UTF-8?q?23=2012=2025=20=D0=A1=D0=B5=D1=81=D1=81=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=8E=D1=82=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F=D1=8E?= =?UTF-8?q?=D1=82=D1=81=D1=8F.=20=D0=9F=D0=BB=D1=8E=D1=81=20=D1=81=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=D0=BD=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/shine/db/dao/SolanaUsersDAO.java | 26 + .../tempToTest/Net_AddUser_Handler.java | 12 + .../auth/AuthWebSocketIntegrationTest.java | 1048 ++++++++--------- src/test/java/test/it/AddUserIT.java | 19 +- src/test/java/test/it/JsonParsers.java | 21 + .../java/test/it/WsTestClient.java | 0 6 files changed, 598 insertions(+), 528 deletions(-) rename src/{main => test}/java/test/it/WsTestClient.java (100%) diff --git a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java index c7843ba..a8c09ad 100644 --- a/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/SolanaUsersDAO.java @@ -66,6 +66,32 @@ public final class SolanaUsersDAO { } } + // -------------------- EXISTS -------------------- + + /** Проверка существования по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */ + public boolean existsByLogin(Connection c, String login) throws SQLException { + String sql = """ + SELECT 1 + FROM solana_users + WHERE LOWER(login) = LOWER(?) + LIMIT 1 + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, login); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } + + /** Проверка существования по login (case-insensitive) без внешнего соединения. Сам открывает/закрывает. */ + public boolean existsByLogin(String login) throws SQLException { + try (Connection c = db.getConnection()) { + return existsByLogin(c, login); + } + } + // -------------------- SELECT -------------------- /** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */ diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java index fe092da..be150ff 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_AddUser_Handler.java @@ -45,6 +45,18 @@ public class Net_AddUser_Handler implements JsonMessageHandler { try { SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); + // ✅ Новая логика: если пользователь уже есть — возвращаем понятную ошибку + SolanaUserEntry exists = dao.getByLogin(req.getLogin()); + if (exists != null) { + log.info("⚠️ AddUser: user already exists, login={}", req.getLogin()); + return NetExceptionResponseFactory.error( + req, + 409, // CONFLICT + "USER_ALREADY_EXISTS", + "Пользователь с таким login уже существует в системе" + ); + } + SolanaUserEntry user = new SolanaUserEntry( req.getLogin(), req.getBlockchainName(), diff --git a/src/test/java/shine/auth/AuthWebSocketIntegrationTest.java b/src/test/java/shine/auth/AuthWebSocketIntegrationTest.java index 41f1c51..11b25c7 100644 --- a/src/test/java/shine/auth/AuthWebSocketIntegrationTest.java +++ b/src/test/java/shine/auth/AuthWebSocketIntegrationTest.java @@ -1,524 +1,524 @@ -package shine.auth; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import utils.crypto.Ed25519Util; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.WebSocket; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Base64; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; - -/** - * Интеграционные тесты авторификации по JSON-протоколу через WebSocket. - * - * Требуется запущенный сервер на: - * ws://localhost:7070/ws - * - * Операции: - * - AddUser - * - AuthChallenge - * - CreateAuthSession - * - RefreshSession - * - CloseActiveSession - * - (позже) ListSessions - */ -public class AuthWebSocketIntegrationTest { - - private static final String WS_URI = "ws://localhost:7070/ws"; - private static final ObjectMapper JSON = new ObjectMapper(); - private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); - - /** Таймаут ожидания ответа от сервера в каждом helper-е (секунд). */ - private static final long WS_TIMEOUT_SEC = 15; - - // ======================================================================== - // DTO - // ======================================================================== - - /** Тестовый пользователь. */ - private static class TestUser { - String login; - long loginId; - long bchId; - - byte[] loginPriv; - byte[] devicePriv; - - String loginPubB64; - String devicePubB64; - } - - /** Токены созданной сессии. */ - private static class SessionTokens { - String sessionId; - String sessionPwd; - String storagePwd; - } - - // ======================================================================== - // ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ - // ======================================================================== - - /** - * Создать тестового пользователя с уникальным логином и ключами Ed25519. - */ - private TestUser createRandomUser() { - TestUser u = new TestUser(); - - long ts = System.currentTimeMillis(); - u.login = "anya_test_auth_scenario_" + ts; - u.loginId = ts; // просто уникальный long - u.bchId = ts % 1_000_000; // что-нибудь псевдоуникальное - - // Генерируем ключи детерминированно от логина — чтобы AddUser и Auth совпадали - u.loginPriv = Ed25519Util.generatePrivateKeyFromString("login-key-" + u.login); - u.devicePriv = Ed25519Util.generatePrivateKeyFromString("device-key-" + u.login); - - byte[] loginPub = Ed25519Util.derivePublicKey(u.loginPriv); - byte[] devicePub = Ed25519Util.derivePublicKey(u.devicePriv); - - u.loginPubB64 = Ed25519Util.keyToBase64(loginPub); - u.devicePubB64 = Ed25519Util.keyToBase64(devicePub); - - return u; - } - - /** - * Универсальный helper для одношаговой операции (AddUser, RefreshSession и т.п.). - * Открывает WebSocket, отправляет JSON, ждёт один ответ. - * - * @param requestJson JSON-запрос - * @param label ярлык для логов - * @return JsonNode root ответа - */ - private JsonNode callSingleJsonOp(String requestJson, String label) throws Exception { - System.out.println(); - System.out.println("===== " + label + " ====="); - System.out.println("📤 Request:"); - System.out.println(requestJson); - - CompletableFuture future = new CompletableFuture<>(); - - HTTP_CLIENT.newWebSocketBuilder() - .connectTimeout(Duration.ofSeconds(WS_TIMEOUT_SEC)) - .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { - - @Override - public void onOpen(WebSocket webSocket) { - webSocket.request(1); - webSocket.sendText(requestJson, true); - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence data, - boolean last) { - String msg = data.toString(); - System.out.println("📥 Response:"); - System.out.println(msg); - System.out.println("----------------------------------------"); - try { - JsonNode root = JSON.readTree(msg); - future.complete(root); - } catch (Exception e) { - future.completeExceptionally(e); - } finally { - try { - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done"); - } catch (Exception ignored) { - } - } - webSocket.request(1); - return CompletableFuture.completedFuture(null); - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - if (!future.isDone()) { - future.completeExceptionally(error); - } - } - - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - if (!future.isDone()) { - future.completeExceptionally(new IllegalStateException( - "WebSocket closed before response. code=" + statusCode + ", reason=" + reason)); - } - return CompletableFuture.completedFuture(null); - } - }); - - return future.get(WS_TIMEOUT_SEC, TimeUnit.SECONDS); - } - - /** - * Helper для двухшагового сценария: - * 1) AuthChallenge - * 2) CreateAuthSession - */ - private SessionTokens createSessionForUser(TestUser user, String logPrefix) throws Exception { - System.out.println(); - System.out.println("===== " + logPrefix + " createSessionForUser: " + user.login + " ====="); - - CompletableFuture resultFuture = new CompletableFuture<>(); - - HTTP_CLIENT.newWebSocketBuilder() - .connectTimeout(Duration.ofSeconds(WS_TIMEOUT_SEC)) - .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { - - int step = 0; - WebSocket ws; - String currentAuthNonce; - - @Override - public void onOpen(WebSocket webSocket) { - this.ws = webSocket; - webSocket.request(1); - - // Шаг 1: AuthChallenge - String reqId = "auth-challenge-" + UUID.randomUUID(); - String json = """ - { - "op": "AuthChallenge", - "requestId": "%s", - "payload": { - "login": "%s" - } - } - """.formatted(reqId, user.login); - - System.out.println(); - System.out.println(logPrefix + " 📤 [STEP1 AuthChallenge] Request:"); - System.out.println(json); - webSocket.sendText(json, true); - } - - @Override - public CompletionStage onText(WebSocket webSocket, - CharSequence data, - boolean last) { - String msg = data.toString(); - System.out.println(); - System.out.println(logPrefix + " 📥 Incoming message (step " + step + "):"); - System.out.println(msg); - System.out.println("--------------------------------------------------"); - - try { - if (step == 0) { - // Ответ на AuthChallenge - JsonNode root = JSON.readTree(msg); - int status = root.path("status").asInt(); - if (status != 200) { - String code = root.path("payload").path("code").asText(); - String message = root.path("payload").path("message").asText(); - throw new IllegalStateException( - "AuthChallenge failed: status=" + status + - ", code=" + code + - ", message=" + message); - } - - currentAuthNonce = root.path("payload").path("authNonce").asText(null); - if (currentAuthNonce == null || currentAuthNonce.isBlank()) { - throw new IllegalStateException("AuthChallenge: empty authNonce in response"); - } - System.out.println(logPrefix + " 🔑 authNonce = " + currentAuthNonce); - - // Шаг 2: CreateAuthSession - long timeMs = System.currentTimeMillis(); - String signatureB64 = buildAuthorificatedSignature( - user.devicePriv, - currentAuthNonce, - timeMs - ); - String storagePwd = generateFakeStoragePwd(); - - String reqId2 = "create-session-" + UUID.randomUUID(); - String json2 = """ - { - "op": "CreateAuthSession", - "requestId": "%s", - "payload": { - "storagePwd": "%s", - "timeMs": %d, - "signatureB64": "%s", - "clientInfo": "AuthTestClient/1.0" - } - } - """.formatted( - reqId2, - storagePwd, - timeMs, - signatureB64 - ); - - System.out.println(); - System.out.println(logPrefix + " 📤 [STEP2 CreateAuthSession] Request:"); - System.out.println(json2); - - step = 1; - webSocket.sendText(json2, true); - } else if (step == 1) { - // Ответ на CreateAuthSession - JsonNode root = JSON.readTree(msg); - int status = root.path("status").asInt(); - if (status != 200) { - String code = root.path("payload").path("code").asText(); - String message = root.path("payload").path("message").asText(); - throw new IllegalStateException( - "CreateAuthSession failed: status=" + status + - ", code=" + code + - ", message=" + message); - } - - String sessionId = root.path("payload").path("sessionId").asText(null); - String sessionPwd = root.path("payload").path("sessionPwd").asText(null); - if (sessionId == null || sessionPwd == null) { - throw new IllegalStateException("CreateAuthSession: sessionId or sessionPwd is null"); - } - - SessionTokens tokens = new SessionTokens(); - tokens.sessionId = sessionId; - tokens.sessionPwd = sessionPwd; - tokens.storagePwd = null; // мы знаем, какой отправляли, при желании можно сохранить - - System.out.println(logPrefix + " 🆔 sessionId = " + sessionId); - System.out.println(logPrefix + " 🔐 sessionPwd = " + sessionPwd); - - resultFuture.complete(tokens); - - try { - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session created"); - } catch (Exception ignored) { - } - } else { - // Лишние сообщения — считаем ошибкой - throw new IllegalStateException("Unexpected extra message on step=" + step); - } - } catch (Exception ex) { - if (!resultFuture.isDone()) { - resultFuture.completeExceptionally(ex); - } - try { - webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "error in test"); - } catch (Exception ignored) { - } - } finally { - webSocket.request(1); - } - - return CompletableFuture.completedFuture(null); - } - - @Override - public void onError(WebSocket webSocket, Throwable error) { - System.out.println(logPrefix + " ❌ WebSocket error: " + error.getMessage()); - if (!resultFuture.isDone()) { - resultFuture.completeExceptionally(error); - } - } - - @Override - public CompletionStage onClose(WebSocket webSocket, - int statusCode, - String reason) { - System.out.println(logPrefix + " 🔚 WebSocket closed. code=" + statusCode + ", reason=" + reason); - if (!resultFuture.isDone()) { - resultFuture.completeExceptionally( - new IllegalStateException("Closed before session tokens were received")); - } - return CompletableFuture.completedFuture(null); - } - }); - - // ждём результат или ошибку - return resultFuture.get(WS_TIMEOUT_SEC, TimeUnit.SECONDS); - } - - /** - * Собрать подпись над строкой "AUTHORIFICATED:" + timeMs + authNonce - * приватным ключом устройства. - */ - private String buildAuthorificatedSignature(byte[] devicePrivKey, - String authNonce, - long timeMs) { - String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; - byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); - byte[] sig = Ed25519Util.sign(preimage, devicePrivKey); - return Base64.getEncoder().encodeToString(sig); - } - - /** Просто base64 от 32 байт 1..32 — для storagePwd. */ - private String generateFakeStoragePwd() { - byte[] data = new byte[32]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) (i + 1); - } - return Base64.getEncoder().encodeToString(data); - } - - // ======================================================================== - // ТЕСТЫ - // ======================================================================== - - /** - * 1) Регистрируем пользователя через AddUser - */ - @Test - void addUser_shouldSucceed() throws Exception { - TestUser user = createRandomUser(); - - String reqId = "add-" + UUID.randomUUID(); - String json = """ - { - "op": "AddUser", - "requestId": "%s", - "payload": { - "login": "%s", - "loginId": %d, - "bchId": %d, - "loginKey": "%s", - "deviceKey": "%s", - "bchLimit": 1000000 - } - } - """.formatted( - reqId, - user.login, - user.loginId, - user.bchId, - user.loginPubB64, - user.devicePubB64 - ); - - JsonNode resp = callSingleJsonOp(json, "TEST addUser_shouldSucceed"); - int status = resp.path("status").asInt(); - String code = resp.path("payload").path("code").asText(null); - - Assertions.assertEquals( - 200, - status, - "Ожидался status=200 при AddUser, но сервер вернул: status=" + status + ", code=" + code - ); - - System.out.println("✅ [TEST] AddUser прошёл успешно для login=" + user.login); - } - - /** - * 2) Создать пользователя и сразу сделать CreateAuthSession (AuthChallenge + CreateAuthSession). - */ - @Test - void createSession_flow_shouldReturnSessionIdAndPwd() throws Exception { - TestUser user = createRandomUser(); - - // Сначала регистрируем пользователя - { - String reqId = "add-" + UUID.randomUUID(); - String json = """ - { - "op": "AddUser", - "requestId": "%s", - "payload": { - "login": "%s", - "loginId": %d, - "bchId": %d, - "loginKey": "%s", - "deviceKey": "%s", - "bchLimit": 1000000 - } - } - """.formatted( - reqId, - user.login, - user.loginId, - user.bchId, - user.loginPubB64, - user.devicePubB64 - ); - JsonNode resp = callSingleJsonOp(json, "TEST createSession_flow / AddUser"); - int status = resp.path("status").asInt(); - Assertions.assertEquals(200, status, "AddUser должен вернуть 200"); - } - - SessionTokens tokens = createSessionForUser(user, "[createSession_flow]"); - Assertions.assertNotNull(tokens.sessionId, "sessionId не должен быть null"); - Assertions.assertNotNull(tokens.sessionPwd, "sessionPwd не должен быть null"); - - System.out.println("✅ [TEST] Сессия успешно создана: sessionId=" + tokens.sessionId); - } - - /** - * 3) Сценарий с двумя сессиями (упрощённая версия): - * - создаём пользователя - * - создаём первую сессию - * - создаём вторую сессию - * - убеждаемся, что sessionId разные - * - * Полный цикл с ListSessions / CloseActiveSession можно будет нарастить поверх - * этого теста, когда добавим JSON-обработчик Net_ListSessions. - */ - @Test - void fullTwoSessionLifecycleScenario() throws Exception { - TestUser user = createRandomUser(); - - // 1) AddUser - String reqId = "add-" + UUID.randomUUID(); - String jsonAdd = """ - { - "op": "AddUser", - "requestId": "%s", - "payload": { - "login": "%s", - "loginId": %d, - "bchId": %d, - "loginKey": "%s", - "deviceKey": "%s", - "bchLimit": 1000000 - } - } - """.formatted( - reqId, - user.login, - user.loginId, - user.bchId, - user.loginPubB64, - user.devicePubB64 - ); - - JsonNode addResp = callSingleJsonOp(jsonAdd, "SCENARIO fullTwoSessionLifecycle / AddUser"); - int addStatus = addResp.path("status").asInt(); - Assertions.assertEquals(200, addStatus, "AddUser должен вернуть 200"); - - System.out.println("✅ [SC] Пользователь создан: " + user.login); - - // 2) Первая сессия - SessionTokens s1 = createSessionForUser(user, "[SC S1]"); - // 3) Вторая сессия - SessionTokens s2 = createSessionForUser(user, "[SC S2]"); - - Assertions.assertNotEquals( - s1.sessionId, - s2.sessionId, - "Первая и вторая сессия должны иметь разные sessionId" - ); - - System.out.println(); - System.out.println("✅ [SC] Полный сценарий (упрощённый) успешно отработал:"); - System.out.println(" session1 = " + s1.sessionId); - System.out.println(" session2 = " + s2.sessionId); - - System.out.println("\nℹ️ ListSessions / CloseActiveSession / повторные проверки можно будет добавить, " + - "когда JSON-обработчик Net_ListSessions будет реализован на сервере."); - } -} +//package shine.auth; +// +//import com.fasterxml.jackson.databind.JsonNode; +//import com.fasterxml.jackson.databind.ObjectMapper; +//import org.junit.jupiter.api.Assertions; +//import org.junit.jupiter.api.Test; +//import utils.crypto.Ed25519Util; +// +//import java.net.URI; +//import java.net.http.HttpClient; +//import java.net.http.WebSocket; +//import java.nio.charset.StandardCharsets; +//import java.time.Duration; +//import java.util.Base64; +//import java.util.UUID; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.CompletionStage; +//import java.util.concurrent.TimeUnit; +// +///** +// * Интеграционные тесты авторификации по JSON-протоколу через WebSocket. +// * +// * Требуется запущенный сервер на: +// * ws://localhost:7070/ws +// * +// * Операции: +// * - AddUser +// * - AuthChallenge +// * - CreateAuthSession +// * - RefreshSession +// * - CloseActiveSession +// * - (позже) ListSessions +// */ +//public class AuthWebSocketIntegrationTest { +// +// private static final String WS_URI = "ws://localhost:7070/ws"; +// private static final ObjectMapper JSON = new ObjectMapper(); +// private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); +// +// /** Таймаут ожидания ответа от сервера в каждом helper-е (секунд). */ +// private static final long WS_TIMEOUT_SEC = 15; +// +// // ======================================================================== +// // DTO +// // ======================================================================== +// +// /** Тестовый пользователь. */ +// private static class TestUser { +// String login; +// long loginId; +// long bchId; +// +// byte[] loginPriv; +// byte[] devicePriv; +// +// String loginPubB64; +// String devicePubB64; +// } +// +// /** Токены созданной сессии. */ +// private static class SessionTokens { +// String sessionId; +// String sessionPwd; +// String storagePwd; +// } +// +// // ======================================================================== +// // ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ +// // ======================================================================== +// +// /** +// * Создать тестового пользователя с уникальным логином и ключами Ed25519. +// */ +// private TestUser createRandomUser() { +// TestUser u = new TestUser(); +// +// long ts = System.currentTimeMillis(); +// u.login = "anya_test_auth_scenario_" + ts; +// u.loginId = ts; // просто уникальный long +// u.bchId = ts % 1_000_000; // что-нибудь псевдоуникальное +// +// // Генерируем ключи детерминированно от логина — чтобы AddUser и Auth совпадали +// u.loginPriv = Ed25519Util.generatePrivateKeyFromString("login-key-" + u.login); +// u.devicePriv = Ed25519Util.generatePrivateKeyFromString("device-key-" + u.login); +// +// byte[] loginPub = Ed25519Util.derivePublicKey(u.loginPriv); +// byte[] devicePub = Ed25519Util.derivePublicKey(u.devicePriv); +// +// u.loginPubB64 = Ed25519Util.keyToBase64(loginPub); +// u.devicePubB64 = Ed25519Util.keyToBase64(devicePub); +// +// return u; +// } +// +// /** +// * Универсальный helper для одношаговой операции (AddUser, RefreshSession и т.п.). +// * Открывает WebSocket, отправляет JSON, ждёт один ответ. +// * +// * @param requestJson JSON-запрос +// * @param label ярлык для логов +// * @return JsonNode root ответа +// */ +// private JsonNode callSingleJsonOp(String requestJson, String label) throws Exception { +// System.out.println(); +// System.out.println("===== " + label + " ====="); +// System.out.println("📤 Request:"); +// System.out.println(requestJson); +// +// CompletableFuture future = new CompletableFuture<>(); +// +// HTTP_CLIENT.newWebSocketBuilder() +// .connectTimeout(Duration.ofSeconds(WS_TIMEOUT_SEC)) +// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { +// +// @Override +// public void onOpen(WebSocket webSocket) { +// webSocket.request(1); +// webSocket.sendText(requestJson, true); +// } +// +// @Override +// public CompletionStage onText(WebSocket webSocket, +// CharSequence data, +// boolean last) { +// String msg = data.toString(); +// System.out.println("📥 Response:"); +// System.out.println(msg); +// System.out.println("----------------------------------------"); +// try { +// JsonNode root = JSON.readTree(msg); +// future.complete(root); +// } catch (Exception e) { +// future.completeExceptionally(e); +// } finally { +// try { +// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "test done"); +// } catch (Exception ignored) { +// } +// } +// webSocket.request(1); +// return CompletableFuture.completedFuture(null); +// } +// +// @Override +// public void onError(WebSocket webSocket, Throwable error) { +// if (!future.isDone()) { +// future.completeExceptionally(error); +// } +// } +// +// @Override +// public CompletionStage onClose(WebSocket webSocket, +// int statusCode, +// String reason) { +// if (!future.isDone()) { +// future.completeExceptionally(new IllegalStateException( +// "WebSocket closed before response. code=" + statusCode + ", reason=" + reason)); +// } +// return CompletableFuture.completedFuture(null); +// } +// }); +// +// return future.get(WS_TIMEOUT_SEC, TimeUnit.SECONDS); +// } +// +// /** +// * Helper для двухшагового сценария: +// * 1) AuthChallenge +// * 2) CreateAuthSession +// */ +// private SessionTokens createSessionForUser(TestUser user, String logPrefix) throws Exception { +// System.out.println(); +// System.out.println("===== " + logPrefix + " createSessionForUser: " + user.login + " ====="); +// +// CompletableFuture resultFuture = new CompletableFuture<>(); +// +// HTTP_CLIENT.newWebSocketBuilder() +// .connectTimeout(Duration.ofSeconds(WS_TIMEOUT_SEC)) +// .buildAsync(URI.create(WS_URI), new WebSocket.Listener() { +// +// int step = 0; +// WebSocket ws; +// String currentAuthNonce; +// +// @Override +// public void onOpen(WebSocket webSocket) { +// this.ws = webSocket; +// webSocket.request(1); +// +// // Шаг 1: AuthChallenge +// String reqId = "auth-challenge-" + UUID.randomUUID(); +// String json = """ +// { +// "op": "AuthChallenge", +// "requestId": "%s", +// "payload": { +// "login": "%s" +// } +// } +// """.formatted(reqId, user.login); +// +// System.out.println(); +// System.out.println(logPrefix + " 📤 [STEP1 AuthChallenge] Request:"); +// System.out.println(json); +// webSocket.sendText(json, true); +// } +// +// @Override +// public CompletionStage onText(WebSocket webSocket, +// CharSequence data, +// boolean last) { +// String msg = data.toString(); +// System.out.println(); +// System.out.println(logPrefix + " 📥 Incoming message (step " + step + "):"); +// System.out.println(msg); +// System.out.println("--------------------------------------------------"); +// +// try { +// if (step == 0) { +// // Ответ на AuthChallenge +// JsonNode root = JSON.readTree(msg); +// int status = root.path("status").asInt(); +// if (status != 200) { +// String code = root.path("payload").path("code").asText(); +// String message = root.path("payload").path("message").asText(); +// throw new IllegalStateException( +// "AuthChallenge failed: status=" + status + +// ", code=" + code + +// ", message=" + message); +// } +// +// currentAuthNonce = root.path("payload").path("authNonce").asText(null); +// if (currentAuthNonce == null || currentAuthNonce.isBlank()) { +// throw new IllegalStateException("AuthChallenge: empty authNonce in response"); +// } +// System.out.println(logPrefix + " 🔑 authNonce = " + currentAuthNonce); +// +// // Шаг 2: CreateAuthSession +// long timeMs = System.currentTimeMillis(); +// String signatureB64 = buildAuthorificatedSignature( +// user.devicePriv, +// currentAuthNonce, +// timeMs +// ); +// String storagePwd = generateFakeStoragePwd(); +// +// String reqId2 = "create-session-" + UUID.randomUUID(); +// String json2 = """ +// { +// "op": "CreateAuthSession", +// "requestId": "%s", +// "payload": { +// "storagePwd": "%s", +// "timeMs": %d, +// "signatureB64": "%s", +// "clientInfo": "AuthTestClient/1.0" +// } +// } +// """.formatted( +// reqId2, +// storagePwd, +// timeMs, +// signatureB64 +// ); +// +// System.out.println(); +// System.out.println(logPrefix + " 📤 [STEP2 CreateAuthSession] Request:"); +// System.out.println(json2); +// +// step = 1; +// webSocket.sendText(json2, true); +// } else if (step == 1) { +// // Ответ на CreateAuthSession +// JsonNode root = JSON.readTree(msg); +// int status = root.path("status").asInt(); +// if (status != 200) { +// String code = root.path("payload").path("code").asText(); +// String message = root.path("payload").path("message").asText(); +// throw new IllegalStateException( +// "CreateAuthSession failed: status=" + status + +// ", code=" + code + +// ", message=" + message); +// } +// +// String sessionId = root.path("payload").path("sessionId").asText(null); +// String sessionPwd = root.path("payload").path("sessionPwd").asText(null); +// if (sessionId == null || sessionPwd == null) { +// throw new IllegalStateException("CreateAuthSession: sessionId or sessionPwd is null"); +// } +// +// SessionTokens tokens = new SessionTokens(); +// tokens.sessionId = sessionId; +// tokens.sessionPwd = sessionPwd; +// tokens.storagePwd = null; // мы знаем, какой отправляли, при желании можно сохранить +// +// System.out.println(logPrefix + " 🆔 sessionId = " + sessionId); +// System.out.println(logPrefix + " 🔐 sessionPwd = " + sessionPwd); +// +// resultFuture.complete(tokens); +// +// try { +// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "session created"); +// } catch (Exception ignored) { +// } +// } else { +// // Лишние сообщения — считаем ошибкой +// throw new IllegalStateException("Unexpected extra message on step=" + step); +// } +// } catch (Exception ex) { +// if (!resultFuture.isDone()) { +// resultFuture.completeExceptionally(ex); +// } +// try { +// webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "error in test"); +// } catch (Exception ignored) { +// } +// } finally { +// webSocket.request(1); +// } +// +// return CompletableFuture.completedFuture(null); +// } +// +// @Override +// public void onError(WebSocket webSocket, Throwable error) { +// System.out.println(logPrefix + " ❌ WebSocket error: " + error.getMessage()); +// if (!resultFuture.isDone()) { +// resultFuture.completeExceptionally(error); +// } +// } +// +// @Override +// public CompletionStage onClose(WebSocket webSocket, +// int statusCode, +// String reason) { +// System.out.println(logPrefix + " 🔚 WebSocket closed. code=" + statusCode + ", reason=" + reason); +// if (!resultFuture.isDone()) { +// resultFuture.completeExceptionally( +// new IllegalStateException("Closed before session tokens were received")); +// } +// return CompletableFuture.completedFuture(null); +// } +// }); +// +// // ждём результат или ошибку +// return resultFuture.get(WS_TIMEOUT_SEC, TimeUnit.SECONDS); +// } +// +// /** +// * Собрать подпись над строкой "AUTHORIFICATED:" + timeMs + authNonce +// * приватным ключом устройства. +// */ +// private String buildAuthorificatedSignature(byte[] devicePrivKey, +// String authNonce, +// long timeMs) { +// String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce; +// byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8); +// byte[] sig = Ed25519Util.sign(preimage, devicePrivKey); +// return Base64.getEncoder().encodeToString(sig); +// } +// +// /** Просто base64 от 32 байт 1..32 — для storagePwd. */ +// private String generateFakeStoragePwd() { +// byte[] data = new byte[32]; +// for (int i = 0; i < data.length; i++) { +// data[i] = (byte) (i + 1); +// } +// return Base64.getEncoder().encodeToString(data); +// } +// +// // ======================================================================== +// // ТЕСТЫ +// // ======================================================================== +// +// /** +// * 1) Регистрируем пользователя через AddUser +// */ +// @Test +// void addUser_shouldSucceed() throws Exception { +// TestUser user = createRandomUser(); +// +// String reqId = "add-" + UUID.randomUUID(); +// String json = """ +// { +// "op": "AddUser", +// "requestId": "%s", +// "payload": { +// "login": "%s", +// "loginId": %d, +// "bchId": %d, +// "loginKey": "%s", +// "deviceKey": "%s", +// "bchLimit": 1000000 +// } +// } +// """.formatted( +// reqId, +// user.login, +// user.loginId, +// user.bchId, +// user.loginPubB64, +// user.devicePubB64 +// ); +// +// JsonNode resp = callSingleJsonOp(json, "TEST addUser_shouldSucceed"); +// int status = resp.path("status").asInt(); +// String code = resp.path("payload").path("code").asText(null); +// +// Assertions.assertEquals( +// 200, +// status, +// "Ожидался status=200 при AddUser, но сервер вернул: status=" + status + ", code=" + code +// ); +// +// System.out.println("✅ [TEST] AddUser прошёл успешно для login=" + user.login); +// } +// +// /** +// * 2) Создать пользователя и сразу сделать CreateAuthSession (AuthChallenge + CreateAuthSession). +// */ +// @Test +// void createSession_flow_shouldReturnSessionIdAndPwd() throws Exception { +// TestUser user = createRandomUser(); +// +// // Сначала регистрируем пользователя +// { +// String reqId = "add-" + UUID.randomUUID(); +// String json = """ +// { +// "op": "AddUser", +// "requestId": "%s", +// "payload": { +// "login": "%s", +// "loginId": %d, +// "bchId": %d, +// "loginKey": "%s", +// "deviceKey": "%s", +// "bchLimit": 1000000 +// } +// } +// """.formatted( +// reqId, +// user.login, +// user.loginId, +// user.bchId, +// user.loginPubB64, +// user.devicePubB64 +// ); +// JsonNode resp = callSingleJsonOp(json, "TEST createSession_flow / AddUser"); +// int status = resp.path("status").asInt(); +// Assertions.assertEquals(200, status, "AddUser должен вернуть 200"); +// } +// +// SessionTokens tokens = createSessionForUser(user, "[createSession_flow]"); +// Assertions.assertNotNull(tokens.sessionId, "sessionId не должен быть null"); +// Assertions.assertNotNull(tokens.sessionPwd, "sessionPwd не должен быть null"); +// +// System.out.println("✅ [TEST] Сессия успешно создана: sessionId=" + tokens.sessionId); +// } +// +// /** +// * 3) Сценарий с двумя сессиями (упрощённая версия): +// * - создаём пользователя +// * - создаём первую сессию +// * - создаём вторую сессию +// * - убеждаемся, что sessionId разные +// * +// * Полный цикл с ListSessions / CloseActiveSession можно будет нарастить поверх +// * этого теста, когда добавим JSON-обработчик Net_ListSessions. +// */ +// @Test +// void fullTwoSessionLifecycleScenario() throws Exception { +// TestUser user = createRandomUser(); +// +// // 1) AddUser +// String reqId = "add-" + UUID.randomUUID(); +// String jsonAdd = """ +// { +// "op": "AddUser", +// "requestId": "%s", +// "payload": { +// "login": "%s", +// "loginId": %d, +// "bchId": %d, +// "loginKey": "%s", +// "deviceKey": "%s", +// "bchLimit": 1000000 +// } +// } +// """.formatted( +// reqId, +// user.login, +// user.loginId, +// user.bchId, +// user.loginPubB64, +// user.devicePubB64 +// ); +// +// JsonNode addResp = callSingleJsonOp(jsonAdd, "SCENARIO fullTwoSessionLifecycle / AddUser"); +// int addStatus = addResp.path("status").asInt(); +// Assertions.assertEquals(200, addStatus, "AddUser должен вернуть 200"); +// +// System.out.println("✅ [SC] Пользователь создан: " + user.login); +// +// // 2) Первая сессия +// SessionTokens s1 = createSessionForUser(user, "[SC S1]"); +// // 3) Вторая сессия +// SessionTokens s2 = createSessionForUser(user, "[SC S2]"); +// +// Assertions.assertNotEquals( +// s1.sessionId, +// s2.sessionId, +// "Первая и вторая сессия должны иметь разные sessionId" +// ); +// +// System.out.println(); +// System.out.println("✅ [SC] Полный сценарий (упрощённый) успешно отработал:"); +// System.out.println(" session1 = " + s1.sessionId); +// System.out.println(" session2 = " + s2.sessionId); +// +// System.out.println("\nℹ️ ListSessions / CloseActiveSession / повторные проверки можно будет добавить, " + +// "когда JSON-обработчик Net_ListSessions будет реализован на сервере."); +// } +//} diff --git a/src/test/java/test/it/AddUserIT.java b/src/test/java/test/it/AddUserIT.java index 524ebfe..6cd9260 100644 --- a/src/test/java/test/it/AddUserIT.java +++ b/src/test/java/test/it/AddUserIT.java @@ -13,19 +13,30 @@ public class AddUserIT { try (WsTestClient client = new WsTestClient(TestConfig.WS_URI)) { String reqId = "it-adduser-1"; - String resp = client.request(reqId, JsonBuilders.addUser(reqId), Duration.ofSeconds(5)); + String reqJson = JsonBuilders.addUser(reqId); + + TestLog.section("AddUserIT: AddUser"); + TestLog.req("AddUser requestId=" + reqId, reqJson); + + String resp = client.request(reqId, reqJson, Duration.ofSeconds(5)); + TestLog.resp("AddUser responseId=" + reqId, resp); int st = JsonParsers.status(resp); - // ВАЖНО: тут подставь свой реальный код "уже существует", если он не 200. - // Я оставляю пример: 409. boolean created = (st == 200); boolean already = (st == 409); + if (already) { + String code = JsonParsers.errorCode(resp); + // если сервер кладет code в payload.code — парсер должен это поддерживать (см. ниже) + assertEquals("USER_ALREADY_EXISTS", code, + "Expected errorCode=USER_ALREADY_EXISTS, but got: " + code + ", resp=" + resp); + } + if (created) { System.out.println("✅ AddUser: создан/добавлен (status=200)"); } else if (already) { - System.out.println("✅ AddUser: возможно уже есть в базе (status=409)"); + System.out.println("✅ AddUser: уже есть в системе (status=409, USER_ALREADY_EXISTS)"); } else { fail("❌ AddUser: неожиданный status=" + st + ", resp=" + resp); } diff --git a/src/test/java/test/it/JsonParsers.java b/src/test/java/test/it/JsonParsers.java index 2bf2c83..221cfa0 100644 --- a/src/test/java/test/it/JsonParsers.java +++ b/src/test/java/test/it/JsonParsers.java @@ -79,4 +79,25 @@ public final class JsonParsers { } catch (Exception ignored) {} return res; } + + public static String errorCode(String json) { + try { + JsonNode root = MAPPER.readTree(json); + + // поддержка старого формата (верхний уровень) + if (root.has("errorCode")) return root.get("errorCode").asText(); + // поддержка нового формата (верхний уровень) + if (root.has("code")) return root.get("code").asText(); + + JsonNode payload = root.get("payload"); + if (payload != null) { + // поддержка старого формата (внутри payload) + if (payload.has("errorCode")) return payload.get("errorCode").asText(); + // поддержка нового формата (внутри payload) + if (payload.has("code")) return payload.get("code").asText(); + } + } catch (Exception ignored) {} + + return null; + } } \ No newline at end of file diff --git a/src/main/java/test/it/WsTestClient.java b/src/test/java/test/it/WsTestClient.java similarity index 100% rename from src/main/java/test/it/WsTestClient.java rename to src/test/java/test/it/WsTestClient.java