Добавил запрос проверить есть ли в системе такой пользователь и получить его данные.

И тесты добавил.

Все тесты проходят
This commit is contained in:
AidarKC 2026-01-28 20:33:06 +03:00
parent 43b0efb4d3
commit ebf7c9f18e
13 changed files with 621 additions and 67 deletions

View File

@ -101,14 +101,26 @@ public final class DatabaseInitializer {
st.execute("PRAGMA foreign_keys = ON"); st.execute("PRAGMA foreign_keys = ON");
// 1. solana_users // 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(""" st.executeUpdate("""
CREATE TABLE IF NOT EXISTS solana_users ( CREATE TABLE IF NOT EXISTS solana_users (
login TEXT NOT NULL PRIMARY KEY, login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
device_key TEXT NOT NULL, blockchain_name TEXT NOT NULL,
solana_key TEXT 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(""" st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_solana_users_login CREATE INDEX IF NOT EXISTS idx_solana_users_login
ON solana_users (login); ON solana_users (login);

View File

@ -13,9 +13,11 @@ import java.util.List;
* Таблица: solana_users * Таблица: solana_users
* *
* Колонки: * Колонки:
* - login TEXT PRIMARY KEY * - login TEXT PRIMARY KEY (COLLATE NOCASE)
* - device_key TEXT NOT NULL * - blockchain_name TEXT NOT NULL
* - solana_key TEXT NULLABLE * - solana_key TEXT NOT NULL
* - blockchain_key TEXT NOT NULL
* - device_key TEXT NOT NULL
* *
* Правило работы с соединениями: * Правило работы с соединениями:
* - методы с Connection НЕ закрывают соединение * - методы с Connection НЕ закрывают соединение
@ -42,14 +44,17 @@ public final class SolanaUsersDAO {
/** Вставка с внешним соединением. Соединение НЕ закрывает. */ /** Вставка с внешним соединением. Соединение НЕ закрывает. */
public void insert(Connection c, SolanaUserEntry user) throws SQLException { public void insert(Connection c, SolanaUserEntry user) throws SQLException {
String sql = """ String sql = """
INSERT INTO solana_users (login, device_key, solana_key) INSERT INTO solana_users (
VALUES (?, ?, ?) login, blockchain_name, solana_key, blockchain_key, device_key
) VALUES (?, ?, ?, ?, ?)
"""; """;
try (PreparedStatement ps = c.prepareStatement(sql)) { try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, user.getLogin()); ps.setString(1, user.getLogin());
ps.setString(2, user.getDeviceKey()); ps.setString(2, user.getBlockchainName());
ps.setString(3, user.getSolanaKey()); ps.setString(3, user.getSolanaKey());
ps.setString(4, user.getBlockchainKey());
ps.setString(5, user.getDeviceKey());
ps.executeUpdate(); 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 -------------------- // -------------------- SELECT --------------------
/** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */ /** Получить по login (case-insensitive) с внешним соединением. Соединение НЕ закрывает. */
public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException { public SolanaUserEntry getByLogin(Connection c, String login) throws SQLException {
String sql = """ String sql = """
SELECT login, device_key, solana_key SELECT
login,
blockchain_name,
solana_key,
blockchain_key,
device_key
FROM solana_users FROM solana_users
WHERE LOWER(login) = LOWER(?) 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<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException { public List<SolanaUserEntry> searchByLoginPrefix(Connection c, String prefix) throws SQLException {
String sql = """ String sql = """
SELECT login, device_key, solana_key SELECT
login,
blockchain_name,
solana_key,
blockchain_key,
device_key
FROM solana_users FROM solana_users
WHERE LOWER(login) LIKE ? WHERE LOWER(login) LIKE ?
ORDER BY login ORDER BY login
@ -145,14 +213,13 @@ public final class SolanaUsersDAO {
// -------------------- MAPPER -------------------- // -------------------- MAPPER --------------------
private SolanaUserEntry mapRow(ResultSet rs) throws SQLException { private SolanaUserEntry mapRow(ResultSet rs) throws SQLException {
SolanaUserEntry e = new SolanaUserEntry( SolanaUserEntry e = new SolanaUserEntry();
rs.getString("login"),
rs.getString("device_key")
);
String solanaKey = rs.getString("solana_key"); e.setLogin(rs.getString("login"));
if (rs.wasNull()) solanaKey = null; e.setBlockchainName(rs.getString("blockchain_name"));
e.setSolanaKey(solanaKey); e.setSolanaKey(rs.getString("solana_key"));
e.setBlockchainKey(rs.getString("blockchain_key"));
e.setDeviceKey(rs.getString("device_key"));
return e; return e;
} }

View File

@ -1,18 +1,17 @@
package shine.db.dao; package shine.db.dao;
import shine.db.SqliteDbController; import shine.db.SqliteDbController;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry; import shine.db.entities.SolanaUserEntry;
import java.sql.*; import java.sql.*;
/** /**
* UserCreateDAO атомарное добавление пользователя: * 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 ...) * - blockchain_state (blockchain_name, login, blockchain_key, size_limit, ... last_block_number=-1 ...)
* *
* ВАЖНО: * ВАЖНО:
* - только INSERT/UPSERT * - только INSERT (без перезаписи существующих записей)
* - если login или blockchainName заняты возвращаем false (пользователь уже есть/занято) * - если login или blockchainName заняты возвращаем false (пользователь уже есть/занято)
*/ */
public final class UserCreateDAO { public final class UserCreateDAO {
@ -20,7 +19,6 @@ public final class UserCreateDAO {
private static volatile UserCreateDAO instance; private static volatile UserCreateDAO instance;
private final SqliteDbController db = SqliteDbController.getInstance(); private final SqliteDbController db = SqliteDbController.getInstance();
private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance(); private final SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
private final BlockchainStateDAO stateDao = BlockchainStateDAO.getInstance();
private UserCreateDAO() {} private UserCreateDAO() {}
@ -38,9 +36,10 @@ public final class UserCreateDAO {
*/ */
public boolean insertUserWithBlockchain( public boolean insertUserWithBlockchain(
String login, String login,
String deviceKey,
String blockchainName, String blockchainName,
String solanaKey,
String blockchainKey, String blockchainKey,
String deviceKey,
long sizeLimit, long sizeLimit,
long nowMs long nowMs
) throws SQLException { ) throws SQLException {
@ -55,25 +54,25 @@ public final class UserCreateDAO {
} }
try { try {
// 1) user // 1) solana_users
SolanaUserEntry u = new SolanaUserEntry(login, deviceKey, deviceKey); SolanaUserEntry u = new SolanaUserEntry();
usersDao.insert(c, u); // если login занят -> constraint u.setLogin(login);
u.setBlockchainName(blockchainName);
u.setSolanaKey(solanaKey);
u.setBlockchainKey(blockchainKey);
u.setDeviceKey(deviceKey);
// 2) blockchain_state usersDao.insert(c, u); // если login занят (NOCASE) или blockchainName (unique) -> constraint
BlockchainStateEntry st = new BlockchainStateEntry();
st.setBlockchainName(blockchainName);
st.setLogin(login);
st.setBlockchainKey(blockchainKey);
st.setSizeLimit(sizeLimit);
st.setFileSizeBytes(0L);
// старт: блоков ещё нет // 2) blockchain_state строго INSERT, без UPSERT (иначе можно перезаписать существующую цепочку)
st.setLastBlockNumber(-1); insertBlockchainStateStrict(
st.setLastBlockHash(null); c,
blockchainName,
st.setUpdatedAtMs(nowMs); login,
blockchainKey,
stateDao.upsert(c, st); // если blockchainName занят -> constraint (PK) sizeLimit,
nowMs
);
c.commit(); c.commit();
return true; 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)
}
}
} }

View File

@ -8,38 +8,57 @@ import java.util.Base64;
* Таблица: solana_users * Таблица: solana_users
* *
* Поля: * Поля:
* - login PRIMARY KEY (TEXT) * - login PRIMARY KEY (TEXT) (case-insensitive на уровне COLLATE NOCASE)
* - device_key TEXT NOT NULL * - blockchain_name TEXT NOT NULL
* - solana_key TEXT NULLABLE * - solana_key TEXT NOT NULL
* - blockchain_key TEXT NOT NULL
* - device_key TEXT NOT NULL
*/ */
public class SolanaUserEntry { public class SolanaUserEntry {
private String login; private String login;
private String deviceKey;
private String blockchainName;
/** Ключ пользователя Solana (публичный ключ логина) */
private String solanaKey; private String solanaKey;
/** Ключ блокчейна (публичный ключ блокчейна) */
private String blockchainKey;
/** Ключ устройства (публичный ключ устройства) */
private String deviceKey;
public SolanaUserEntry() {} public SolanaUserEntry() {}
public SolanaUserEntry(String login, String deviceKey) { public SolanaUserEntry(String login,
String blockchainName,
String solanaKey,
String blockchainKey,
String deviceKey) {
this.login = login; this.login = login;
this.deviceKey = deviceKey; this.blockchainName = blockchainName;
}
public SolanaUserEntry(String login, String deviceKey, String solanaKey) {
this.login = login;
this.deviceKey = deviceKey;
this.solanaKey = solanaKey; this.solanaKey = solanaKey;
this.blockchainKey = blockchainKey;
this.deviceKey = deviceKey;
} }
public String getLogin() { return login; } public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; } public void setLogin(String login) { this.login = login; }
public String getDeviceKey() { return deviceKey; } public String getBlockchainName() { return blockchainName; }
public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; } public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }
public String getSolanaKey() { return solanaKey; } public String getSolanaKey() { return solanaKey; }
public void setSolanaKey(String solanaKey) { this.solanaKey = 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() { public byte[] getDeviceKeyByte() {
if (deviceKey == null) return null; if (deviceKey == null) return null;
String s = deviceKey.trim(); String s = deviceKey.trim();

View File

@ -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.Net_AddUser_Handler;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request; 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_GetUserParam_Handler;
import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler; import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_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(...) // Map.of(...) поддерживает максимум 10 пар => используем Map.ofEntries(...)
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries( private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
Map.entry("AddUser", new Net_AddUser_Handler()), Map.entry("AddUser", new Net_AddUser_Handler()),
Map.entry("GetUser", new Net_GetUser_Handler()),
// --- auth --- // --- auth ---
Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()), Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()),
@ -75,6 +79,7 @@ public final class JsonHandlerRegistry {
private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries( private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
Map.entry("AddUser", Net_AddUser_Request.class), Map.entry("AddUser", Net_AddUser_Request.class),
Map.entry("GetUser", Net_GetUser_Request.class),
// --- auth --- // --- auth ---
Map.entry("AuthChallenge", Net_AuthChallenge_Request.class), Map.entry("AuthChallenge", Net_AuthChallenge_Request.class),

View File

@ -61,6 +61,17 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
: req.getBchLimit(); : req.getBchLimit();
try { 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()); byte[] blockchainKey32 = Base64.getDecoder().decode(req.getBlockchainKey());
if (blockchainKey32.length != 32) { if (blockchainKey32.length != 32) {
return NetExceptionResponseFactory.error( 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(); SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance(); BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
@ -79,8 +100,8 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
try (Connection c = db.getConnection()) { try (Connection c = db.getConnection()) {
c.setAutoCommit(false); c.setAutoCommit(false);
// 1. Проверяем, что пользователя нет // 1. Проверяем, что пользователя нет (case-insensitive)
if (usersDAO.getByLogin(req.getLogin()) != null) { if (usersDAO.getByLogin(c, req.getLogin()) != null) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
409, 409,
@ -89,26 +110,38 @@ public class Net_AddUser_Handler implements JsonMessageHandler {
); );
} }
// 2. Проверяем, что blockchain_state ещё нет // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
if (stateDAO.getByBlockchainName(req.getBlockchainName()) != null) { if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
return NetExceptionResponseFactory.error( return NetExceptionResponseFactory.error(
req, req,
409, 409,
"BLOCKCHAIN_ALREADY_EXISTS", "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 уже существует" "blockchain_state уже существует"
); );
} }
// 3. Создаём пользователя (solanaKey + deviceKey) // 4. Создаём пользователя (все поля теперь лежат в solana_users)
SolanaUserEntry user = new SolanaUserEntry( SolanaUserEntry user = new SolanaUserEntry();
req.getLogin(), user.setLogin(req.getLogin());
req.getSolanaKey(), user.setBlockchainName(req.getBlockchainName());
req.getDeviceKey() user.setSolanaKey(req.getSolanaKey());
); user.setBlockchainKey(req.getBlockchainKey());
user.setDeviceKey(req.getDeviceKey());
usersDAO.insert(c, user); usersDAO.insert(c, user);
// 4. Создаём INITIAL blockchain_state (blockchainKey) // 5. Создаём INITIAL blockchain_state (для работы сервера)
BlockchainStateEntry st = new BlockchainStateEntry(); BlockchainStateEntry st = new BlockchainStateEntry();
st.setBlockchainName(req.getBlockchainName()); st.setBlockchainName(req.getBlockchainName());
st.setLogin(req.getLogin()); st.setLogin(req.getLogin());

View File

@ -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",
"Внутренняя ошибка сервера"
);
}
}
}

View File

@ -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; package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response; import server.logic.ws_protocol.JSON.entyties.Net_Response;

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -13,6 +13,11 @@ import static org.junit.jupiter.api.Assertions.fail;
/** /**
* IT_01_AddUser * IT_01_AddUser
* Создаёт 3 пользователей: TestUser1/2/3 (200 OK или 409 USER_ALREADY_EXISTS). * Создаёт 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 { public class IT_01_AddUser {
@ -27,14 +32,32 @@ public class IT_01_AddUser {
Duration t = Duration.ofSeconds(5); Duration t = Duration.ofSeconds(5);
try (WsSession ws = WsSession.open()) { try (WsSession ws = WsSession.open()) {
r.ok("AddUser USER1: " + TestConfig.LOGIN()); 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()); 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()); 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) { } catch (Throwable e) {
r.fail("IT_01_AddUser упал: " + e.getMessage()); r.fail("IT_01_AddUser упал: " + e.getMessage());
} }
@ -50,14 +73,138 @@ public class IT_01_AddUser {
} }
if (st == 409) { if (st == 409) {
String code = JsonParsers.errorCode(resp); String code = JsonParsers.errorCode(resp);
// раньше был только USER_ALREADY_EXISTS, теперь добавились ещё варианты
if ("USER_ALREADY_EXISTS".equals(code)) { if ("USER_ALREADY_EXISTS".equals(code)) {
r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)"); r.ok("AddUser: status=409 USER_ALREADY_EXISTS (уже был)");
return; 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); r.fail("AddUser: status=409 но code=" + code + ", resp=" + resp);
fail("AddUser unexpected 409 code=" + code); fail("AddUser unexpected 409 code=" + code);
} }
r.fail("AddUser: неожиданный status=" + st + ", resp=" + resp); r.fail("AddUser: неожиданный status=" + st + ", resp=" + resp);
fail("AddUser unexpected status=" + st); 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();
}
} }

View File

@ -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 ---------------- // ---------------- AuthChallenge ----------------
public static String authChallenge(String login) { public static String authChallenge(String login) {

View File

@ -113,4 +113,50 @@ public final class JsonParsers {
return null; 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;
}
}
} }