1032 lines
47 KiB
Java
1032 lines
47 KiB
Java
package Test;
|
||
|
||
import com.fasterxml.jackson.databind.JsonNode;
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import utils.crypto.Ed25519Util;
|
||
|
||
import java.net.URI;
|
||
import java.net.http.HttpClient;
|
||
import java.net.http.WebSocket;
|
||
import java.net.http.WebSocket.Listener;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.Base64;
|
||
import java.util.List;
|
||
import java.util.ArrayList;
|
||
import java.util.concurrent.CompletableFuture;
|
||
import java.util.concurrent.CompletionStage;
|
||
import java.util.concurrent.CountDownLatch;
|
||
|
||
/**
|
||
* Большой сценарий тестирования авторизации и работы с сессиями:
|
||
*
|
||
* 1) AddUser — создаём пользователя в локальной БД.
|
||
*
|
||
* 2) Сессия 1:
|
||
* - AuthChallenge + CreateAuthSession → первая сессия (SESSION1_ID/SESSION1_PWD).
|
||
*
|
||
* 3) Сессия 2:
|
||
* - AuthChallenge + CreateAuthSession → вторая сессия (SESSION2_ID/SESSION2_PWD).
|
||
* - ListSessions (внутри второй сессии, AUTH_STATUS_USER).
|
||
*
|
||
* 4) Новое подключение:
|
||
* - AuthChallenge → AUTH_IN_PROGRESS.
|
||
* - ListSessions c timeMs + signatureB64 (подпись по authNonce).
|
||
*
|
||
* 5) Новое подключение:
|
||
* - RefreshSession по первой сессии.
|
||
* - CloseActiveSession по второй сессии (закрываем SESSION2_ID).
|
||
*
|
||
* 6) Новое подключение:
|
||
* - AuthChallenge → AUTH_IN_PROGRESS.
|
||
* - ListSessions (ожидаем, что вторая сессия исчезла, осталась только первая).
|
||
*
|
||
* 7) Новое подключение:
|
||
* - AuthChallenge → AUTH_IN_PROGRESS.
|
||
* - CloseActiveSession по первой сессии (SESSION1_ID) без Refresh.
|
||
*
|
||
* 8) Новое подключение:
|
||
* - AuthChallenge → AUTH_IN_PROGRESS.
|
||
* - ListSessions (ожидаем пустой список сессий).
|
||
*/
|
||
public class Test_AddUser_and_Authorification {
|
||
|
||
// Адрес сервера
|
||
private static final String WS_URI = "ws://localhost:7070/ws";
|
||
|
||
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
|
||
|
||
// Тестовые данные пользователя
|
||
private static final String TEST_LOGIN = "anya24";
|
||
private static final long TEST_LOGIN_ID = 1030120L;
|
||
private static final long TEST_BCH_ID = 4222L;
|
||
private static final int TEST_BCH_LIMIT = 1_000_000;
|
||
|
||
// Краткая строка clientInfo, которую клиент шлёт
|
||
private static final String TEST_CLIENT_INFO = "JavaTestClient/1.0";
|
||
|
||
// --- Тестовые пары ключей ---
|
||
// loginKey — ключ аккаунта (например, "основной")
|
||
// deviceKey — ключ устройства, которым подписываем авторизацию / управление сессиями
|
||
|
||
private static final byte[] LOGIN_PRIV_KEY;
|
||
private static final String LOGIN_PUBKEY_B64;
|
||
|
||
private static final byte[] DEVICE_PRIV_KEY;
|
||
private static final String DEVICE_PUBKEY_B64;
|
||
|
||
static {
|
||
// Детерминированное "семя" для логин-ключа
|
||
LOGIN_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-login-11" + TEST_LOGIN);
|
||
byte[] loginPub = Ed25519Util.derivePublicKey(LOGIN_PRIV_KEY);
|
||
LOGIN_PUBKEY_B64 = Ed25519Util.keyToBase64(loginPub);
|
||
|
||
// Детерминированное "семя" для девайс-ключа
|
||
DEVICE_PRIV_KEY = Ed25519Util.generatePrivateKeyFromString("test-ed25519-device-" + TEST_LOGIN);
|
||
byte[] devicePub = Ed25519Util.derivePublicKey(DEVICE_PRIV_KEY);
|
||
DEVICE_PUBKEY_B64 = Ed25519Util.keyToBase64(devicePub);
|
||
}
|
||
|
||
// --- Глобальные переменные между сценариями ---
|
||
|
||
/** Первая сессия (создана в сценарии 1). */
|
||
private static String SESSION1_ID;
|
||
private static String SESSION1_PWD;
|
||
private static String SESSION1_STORAGE_PWD;
|
||
|
||
/** Вторая сессия (создана в сценарии 2). */
|
||
private static String SESSION2_ID;
|
||
private static String SESSION2_PWD;
|
||
private static String SESSION2_STORAGE_PWD;
|
||
|
||
public static void main(String[] args) throws Exception {
|
||
System.out.println("Подключаемся к " + WS_URI);
|
||
|
||
scenario1_AddUser_And_CreateFirstSession();
|
||
|
||
scenario2_CreateSecondSession_And_ListInside();
|
||
|
||
scenario3_ListSessions_AuthInProgress("S3: ListSessions (AUTH_IN_PROGRESS, две сессии ожидаются)", true, true);
|
||
|
||
scenario4_RefreshFirstSession_And_CloseSecond();
|
||
|
||
scenario5_ListSessions_AuthInProgress_AfterClosingSecond();
|
||
|
||
scenario6_CloseFirstSession_AuthInProgress();
|
||
|
||
scenario7_ListSessions_AuthInProgress_NoSessions();
|
||
|
||
System.out.println("\n\nВсе сценарии завершены, выходим.");
|
||
}
|
||
|
||
// ==========================================================
|
||
// SCENARIO 1
|
||
// ==========================================================
|
||
|
||
private static void scenario1_AddUser_And_CreateFirstSession() throws Exception {
|
||
printSection("СЦЕНАРИЙ 1: AddUser + AuthChallenge + CreateAuthSession (первая сессия)");
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
client.newWebSocketBuilder()
|
||
.buildAsync(URI.create(WS_URI), new Listener() {
|
||
|
||
private int step = 0; // 0 - AddUser, 1 - AuthChallenge, 2 - CreateAuthSession
|
||
private String authNonceLocal;
|
||
|
||
@Override
|
||
public void onOpen(WebSocket webSocket) {
|
||
System.out.println("✅ [S1] WebSocket подключен");
|
||
webSocket.request(1);
|
||
|
||
String json = buildAddUserJson();
|
||
System.out.println("\n📤 [S1 / Шаг 1] Отправляем AddUser:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
Listener.super.onOpen(webSocket);
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onText(WebSocket webSocket,
|
||
CharSequence data,
|
||
boolean last) {
|
||
String message = data.toString();
|
||
System.out.println("\n📥 [S1] Ответ на шаг " + (step + 1) + ":");
|
||
System.out.println(message);
|
||
System.out.println("-----------------------------------------------------");
|
||
|
||
try {
|
||
if (step == 0) {
|
||
// Ответ на AddUser
|
||
int status = extractStatus(message);
|
||
boolean ok = (status == 200);
|
||
printTestResult(
|
||
"S1/AddUser",
|
||
ok,
|
||
"status=" + status + (ok ? " (пользователь создан/добавлен)" : " (ожидали 200)")
|
||
);
|
||
|
||
// Переходим к AuthChallenge
|
||
step = 1;
|
||
String json = buildAuthStep1Json();
|
||
System.out.println("\n📤 [S1 / Шаг 2] Отправляем AuthChallenge:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 1) {
|
||
// Ответ на AuthChallenge
|
||
int status = extractStatus(message);
|
||
String nonce = extractAuthNonce(message);
|
||
boolean ok = (status == 200 && nonce != null && !nonce.isBlank());
|
||
printTestResult(
|
||
"S1/AuthChallenge",
|
||
ok,
|
||
"status=" + status + ", authNonce=" + nonce
|
||
);
|
||
|
||
authNonceLocal = nonce;
|
||
|
||
// Переходим к CreateAuthSession
|
||
step = 2;
|
||
SESSION1_STORAGE_PWD = generateFakeStoragePwd();
|
||
String json = buildAuthStep2Json(authNonceLocal, SESSION1_STORAGE_PWD);
|
||
System.out.println("\n📤 [S1 / Шаг 3] Отправляем CreateAuthSession (первая сессия):");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 2) {
|
||
// Ответ на CreateAuthSession — здесь мы получаем SESSION1_ID / SESSION1_PWD
|
||
int status = extractStatus(message);
|
||
String sessionId = extractSessionId(message);
|
||
String sessionPwd = extractSessionPwd(message);
|
||
|
||
boolean ok = (status == 200
|
||
&& sessionId != null && !sessionId.isBlank()
|
||
&& sessionPwd != null && !sessionPwd.isBlank());
|
||
|
||
SESSION1_ID = sessionId;
|
||
SESSION1_PWD = sessionPwd;
|
||
|
||
printTestResult(
|
||
"S1/CreateAuthSession (первая сессия)",
|
||
ok,
|
||
"status=" + status +
|
||
", sessionId=" + sessionId +
|
||
", sessionPwd=" + (sessionPwd != null ? "[получен]" : "null")
|
||
);
|
||
|
||
System.out.println("🆔 [S1] SESSION1_ID=" + SESSION1_ID);
|
||
System.out.println("🔐 [S1] SESSION1_PWD=" + SESSION1_PWD);
|
||
|
||
step = 3;
|
||
System.out.println("✅ [S1] Все шаги выполнены, закрываем соединение");
|
||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario1 done");
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
e.printStackTrace(System.out);
|
||
}
|
||
|
||
webSocket.request(1);
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
|
||
@Override
|
||
public void onError(WebSocket webSocket, Throwable error) {
|
||
System.out.println("❌ [S1] Ошибка WebSocket-клиента: " + error.getMessage());
|
||
error.printStackTrace(System.out);
|
||
latch.countDown();
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onClose(WebSocket webSocket,
|
||
int statusCode,
|
||
String reason) {
|
||
System.out.println("🔚 [S1] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||
latch.countDown();
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
}).join();
|
||
|
||
latch.await();
|
||
}
|
||
|
||
// ==========================================================
|
||
// SCENARIO 2
|
||
// ==========================================================
|
||
|
||
private static void scenario2_CreateSecondSession_And_ListInside() throws Exception {
|
||
printSection("СЦЕНАРИЙ 2: Создать вторую сессию и внутри неё вызвать ListSessions");
|
||
|
||
if (SESSION1_ID == null || SESSION1_PWD == null) {
|
||
System.out.println("⚠️ [S2] Первая сессия не создана, пропускаем сценарий 2.");
|
||
return;
|
||
}
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
client.newWebSocketBuilder()
|
||
.buildAsync(URI.create(WS_URI), new Listener() {
|
||
|
||
private int step = 0; // 0 - AuthChallenge, 1 - CreateAuthSession(вторая), 2 - ListSessions
|
||
private String authNonceLocal;
|
||
|
||
@Override
|
||
public void onOpen(WebSocket webSocket) {
|
||
System.out.println("✅ [S2] WebSocket подключен");
|
||
webSocket.request(1);
|
||
|
||
String json = buildAuthStep1Json();
|
||
System.out.println("\n📤 [S2 / Шаг 1] Отправляем AuthChallenge:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
Listener.super.onOpen(webSocket);
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onText(WebSocket webSocket,
|
||
CharSequence data,
|
||
boolean last) {
|
||
String message = data.toString();
|
||
System.out.println("\n📥 [S2] Ответ на шаг " + (step + 1) + ":");
|
||
System.out.println(message);
|
||
System.out.println("-----------------------------------------------------");
|
||
|
||
try {
|
||
if (step == 0) {
|
||
int status = extractStatus(message);
|
||
String nonce = extractAuthNonce(message);
|
||
boolean ok = (status == 200 && nonce != null && !nonce.isBlank());
|
||
printTestResult(
|
||
"S2/AuthChallenge",
|
||
ok,
|
||
"status=" + status + ", authNonce=" + nonce
|
||
);
|
||
authNonceLocal = nonce;
|
||
|
||
step = 1;
|
||
SESSION2_STORAGE_PWD = generateFakeStoragePwd();
|
||
String json = buildAuthStep2Json(authNonceLocal, SESSION2_STORAGE_PWD);
|
||
System.out.println("\n📤 [S2 / Шаг 2] Отправляем CreateAuthSession (вторая сессия):");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 1) {
|
||
int status = extractStatus(message);
|
||
String sessionId = extractSessionId(message);
|
||
String sessionPwd = extractSessionPwd(message);
|
||
|
||
boolean ok = (status == 200
|
||
&& sessionId != null && !sessionId.isBlank()
|
||
&& sessionPwd != null && !sessionPwd.isBlank());
|
||
|
||
SESSION2_ID = sessionId;
|
||
SESSION2_PWD = sessionPwd;
|
||
|
||
printTestResult(
|
||
"S2/CreateAuthSession (вторая сессия)",
|
||
ok,
|
||
"status=" + status +
|
||
", sessionId=" + sessionId +
|
||
", sessionPwd=" + (sessionPwd != null ? "[получен]" : "null")
|
||
);
|
||
|
||
System.out.println("🆔 [S2] SESSION2_ID=" + SESSION2_ID);
|
||
System.out.println("🔐 [S2] SESSION2_PWD=" + SESSION2_PWD);
|
||
|
||
// Теперь вызываем ListSessions внутри второй сессии (AUTH_STATUS_USER)
|
||
step = 2;
|
||
String json = buildListSessionsJson(0L, "", "test-list-in-session2");
|
||
System.out.println("\n📤 [S2 / Шаг 3] Отправляем ListSessions (внутри второй сессии):");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 2) {
|
||
int status = extractStatus(message);
|
||
List<String> sessionIds = extractSessionIds(message);
|
||
|
||
boolean has1 = sessionIds.contains(SESSION1_ID);
|
||
boolean has2 = sessionIds.contains(SESSION2_ID);
|
||
|
||
boolean ok = (status == 200 && has1 && has2);
|
||
|
||
printTestResult(
|
||
"S2/ListSessions (ожидаем 1 и 2 сессии)",
|
||
ok,
|
||
"status=" + status +
|
||
", sessions=" + sessionIds +
|
||
", contains SESSION1=" + has1 +
|
||
", contains SESSION2=" + has2
|
||
);
|
||
|
||
step = 3;
|
||
System.out.println("✅ [S2] Все шаги выполнены, закрываем соединение");
|
||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario2 done");
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
e.printStackTrace(System.out);
|
||
}
|
||
|
||
webSocket.request(1);
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
|
||
@Override
|
||
public void onError(WebSocket webSocket, Throwable error) {
|
||
System.out.println("❌ [S2] Ошибка WebSocket-клиента: " + error.getMessage());
|
||
error.printStackTrace(System.out);
|
||
latch.countDown();
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onClose(WebSocket webSocket,
|
||
int statusCode,
|
||
String reason) {
|
||
System.out.println("🔚 [S2] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||
latch.countDown();
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
}).join();
|
||
|
||
latch.await();
|
||
}
|
||
|
||
// ==========================================================
|
||
// SCENARIO 3 / 5 / 7: ListSessions
|
||
// ==========================================================
|
||
|
||
/**
|
||
* Общий сценарий: AuthChallenge → ListSessions в статусе AUTH_IN_PROGRESS.
|
||
*
|
||
* @param title заголовок для вывода
|
||
* @param expectSession1Present ожидать ли первую сессию в списке
|
||
* @param expectSession2Present ожидать ли вторую сессию в списке
|
||
*/
|
||
private static void scenario3_ListSessions_AuthInProgress(
|
||
String title,
|
||
boolean expectSession1Present,
|
||
boolean expectSession2Present
|
||
) throws Exception {
|
||
|
||
printSection(title);
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
client.newWebSocketBuilder()
|
||
.buildAsync(URI.create(WS_URI), new Listener() {
|
||
|
||
private int step = 0; // 0 - AuthChallenge, 1 - ListSessions (AUTH_IN_PROGRESS)
|
||
private String authNonceLocal;
|
||
|
||
@Override
|
||
public void onOpen(WebSocket webSocket) {
|
||
System.out.println("✅ [S-List] WebSocket подключен");
|
||
webSocket.request(1);
|
||
|
||
String json = buildAuthStep1Json();
|
||
System.out.println("\n📤 [S-List / Шаг 1] Отправляем AuthChallenge:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
Listener.super.onOpen(webSocket);
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onText(WebSocket webSocket,
|
||
CharSequence data,
|
||
boolean last) {
|
||
String message = data.toString();
|
||
System.out.println("\n📥 [S-List] Ответ на шаг " + (step + 1) + ":");
|
||
System.out.println(message);
|
||
System.out.println("-----------------------------------------------------");
|
||
|
||
try {
|
||
if (step == 0) {
|
||
int status = extractStatus(message);
|
||
String nonce = extractAuthNonce(message);
|
||
boolean ok = (status == 200 && nonce != null && !nonce.isBlank());
|
||
printTestResult(
|
||
"S-List/AuthChallenge",
|
||
ok,
|
||
"status=" + status + ", authNonce=" + nonce
|
||
);
|
||
authNonceLocal = nonce;
|
||
|
||
// Теперь в статусе AUTH_IN_PROGRESS вызываем ListSessions
|
||
long timeMs = System.currentTimeMillis();
|
||
String sig = signAuthorificated(authNonceLocal, timeMs);
|
||
|
||
step = 1;
|
||
String json = buildListSessionsJson(timeMs, sig, "test-list-auth-in-progress");
|
||
System.out.println("\n📤 [S-List / Шаг 2] Отправляем ListSessions (AUTH_IN_PROGRESS):");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 1) {
|
||
int status = extractStatus(message);
|
||
List<String> sessionIds = extractSessionIds(message);
|
||
|
||
boolean has1 = (SESSION1_ID != null && sessionIds.contains(SESSION1_ID));
|
||
boolean has2 = (SESSION2_ID != null && sessionIds.contains(SESSION2_ID));
|
||
|
||
boolean ok =
|
||
status == 200
|
||
&& (expectSession1Present == has1)
|
||
&& (expectSession2Present == has2);
|
||
|
||
printTestResult(
|
||
"S-List/ListSessions (ожидаемые сессии)",
|
||
ok,
|
||
"status=" + status +
|
||
", sessions=" + sessionIds +
|
||
", expect1=" + expectSession1Present + ", has1=" + has1 +
|
||
", expect2=" + expectSession2Present + ", has2=" + has2
|
||
);
|
||
|
||
step = 2;
|
||
System.out.println("✅ [S-List] Все шаги выполнены, закрываем соединение");
|
||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario-list done");
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
e.printStackTrace(System.out);
|
||
}
|
||
|
||
webSocket.request(1);
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
|
||
@Override
|
||
public void onError(WebSocket webSocket, Throwable error) {
|
||
System.out.println("❌ [S-List] Ошибка WebSocket-клиента: " + error.getMessage());
|
||
error.printStackTrace(System.out);
|
||
latch.countDown();
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onClose(WebSocket webSocket,
|
||
int statusCode,
|
||
String reason) {
|
||
System.out.println("🔚 [S-List] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||
latch.countDown();
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
}).join();
|
||
|
||
latch.await();
|
||
}
|
||
|
||
private static void scenario5_ListSessions_AuthInProgress_AfterClosingSecond() throws Exception {
|
||
scenario3_ListSessions_AuthInProgress(
|
||
"СЦЕНАРИЙ 5: ListSessions (AUTH_IN_PROGRESS) после закрытия второй сессии — должна остаться только первая",
|
||
true,
|
||
false
|
||
);
|
||
}
|
||
|
||
private static void scenario7_ListSessions_AuthInProgress_NoSessions() throws Exception {
|
||
scenario3_ListSessions_AuthInProgress(
|
||
"СЦЕНАРИЙ 7: ListSessions (AUTH_IN_PROGRESS) после закрытия обеих сессий — ожидаем пустой список",
|
||
false,
|
||
false
|
||
);
|
||
}
|
||
|
||
// ==========================================================
|
||
// SCENARIO 4
|
||
// ==========================================================
|
||
|
||
private static void scenario4_RefreshFirstSession_And_CloseSecond() throws Exception {
|
||
printSection("СЦЕНАРИЙ 4: Refresh первой сессии и Close второй сессии (из первой)");
|
||
|
||
if (SESSION1_ID == null || SESSION1_PWD == null || SESSION2_ID == null) {
|
||
System.out.println("⚠️ [S4] Нет нужных сессий (SESSION1/SESSION2), пропускаем сценарий 4.");
|
||
return;
|
||
}
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
client.newWebSocketBuilder()
|
||
.buildAsync(URI.create(WS_URI), new Listener() {
|
||
|
||
private int step = 0; // 0 - Refresh(1), 1 - Close(2)
|
||
|
||
@Override
|
||
public void onOpen(WebSocket webSocket) {
|
||
System.out.println("✅ [S4] WebSocket подключен");
|
||
webSocket.request(1);
|
||
|
||
String json = buildRefreshSessionJson(SESSION1_ID, SESSION1_PWD, "test-refresh-session1");
|
||
System.out.println("\n📤 [S4 / Шаг 1] Отправляем RefreshSession для SESSION1:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
Listener.super.onOpen(webSocket);
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onText(WebSocket webSocket,
|
||
CharSequence data,
|
||
boolean last) {
|
||
String message = data.toString();
|
||
System.out.println("\n📥 [S4] Ответ на шаг " + (step + 1) + ":");
|
||
System.out.println(message);
|
||
System.out.println("-----------------------------------------------------");
|
||
|
||
try {
|
||
if (step == 0) {
|
||
int status = extractStatus(message);
|
||
String storagePwd = extractStoragePwd(message);
|
||
boolean ok = (status == 200 && storagePwd != null);
|
||
printTestResult(
|
||
"S4/RefreshSession (SESSION1)",
|
||
ok,
|
||
"status=" + status + ", storagePwd=" + (storagePwd != null ? "[получен]" : "null")
|
||
);
|
||
|
||
// Теперь, находясь внутри первой сессии (AUTH_STATUS_USER),
|
||
// закрываем вторую сессию
|
||
step = 1;
|
||
String json = buildCloseSessionJson(
|
||
SESSION2_ID,
|
||
0L,
|
||
"",
|
||
"test-close-session2-from-session1"
|
||
);
|
||
System.out.println("\n📤 [S4 / Шаг 2] Отправляем CloseActiveSession для SESSION2:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 1) {
|
||
int status = extractStatus(message);
|
||
boolean ok = (status == 200);
|
||
printTestResult(
|
||
"S4/CloseActiveSession (SESSION2)",
|
||
ok,
|
||
"status=" + status + " (ожидали 200)"
|
||
);
|
||
|
||
step = 2;
|
||
System.out.println("✅ [S4] Все шаги выполнены, закрываем соединение");
|
||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario4 done");
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace(System.out);
|
||
}
|
||
|
||
webSocket.request(1);
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
|
||
@Override
|
||
public void onError(WebSocket webSocket, Throwable error) {
|
||
System.out.println("❌ [S4] Ошибка WebSocket-клиента: " + error.getMessage());
|
||
error.printStackTrace(System.out);
|
||
latch.countDown();
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onClose(WebSocket webSocket,
|
||
int statusCode,
|
||
String reason) {
|
||
System.out.println("🔚 [S4] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||
latch.countDown();
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
}).join();
|
||
|
||
latch.await();
|
||
}
|
||
|
||
// ==========================================================
|
||
// SCENARIO 6
|
||
// ==========================================================
|
||
|
||
private static void scenario6_CloseFirstSession_AuthInProgress() throws Exception {
|
||
printSection("СЦЕНАРИЙ 6: Close первой сессии (SESSION1) в статусе AUTH_IN_PROGRESS без Refresh");
|
||
|
||
if (SESSION1_ID == null) {
|
||
System.out.println("⚠️ [S6] Первая сессия не создана, пропускаем сценарий 6.");
|
||
return;
|
||
}
|
||
|
||
CountDownLatch latch = new CountDownLatch(1);
|
||
HttpClient client = HttpClient.newHttpClient();
|
||
|
||
client.newWebSocketBuilder()
|
||
.buildAsync(URI.create(WS_URI), new Listener() {
|
||
|
||
private int step = 0; // 0 - AuthChallenge, 1 - CloseActiveSession(SESSION1)
|
||
private String authNonceLocal;
|
||
|
||
@Override
|
||
public void onOpen(WebSocket webSocket) {
|
||
System.out.println("✅ [S6] WebSocket подключен");
|
||
webSocket.request(1);
|
||
|
||
String json = buildAuthStep1Json();
|
||
System.out.println("\n📤 [S6 / Шаг 1] Отправляем AuthChallenge:");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
Listener.super.onOpen(webSocket);
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onText(WebSocket webSocket,
|
||
CharSequence data,
|
||
boolean last) {
|
||
String message = data.toString();
|
||
System.out.println("\n📥 [S6] Ответ на шаг " + (step + 1) + ":");
|
||
System.out.println(message);
|
||
System.out.println("-----------------------------------------------------");
|
||
|
||
try {
|
||
if (step == 0) {
|
||
int status = extractStatus(message);
|
||
String nonce = extractAuthNonce(message);
|
||
boolean ok = (status == 200 && nonce != null && !nonce.isBlank());
|
||
printTestResult(
|
||
"S6/AuthChallenge",
|
||
ok,
|
||
"status=" + status + ", authNonce=" + nonce
|
||
);
|
||
authNonceLocal = nonce;
|
||
|
||
// Теперь в AUTH_IN_PROGRESS закрываем первую сессию
|
||
long timeMs = System.currentTimeMillis();
|
||
String sig = signAuthorificated(authNonceLocal, timeMs);
|
||
|
||
step = 1;
|
||
String json = buildCloseSessionJson(
|
||
SESSION1_ID,
|
||
timeMs,
|
||
sig,
|
||
"test-close-session1-auth-in-progress"
|
||
);
|
||
System.out.println("\n📤 [S6 / Шаг 2] Отправляем CloseActiveSession для SESSION1 (AUTH_IN_PROGRESS):");
|
||
System.out.println(json);
|
||
webSocket.sendText(json, true);
|
||
|
||
} else if (step == 1) {
|
||
int status = extractStatus(message);
|
||
boolean ok = (status == 200);
|
||
printTestResult(
|
||
"S6/CloseActiveSession (SESSION1, AUTH_IN_PROGRESS)",
|
||
ok,
|
||
"status=" + status + " (ожидали 200)"
|
||
);
|
||
|
||
step = 2;
|
||
System.out.println("✅ [S6] Все шаги выполнены, закрываем соединение");
|
||
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "scenario6 done");
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace(System.out);
|
||
}
|
||
|
||
webSocket.request(1);
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
|
||
@Override
|
||
public void onError(WebSocket webSocket, Throwable error) {
|
||
System.out.println("❌ [S6] Ошибка WebSocket-клиента: " + error.getMessage());
|
||
error.printStackTrace(System.out);
|
||
latch.countDown();
|
||
}
|
||
|
||
@Override
|
||
public CompletionStage<?> onClose(WebSocket webSocket,
|
||
int statusCode,
|
||
String reason) {
|
||
System.out.println("🔚 [S6] Соединение закрыто. Код=" + statusCode + ", причина=" + reason);
|
||
latch.countDown();
|
||
return CompletableFuture.completedFuture(null);
|
||
}
|
||
}).join();
|
||
|
||
latch.await();
|
||
}
|
||
|
||
// ==========================================================
|
||
// JSON BUILDERS
|
||
// ==========================================================
|
||
|
||
// 1) AddUser с payload (loginKey != deviceKey)
|
||
private static String buildAddUserJson() {
|
||
return """
|
||
{
|
||
"op": "AddUser",
|
||
"requestId": "test-add-1",
|
||
"payload": {
|
||
"login": "%s",
|
||
"loginId": %d,
|
||
"bchId": %d,
|
||
"loginKey": "%s",
|
||
"deviceKey": "%s",
|
||
"bchLimit": %d
|
||
}
|
||
}
|
||
""".formatted(
|
||
TEST_LOGIN,
|
||
TEST_LOGIN_ID,
|
||
TEST_BCH_ID,
|
||
LOGIN_PUBKEY_B64, // loginKey
|
||
DEVICE_PUBKEY_B64, // deviceKey
|
||
TEST_BCH_LIMIT
|
||
);
|
||
}
|
||
|
||
// 2) Шаг 1 авторизации: запрос authNonce
|
||
private static String buildAuthStep1Json() {
|
||
return """
|
||
{
|
||
"op": "AuthChallenge",
|
||
"requestId": "test-auth-1",
|
||
"payload": {
|
||
"login": "%s"
|
||
}
|
||
}
|
||
""".formatted(TEST_LOGIN);
|
||
}
|
||
|
||
/**
|
||
* 3) Шаг 2 авторизации: подтверждение подписью.
|
||
*
|
||
* @param authNonce одноразовый nonce с шага 1
|
||
* @param storagePwd клиентский storagePwd
|
||
*/
|
||
private static String buildAuthStep2Json(String authNonce, String storagePwd) {
|
||
if (authNonce == null) {
|
||
authNonce = "";
|
||
}
|
||
if (storagePwd == null || storagePwd.isBlank()) {
|
||
storagePwd = generateFakeStoragePwd();
|
||
}
|
||
|
||
long timeMs = System.currentTimeMillis();
|
||
String sigB64 = signAuthorificated(authNonce, timeMs);
|
||
|
||
return """
|
||
{
|
||
"op": "CreateAuthSession",
|
||
"requestId": "test-auth-2",
|
||
"payload": {
|
||
"storagePwd": "%s",
|
||
"timeMs": %d,
|
||
"signatureB64": "%s",
|
||
"clientInfo": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
storagePwd,
|
||
timeMs,
|
||
sigB64,
|
||
TEST_CLIENT_INFO
|
||
);
|
||
}
|
||
|
||
// 4) RefreshSession: всё в payload
|
||
private static String buildRefreshSessionJson(String sessionId, String sessionPwd, String requestId) {
|
||
return """
|
||
{
|
||
"op": "RefreshSession",
|
||
"requestId": "%s",
|
||
"payload": {
|
||
"sessionId": "%s",
|
||
"sessionPwd": "%s",
|
||
"clientInfo": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
requestId,
|
||
sessionId,
|
||
sessionPwd,
|
||
TEST_CLIENT_INFO
|
||
);
|
||
}
|
||
|
||
// 5) ListSessions
|
||
private static String buildListSessionsJson(long timeMs, String signatureB64, String requestId) {
|
||
if (signatureB64 == null) {
|
||
signatureB64 = "";
|
||
}
|
||
return """
|
||
{
|
||
"op": "ListSessions",
|
||
"requestId": "%s",
|
||
"payload": {
|
||
"timeMs": %d,
|
||
"signatureB64": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
requestId,
|
||
timeMs,
|
||
signatureB64
|
||
);
|
||
}
|
||
|
||
// 6) CloseActiveSession
|
||
private static String buildCloseSessionJson(String sessionId,
|
||
long timeMs,
|
||
String signatureB64,
|
||
String requestId) {
|
||
if (signatureB64 == null) {
|
||
signatureB64 = "";
|
||
}
|
||
return """
|
||
{
|
||
"op": "CloseActiveSession",
|
||
"requestId": "%s",
|
||
"payload": {
|
||
"sessionId": "%s",
|
||
"timeMs": %d,
|
||
"signatureB64": "%s"
|
||
}
|
||
}
|
||
""".formatted(
|
||
requestId,
|
||
sessionId,
|
||
timeMs,
|
||
signatureB64
|
||
);
|
||
}
|
||
|
||
// просто для теста: base64 от 32 байт "storage" ключа
|
||
private static String generateFakeStoragePwd() {
|
||
byte[] data = new byte[32];
|
||
for (int i = 0; i < data.length; i++) {
|
||
data[i] = (byte) (i + 1);
|
||
}
|
||
return Base64.getEncoder().encodeToString(data);
|
||
}
|
||
|
||
/**
|
||
* Подписывает строку "AUTHORIFICATED:" + timeMs + authNonce приватным ключом устройства.
|
||
*/
|
||
private static String signAuthorificated(String authNonce, long timeMs) {
|
||
String preimageStr = "AUTHORIFICATED:" + timeMs + authNonce;
|
||
byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);
|
||
|
||
byte[] sig = Ed25519Util.sign(preimage, DEVICE_PRIV_KEY);
|
||
return Base64.getEncoder().encodeToString(sig);
|
||
}
|
||
|
||
// ==========================================================
|
||
// JSON HELPERS
|
||
// ==========================================================
|
||
|
||
private static String extractAuthNonce(String json) {
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
JsonNode payload = root.get("payload");
|
||
if (payload != null && payload.has("authNonce")) {
|
||
return payload.get("authNonce").asText();
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить authNonce из ответа: " + e.getMessage());
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static String extractSessionPwd(String json) {
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
JsonNode payload = root.get("payload");
|
||
if (payload != null && payload.has("sessionPwd")) {
|
||
return payload.get("sessionPwd").asText();
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить sessionPwd из ответа: " + e.getMessage());
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static String extractSessionId(String json) {
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
JsonNode payload = root.get("payload");
|
||
if (payload != null && payload.has("sessionId")) {
|
||
return payload.get("sessionId").asText();
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить sessionId из ответа: " + e.getMessage());
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static String extractStoragePwd(String json) {
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
JsonNode payload = root.get("payload");
|
||
if (payload != null && payload.has("storagePwd")) {
|
||
return payload.get("storagePwd").asText();
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить storagePwd из ответа: " + e.getMessage());
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static int extractStatus(String json) {
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
if (root.has("status")) {
|
||
return root.get("status").asInt();
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить status из ответа: " + e.getMessage());
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
private static List<String> extractSessionIds(String json) {
|
||
List<String> result = new ArrayList<>();
|
||
try {
|
||
JsonNode root = JSON_MAPPER.readTree(json);
|
||
JsonNode payload = root.get("payload");
|
||
if (payload == null || payload.isNull()) {
|
||
return result;
|
||
}
|
||
JsonNode sessionsNode = payload.get("sessions");
|
||
if (sessionsNode == null || !sessionsNode.isArray()) {
|
||
return result;
|
||
}
|
||
for (JsonNode s : sessionsNode) {
|
||
JsonNode idNode = s.get("sessionId");
|
||
if (idNode != null && !idNode.isNull()) {
|
||
result.add(idNode.asText());
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
System.out.println("⚠️ Не удалось распарсить список sessions из ответа: " + e.getMessage());
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// ==========================================================
|
||
// OUTPUT HELPERS
|
||
// ==========================================================
|
||
|
||
private static void printSection(String title) {
|
||
System.out.println("\n\n==================================================");
|
||
System.out.println(title);
|
||
System.out.println("==================================================\n");
|
||
}
|
||
|
||
private static void printTestResult(String name, boolean ok, String details) {
|
||
if (ok) {
|
||
System.out.println("✅ " + name + " — " + details);
|
||
} else {
|
||
System.out.println("❌ " + name + " — " + details);
|
||
}
|
||
}
|
||
} |