From 22fb35d1d417353da7d912d35261925560acf48f52ff581eb0be153168b2214b Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 28 Jan 2026 21:23:01 +0300 Subject: [PATCH] 28 01 25 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил запрос поиска пользователей по начаоу логина. И тест добавил. Все тесты проходят. --- .../ws_protocol/JSON/JsonHandlerRegistry.java | 6 ++ .../tempToTest/Net_SearchUsers_Handler.java | 77 +++++++++++++++++++ .../entyties/Net_SearchUsers_Request.java | 24 ++++++ .../entyties/Net_SearchUsers_Response.java | 29 +++++++ .../java/test/it/cases/IT_01_AddUser.java | 48 +++++++++++- .../java/test/it/utils/json/JsonBuilders.java | 15 ++++ .../java/test/it/utils/json/JsonParsers.java | 19 +++++ 7 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_SearchUsers_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Response.java 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 6a4ea41..a2b2df8 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 @@ -31,6 +31,10 @@ import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Re import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler; import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request; +// --- NEW: SearchUsers --- +import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_SearchUsers_Handler; +import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_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; @@ -54,6 +58,7 @@ public final class JsonHandlerRegistry { private static final Map HANDLERS = Map.ofEntries( Map.entry("AddUser", new Net_AddUser_Handler()), Map.entry("GetUser", new Net_GetUser_Handler()), + Map.entry("SearchUsers", new Net_SearchUsers_Handler()), // --- auth --- Map.entry("AuthChallenge", new Net_AuthChallenge_Handler()), @@ -80,6 +85,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), + Map.entry("SearchUsers", Net_SearchUsers_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_SearchUsers_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_SearchUsers_Handler.java new file mode 100644 index 0000000..f1abfd7 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/Net_SearchUsers_Handler.java @@ -0,0 +1,77 @@ +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_SearchUsers_Request; +import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_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; +import java.util.ArrayList; +import java.util.List; + +public class Net_SearchUsers_Handler implements JsonMessageHandler { + + private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest; + + if (req.getPrefix() == null || req.getPrefix().isBlank()) { + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.BAD_REQUEST, + "BAD_FIELDS", + "Некорректные поля: prefix" + ); + } + + String prefix = req.getPrefix().trim(); + + try { + SolanaUsersDAO dao = SolanaUsersDAO.getInstance(); + List users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5 + + List logins = new ArrayList<>(); + for (SolanaUserEntry u : users) { + if (u != null && u.getLogin() != null) { + logins.add(u.getLogin()); // регистр как в БД + } + } + + Net_SearchUsers_Response resp = new Net_SearchUsers_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setLogins(logins); + + log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size()); + return resp; + + } catch (SQLException e) { + log.error("❌ DB error SearchUsers", e); + return NetExceptionResponseFactory.error( + req, + WireCodes.Status.SERVER_DATA_ERROR, + "DB_ERROR", + "Ошибка БД" + ); + } catch (Exception e) { + log.error("❌ Internal error SearchUsers", 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_SearchUsers_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Request.java new file mode 100644 index 0000000..1e974d5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Request.java @@ -0,0 +1,24 @@ +package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +/** + * Запрос SearchUsers — поиск логинов по префиксу. + * + * Клиент отправляет: + * { + * "op": "SearchUsers", + * "requestId": "su-1", + * "payload": { "prefix": "any" } + * } + * + * Поиск по prefix выполняется без учёта регистра. + * В ответе возвращаем логины с тем регистром, как в БД. + */ +public class Net_SearchUsers_Request extends Net_Request { + + private String prefix; + + public String getPrefix() { return prefix; } + public void setPrefix(String prefix) { this.prefix = prefix; } +} \ 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_SearchUsers_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Response.java new file mode 100644 index 0000000..ede19b5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_SearchUsers_Response.java @@ -0,0 +1,29 @@ +package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +import java.util.ArrayList; +import java.util.List; + +/** + * Ответ SearchUsers. + * + * Всегда status=200. + * + * Пример: + * { + * "op": "SearchUsers", + * "requestId": "su-1", + * "status": 200, + * "payload": { + * "logins": ["Anya", "andrew", "Angel"] + * } + * } + */ +public class Net_SearchUsers_Response extends Net_Response { + + private List logins = new ArrayList<>(); + + public List getLogins() { return logins; } + public void setLogins(List logins) { this.logins = logins; } +} \ 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 602bb71..d2ce156 100644 --- a/src/test/java/test/it/cases/IT_01_AddUser.java +++ b/src/test/java/test/it/cases/IT_01_AddUser.java @@ -7,6 +7,7 @@ import test.it.utils.log.TestResult; import test.it.utils.ws.WsSession; import java.time.Duration; +import java.util.List; import static org.junit.jupiter.api.Assertions.fail; @@ -18,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.fail; * - теперь AddUser может вернуть 409 не только USER_ALREADY_EXISTS, * но и BLOCKCHAIN_ALREADY_EXISTS / BLOCKCHAIN_STATE_ALREADY_EXISTS. * - дополнительно проверяем GetUser (status=200 всегда). + * - добавлен SearchUsers: поиск по префиксу (первые 3 символа). */ public class IT_01_AddUser { @@ -48,7 +50,7 @@ public class IT_01_AddUser { checkAddUser200or409(r, resp3); checkGetUserMustExist(r, ws, TestConfig.LOGIN3(), t); - // Доп: проверяем case-insensitive поиск + // Доп: проверяем case-insensitive поиск в GetUser String mixed = mixCase(TestConfig.LOGIN()); r.ok("GetUser case-insensitive: запрос=" + mixed + " (должен найти " + TestConfig.LOGIN() + ")"); checkGetUserMustExist(r, ws, mixed, t); @@ -58,6 +60,12 @@ public class IT_01_AddUser { r.ok("GetUser missing: " + missing); checkGetUserMustNotExist(r, ws, missing, t); + // SearchUsers: один раз ищем по первым трём символам логина USER1 + String prefix3 = first3(TestConfig.LOGIN()); + String prefix3Mixed = mixCase(prefix3); + r.ok("SearchUsers: prefix(3)='" + prefix3Mixed + "' (должен вернуть список и содержать " + TestConfig.LOGIN() + ")"); + checkSearchUsersMustContain(r, ws, prefix3Mixed, TestConfig.LOGIN(), t); + } catch (Throwable e) { r.fail("IT_01_AddUser упал: " + e.getMessage()); } @@ -183,6 +191,37 @@ public class IT_01_AddUser { r.ok("GetUser: exists=false (ok)"); } + private static void checkSearchUsersMustContain(TestResult r, WsSession ws, String prefix, String expectedLogin, Duration t) { + String resp = ws.call("SearchUsers#" + prefix, JsonBuilders.searchUsers(prefix), t); + + int st = JsonParsers.status(resp); + if (st != 200) { + r.fail("SearchUsers: ожидали status=200, получили " + st + ", resp=" + resp); + fail("SearchUsers unexpected status=" + st); + } + + List logins = JsonParsers.searchLogins(resp); + if (logins == null || logins.isEmpty()) { + r.fail("SearchUsers: ожидали непустой список, resp=" + resp); + fail("SearchUsers expected non-empty list"); + } + + // ВАЖНО: ожидаемый логин должен быть в ответе в регистре БД (каноничный expectedLogin) + boolean found = false; + for (String s : logins) { + if (expectedLogin.equals(s)) { + found = true; + break; + } + } + if (!found) { + r.fail("SearchUsers: ожидаемый логин не найден. expected=" + expectedLogin + ", got=" + logins + ", resp=" + resp); + fail("SearchUsers expected login not found"); + } + + r.ok("SearchUsers: ok, prefix=" + prefix + ", results=" + logins.size() + ", contains=" + expectedLogin); + } + private static String canonicalLogin(String anyCaseLogin) { if (anyCaseLogin == null) return null; String x = anyCaseLogin.trim(); @@ -204,6 +243,13 @@ public class IT_01_AddUser { return Character.toUpperCase(x.charAt(0)) + x.substring(1).toLowerCase(); } + private static String first3(String s) { + if (s == null) return ""; + String x = s.trim(); + if (x.length() <= 3) return x; + return x.substring(0, 3); + } + private static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); } diff --git a/src/test/java/test/it/utils/json/JsonBuilders.java b/src/test/java/test/it/utils/json/JsonBuilders.java index c51fd98..11dbb5b 100644 --- a/src/test/java/test/it/utils/json/JsonBuilders.java +++ b/src/test/java/test/it/utils/json/JsonBuilders.java @@ -60,6 +60,21 @@ public final class JsonBuilders { """.formatted(requestId, login); } + // ---------------- SearchUsers ---------------- + + public static String searchUsers(String prefix) { + String requestId = TestIds.next("searchusers"); + return """ + { + "op": "SearchUsers", + "requestId": "%s", + "payload": { + "prefix": "%s" + } + } + """.formatted(requestId, prefix); + } + // ---------------- 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 a58f277..5d21716 100644 --- a/src/test/java/test/it/utils/json/JsonParsers.java +++ b/src/test/java/test/it/utils/json/JsonParsers.java @@ -147,6 +147,25 @@ public final class JsonParsers { return getPayloadText(json, "deviceKey"); } + // ---------------- SearchUsers helpers ---------------- + + public static List searchLogins(String json) { + List res = new ArrayList<>(); + try { + JsonNode root = MAPPER.readTree(json); + JsonNode payload = root.get("payload"); + if (payload == null) return res; + + JsonNode arr = payload.get("logins"); + if (arr == null || !arr.isArray()) return res; + + for (JsonNode x : arr) { + if (x != null && !x.isNull()) res.add(x.asText()); + } + } catch (Exception ignored) {} + return res; + } + private static String getPayloadText(String json, String field) { try { JsonNode root = MAPPER.readTree(json);