Доделал API функции для авторификации и работы с сессиями сервер и документ для разработчиков по

Авторификациии и серверам

Всё работает
This commit is contained in:
AidarKC
2026-03-27 22:06:19 +03:00
parent 51de9779e3
commit 1aabcf4d80
23 changed files with 1117 additions and 574 deletions
+72 -1
View File
@@ -1,6 +1,7 @@
package test.it.cases;
import test.it.utils.TestConfig;
import test.it.utils.TestIds;
import test.it.utils.json.JsonBuilders;
import test.it.utils.json.JsonParsers;
import test.it.utils.log.TestResult;
@@ -66,6 +67,8 @@ public class IT_01_AddUser {
r.ok("SearchUsers: prefix(3)='" + prefix3Mixed + "' (должен вернуть список и содержать " + TestConfig.LOGIN() + ")");
checkSearchUsersMustContain(r, ws, prefix3Mixed, TestConfig.LOGIN(), t);
checkNegativeRequests(r, ws, t);
} catch (Throwable e) {
r.fail("IT_01_AddUser упал: " + e.getMessage());
}
@@ -222,6 +225,74 @@ public class IT_01_AddUser {
r.ok("SearchUsers: ok, prefix=" + prefix + ", results=" + logins.size() + ", contains=" + expectedLogin);
}
private static void checkNegativeRequests(TestResult r, WsSession ws, Duration t) {
String badAddUserReqId = TestIds.next("bad-adduser");
String badAddUser = """
{
"op": "AddUser",
"requestId": "%s",
"payload": {
"login": "",
"blockchainName": "%s",
"solanaKey": "%s",
"blockchainKey": "%s",
"deviceKey": "%s",
"bchLimit": %d
}
}
""".formatted(
badAddUserReqId,
TestConfig.BCH_NAME(),
TestConfig.SOLANA_PUBKEY_B64(),
TestConfig.BLOCKCHAIN_PUBKEY_B64(),
TestConfig.DEVICE_PUBKEY_B64(),
TestConfig.TEST_BCH_LIMIT
);
String badAddUserResp = ws.call("AddUser#NEGATIVE", badAddUser, t);
assertErrorFormat(badAddUserResp, "AddUser", badAddUserReqId, "BAD_FIELDS");
r.ok("Negative AddUser: error format OK");
String badGetUserReqId = TestIds.next("bad-getuser");
String badGetUser = """
{
"op": "GetUser",
"requestId": "%s",
"payload": {
"login": ""
}
}
""".formatted(badGetUserReqId);
String badGetUserResp = ws.call("GetUser#NEGATIVE", badGetUser, t);
assertErrorFormat(badGetUserResp, "GetUser", badGetUserReqId, "BAD_FIELDS");
r.ok("Negative GetUser: error format OK");
String badSearchReqId = TestIds.next("bad-searchusers");
String badSearch = """
{
"op": "SearchUsers",
"requestId": "%s",
"payload": {
"prefix": ""
}
}
""".formatted(badSearchReqId);
String badSearchResp = ws.call("SearchUsers#NEGATIVE", badSearch, t);
assertErrorFormat(badSearchResp, "SearchUsers", badSearchReqId, "BAD_FIELDS");
r.ok("Negative SearchUsers: error format OK");
}
private static void assertErrorFormat(String resp, String op, String requestId, String code) {
int status = JsonParsers.status(resp);
if (status >= 200 && status < 300) fail("Expected non-2xx status: " + resp);
if (!Boolean.FALSE.equals(JsonParsers.ok(resp))) fail("Expected ok=false: " + resp);
if (!op.equals(JsonParsers.op(resp))) fail("Unexpected op: " + resp);
if (!requestId.equals(JsonParsers.requestId(resp))) fail("Unexpected requestId: " + resp);
if (!code.equals(JsonParsers.errorCode(resp))) fail("Unexpected error code: " + resp);
if (!JsonParsers.payloadIsObject(resp)) fail("payload must be object: " + resp);
if (JsonParsers.payloadSize(resp) != 0) fail("error payload must be empty object: " + resp);
if (isBlank(JsonParsers.message(resp))) fail("error message must be present: " + resp);
}
private static String canonicalLogin(String anyCaseLogin) {
if (anyCaseLogin == null) return null;
String x = anyCaseLogin.trim();
@@ -253,4 +324,4 @@ public class IT_01_AddUser {
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
}
}
+145 -5
View File
@@ -1,13 +1,16 @@
package test.it.cases;
import test.it.utils.TestConfig;
import test.it.utils.TestIds;
import test.it.utils.json.JsonBuilders;
import test.it.utils.json.JsonParsers;
import test.it.utils.log.TestLog;
import test.it.utils.log.TestResult;
import test.it.utils.ws.WsSession;
import utils.crypto.Ed25519Util;
import java.time.Duration;
import java.util.Base64;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@@ -54,6 +57,7 @@ public class IT_02_Sessions {
String listResp = ws.call("ListSessions(AUTH_STATUS_USER)", JsonBuilders.listSessions(0L, ""), t);
assertEquals(200, JsonParsers.status(listResp), "ListSessions(AUTH_STATUS_USER) must be 200");
assertEquals(Boolean.TRUE, JsonParsers.ok(listResp), "ListSessions(AUTH_STATUS_USER) ok must be true");
List<String> ids = JsonParsers.sessionIds(listResp);
r.ok("ListSessions(AUTH_STATUS_USER): " + ids);
@@ -82,6 +86,7 @@ public class IT_02_Sessions {
String listResp = ws.call("ListSessions(final)", JsonBuilders.listSessions(0L, ""), t);
assertEquals(200, JsonParsers.status(listResp));
assertEquals(Boolean.TRUE, JsonParsers.ok(listResp));
List<String> ids = JsonParsers.sessionIds(listResp);
r.ok("Final ListSessions: " + ids);
@@ -93,6 +98,8 @@ public class IT_02_Sessions {
r.ok("ИТОГ OK: после теста в БД остались 3 активные сессии (S1,S2,S3)");
}
checkNegativeRequests(t, r, s1);
} catch (Throwable e) {
r.fail("IT_02_Sessions(v2) упал: " + e.getMessage());
}
@@ -106,10 +113,11 @@ public class IT_02_Sessions {
// шаг 1: AuthChallenge
String nonceResp = ws.call("AuthChallenge(" + label + ")", JsonBuilders.authChallenge(login), t);
assertEquals(200, JsonParsers.status(nonceResp), "AuthChallenge(" + label + ") must be 200");
assertEquals(Boolean.TRUE, JsonParsers.ok(nonceResp), "AuthChallenge(" + label + ") ok must be true");
String authNonce = JsonParsers.authNonce(nonceResp);
assertNotNull(authNonce, "authNonce must not be null for " + label);
String sessionKey = TestConfig.sessionKey(login);
SessionMaterial sessionMaterial = newSessionMaterial();
// storagePwd на клиенте (сохраняем, чтобы потом проверить, что сервер вернул именно его)
String storagePwd = TestConfig.fakeStoragePwd();
@@ -117,19 +125,18 @@ public class IT_02_Sessions {
// шаг 2: CreateAuthSession (device подпись + deviceKey + sessionKey)
String createResp = ws.call(
"CreateAuthSession(" + label + ")",
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionKey),
JsonBuilders.createAuthSessionV2(login, authNonce, storagePwd, sessionMaterial.sessionKey()),
t
);
assertEquals(200, JsonParsers.status(createResp), "CreateAuthSession(" + label + ") must be 200");
assertEquals(Boolean.TRUE, JsonParsers.ok(createResp), "CreateAuthSession(" + label + ") ok must be true");
String sid = JsonParsers.sessionId(createResp);
assertNotNull(sid, "sessionId must not be null");
r.ok("Создана сессия " + label + ": sessionId=" + sid);
byte[] sessionPrivKey = TestConfig.getSessionPrivatKey(login);
return new Session(sid, sessionKey, sessionPrivKey, storagePwd);
return new Session(sid, sessionMaterial.sessionKey(), sessionMaterial.sessionPrivKey(), storagePwd);
}
}
@@ -137,12 +144,14 @@ public class IT_02_Sessions {
// шаг 1: SessionChallenge(sessionId)
String chResp = ws.call("SessionChallenge " + label, JsonBuilders.sessionChallenge(s.sessionId), t);
assertEquals(200, JsonParsers.status(chResp), "SessionChallenge must be 200");
assertEquals(Boolean.TRUE, JsonParsers.ok(chResp), "SessionChallenge ok must be true");
String nonce = JsonParsers.sessionNonce(chResp);
assertNotNull(nonce, "SessionChallenge nonce must not be null");
// шаг 2: SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
String loginResp = ws.call("SessionLogin " + label, JsonBuilders.sessionLogin(s.sessionId, s.sessionKey, nonce, s.sessionPrivKey), t);
assertEquals(200, JsonParsers.status(loginResp), "SessionLogin must be 200");
assertEquals(Boolean.TRUE, JsonParsers.ok(loginResp), "SessionLogin ok must be true");
String storagePwd = JsonParsers.storagePwd(loginResp);
assertNotNull(storagePwd, "storagePwd must not be null after SessionLogin");
@@ -151,5 +160,136 @@ public class IT_02_Sessions {
r.ok(label + ": SessionLogin OK, storagePwd verified");
}
private static void checkNegativeRequests(Duration t, TestResult r, Session session) {
try (WsSession ws = WsSession.open()) {
String reqId = TestIds.next("bad-authchallenge");
String badReq = """
{
"op": "AuthChallenge",
"requestId": "%s",
"payload": {
"login": "NoSuchUser_123456"
}
}
""".formatted(reqId);
String resp = ws.call("AuthChallenge#NEGATIVE", badReq, t);
assertErrorFormat(resp, "AuthChallenge", reqId, "UNKNOWN_USER");
r.ok("Negative AuthChallenge: error format OK");
}
try (WsSession ws = WsSession.open()) {
String nonceResp = ws.call("AuthChallenge(NEG_CREATE)", JsonBuilders.authChallenge(LOGIN), t);
assertEquals(200, JsonParsers.status(nonceResp));
String authNonce = JsonParsers.authNonce(nonceResp);
SessionMaterial badSession = newSessionMaterial();
String reqId = TestIds.next("bad-create");
String badCreate = """
{
"op": "CreateAuthSession",
"requestId": "%s",
"payload": {
"login": "%s",
"sessionKey": "%s",
"storagePwd": "%s",
"timeMs": %d,
"authNonce": "%s",
"deviceKey": "%s",
"signatureB64": "%s",
"clientInfo": "%s"
}
}
""".formatted(
reqId,
LOGIN,
badSession.sessionKey(),
TestConfig.fakeStoragePwd(),
System.currentTimeMillis(),
authNonce,
"WRONG_DEVICE_KEY",
"AAAA",
TestConfig.TEST_CLIENT_INFO
);
String resp = ws.call("CreateAuthSession#NEGATIVE", badCreate, t);
assertErrorFormat(resp, "CreateAuthSession", reqId, "DEVICE_KEY_NOT_ACTUAL");
r.ok("Negative CreateAuthSession: error format OK");
}
try (WsSession ws = WsSession.open()) {
String reqId = TestIds.next("bad-sessionchallenge");
String badReq = """
{
"op": "SessionChallenge",
"requestId": "%s",
"payload": {
"sessionId": "missing-session-id"
}
}
""".formatted(reqId);
String resp = ws.call("SessionChallenge#NEGATIVE", badReq, t);
assertErrorFormat(resp, "SessionChallenge", reqId, "SESSION_NOT_FOUND");
r.ok("Negative SessionChallenge: error format OK");
}
try (WsSession ws = WsSession.open()) {
String chResp = ws.call("SessionChallenge NEG_LOGIN", JsonBuilders.sessionChallenge(session.sessionId), t);
assertEquals(200, JsonParsers.status(chResp));
String nonce = JsonParsers.sessionNonce(chResp);
SessionMaterial wrongSession = newSessionMaterial();
long timeMs = System.currentTimeMillis();
String signatureB64 = JsonBuilders.signSessionLogin(session.sessionId, timeMs, nonce, wrongSession.sessionPrivKey());
String reqId = TestIds.next("bad-sessionlogin");
String badLoginReq = """
{
"op": "SessionLogin",
"requestId": "%s",
"payload": {
"sessionId": "%s",
"sessionKey": "%s",
"timeMs": %d,
"signatureB64": "%s",
"clientInfo": "%s"
}
}
""".formatted(
reqId,
session.sessionId,
wrongSession.sessionKey(),
timeMs,
signatureB64,
TestConfig.TEST_CLIENT_INFO
);
String badLoginResp = ws.call("SessionLogin#NEGATIVE", badLoginReq, t);
assertErrorFormat(
badLoginResp,
"SessionLogin",
reqId,
"SESSION_KEY_NOT_ACTUAL"
);
r.ok("Negative SessionLogin: error format OK");
}
}
private static void assertErrorFormat(String resp, String op, String requestId, String code) {
int status = JsonParsers.status(resp);
assertFalse(status >= 200 && status < 300, "Expected non-2xx status: " + resp);
assertEquals(Boolean.FALSE, JsonParsers.ok(resp), "Expected ok=false: " + resp);
assertEquals(op, JsonParsers.op(resp), "Unexpected op: " + resp);
assertEquals(requestId, JsonParsers.requestId(resp), "Unexpected requestId: " + resp);
assertEquals(code, JsonParsers.errorCode(resp), "Unexpected error code: " + resp);
assertTrue(JsonParsers.payloadIsObject(resp), "payload must be object: " + resp);
assertEquals(0, JsonParsers.payloadSize(resp), "error payload must be empty object: " + resp);
assertNotNull(JsonParsers.message(resp), "message must be present: " + resp);
}
private static SessionMaterial newSessionMaterial() {
byte[] sessionPrivKey = Ed25519Util.generatePrivateKey();
byte[] sessionPubKey = Ed25519Util.derivePublicKey(sessionPrivKey);
String sessionKey = "ed25519/" + Base64.getEncoder().encodeToString(sessionPubKey);
return new SessionMaterial(sessionKey, sessionPrivKey);
}
private record Session(String sessionId, String sessionKey, byte[] sessionPrivKey, String storagePwd) {}
private record SessionMaterial(String sessionKey, byte[] sessionPrivKey) {}
}
@@ -19,6 +19,62 @@ public final class JsonParsers {
}
}
public static String op(String json) {
try {
JsonNode root = MAPPER.readTree(json);
return root.has("op") && !root.get("op").isNull() ? root.get("op").asText() : null;
} catch (Exception e) {
return null;
}
}
public static String requestId(String json) {
try {
JsonNode root = MAPPER.readTree(json);
return root.has("requestId") && !root.get("requestId").isNull() ? root.get("requestId").asText() : null;
} catch (Exception e) {
return null;
}
}
public static Boolean ok(String json) {
try {
JsonNode root = MAPPER.readTree(json);
return root.has("ok") ? root.get("ok").asBoolean() : null;
} catch (Exception e) {
return null;
}
}
public static String message(String json) {
try {
JsonNode root = MAPPER.readTree(json);
return root.has("message") && !root.get("message").isNull() ? root.get("message").asText() : null;
} catch (Exception e) {
return null;
}
}
public static boolean payloadIsObject(String json) {
try {
JsonNode root = MAPPER.readTree(json);
JsonNode payload = root.get("payload");
return payload != null && payload.isObject();
} catch (Exception e) {
return false;
}
}
public static int payloadSize(String json) {
try {
JsonNode root = MAPPER.readTree(json);
JsonNode payload = root.get("payload");
return payload != null && payload.isObject() ? payload.size() : -1;
} catch (Exception e) {
return -1;
}
}
public static String authNonce(String json) {
try {
JsonNode root = MAPPER.readTree(json);
@@ -97,6 +153,7 @@ public final class JsonParsers {
try {
JsonNode root = MAPPER.readTree(json);
if (root.has("error")) return root.get("error").asText();
// поддержка старого формата (верхний уровень)
if (root.has("errorCode")) return root.get("errorCode").asText();
// поддержка нового формата (верхний уровень)
@@ -106,6 +163,7 @@ public final class JsonParsers {
if (payload != null) {
// поддержка старого формата (внутри payload)
if (payload.has("errorCode")) return payload.get("errorCode").asText();
if (payload.has("error")) return payload.get("error").asText();
// поддержка нового формата (внутри payload)
if (payload.has("code")) return payload.get("code").asText();
}
@@ -214,4 +272,4 @@ public final class JsonParsers {
return null;
}
}
}
}