SHA256
27 03 25
Доделал API функции для авторификации и работы с сессиями сервер и документ для разработчиков по Авторификациии и серверам Всё работает
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user