Merge branch 'main' into codex/analyze-block-addition-functionality-and-create-api-docs
This commit is contained in:
commit
1c9841b4a6
@ -21,6 +21,10 @@
|
||||
Глава API по записи блоков: формат `AddBlock`, коды ошибок, поддержанные типы/подтипы блоков и рекомендации по ресинхронизации.
|
||||
|
||||
1. **01_Connection_and_Sessions.md**
|
||||
0. **API/05_Technical_Requests_API.md**
|
||||
Технические запросы без авторизации: `Ping` для keep-alive и `GetServerInfo` для проверки доступности узла и чтения его публичной информации.
|
||||
|
||||
1. **01_Connection_and_Sessions.md**
|
||||
Процесс подключения к WebSocket, авторизация (двухшаговая), создание сессии, вход в существующую сессию, просмотр и закрытие сессий.
|
||||
|
||||
2. **02_Blockchain_Structure_and_Block_Types.md**
|
||||
|
||||
137
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
137
Dev_Docs/API/05_Technical_Requests_API.md
Normal file
@ -0,0 +1,137 @@
|
||||
# API для разработчиков: Технические запросы
|
||||
|
||||
Этот файл описывает технические запросы, которые не требуют авторизации и нужны для служебной работы клиента с сервером.
|
||||
|
||||
Сейчас здесь два метода:
|
||||
|
||||
- `Ping` — keep-alive запрос для поддержания живого WebSocket-соединения;
|
||||
- `GetServerInfo` — запрос базовой публичной информации о сервере для выбора узла в децентрализованной сети.
|
||||
|
||||
Логика раздела такая:
|
||||
|
||||
- `Ping` нужен для регулярной проверки, что соединение всё ещё живо;
|
||||
- `GetServerInfo` нужен до авторизации и до работы с данными, чтобы клиент понял, что сервер доступен, и показал пользователю краткую карточку этого узла.
|
||||
|
||||
Ниже сначала описаны назначение методов, затем точные форматы запросов и ответов.
|
||||
|
||||
## 1. `Ping`
|
||||
|
||||
### Назначение
|
||||
|
||||
Служебный keep-alive запрос.
|
||||
|
||||
Клиент может отправлять его периодически, чтобы:
|
||||
|
||||
- поддерживать активное WebSocket-соединение;
|
||||
- понимать, что сервер отвечает;
|
||||
- при необходимости получать текущее серверное время.
|
||||
|
||||
### Запрос
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "Ping",
|
||||
"requestId": "ping-001",
|
||||
"payload": {
|
||||
"ts": 1774700000123
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поле `ts` в запросе необязательно для логики сервера. Сервер его не валидирует и не использует для принятия решения.
|
||||
|
||||
### Успешный ответ
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "Ping",
|
||||
"requestId": "ping-001",
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"ts": 1774700000456
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Специфические коды ошибок `Ping`
|
||||
|
||||
- У `Ping` нет специальных прикладных ошибок.
|
||||
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||
|
||||
---
|
||||
|
||||
## 2. `GetServerInfo`
|
||||
|
||||
### Назначение
|
||||
|
||||
Запрос публичной информации о сервере.
|
||||
|
||||
Он нужен клиенту для выбора сервера в децентрализованной сети. По этому запросу клиент может:
|
||||
|
||||
- проверить, что сервер вообще доступен;
|
||||
- показать URL и версию сервера;
|
||||
- показать физический регион или адрес размещения;
|
||||
- показать описание сервера;
|
||||
- показать поле `origin` как комментарий о природе этого узла;
|
||||
- показать дополнительную текстовую информацию.
|
||||
|
||||
Этот запрос доступен без авторизации.
|
||||
|
||||
### Источник данных
|
||||
|
||||
- `version` берётся из Gradle build и подставляется в `application.properties`;
|
||||
- остальные поля читаются из настроек сервера;
|
||||
- если значение в конфиге не задано, сервер возвращает пустую строку.
|
||||
|
||||
### Запрос
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetServerInfo",
|
||||
"requestId": "srv-001",
|
||||
"payload": {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Успешный ответ
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "GetServerInfo",
|
||||
"requestId": "srv-001",
|
||||
"status": 200,
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"url": "wss://node.example.org/ws",
|
||||
"version": "1.0",
|
||||
"physicalRegion": "Грузия, Тбилиси",
|
||||
"description": "Public community SHiNE node",
|
||||
"origin": "Community-operated node",
|
||||
"extraInfo": "IPv4 + IPv6; test federation enabled"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Поля ответа
|
||||
|
||||
- `url` — публичный URL сервера.
|
||||
- `version` — версия сервера из Gradle build.
|
||||
- `physicalRegion` — физический регион или адрес размещения сервера.
|
||||
- `description` — человекочитаемое описание сервера.
|
||||
- `origin` — комментарий о том, какой это сервер.
|
||||
- `extraInfo` — любая дополнительная информация о сервере.
|
||||
|
||||
### Специфические коды ошибок `GetServerInfo`
|
||||
|
||||
- У `GetServerInfo` нет специальных прикладных ошибок при штатной работе.
|
||||
- Если произойдёт непредвиденная проблема, сервер вернёт общую ошибку из раздела `00`, обычно `500 / INTERNAL_ERROR`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Короткое резюме
|
||||
|
||||
- `Ping` нужен для keep-alive и проверки, что WebSocket-соединение живо.
|
||||
- `GetServerInfo` нужен для выбора сервера в сети и показа публичной информации об узле.
|
||||
- Оба запроса доступны без авторизации.
|
||||
10
build.gradle
10
build.gradle
@ -5,12 +5,19 @@ plugins {
|
||||
}
|
||||
|
||||
group = 'shine'
|
||||
version = '1.0'
|
||||
version = '1.1_codex'
|
||||
|
||||
tasks.jar {
|
||||
enabled = false // это что бы не создавала обычный джар, а будет только толстый джар
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
filteringCharset = 'UTF-8'
|
||||
filesMatching('application.properties') {
|
||||
expand(projectVersion: project.version.toString())
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@ -142,4 +149,3 @@ tasks.register('itDeployServer', JavaExec) {
|
||||
|
||||
dependsOn testClasses
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,12 @@ public final class AppConfig {
|
||||
return properties.getProperty(name);
|
||||
}
|
||||
|
||||
/** Вернёт строку или пустую строку, если параметр не найден. */
|
||||
public String getStringOrEmpty(String name) {
|
||||
String value = properties.getProperty(name);
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
/** Можно добавить методы для удобства */
|
||||
public int getInt(String name, int defaultValue) {
|
||||
String v = properties.getProperty(name);
|
||||
|
||||
@ -47,7 +47,9 @@ import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Ha
|
||||
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;
|
||||
|
||||
// --- NEW: Ping ---
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetServerInfo_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;
|
||||
|
||||
import java.util.Map;
|
||||
@ -85,7 +87,8 @@ public final class JsonHandlerRegistry {
|
||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
||||
|
||||
// --- system ---
|
||||
Map.entry("Ping", new Net_Ping_Handler())
|
||||
Map.entry("Ping", new Net_Ping_Handler()),
|
||||
Map.entry("GetServerInfo", new Net_GetServerInfo_Handler())
|
||||
|
||||
// --- subscriptions ---
|
||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||
@ -118,7 +121,8 @@ public final class JsonHandlerRegistry {
|
||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
||||
|
||||
// --- system ---
|
||||
Map.entry("Ping", Net_Ping_Request.class)
|
||||
Map.entry("Ping", Net_Ping_Request.class),
|
||||
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class)
|
||||
);
|
||||
|
||||
private JsonHandlerRegistry() { }
|
||||
@ -130,4 +134,4 @@ public final class JsonHandlerRegistry {
|
||||
public static Map<String, Class<? extends Net_Request>> getRequestTypes() {
|
||||
return REQUEST_TYPES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
server.1port=7070
|
||||
db.path=data/shine.sqlite
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Server public info
|
||||
# Эти поля используются JSON-операцией GetServerInfo.
|
||||
# Если какое-то значение не задано, сервер вернёт пустую строку.
|
||||
# ------------------------------------------------------------
|
||||
server.version=${projectVersion}
|
||||
server.info.url=
|
||||
server.info.physicalRegion=
|
||||
server.info.description=
|
||||
server.info.origin=
|
||||
server.info.extraInfo=
|
||||
|
||||
@ -36,6 +36,8 @@ public class IT_01_AddUser {
|
||||
|
||||
try (WsSession ws = WsSession.open()) {
|
||||
|
||||
checkPingAndServerInfo(r, ws, t);
|
||||
|
||||
r.ok("AddUser USER1: " + TestConfig.LOGIN());
|
||||
String resp1 = ws.call("AddUser#USER1", JsonBuilders.addUser(TestConfig.LOGIN()), t);
|
||||
checkAddUser200or409(r, resp1);
|
||||
@ -76,6 +78,38 @@ public class IT_01_AddUser {
|
||||
return r.summaryLine();
|
||||
}
|
||||
|
||||
private static void checkPingAndServerInfo(TestResult r, WsSession ws, Duration t) {
|
||||
String pingResp = ws.call("Ping", JsonBuilders.ping(System.currentTimeMillis()), t);
|
||||
if (JsonParsers.status(pingResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(pingResp))) {
|
||||
r.fail("Ping: ожидали status=200 и ok=true, resp=" + pingResp);
|
||||
fail("Ping unexpected response");
|
||||
}
|
||||
|
||||
Long serverTs = JsonParsers.pingTs(pingResp);
|
||||
if (serverTs == null || serverTs <= 0) {
|
||||
r.fail("Ping: сервер не вернул ts, resp=" + pingResp);
|
||||
fail("Ping missing ts");
|
||||
}
|
||||
r.ok("Ping: ok, serverTs=" + serverTs);
|
||||
|
||||
String infoResp = ws.call("GetServerInfo", JsonBuilders.getServerInfo(), t);
|
||||
if (JsonParsers.status(infoResp) != 200 || !Boolean.TRUE.equals(JsonParsers.ok(infoResp))) {
|
||||
r.fail("GetServerInfo: ожидали status=200 и ok=true, resp=" + infoResp);
|
||||
fail("GetServerInfo unexpected response");
|
||||
}
|
||||
if (!JsonParsers.payloadIsObject(infoResp)) {
|
||||
r.fail("GetServerInfo: payload должен быть объектом, resp=" + infoResp);
|
||||
fail("GetServerInfo payload is not object");
|
||||
}
|
||||
|
||||
r.ok("GetServerInfo: ok, url='" + safe(JsonParsers.payloadText(infoResp, "url"))
|
||||
+ "', version='" + safe(JsonParsers.payloadText(infoResp, "version"))
|
||||
+ "', physicalRegion='" + safe(JsonParsers.payloadText(infoResp, "physicalRegion"))
|
||||
+ "', description='" + safe(JsonParsers.payloadText(infoResp, "description"))
|
||||
+ "', origin='" + safe(JsonParsers.payloadText(infoResp, "origin"))
|
||||
+ "', extraInfo='" + safe(JsonParsers.payloadText(infoResp, "extraInfo")) + "'");
|
||||
}
|
||||
|
||||
private static void checkAddUser200or409(TestResult r, String resp) {
|
||||
int st = JsonParsers.status(resp);
|
||||
if (st == 200) {
|
||||
@ -324,4 +358,8 @@ public class IT_01_AddUser {
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String safe(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package test.it.runner;
|
||||
|
||||
import test.it.cases.IT_01_AddUser;
|
||||
import test.it.cases.IT_00_TechnicalRequests;
|
||||
import test.it.cases.IT_02_Sessions;
|
||||
import test.it.cases.IT_03_AddBlock_NoAuth;
|
||||
import test.it.cases.IT_04_UserParams_NoAuth;
|
||||
@ -37,6 +38,9 @@ public class IT_RunAllMain {
|
||||
TestLog.title("IT RUN: запуск всех тестов подряд"
|
||||
+ (STOP_ON_FIRST_FAIL ? " (STOP_ON_FIRST_FAIL=ON)" : " (STOP_ON_FIRST_FAIL=OFF)"));
|
||||
|
||||
String s0 = IT_00_TechnicalRequests.run(); summaries.add(s0);
|
||||
if (s0.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
|
||||
|
||||
String s1 = IT_01_AddUser.run(); summaries.add(s1);
|
||||
if (s1.contains("FAIL:")) { failed++; if (STOP_ON_FIRST_FAIL) return finishEarly(summaries, failed); }
|
||||
|
||||
@ -69,4 +73,4 @@ public class IT_RunAllMain {
|
||||
|
||||
return failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +90,35 @@ public final class JsonBuilders {
|
||||
""".formatted(requestId, login);
|
||||
}
|
||||
|
||||
// ---------------- Ping ----------------
|
||||
|
||||
public static String ping(long ts) {
|
||||
String requestId = TestIds.next("ping");
|
||||
return """
|
||||
{
|
||||
"op": "Ping",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"ts": %d
|
||||
}
|
||||
}
|
||||
""".formatted(requestId, ts);
|
||||
}
|
||||
|
||||
// ---------------- GetServerInfo ----------------
|
||||
|
||||
public static String getServerInfo() {
|
||||
String requestId = TestIds.next("serverinfo");
|
||||
return """
|
||||
{
|
||||
"op": "GetServerInfo",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
}
|
||||
}
|
||||
""".formatted(requestId);
|
||||
}
|
||||
|
||||
// ---------------- AuthChallenge ----------------
|
||||
|
||||
public static String authChallenge(String login) {
|
||||
|
||||
@ -132,6 +132,21 @@ public final class JsonParsers {
|
||||
}
|
||||
}
|
||||
|
||||
public static Long pingTs(String json) {
|
||||
try {
|
||||
JsonNode root = MAPPER.readTree(json);
|
||||
JsonNode payload = root.get("payload");
|
||||
if (payload != null && payload.has("ts")) return payload.get("ts").asLong();
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String payloadText(String json, String field) {
|
||||
return getPayloadText(json, field);
|
||||
}
|
||||
|
||||
public static List<String> sessionIds(String json) {
|
||||
List<String> res = new ArrayList<>();
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user