diff --git a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 626734c..c506b5f 100644 --- a/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -101,14 +101,26 @@ public final class DatabaseInitializer { st.execute("PRAGMA foreign_keys = ON"); // 1. solana_users + // ВАЖНО: + // - Все требуемые поля теперь лежат в solana_users: + // login, blockchain_name, solana_key, blockchain_key, device_key + // - Поиск по login в DAO сделан case-insensitive. + // - Для защиты от дублей "Anya" и "anya" добавляем COLLATE NOCASE на PRIMARY KEY. st.executeUpdate(""" CREATE TABLE IF NOT EXISTS solana_users ( - login TEXT NOT NULL PRIMARY KEY, - device_key TEXT NOT NULL, - solana_key TEXT + login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE, + blockchain_name TEXT NOT NULL, + solana_key TEXT NOT NULL, + blockchain_key TEXT NOT NULL, + device_key TEXT NOT NULL ); """); + st.executeUpdate(""" + CREATE UNIQUE INDEX IF NOT EXISTS uq_solana_users_blockchain_name + ON solana_users (blockchain_name); + """); + st.executeUpdate(""" CREATE INDEX IF NOT EXISTS idx_solana_users_login ON solana_users (login); 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 e1cd45a..3474a98 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 @@ -13,9 +13,11 @@ import java.util.List; * Таблица: solana_users * * Колонки: - * - login TEXT PRIMARY KEY - * - device_key TEXT NOT NULL - * - solana_key TEXT NULLABLE + * - login TEXT PRIMARY KEY (COLLATE NOCASE) + * - blockchain_name TEXT NOT NULL + * - solana_key TEXT NOT NULL + * - blockchain_key TEXT NOT NULL + * - device_key TEXT NOT NULL * * Правило работы с соединениями: * - методы с Connection НЕ закрывают соединение @@ -42,14 +44,17 @@ public final class SolanaUsersDAO { /** Вставка с внешним соединением. Соединение НЕ закрывает. */ public void insert(Connection c, SolanaUserEntry user) throws SQLException { String sql = """ - INSERT INTO solana_users (login, device_key, solana_key) - VALUES (?, ?, ?) + INSERT INTO solana_users ( + login, blockchain_name, solana_key, blockchain_key, device_key + ) VALUES (?, ?, ?, ?, ?) """; try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, user.getLogin()); - ps.setString(2, user.getDeviceKey()); + ps.setString(2, user.getBlockchainName()); ps.setString(3, user.getSolanaKey()); + ps.setString(4, user.getBlockchainKey()); + ps.setString(5, user.getDeviceKey()); ps.executeUpdate(); } } @@ -87,12 +92,41 @@ public final class SolanaUsersDAO { } } + /** Проверка существования по blockchain_name (case-sensitive, как в БД) с внешним соединением. */ + public boolean existsByBlockchainName(Connection c, String blockchainName) throws SQLException { + String sql = """ + SELECT 1 + FROM solana_users + WHERE blockchain_name = ? + LIMIT 1 + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, blockchainName); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } + + /** Проверка существования по blockchain_name без внешнего соединения. */ + public boolean existsByBlockchainName(String blockchainName) throws SQLException { + try (Connection c = db.getConnection()) { + return existsByBlockchainName(c, blockchainName); + } + } + // -------------------- SELECT -------------------- /** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */ public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException { String sql = """ - SELECT login, device_key, solana_key + SELECT + login, + blockchain_name, + solana_key, + blockchain_key, + device_key FROM solana_users WHERE LOWER(login) = LOWER(?) """; @@ -113,10 +147,44 @@ public final class SolanaUsersDAO { } } + /** Получить по blockchain_name (case-sensitive) с внешним соединением. Соединение НЕ закрывает. */ + public SolanaUserEntry getByBlockchainName(Connection c, String blockchainName) throws SQLException { + String sql = """ + SELECT + login, + blockchain_name, + solana_key, + blockchain_key, + device_key + FROM solana_users + WHERE blockchain_name = ? + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, blockchainName); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + return mapRow(rs); + } + } + } + + /** Получить по blockchain_name без внешнего соединения. */ + public SolanaUserEntry getByBlockchainName(String blockchainName) throws SQLException { + try (Connection c = db.getConnection()) { + return getByBlockchainName(c, blockchainName); + } + } + /** Поиск по префиксу с внешним соединением. Соединение НЕ закрывает. */ public List searchByLoginPrefix(Connection c, String prefix) throws SQLException { String sql = """ - SELECT login, device_key, solana_key + SELECT + login, + blockchain_name, + solana_key, + blockchain_key, + device_key FROM solana_users WHERE LOWER(login) LIKE ? ORDER BY login @@ -145,14 +213,13 @@ public final class SolanaUsersDAO { // -------------------- MAPPER -------------------- private SolanaUserEntry mapRow(ResultSet rs) throws SQLException { - SolanaUserEntry e = new SolanaUserEntry( - rs.getString("login"), - rs.getString("device_key") - ); + SolanaUserEntry e = new SolanaUserEntry(); - String solanaKey = rs.getString("solana_key"); - if (rs.wasNull()) solanaKey = null; - e.setSolanaKey(solanaKey); + e.setLogin(rs.getString("login")); + e.setBlockchainName(rs.getString("blockchain_name")); + e.setSolanaKey(rs.getString("solana_key")); + e.setBlockchainKey(rs.getString("blockchain_key")); + e.setDeviceKey(rs.getString("device_key")); return e; } diff --git a/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java b/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java index 6d7bd2a..1bb27b3 100644 --- a/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java +++ b/shine-server-db/src/main/java/shine/db/dao/UserCreateDAO.java @@ -1,18 +1,17 @@ package shine.db.dao; import shine.db.SqliteDbController; -import shine.db.entities.BlockchainStateEntry; import shine.db.entities.SolanaUserEntry; import java.sql.*; /** * UserCreateDAO — атомарное добавление пользователя: - * - solana_users (login, device_key) + * - solana_users (login, blockchain_name, solana_key, blockchain_key, device_key) * - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...) * * ВАЖНО: - * - только INSERT/UPSERT + * - только INSERT (без перезаписи существующих записей) * - если login или blockchainName заняты — возвращаем false (пользователь уже есть/занято) */ public final class UserCreateDAO { @@ -20,7 +19,6 @@ public final class UserCreateDAO { private static volatile UserCreateDAO instance; private final SqliteDbController db = SqliteDbController.getInstance(); private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); - private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance(); private UserCreateDAO() {} @@ -38,9 +36,10 @@ public final class UserCreateDAO { */ public boolean insertUserWithBlockchain( String login, - String deviceKey, String blockchainName, + String solanaKey, String blockchainKey, + String deviceKey, long sizeLimit, long nowMs ) throws SQLException { @@ -55,25 +54,25 @@ public final class UserCreateDAO { } try { - // 1) user - SolanaUserEntry u = new SolanaUserEntry(login, deviceKey, deviceKey); - usersDao.insert(c, u); // если login занят -> constraint + // 1) solana_users + SolanaUserEntry u = new SolanaUserEntry(); + u.setLogin(login); + u.setBlockchainName(blockchainName); + u.setSolanaKey(solanaKey); + u.setBlockchainKey(blockchainKey); + u.setDeviceKey(deviceKey); - // 2) blockchain_state - BlockchainStateEntry st = new BlockchainStateEntry(); - st.setBlockchainName(blockchainName); - st.setLogin(login); - st.setBlockchainKey(blockchainKey); - st.setSizeLimit(sizeLimit); - st.setFileSizeBytes(0L); + usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint - // старт: блоков ещё нет - st.setLastBlockNumber(-1); - st.setLastBlockHash(null); - - st.setUpdatedAtMs(nowMs); - - stateDao.upsert(c, st); // если blockchainName занят -> constraint (PK) + // 2) blockchain_state — строго INSERT, без UPSERT (иначе можно перезаписать существующую цепочку) + insertBlockchainStateStrict( + c, + blockchainName, + login, + blockchainKey, + sizeLimit, + nowMs + ); c.commit(); return true; @@ -92,4 +91,43 @@ public final class UserCreateDAO { } } } + + private static void insertBlockchainStateStrict( + Connection c, + String blockchainName, + String login, + String blockchainKey, + long sizeLimit, + long nowMs + ) throws SQLException { + + String sql = """ + INSERT INTO blockchain_state ( + blockchain_name, + login, + blockchain_key, + size_limit, + file_size_bytes, + last_block_number, + last_block_hash, + updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """; + + try (PreparedStatement ps = c.prepareStatement(sql)) { + int i = 1; + ps.setString(i++, blockchainName); + ps.setString(i++, login); + ps.setString(i++, blockchainKey); + + ps.setLong(i++, sizeLimit); + ps.setLong(i++, 0L); + + ps.setInt(i++, -1); + ps.setNull(i++, Types.BLOB); // старт: блоков ещё нет + ps.setLong(i++, nowMs); + + ps.executeUpdate(); // если blockchainName занят -> constraint (PK) + } + } } \ No newline at end of file diff --git a/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java index 854986e..b7dbd7c 100644 --- a/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java +++ b/shine-server-db/src/main/java/shine/db/entities/SolanaUserEntry.java @@ -8,38 +8,57 @@ import java.util.Base64; * Таблица: solana_users * * Поля: - * - login — PRIMARY KEY (TEXT) - * - device_key — TEXT NOT NULL - * - solana_key — TEXT NULLABLE + * - login — PRIMARY KEY (TEXT) (case-insensitive на уровне COLLATE NOCASE) + * - blockchain_name — TEXT NOT NULL + * - solana_key — TEXT NOT NULL + * - blockchain_key — TEXT NOT NULL + * - device_key — TEXT NOT NULL */ public class SolanaUserEntry { private String login; - private String deviceKey; + + private String blockchainName; + + /** Ключ пользователя Solana (публичный ключ логина) */ private String solanaKey; + /** Ключ блокчейна (публичный ключ блокчейна) */ + private String blockchainKey; + + /** Ключ устройства (публичный ключ устройства) */ + private String deviceKey; + public SolanaUserEntry() {} - public SolanaUserEntry(String login, String deviceKey) { + public SolanaUserEntry(String login, + String blockchainName, + String solanaKey, + String blockchainKey, + String deviceKey) { this.login = login; - this.deviceKey = deviceKey; - } - - public SolanaUserEntry(String login, String deviceKey, String solanaKey) { - this.login = login; - this.deviceKey = deviceKey; + this.blockchainName = blockchainName; this.solanaKey = solanaKey; + this.blockchainKey = blockchainKey; + this.deviceKey = deviceKey; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } - public String getDeviceKey() { return deviceKey; } - public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } public String getSolanaKey() { return solanaKey; } public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; } + public String getBlockchainKey() { return blockchainKey; } + public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; } + + public String getDeviceKey() { return deviceKey; } + public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } + + // оставляю этот метод как утилиту (иногда удобно), но он работает только для deviceKey: public byte[] getDeviceKeyByte() { if (deviceKey == null) return null; String s = deviceKey.trim(); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 10af81a..6a4ea41 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -28,6 +28,9 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_R import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request; +import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler; +import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request; + import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler; import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler; import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler; @@ -50,6 +53,7 @@ public final class JsonHandlerRegistry { // Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...) private static final Map HANDLERS = Map.ofEntries( Map.entry("AddUser", new Net_AddUser_Handler()), + Map.entry("GetUser", new Net_GetUser_Handler()), // --- auth --- Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()), @@ -75,6 +79,7 @@ public final class JsonHandlerRegistry { private static final Map> REQUEST_TYPES = Map.ofEntries( Map.entry("AddUser", Net_AddUser_Request.class), + Map.entry("GetUser", Net_GetUser_Request.class), // --- auth --- Map.entry("AuthChallenge", Net_AuthChallenge_Request.class), 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 9e86cf1..da58433 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 @@ -61,6 +61,17 @@ public class Net_AddUser_Handler implements JsonMessageHandler { : req.getBchLimit(); try { + // базовая валидация форматов ключей: Base64(32 bytes) + byte[] solanaKey32 = Base64.getDecoder().decode(req.getSolanaKey()); + if (solanaKey32.length != 32) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_SOLANA_KEY", + "solanaKey должен быть Base64(32 bytes)" + ); + } + byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey()); if (blockchainKey32.length != 32) { return NetExceptionResponseFactory.error( @@ -71,6 +82,16 @@ public class Net_AddUser_Handler implements JsonMessageHandler { ); } + byte[] deviceKey32 = Base64.getDecoder().decode(req.getDeviceKey()); + if (deviceKey32.length != 32) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_DEVICE_KEY", + "deviceKey должен быть Base64(32 bytes)" + ); + } + SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); @@ -79,8 +100,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler { try (Connection c = db.getConnection()) { c.setAutoCommit(false); - // 1. Проверяем, что пользователя нет - if (usersDAO.getByLogin(req.getLogin()) != null) { + // 1. Проверяем, что пользователя нет (case-insensitive) + if (usersDAO.getByLogin(c, req.getLogin()) != null) { return NetExceptionResponseFactory.error( req, 409, @@ -89,26 +110,38 @@ public class Net_AddUser_Handler implements JsonMessageHandler { ); } - // 2. Проверяем, что blockchain_state ещё нет - if (stateDAO.getByBlockchainName(req.getBlockchainName()) != null) { + // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД) + if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) { return NetExceptionResponseFactory.error( req, 409, "BLOCKCHAIN_ALREADY_EXISTS", + "Пользователь с таким blockchainName уже существует" + ); + } + + // 3. На всякий случай оставляем старую проверку blockchain_state, + // потому что эта таблица нужна серверу (состояние цепочки/лимиты). + if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) { + return NetExceptionResponseFactory.error( + req, + 409, + "BLOCKCHAIN_STATE_ALREADY_EXISTS", "blockchain_state уже существует" ); } - // 3. Создаём пользователя (solanaKey + deviceKey) - SolanaUserEntry user = new SolanaUserEntry( - req.getLogin(), - req.getSolanaKey(), - req.getDeviceKey() - ); + // 4. Создаём пользователя (все поля теперь лежат в solana_users) + SolanaUserEntry user = new SolanaUserEntry(); + user.setLogin(req.getLogin()); + user.setBlockchainName(req.getBlockchainName()); + user.setSolanaKey(req.getSolanaKey()); + user.setBlockchainKey(req.getBlockchainKey()); + user.setDeviceKey(req.getDeviceKey()); usersDAO.insert(c, user); - // 4. Создаём INITIAL blockchain_state (blockchainKey) + // 5. Создаём INITIAL blockchain_state (для работы сервера) BlockchainStateEntry st = new BlockchainStateEntry(); st.setBlockchainName(req.getBlockchainName()); st.setLogin(req.getLogin()); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java new file mode 100644 index 0000000..dc97598 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_GetUser_Handler.java @@ -0,0 +1,84 @@ +package server.logic.ws_protocol.JSON.handlers.tempToTest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request; +import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.dao.SolanaUsersDAO; +import shine.db.entities.SolanaUserEntry; + +import java.sql.SQLException; + +public class Net_GetUser_Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_GetUser_Request req = (Net_GetUser_Request) baseRequest; + + if (req.getLogin() == null || req.getLogin().isBlank()) { + // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200. + // Поэтому BAD_REQUEST оставляем только на реально пустой login. + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_FIELDS", + "Некорректные поля: login" + ); + } + + SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance(); + + try { + SolanaUserEntry u = usersDAO.getByLogin(req.getLogin()); + + Net_GetUser_Response resp = new Net_GetUser_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + if (u == null) { + resp.setExists(false); + log.info("ℹ️ GetUser: not found for login={}", req.getLogin()); + return resp; + } + + // ВАЖНО: + // - Поиск по login был case-insensitive, + // - а тут возвращаем login/blockchainName как в БД (с исходным регистром). + resp.setExists(true); + resp.setLogin(u.getLogin()); + resp.setBlockchainName(u.getBlockchainName()); + resp.setSolanaKey(u.getSolanaKey()); + resp.setBlockchainKey(u.getBlockchainKey()); + resp.setDeviceKey(u.getDeviceKey()); + + log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName()); + return resp; + + } catch (SQLException e) { + log.error("❌ DB error GetUser", e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.SERVER_DATA_ERROR, + "DB_ERROR", + "Ошибка БД" + ); + } catch (Exception e) { + log.error("❌ Internal error GetUser", e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.INTERNAL_ERROR, + "INTERNAL_ERROR", + "Внутренняя ошибка сервера" + ); + } + } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java index e1da694..465cdcb 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java @@ -1,3 +1,4 @@ +// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties; import server.logic.ws_protocol.JSON.entyties.Net_Response; diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Request.java new file mode 100644 index 0000000..4efa364 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Request.java @@ -0,0 +1,27 @@ +package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +/** + * Запрос GetUser — проверка/получение пользователя по login. + * + * Клиент отправляет: + * + * { + * "op": "GetUser", + * "requestId": "u-1", + * "payload": { + * "login": "AnYa" + * } + * } + * + * Поиск по login выполняется без учёта регистра. + * В ответе возвращаем login/blockchainName с тем регистром, как в БД. + */ +public class Net_GetUser_Request extends Net_Request { + + private String login; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } +} \ No newline at end of file diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java new file mode 100644 index 0000000..73c55cd --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_GetUser_Response.java @@ -0,0 +1,60 @@ +package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +/** + * Ответ GetUser. + * + * Всегда status=200. + * + * Пример (нет пользователя): + * { + * "op": "GetUser", + * "requestId": "u-1", + * "status": 200, + * "payload": { "exists": false } + * } + * + * Пример (есть пользователь): + * { + * "op": "GetUser", + * "requestId": "u-1", + * "status": 200, + * "payload": { + * "exists": true, + * "login": "Anya", + * "blockchainName": "anya-001", + * "solanaKey": "...", + * "blockchainKey": "...", + * "deviceKey": "..." + * } + * } + */ +public class Net_GetUser_Response extends Net_Response { + + private Boolean exists; + + private String login; + private String blockchainName; + private String solanaKey; + private String blockchainKey; + private String deviceKey; + + public Boolean getExists() { return exists; } + public void setExists(Boolean exists) { this.exists = exists; } + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } + + public String getSolanaKey() { return solanaKey; } + public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; } + + public String getBlockchainKey() { return blockchainKey; } + public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; } + + public String getDeviceKey() { return deviceKey; } + public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } +} \ No newline at end of file diff --git a/src/test/java/test/it/cases/IT_01_AddUser.java b/src/test/java/test/it/cases/IT_01_AddUser.java index 5ec3f04..602bb71 100644 --- a/src/test/java/test/it/cases/IT_01_AddUser.java +++ b/src/test/java/test/it/cases/IT_01_AddUser.java @@ -13,6 +13,11 @@ import static org.junit.jupiter.api.Assertions.fail; /** * IT_01_AddUser * Создаёт 3 пользователей: TestUser1/2/3 (200 OK или 409 USER_ALREADY_EXISTS). + * + * Обновление: + * - теперь AddUser может вернуть 409 не только USER_ALREADY_EXISTS, + * но и BLOCKCHAIN_ALREADY_EXISTS / BLOCKCHAIN_STATE_ALREADY_EXISTS. + * - дополнительно проверяем GetUser (status=200 всегда). */ public class IT_01_AddUser { @@ -27,14 +32,32 @@ public class IT_01_AddUser { Duration t = Duration.ofSeconds(5); try (WsSession ws = WsSession.open()) { + r.ok("AddUser USER1: " + TestConfig.LOGIN()); - checkAddUser200or409(r, ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t)); + String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t); + checkAddUser200or409(r, resp1); + checkGetUserMustExist(r, ws, TestConfig.LOGIN(), t); r.ok("AddUser USER2: " + TestConfig.LOGIN2()); - checkAddUser200or409(r, ws.call("AddUser#USER2", JsonBuilders.addUser(TestConfig.LOGIN2()), t)); + String resp2 = ws.call("AddUser#USER2", JsonBuilders.addUser(TestConfig.LOGIN2()), t); + checkAddUser200or409(r, resp2); + checkGetUserMustExist(r, ws, TestConfig.LOGIN2(), t); r.ok("AddUser USER3: " + TestConfig.LOGIN3()); - checkAddUser200or409(r, ws.call("AddUser#USER3", JsonBuilders.addUser(TestConfig.LOGIN3()), t)); + String resp3 = ws.call("AddUser#USER3", JsonBuilders.addUser(TestConfig.LOGIN3()), t); + checkAddUser200or409(r, resp3); + checkGetUserMustExist(r, ws, TestConfig.LOGIN3(), t); + + // Доп: проверяем case-insensitive поиск + String mixed = mixCase(TestConfig.LOGIN()); + r.ok("GetUser case-insensitive: запрос=" + mixed + " (должен найти " + TestConfig.LOGIN() + ")"); + checkGetUserMustExist(r, ws, mixed, t); + + // Доп: проверяем "не существует" (но status=200) + String missing = "NoSuchUser_987654321"; + r.ok("GetUser missing: " + missing); + checkGetUserMustNotExist(r, ws, missing, t); + } catch (Throwable e) { r.fail("IT_01_AddUser упал: " + e.getMessage()); } @@ -50,14 +73,138 @@ public class IT_01_AddUser { } if (st == 409) { String code = JsonParsers.errorCode(resp); + + // раньше был только USER_ALREADY_EXISTS, теперь добавились ещё варианты if ("USER_ALREADY_EXISTS".equals(code)) { r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)"); return; } + if ("BLOCKCHAIN_ALREADY_EXISTS".equals(code)) { + r.ok("AddUser: status=409 BLOCKCHAIN_ALREADY_EXISTS (blockchainName уже занят)"); + return; + } + if ("BLOCKCHAIN_STATE_ALREADY_EXISTS".equals(code)) { + r.ok("AddUser: status=409 BLOCKCHAIN_STATE_ALREADY_EXISTS (blockchain_state уже есть)"); + 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); } + + private static void checkGetUserMustExist(TestResult r, WsSession ws, String loginQuery, Duration t) { + String resp = ws.call("GetUser#" + loginQuery, JsonBuilders.getUser(loginQuery), t); + + int st = JsonParsers.status(resp); + if (st != 200) { + r.fail("GetUser: ожидали status=200, получили " + st + ", resp=" + resp); + fail("GetUser unexpected status=" + st); + } + + Boolean exists = JsonParsers.exists(resp); + if (exists == null || !exists) { + r.fail("GetUser: ожидали exists=true, resp=" + resp); + fail("GetUser expected exists=true"); + } + + // Проверяем, что сервер возвращает данные + String login = JsonParsers.userLogin(resp); + String blockchainName = JsonParsers.userBlockchainName(resp); + String solanaKey = JsonParsers.userSolanaKey(resp); + String blockchainKey = JsonParsers.userBlockchainKey(resp); + String deviceKey = JsonParsers.userDeviceKey(resp); + + if (isBlank(login) || isBlank(blockchainName) || isBlank(solanaKey) || isBlank(blockchainKey) || isBlank(deviceKey)) { + r.fail("GetUser: exists=true, но поля пустые/неполные, resp=" + resp); + fail("GetUser returned incomplete user data"); + } + + // ВАЖНО: + // Поиск делается без учета регистра, но login/blockchainName должны вернуться как в БД. + // Для тех логинов, которые мы создаем в тесте, это ровно TestConfig.LOGIN*(). + // Поэтому если запрос был смешанный регистр — сравниваем не с loginQuery, а с "каноничным" логином из конфига. + String canonical = canonicalLogin(loginQuery); + if (canonical != null) { + if (!login.equals(canonical)) { + r.fail("GetUser: login должен вернуться как в БД. expected=" + canonical + ", got=" + login + ", resp=" + resp); + fail("GetUser wrong login case"); + } + + String expectedBch = TestConfig.getBlockchainName(canonical); + if (!blockchainName.equals(expectedBch)) { + r.fail("GetUser: blockchainName должен вернуться как в БД. expected=" + expectedBch + ", got=" + blockchainName + ", resp=" + resp); + fail("GetUser wrong blockchainName"); + } + + // ключи должны совпадать с теми, что AddUser использует при регистрации + String expSol = TestConfig.solanaPublicKeyB64(canonical); + String expBchKey = TestConfig.blockchainPublicKeyB64(canonical); + String expDev = TestConfig.devicePublicKeyB64(canonical); + + if (!solanaKey.equals(expSol)) { + r.fail("GetUser: solanaKey mismatch, resp=" + resp); + fail("GetUser solanaKey mismatch"); + } + if (!blockchainKey.equals(expBchKey)) { + r.fail("GetUser: blockchainKey mismatch, resp=" + resp); + fail("GetUser blockchainKey mismatch"); + } + if (!deviceKey.equals(expDev)) { + r.fail("GetUser: deviceKey mismatch, resp=" + resp); + fail("GetUser deviceKey mismatch"); + } + } + + r.ok("GetUser: exists=true, login=" + login + ", blockchainName=" + blockchainName); + } + + private static void checkGetUserMustNotExist(TestResult r, WsSession ws, String loginQuery, Duration t) { + String resp = ws.call("GetUser#" + loginQuery, JsonBuilders.getUser(loginQuery), t); + + int st = JsonParsers.status(resp); + if (st != 200) { + r.fail("GetUser(not exist): ожидали status=200, получили " + st + ", resp=" + resp); + fail("GetUser(not exist) unexpected status=" + st); + } + + Boolean exists = JsonParsers.exists(resp); + if (exists == null) { + r.fail("GetUser(not exist): payload.exists отсутствует, resp=" + resp); + fail("GetUser(not exist) missing exists"); + } + if (exists) { + r.fail("GetUser(not exist): ожидали exists=false, resp=" + resp); + fail("GetUser(not exist) expected exists=false"); + } + + r.ok("GetUser: exists=false (ok)"); + } + + private static String canonicalLogin(String anyCaseLogin) { + if (anyCaseLogin == null) return null; + String x = anyCaseLogin.trim(); + if (x.isEmpty()) return null; + + // Привязка только к нашим тестовым логинам, чтобы не гадать. + if (x.equalsIgnoreCase(TestConfig.LOGIN())) return TestConfig.LOGIN(); + if (x.equalsIgnoreCase(TestConfig.LOGIN2())) return TestConfig.LOGIN2(); + if (x.equalsIgnoreCase(TestConfig.LOGIN3())) return TestConfig.LOGIN3(); + + return null; + } + + private static String mixCase(String s) { + if (s == null) return null; + String x = s.trim(); + if (x.length() < 2) return x; + // простой "микс" без рандома, чтобы тест был детерминированный + return Character.toUpperCase(x.charAt(0)) + x.substring(1).toLowerCase(); + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } } \ No newline at end of file diff --git a/src/test/java/test/it/utils/json/JsonBuilders.java b/src/test/java/test/it/utils/json/JsonBuilders.java index 1dd15b7..c51fd98 100644 --- a/src/test/java/test/it/utils/json/JsonBuilders.java +++ b/src/test/java/test/it/utils/json/JsonBuilders.java @@ -45,6 +45,21 @@ public final class JsonBuilders { ); } + // ---------------- GetUser ---------------- + + public static String getUser(String login) { + String requestId = TestIds.next("getuser"); + return """ + { + "op": "GetUser", + "requestId": "%s", + "payload": { + "login": "%s" + } + } + """.formatted(requestId, login); + } + // ---------------- AuthChallenge ---------------- public static String authChallenge(String login) { diff --git a/src/test/java/test/it/utils/json/JsonParsers.java b/src/test/java/test/it/utils/json/JsonParsers.java index 107e4e7..a58f277 100644 --- a/src/test/java/test/it/utils/json/JsonParsers.java +++ b/src/test/java/test/it/utils/json/JsonParsers.java @@ -113,4 +113,50 @@ public final class JsonParsers { return null; } + + // ---------------- GetUser helpers ---------------- + + public static Boolean exists(String json) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has("exists")) return payload.get("exists").asBoolean(); + return null; + } catch (Exception e) { + return null; + } + } + + public static String userLogin(String json) { + return getPayloadText(json, "login"); + } + + public static String userBlockchainName(String json) { + return getPayloadText(json, "blockchainName"); + } + + public static String userSolanaKey(String json) { + return getPayloadText(json, "solanaKey"); + } + + public static String userBlockchainKey(String json) { + return getPayloadText(json, "blockchainKey"); + } + + public static String userDeviceKey(String json) { + return getPayloadText(json, "deviceKey"); + } + + private static String getPayloadText(String json, String field) { + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload != null && payload.has(field) && !payload.get(field).isNull()) { + return payload.get(field).asText(); + } + return null; + } catch (Exception e) { + return null; + } + } } \ No newline at end of file