Добавлено тестовое заполнение и доработки входа
- Добавлено расширенное тестовое заполнение данных через Seed_TestDataPopulation и SeedDataPopulationHelper (включая базовую схему для пользователей 1, 2, 3, их связи и профили). - Убраны лишние проверки из тестового заполнения: сид теперь только заполняет данные. - Исправлен WsTestClient: корректная сборка фрагментированных WS-сообщений до полного JSON. - На странице входа по логину добавлена подсказка про основные тестовые логины 1, 2, 3 (вход без пароля).
This commit is contained in:
parent
30fcde5744
commit
ba3ee4290f
@ -29,23 +29,27 @@ export function render({ navigate }) {
|
||||
passwordInput.className = 'input';
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.value = state.loginDraft.password;
|
||||
passwordInput.placeholder = 'Введите пароль';
|
||||
passwordInput.placeholder = 'Введите пароль (можно оставить пустым)';
|
||||
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'meta-muted';
|
||||
hint.textContent = 'Введите логин и пароль. На следующем шаге сохраните ключи на устройстве.';
|
||||
hint.textContent = 'Введите логин. Пароль может быть пустым. На следующем шаге сохраните ключи на устройстве.';
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.style.display = 'none';
|
||||
|
||||
const testLoginsHint = document.createElement('p');
|
||||
testLoginsHint.className = 'meta-muted';
|
||||
testLoginsHint.textContent = 'Основные тестовые логины: 1, 2, 3 (вход без пароля).';
|
||||
|
||||
form.innerHTML = `
|
||||
<label class="stack"><span class="field-label">Логин</span></label>
|
||||
<label class="stack"><span class="field-label">Пароль</span></label>
|
||||
`;
|
||||
form.children[0].append(loginInput);
|
||||
form.children[1].append(passwordInput);
|
||||
form.append(hint, status);
|
||||
form.append(hint, status, testLoginsHint);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-footer-actions';
|
||||
@ -65,8 +69,8 @@ export function render({ navigate }) {
|
||||
state.loginDraft.login = loginInput.value.trim();
|
||||
state.loginDraft.password = passwordInput.value;
|
||||
|
||||
if (!state.loginDraft.login || !state.loginDraft.password) {
|
||||
status.textContent = 'Введите логин и пароль.';
|
||||
if (!state.loginDraft.login) {
|
||||
status.textContent = 'Введите логин.';
|
||||
status.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export function render({ navigate }) {
|
||||
passwordInput.className = 'input';
|
||||
passwordInput.type = 'password';
|
||||
passwordInput.value = state.registrationDraft.password;
|
||||
passwordInput.placeholder = 'Введите пароль';
|
||||
passwordInput.placeholder = 'Введите пароль (можно оставить пустым)';
|
||||
|
||||
const statusText = document.createElement('p');
|
||||
statusText.className = 'meta-muted';
|
||||
@ -98,12 +98,6 @@ export function render({ navigate }) {
|
||||
state.registrationDraft.login = loginInput.value.trim();
|
||||
state.registrationDraft.password = passwordInput.value;
|
||||
|
||||
if (!state.registrationDraft.password) {
|
||||
formError.textContent = 'Введите пароль.';
|
||||
formError.style.display = '';
|
||||
return;
|
||||
}
|
||||
|
||||
navigate('registration-payment-view');
|
||||
});
|
||||
|
||||
|
||||
@ -472,10 +472,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async derivePasswordKeyBundle(password) {
|
||||
if (!password) throw new Error('Введите пароль');
|
||||
const rootPair = await deriveEd25519FromPassword(password, 'root.key');
|
||||
const blockchainPair = await deriveEd25519FromPassword(password, 'bch.key');
|
||||
const devicePair = await deriveEd25519FromPassword(password, 'dev.key');
|
||||
const normalizedPassword = String(password ?? '');
|
||||
const rootPair = await deriveEd25519FromPassword(normalizedPassword, 'root.key');
|
||||
const blockchainPair = await deriveEd25519FromPassword(normalizedPassword, 'bch.key');
|
||||
const devicePair = await deriveEd25519FromPassword(normalizedPassword, 'dev.key');
|
||||
return { rootPair, blockchainPair, devicePair };
|
||||
}
|
||||
|
||||
@ -528,7 +528,6 @@ export class AuthService {
|
||||
async registerUser(login, password) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
if (!password) throw new Error('Введите пароль');
|
||||
|
||||
const isFree = await this.ensureLoginFree(cleanLogin);
|
||||
if (!isFree) throw new Error('Этот логин уже занят');
|
||||
@ -552,7 +551,6 @@ export class AuthService {
|
||||
async createSessionForExistingUser(login, password) {
|
||||
const cleanLogin = (login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
if (!password) throw new Error('Введите пароль');
|
||||
|
||||
const user = await this.getUser(cleanLogin);
|
||||
if (!user.exists) throw new Error('Пользователь не найден');
|
||||
|
||||
308
src/test/java/test/it/cases/SeedDataPopulationHelper.java
Normal file
308
src/test/java/test/it/cases/SeedDataPopulationHelper.java
Normal file
@ -0,0 +1,308 @@
|
||||
package test.it.cases;
|
||||
|
||||
import blockchain.MsgSubType;
|
||||
import blockchain.body.ConnectionBody;
|
||||
import blockchain.body.HeaderBody;
|
||||
import blockchain.body.TextBody;
|
||||
import blockchain.body.UserParamBody;
|
||||
import test.it.blockchain.AddBlockSender;
|
||||
import test.it.blockchain.ChainState;
|
||||
import test.it.utils.TestIds;
|
||||
import test.it.utils.json.JsonParsers;
|
||||
import test.it.utils.log.TestResult;
|
||||
import test.it.utils.ws.WsSession;
|
||||
import utils.crypto.Ed25519Util;
|
||||
import utils.crypto.HashSHA256Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Вспомогательные функции для массового заполнения тестовой соц-сети через API.
|
||||
* Вся запись состояния делается только через AddUser / AddBlock.
|
||||
*/
|
||||
public final class SeedDataPopulationHelper {
|
||||
|
||||
private static final String BCH_SUFFIX = "001";
|
||||
|
||||
private final WsSession ws;
|
||||
private final TestResult result;
|
||||
private final Duration timeout;
|
||||
private final UserKeys keys;
|
||||
|
||||
private final Map<String, ChainState> states = new LinkedHashMap<>();
|
||||
private final Map<String, AddBlockSender> senders = new LinkedHashMap<>();
|
||||
private final Map<String, byte[]> headerHashes = new LinkedHashMap<>();
|
||||
|
||||
public SeedDataPopulationHelper(WsSession ws, TestResult result, Duration timeout, String password) {
|
||||
this.ws = Objects.requireNonNull(ws, "ws");
|
||||
this.result = Objects.requireNonNull(result, "result");
|
||||
this.timeout = Objects.requireNonNull(timeout, "timeout");
|
||||
this.keys = deriveKeysFromPassword(String.valueOf(password == null ? "" : password));
|
||||
}
|
||||
|
||||
public static UserSpec user(String login, String firstName, String lastName) {
|
||||
return new UserSpec(login, firstName, lastName);
|
||||
}
|
||||
|
||||
public void createUsersAndHeaders(List<UserSpec> users) {
|
||||
for (UserSpec user : users) createUserViaApi(user.login);
|
||||
for (UserSpec user : users) initHeader(user.login);
|
||||
}
|
||||
|
||||
public void applyProfiles(List<UserSpec> users) {
|
||||
for (UserSpec user : users) {
|
||||
upsertProfileParam(user.login, "first_name", user.firstName);
|
||||
upsertProfileParam(user.login, "last_name", user.lastName);
|
||||
upsertProfileParam(user.login, "gender", user.gender);
|
||||
if (notBlank(user.address)) upsertProfileParam(user.login, "address", user.address);
|
||||
if (notBlank(user.web)) upsertProfileParam(user.login, "web", user.web);
|
||||
if (notBlank(user.phone)) upsertProfileParam(user.login, "phone", user.phone);
|
||||
if (user.official != null) upsertProfileParam(user.login, "official", user.official ? "yes" : "no");
|
||||
if (user.shine != null) upsertProfileParam(user.login, "shine", user.shine ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
|
||||
public void addCloseFriend(String from, String to) {
|
||||
sendConnection(from, to, MsgSubType.CONNECTION_CLOSE_FRIEND);
|
||||
}
|
||||
|
||||
public void addMutualCloseFriend(String a, String b) {
|
||||
addCloseFriend(a, b);
|
||||
addCloseFriend(b, a);
|
||||
}
|
||||
|
||||
public void addContact(String from, String to) {
|
||||
sendConnection(from, to, MsgSubType.CONNECTION_CONTACT);
|
||||
}
|
||||
|
||||
public void addMutualContact(String a, String b) {
|
||||
addContact(a, b);
|
||||
addContact(b, a);
|
||||
}
|
||||
|
||||
public void addFollow(String from, String to) {
|
||||
sendConnection(from, to, MsgSubType.CONNECTION_FOLLOW);
|
||||
}
|
||||
|
||||
public void addParent(String whoAddsParent, String parentLogin) {
|
||||
sendConnection(whoAddsParent, parentLogin, MsgSubType.CONNECTION_PARENT);
|
||||
}
|
||||
|
||||
public void addChild(String whoAddsChild, String childLogin) {
|
||||
sendConnection(whoAddsChild, childLogin, MsgSubType.CONNECTION_CHILD);
|
||||
}
|
||||
|
||||
public void addSibling(String from, String to) {
|
||||
sendConnection(from, to, MsgSubType.CONNECTION_SIBLING);
|
||||
}
|
||||
|
||||
public void addMutualSibling(String a, String b) {
|
||||
addSibling(a, b);
|
||||
addSibling(b, a);
|
||||
}
|
||||
|
||||
public void addPost(String login, String text) {
|
||||
String cleanText = String.valueOf(text == null ? "" : text).trim();
|
||||
if (cleanText.isEmpty()) return;
|
||||
AddBlockSender sender = senderFor(login);
|
||||
ChainState state = stateFor(login);
|
||||
ChainState.NextLine line = state.nextTextLineByCode(0);
|
||||
sender.send(TextBody.newPost(
|
||||
line.lineCode,
|
||||
line.prevLineNumber,
|
||||
line.prevLineHash32,
|
||||
line.thisLineNumber,
|
||||
cleanText
|
||||
), timeout);
|
||||
}
|
||||
|
||||
private void upsertProfileParam(String login, String key, String value) {
|
||||
String cleanKey = String.valueOf(key == null ? "" : key).trim();
|
||||
String cleanValue = String.valueOf(value == null ? "" : value).trim();
|
||||
if (cleanKey.isEmpty() || cleanValue.isEmpty()) return;
|
||||
|
||||
AddBlockSender sender = senderFor(login);
|
||||
ChainState state = stateFor(login);
|
||||
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_USER_PARAM);
|
||||
sender.send(new UserParamBody(
|
||||
0,
|
||||
line.prevLineNumber,
|
||||
line.prevLineHash32,
|
||||
line.thisLineNumber,
|
||||
cleanKey,
|
||||
cleanValue
|
||||
), timeout);
|
||||
}
|
||||
|
||||
private void sendConnection(String from, String to, short relationSubType) {
|
||||
AddBlockSender sender = senderFor(from);
|
||||
ChainState state = stateFor(from);
|
||||
byte[] targetHeaderHash = headerHashFor(to);
|
||||
ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
|
||||
sender.send(new ConnectionBody(
|
||||
0,
|
||||
line.prevLineNumber,
|
||||
line.prevLineHash32,
|
||||
line.thisLineNumber,
|
||||
relationSubType,
|
||||
bch(to),
|
||||
0,
|
||||
targetHeaderHash
|
||||
), timeout);
|
||||
}
|
||||
|
||||
private void createUserViaApi(String login) {
|
||||
String requestId = TestIds.next("seed_adduser");
|
||||
String req = """
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"login": "%s",
|
||||
"blockchainName": "%s",
|
||||
"solanaKey": "%s",
|
||||
"blockchainKey": "%s",
|
||||
"deviceKey": "%s",
|
||||
"bchLimit": 50000000
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
requestId,
|
||||
login,
|
||||
bch(login),
|
||||
keys.solanaPublicB64,
|
||||
keys.blockchainPublicB64,
|
||||
keys.devicePublicB64
|
||||
);
|
||||
|
||||
String resp = ws.call("AddUser#" + login, req, timeout);
|
||||
int status = JsonParsers.status(resp);
|
||||
if (status == 200) {
|
||||
result.ok("AddUser " + login + ": created");
|
||||
return;
|
||||
}
|
||||
if (status == 409) {
|
||||
String code = JsonParsers.errorCode(resp);
|
||||
if ("USER_ALREADY_EXISTS".equals(code)
|
||||
|| "BLOCKCHAIN_ALREADY_EXISTS".equals(code)
|
||||
|| "BLOCKCHAIN_STATE_ALREADY_EXISTS".equals(code)) {
|
||||
result.ok("AddUser " + login + ": already exists (" + code + ")");
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("AddUser failed for " + login + ": status=" + status + ", resp=" + resp);
|
||||
}
|
||||
|
||||
private void initHeader(String login) {
|
||||
ChainState state = new ChainState();
|
||||
AddBlockSender sender = new AddBlockSender(ws, state, login, bch(login), keys.blockchainPrivate32);
|
||||
sender.send(new HeaderBody(login), timeout);
|
||||
|
||||
byte[] headerHash = state.getHash32(0);
|
||||
if (headerHash == null || headerHash.length != 32) {
|
||||
throw new IllegalStateException("Header hash is missing for " + login);
|
||||
}
|
||||
|
||||
states.put(login, state);
|
||||
senders.put(login, sender);
|
||||
headerHashes.put(login, headerHash);
|
||||
result.ok("HEADER создан: " + login);
|
||||
}
|
||||
|
||||
private AddBlockSender senderFor(String login) {
|
||||
AddBlockSender sender = senders.get(login);
|
||||
if (sender == null) throw new IllegalStateException("Sender not initialized for login=" + login);
|
||||
return sender;
|
||||
}
|
||||
|
||||
private ChainState stateFor(String login) {
|
||||
ChainState state = states.get(login);
|
||||
if (state == null) throw new IllegalStateException("State not initialized for login=" + login);
|
||||
return state;
|
||||
}
|
||||
|
||||
private byte[] headerHashFor(String login) {
|
||||
byte[] hash = headerHashes.get(login);
|
||||
if (hash == null) throw new IllegalStateException("Header hash not initialized for login=" + login);
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static String bch(String login) {
|
||||
return login + "-" + BCH_SUFFIX;
|
||||
}
|
||||
|
||||
private static boolean notBlank(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static UserKeys deriveKeysFromPassword(String password) {
|
||||
byte[] base = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
||||
String baseB64 = Base64.getEncoder().encodeToString(base);
|
||||
|
||||
byte[] rootPriv = HashSHA256Util.sha256((baseB64 + "root.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] bchPriv = HashSHA256Util.sha256((baseB64 + "bch.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] devPriv = HashSHA256Util.sha256((baseB64 + "dev.key").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
|
||||
String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
|
||||
String devPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(devPriv));
|
||||
|
||||
return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
|
||||
}
|
||||
|
||||
public static final class UserSpec {
|
||||
public final String login;
|
||||
public final String firstName;
|
||||
public final String lastName;
|
||||
|
||||
public String gender = "unknown";
|
||||
public String address = "";
|
||||
public String web = "";
|
||||
public String phone = "";
|
||||
public Boolean official = null;
|
||||
public Boolean shine = null;
|
||||
|
||||
private UserSpec(String login, String firstName, String lastName) {
|
||||
this.login = String.valueOf(login == null ? "" : login).trim();
|
||||
this.firstName = String.valueOf(firstName == null ? "" : firstName).trim();
|
||||
this.lastName = String.valueOf(lastName == null ? "" : lastName).trim();
|
||||
if (this.login.isEmpty()) throw new IllegalArgumentException("login is empty");
|
||||
if (this.firstName.isEmpty()) throw new IllegalArgumentException("firstName is empty for " + this.login);
|
||||
if (this.lastName.isEmpty()) throw new IllegalArgumentException("lastName is empty for " + this.login);
|
||||
}
|
||||
|
||||
public UserSpec male() { this.gender = "male"; return this; }
|
||||
public UserSpec female() { this.gender = "female"; return this; }
|
||||
public UserSpec unknownGender() { this.gender = "unknown"; return this; }
|
||||
public UserSpec address(String value) { this.address = String.valueOf(value == null ? "" : value).trim(); return this; }
|
||||
public UserSpec web(String value) { this.web = String.valueOf(value == null ? "" : value).trim(); return this; }
|
||||
public UserSpec phone(String value) { this.phone = String.valueOf(value == null ? "" : value).trim(); return this; }
|
||||
public UserSpec official(boolean value) { this.official = value; return this; }
|
||||
public UserSpec shine(boolean value) { this.shine = value; return this; }
|
||||
}
|
||||
|
||||
private static final class UserKeys {
|
||||
final String solanaPublicB64;
|
||||
final String blockchainPublicB64;
|
||||
final String devicePublicB64;
|
||||
final byte[] blockchainPrivate32;
|
||||
|
||||
private UserKeys(String solanaPublicB64,
|
||||
String blockchainPublicB64,
|
||||
String devicePublicB64,
|
||||
byte[] blockchainPrivate32) {
|
||||
this.solanaPublicB64 = solanaPublicB64;
|
||||
this.blockchainPublicB64 = blockchainPublicB64;
|
||||
this.devicePublicB64 = devicePublicB64;
|
||||
this.blockchainPrivate32 = blockchainPrivate32;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,266 +1,292 @@
|
||||
package test.it.cases;
|
||||
|
||||
import blockchain.MsgSubType;
|
||||
import blockchain.body.ConnectionBody;
|
||||
import blockchain.body.HeaderBody;
|
||||
import test.it.blockchain.AddBlockSender;
|
||||
import test.it.blockchain.ChainState;
|
||||
import test.it.utils.TestIds;
|
||||
import test.it.utils.json.JsonBuilders;
|
||||
import test.it.utils.json.JsonParsers;
|
||||
import test.it.utils.log.TestResult;
|
||||
import test.it.utils.ws.WsSession;
|
||||
import utils.crypto.Ed25519Util;
|
||||
import utils.crypto.HashSHA256Util;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
/**
|
||||
* Seed_TestDataPopulation
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - НЕ заполняет БД напрямую.
|
||||
* - Создаёт тестовых пользователей 1..9 через API AddUser.
|
||||
* - Создаёт сеть дружбы через API AddBlock (CONNECTION_FRIEND).
|
||||
* Заполняет тестовую соц-сеть через API:
|
||||
* - пользователи + HEADER через AddUser/AddBlock;
|
||||
* - параметры профиля (имя, фамилия, пол, адрес, web, phone, official, shine) через AddBlock USER_PARAM;
|
||||
* - связи (close friend/contact/follow/parent/child/sibling) через AddBlock CONNECTION;
|
||||
* - посты через AddBlock TEXT_POST.
|
||||
*/
|
||||
public class Seed_TestDataPopulation {
|
||||
|
||||
private static final String PASSWORD = "1";
|
||||
// По требованию: пароль для сидов пустой (ключи детерминируются от "").
|
||||
private static final String PASSWORD = "";
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(run());
|
||||
}
|
||||
|
||||
public static String run() {
|
||||
TestResult r = new TestResult("Seed_TestDataPopulation");
|
||||
Duration t = Duration.ofSeconds(5);
|
||||
TestResult result = new TestResult("Seed_TestDataPopulation");
|
||||
Duration timeout = Duration.ofSeconds(20);
|
||||
|
||||
try (WsSession ws = WsSession.open()) {
|
||||
UserKeys keys = deriveKeysFromPassword(PASSWORD);
|
||||
SeedDataPopulationHelper seed = new SeedDataPopulationHelper(ws, result, timeout, PASSWORD);
|
||||
|
||||
List<String> users = List.of("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||
// -----------------------------------------------------------------
|
||||
// 1) Пользователи и профили
|
||||
// -----------------------------------------------------------------
|
||||
List<SeedDataPopulationHelper.UserSpec> users = buildUsers();
|
||||
seed.createUsersAndHeaders(users);
|
||||
seed.applyProfiles(users);
|
||||
|
||||
for (String login : users) {
|
||||
createUserViaApi(ws, r, login, keys, t);
|
||||
}
|
||||
// -----------------------------------------------------------------
|
||||
// 2) Базовые close friend: пользователи 1/2/3 взаимно связаны
|
||||
// -----------------------------------------------------------------
|
||||
seed.addMutualCloseFriend("1", "2");
|
||||
seed.addMutualCloseFriend("1", "3");
|
||||
seed.addMutualCloseFriend("2", "3");
|
||||
|
||||
Map<String, ChainState> states = new HashMap<>();
|
||||
Map<String, AddBlockSender> senders = new HashMap<>();
|
||||
Map<String, byte[]> headerHashes = new HashMap<>();
|
||||
// -----------------------------------------------------------------
|
||||
// 3) Родители:
|
||||
// - user1 <-> parents (взаимно)
|
||||
// - user2 -> parents (только user2 добавил родителей)
|
||||
// - parents -> user3 (только родители добавили user3)
|
||||
// -----------------------------------------------------------------
|
||||
seed.addParent("1", "u1_mom");
|
||||
seed.addParent("1", "u1_dad");
|
||||
seed.addChild("u1_mom", "1");
|
||||
seed.addChild("u1_dad", "1");
|
||||
|
||||
for (String login : users) {
|
||||
ChainState state = new ChainState();
|
||||
AddBlockSender sender = new AddBlockSender(ws, state, login, bch(login), keys.blockchainPrivate32);
|
||||
sender.send(new HeaderBody(login), t);
|
||||
byte[] headerHash = state.getHash32(0);
|
||||
if (headerHash == null) {
|
||||
r.fail("Не удалось получить hash HEADER для " + login);
|
||||
fail("Header hash missing for " + login);
|
||||
}
|
||||
states.put(login, state);
|
||||
senders.put(login, sender);
|
||||
headerHashes.put(login, headerHash);
|
||||
}
|
||||
seed.addParent("2", "u2_mom");
|
||||
seed.addParent("2", "u2_dad");
|
||||
|
||||
// Насыщенная сеть дружбы (взаимно). Целевые контрольные значения:
|
||||
// 1=5 друзей, 2=7 друзей (<8), 3=3 друга.
|
||||
addMutualFriend(senders, states, headerHashes, "1", "2", t);
|
||||
addMutualFriend(senders, states, headerHashes, "1", "3", t);
|
||||
addMutualFriend(senders, states, headerHashes, "1", "4", t);
|
||||
addMutualFriend(senders, states, headerHashes, "1", "5", t);
|
||||
addMutualFriend(senders, states, headerHashes, "1", "6", t);
|
||||
seed.addChild("u3_mom", "3");
|
||||
seed.addChild("u3_dad", "3");
|
||||
|
||||
addMutualFriend(senders, states, headerHashes, "2", "3", t);
|
||||
addMutualFriend(senders, states, headerHashes, "2", "4", t);
|
||||
addMutualFriend(senders, states, headerHashes, "2", "5", t);
|
||||
addMutualFriend(senders, states, headerHashes, "2", "6", t);
|
||||
addMutualFriend(senders, states, headerHashes, "2", "7", t);
|
||||
addMutualFriend(senders, states, headerHashes, "2", "8", t);
|
||||
// -----------------------------------------------------------------
|
||||
// 4) Дети и братья/сёстры
|
||||
// -----------------------------------------------------------------
|
||||
seed.addChild("1", "u1_son");
|
||||
seed.addParent("u1_son", "1");
|
||||
|
||||
addMutualFriend(senders, states, headerHashes, "3", "4", t);
|
||||
addMutualFriend(senders, states, headerHashes, "4", "5", t);
|
||||
addMutualFriend(senders, states, headerHashes, "5", "6", t);
|
||||
addMutualFriend(senders, states, headerHashes, "5", "7", t);
|
||||
addMutualFriend(senders, states, headerHashes, "6", "8", t);
|
||||
addMutualFriend(senders, states, headerHashes, "6", "9", t);
|
||||
addMutualFriend(senders, states, headerHashes, "8", "9", t);
|
||||
seed.addChild("2", "u2_daughter");
|
||||
seed.addParent("u2_daughter", "2");
|
||||
|
||||
// Контакты: 1/2/3 должны быть друг у друга в контактах (взаимно).
|
||||
addMutualContact(senders, states, headerHashes, "1", "2", t);
|
||||
addMutualContact(senders, states, headerHashes, "1", "3", t);
|
||||
addMutualContact(senders, states, headerHashes, "2", "3", t);
|
||||
seed.addMutualSibling("1", "u1_bro");
|
||||
seed.addMutualSibling("2", "u2_sis");
|
||||
seed.addMutualSibling("3", "u3_bro");
|
||||
|
||||
verifyOutFriendsCount(ws, r, "1", 5, t);
|
||||
verifyOutFriendsCount(ws, r, "2", 7, t);
|
||||
verifyOutFriendsCount(ws, r, "3", 3, t);
|
||||
// -----------------------------------------------------------------
|
||||
// 5) Дополнительные close friend-связи (социальный граф)
|
||||
// -----------------------------------------------------------------
|
||||
seed.addMutualCloseFriend("1", "u1_friend");
|
||||
seed.addMutualCloseFriend("2", "u2_friend");
|
||||
seed.addMutualCloseFriend("3", "u3_friend");
|
||||
|
||||
r.ok("Пользователи 1..9 созданы через API, дружеские и контактные связи созданы через AddBlock");
|
||||
seed.addMutualCloseFriend("1", "shared_12");
|
||||
seed.addMutualCloseFriend("2", "shared_12");
|
||||
|
||||
seed.addMutualCloseFriend("2", "shared_23");
|
||||
seed.addMutualCloseFriend("3", "shared_23");
|
||||
|
||||
seed.addMutualCloseFriend("u1_friend", "u2_friend");
|
||||
seed.addMutualCloseFriend("2", "u3_friend");
|
||||
seed.addMutualCloseFriend("3", "u2_friend");
|
||||
seed.addMutualCloseFriend("2", "u2_sis");
|
||||
|
||||
// Смешанные связи по задаче:
|
||||
// сестра пользователя 2 является close friend пользователя 3
|
||||
seed.addMutualCloseFriend("u2_sis", "3");
|
||||
// чей-то отец как close friend
|
||||
seed.addMutualCloseFriend("u1_dad", "shared_12");
|
||||
seed.addMutualCloseFriend("u2_dad", "u3_friend");
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 6) Контакты (user1 ~10, user2 ~5, user3 ~7)
|
||||
// -----------------------------------------------------------------
|
||||
addContacts(seed, "1",
|
||||
"2", "3",
|
||||
"u1_mom", "u1_dad", "u1_son", "u1_bro",
|
||||
"u1_friend", "shared_12",
|
||||
"star_alfa", "star_beta");
|
||||
|
||||
addContacts(seed, "2",
|
||||
"1", "3",
|
||||
"u2_mom", "u2_dad", "u2_daughter");
|
||||
|
||||
addContacts(seed, "3",
|
||||
"1", "2",
|
||||
"u3_mom", "u3_dad", "u3_bro",
|
||||
"shared_23", "star_gamma");
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 7) Подписки
|
||||
// user1 и user2 подписаны на 3 популярных аккаунта; user3 ни на кого не подписан.
|
||||
// -----------------------------------------------------------------
|
||||
addFollows(seed, "1", "star_alfa", "star_beta", "star_gamma");
|
||||
addFollows(seed, "2", "star_alfa", "star_beta", "star_gamma");
|
||||
|
||||
// Дополнительные подписки популярных аккаунтов и друзей
|
||||
seed.addFollow("star_alfa", "star_beta");
|
||||
seed.addFollow("star_beta", "star_gamma");
|
||||
seed.addFollow("star_gamma", "star_alfa");
|
||||
seed.addFollow("u1_friend", "star_alfa");
|
||||
seed.addFollow("u2_friend", "star_beta");
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 8) Посты
|
||||
// -----------------------------------------------------------------
|
||||
// Посты популярных аккаунтов
|
||||
seed.addPost("star_alfa", "Сегодня в 20:00 эфир: как расти в соцсетях без выгорания.");
|
||||
seed.addPost("star_alfa", "Подборка сервисов для авторов: редакторы, аналитика, планировщики.");
|
||||
|
||||
seed.addPost("star_beta", "Max Trend: weekly update. New ideas, less noise.");
|
||||
seed.addPost("star_beta", "Делюсь коротким чек-листом: как стартовать канал за 1 день.");
|
||||
|
||||
seed.addPost("star_gamma", "NeoPulse: сделал новый обзор по AI-инструментам для контента.");
|
||||
seed.addPost("star_gamma", "Сегодня без эфиров, только ответы в комментариях.");
|
||||
|
||||
// Посты о себе у пользователей 1 и 2 (по несколько)
|
||||
seed.addPost("1", "Я — Друг 1. Люблю тестировать новые фичи приложения и писать заметки о городе.");
|
||||
seed.addPost("1", "Проверяю вкладку «Связи»: родители, дети, братья и близкие друзья.");
|
||||
seed.addPost("1", "Если увидите баг по линиям/стрелкам — пишите, поправим.");
|
||||
|
||||
seed.addPost("2", "Я — Друг 2. Сейчас настраиваю профиль и заполняю контакты.");
|
||||
seed.addPost("2", "Подписался на популярных авторов, смотрю их ленту и обсуждения.");
|
||||
|
||||
// Несколько дополнительных постов для живости
|
||||
seed.addPost("u3_friend", "Сегодня был в дороге, позже выложу фото и адреса мест.");
|
||||
seed.addPost("shared_23", "У кого есть идеи по совместному стриму на выходных?");
|
||||
|
||||
result.ok("Тестовые данные заполнены: 20+ пользователей, семейные связи, close friends, контакты, подписки и посты.");
|
||||
} catch (Throwable e) {
|
||||
r.fail("Ошибка IT_07: " + e.getMessage());
|
||||
fail("IT_07 failed", e);
|
||||
result.fail("Ошибка seed: " + e.getMessage());
|
||||
fail("Seed_TestDataPopulation failed", e);
|
||||
}
|
||||
|
||||
return r.summaryLine();
|
||||
return result.summaryLine();
|
||||
}
|
||||
|
||||
private static void createUserViaApi(WsSession ws, TestResult r, String login, UserKeys keys, Duration t) {
|
||||
String requestId = TestIds.next("adduser_a");
|
||||
String req = """
|
||||
{
|
||||
"op": "AddUser",
|
||||
"requestId": "%s",
|
||||
"payload": {
|
||||
"login": "%s",
|
||||
"blockchainName": "%s",
|
||||
"solanaKey": "%s",
|
||||
"blockchainKey": "%s",
|
||||
"deviceKey": "%s",
|
||||
"bchLimit": 50000000
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
requestId,
|
||||
login,
|
||||
bch(login),
|
||||
keys.solanaPublicB64,
|
||||
keys.blockchainPublicB64,
|
||||
keys.devicePublicB64
|
||||
private static void addContacts(SeedDataPopulationHelper seed, String owner, String... contacts) {
|
||||
for (String contact : contacts) {
|
||||
seed.addContact(owner, contact);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addFollows(SeedDataPopulationHelper seed, String owner, String... targets) {
|
||||
for (String target : targets) {
|
||||
seed.addFollow(owner, target);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SeedDataPopulationHelper.UserSpec> buildUsers() {
|
||||
return List.of(
|
||||
// Базовые 1/2/3 (входные тестовые аккаунты)
|
||||
SeedDataPopulationHelper.user("1", "Друг 1", "Иванов")
|
||||
.male()
|
||||
.address("Москва, Тверская улица, 10")
|
||||
.web("telegram:@drug1, viber:+79990000001, site:https://drug1.local")
|
||||
.phone("+7 900 000-00-01")
|
||||
.shine(true),
|
||||
|
||||
SeedDataPopulationHelper.user("2", "Друг 2", "Петров")
|
||||
.male()
|
||||
.address("Санкт-Петербург, Невский проспект, 22")
|
||||
.web("telegram:@drug2, instagram:@drug2_live")
|
||||
.phone("+7 900 000-00-02"),
|
||||
|
||||
SeedDataPopulationHelper.user("3", "Друг 3", "Смирнов")
|
||||
.male()
|
||||
.address("Казань, улица Баумана, 7")
|
||||
.web("telegram:@drug3, site:https://drug3.space")
|
||||
.phone("+7 900 000-00-03"),
|
||||
|
||||
// Родители 1/2/3
|
||||
SeedDataPopulationHelper.user("u1_mom", "Марина", "Иванова")
|
||||
.female()
|
||||
.address("Москва, Кутузовский проспект, 31")
|
||||
.web("telegram:@marina_ivanova"),
|
||||
SeedDataPopulationHelper.user("u1_dad", "Сергей", "Иванов")
|
||||
.male()
|
||||
.address("Москва, Кутузовский проспект, 31")
|
||||
.web("viber:+79995550101"),
|
||||
|
||||
SeedDataPopulationHelper.user("u2_mom", "Ольга", "Петрова")
|
||||
.female()
|
||||
.address("Санкт-Петербург, Литейный проспект, 9")
|
||||
.web("telegram:@olga_petrova"),
|
||||
SeedDataPopulationHelper.user("u2_dad", "Андрей", "Петров")
|
||||
.male()
|
||||
.address("Санкт-Петербург, Литейный проспект, 9")
|
||||
.web("site:https://petrov-family.ru"),
|
||||
|
||||
SeedDataPopulationHelper.user("u3_mom", "Елена", "Смирнова")
|
||||
.female()
|
||||
.address("Казань, улица Декабристов, 12"),
|
||||
SeedDataPopulationHelper.user("u3_dad", "Игорь", "Смирнов")
|
||||
.male()
|
||||
.address("Казань, улица Декабристов, 12"),
|
||||
|
||||
// Дети
|
||||
SeedDataPopulationHelper.user("u1_son", "Кирилл", "Иванов")
|
||||
.male()
|
||||
.web("telegram:@kirill_ivanov_jr"),
|
||||
SeedDataPopulationHelper.user("u2_daughter", "Алиса", "Петрова")
|
||||
.female(),
|
||||
|
||||
// Братья/сёстры
|
||||
SeedDataPopulationHelper.user("u1_bro", "Антон", "Иванов")
|
||||
.male()
|
||||
.web("telegram:@anton_ivanov"),
|
||||
SeedDataPopulationHelper.user("u2_sis", "Наталья", "Петрова")
|
||||
.female()
|
||||
.web("telegram:@natasha_petrova"),
|
||||
SeedDataPopulationHelper.user("u3_bro", "Максим", "Смирнов")
|
||||
.male(),
|
||||
|
||||
// Друзья пользователей
|
||||
SeedDataPopulationHelper.user("u1_friend", "Дмитрий", "Лебедев")
|
||||
.male()
|
||||
.address("Тула, Советская улица, 4"),
|
||||
SeedDataPopulationHelper.user("u2_friend", "Вера", "Кузнецова")
|
||||
.female()
|
||||
.address("Екатеринбург, улица Малышева, 44"),
|
||||
SeedDataPopulationHelper.user("u3_friend", "Роман", "Орлов")
|
||||
.male()
|
||||
.address("Нижний Новгород, Большая Покровская, 15"),
|
||||
|
||||
// Общие друзья для связок 1-2 и 2-3
|
||||
SeedDataPopulationHelper.user("shared_12", "Михаил", "Громов")
|
||||
.male()
|
||||
.web("telegram:@m_gromov, site:https://gromov.media"),
|
||||
SeedDataPopulationHelper.user("shared_23", "София", "Громова")
|
||||
.female()
|
||||
.web("instagram:@sofia_gromova"),
|
||||
|
||||
// Популярные аккаунты
|
||||
SeedDataPopulationHelper.user("star_alfa", "Анна", "Звезда")
|
||||
.female()
|
||||
.official(true)
|
||||
.shine(true)
|
||||
.web("telegram:@anna_star, instagram:@annastar, site:https://annastar.pro"),
|
||||
|
||||
SeedDataPopulationHelper.user("star_beta", "Max", "Trend")
|
||||
.male()
|
||||
.official(true)
|
||||
.web("telegram:@maxtrend, site:https://maxtrend.io"),
|
||||
|
||||
SeedDataPopulationHelper.user("star_gamma", "Neo", "Pulse")
|
||||
.unknownGender()
|
||||
.official(true)
|
||||
.shine(true)
|
||||
.web("site:https://neopulse.media, instagram:@neo.pulse"),
|
||||
|
||||
// Дополнительный англоязычный профиль
|
||||
SeedDataPopulationHelper.user("en_guest", "Alex", "Miller")
|
||||
.male()
|
||||
.address("London, Baker Street, 20")
|
||||
.web("telegram:@alexmiller, site:https://alexm.dev")
|
||||
);
|
||||
|
||||
String resp = ws.call("AddUser#" + login, req, t);
|
||||
int st = JsonParsers.status(resp);
|
||||
|
||||
if (st == 200) {
|
||||
r.ok("AddUser " + login + ": created");
|
||||
return;
|
||||
}
|
||||
|
||||
if (st == 409) {
|
||||
String code = JsonParsers.errorCode(resp);
|
||||
if ("USER_ALREADY_EXISTS".equals(code)
|
||||
|| "BLOCKCHAIN_ALREADY_EXISTS".equals(code)
|
||||
|| "BLOCKCHAIN_STATE_ALREADY_EXISTS".equals(code)) {
|
||||
r.ok("AddUser " + login + ": already exists (" + code + ")");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
r.fail("AddUser " + login + " unexpected status=" + st + ", resp=" + resp);
|
||||
fail("AddUser failed for " + login);
|
||||
}
|
||||
|
||||
private static void sendFriendConnection(AddBlockSender sender,
|
||||
ChainState st,
|
||||
String targetBch,
|
||||
byte[] targetHeaderHash,
|
||||
Duration timeout) {
|
||||
sendConnection(sender, st, MsgSubType.CONNECTION_FRIEND, targetBch, targetHeaderHash, timeout);
|
||||
}
|
||||
|
||||
private static void sendContactConnection(AddBlockSender sender,
|
||||
ChainState st,
|
||||
String targetBch,
|
||||
byte[] targetHeaderHash,
|
||||
Duration timeout) {
|
||||
sendConnection(sender, st, MsgSubType.CONNECTION_CONTACT, targetBch, targetHeaderHash, timeout);
|
||||
}
|
||||
|
||||
private static void sendConnection(AddBlockSender sender,
|
||||
ChainState st,
|
||||
short relationSubType,
|
||||
String targetBch,
|
||||
byte[] targetHeaderHash,
|
||||
Duration timeout) {
|
||||
ChainState.NextLine ln = st.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender.send(new ConnectionBody(
|
||||
0,
|
||||
ln.prevLineNumber,
|
||||
ln.prevLineHash32,
|
||||
ln.thisLineNumber,
|
||||
relationSubType,
|
||||
targetBch,
|
||||
0,
|
||||
targetHeaderHash
|
||||
), timeout);
|
||||
}
|
||||
|
||||
private static void addMutualFriend(Map<String, AddBlockSender> senders,
|
||||
Map<String, ChainState> states,
|
||||
Map<String, byte[]> headerHashes,
|
||||
String a,
|
||||
String b,
|
||||
Duration t) {
|
||||
sendFriendConnection(senders.get(a), states.get(a), bch(b), headerHashes.get(b), t);
|
||||
sendFriendConnection(senders.get(b), states.get(b), bch(a), headerHashes.get(a), t);
|
||||
}
|
||||
|
||||
private static void addMutualContact(Map<String, AddBlockSender> senders,
|
||||
Map<String, ChainState> states,
|
||||
Map<String, byte[]> headerHashes,
|
||||
String a,
|
||||
String b,
|
||||
Duration t) {
|
||||
sendContactConnection(senders.get(a), states.get(a), bch(b), headerHashes.get(b), t);
|
||||
sendContactConnection(senders.get(b), states.get(b), bch(a), headerHashes.get(a), t);
|
||||
}
|
||||
|
||||
private static void verifyOutFriendsCount(WsSession ws, TestResult r, String login, int expectedCount, Duration t) {
|
||||
String resp = ws.call("GetFriendsLists#" + login, JsonBuilders.getFriendsLists(login), t);
|
||||
int st = JsonParsers.status(resp);
|
||||
if (st != 200) {
|
||||
r.fail("GetFriendsLists " + login + " status=" + st + ", resp=" + resp);
|
||||
fail("GetFriendsLists failed for " + login);
|
||||
}
|
||||
|
||||
List<String> out = JsonParsers.friendsOut(resp);
|
||||
if (out.size() != expectedCount) {
|
||||
r.fail("У " + login + " ожидалось out_friends=" + expectedCount + ", фактически=" + out.size() + ", resp=" + resp);
|
||||
fail("Unexpected friends count for " + login);
|
||||
}
|
||||
|
||||
r.ok("GetFriendsLists " + login + ": out_friends=" + out.size());
|
||||
}
|
||||
|
||||
private static String bch(String login) {
|
||||
return login + "-001";
|
||||
}
|
||||
|
||||
private static UserKeys deriveKeysFromPassword(String password) {
|
||||
byte[] base = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
|
||||
String baseB64 = Base64.getEncoder().encodeToString(base);
|
||||
|
||||
byte[] rootPriv = HashSHA256Util.sha256((baseB64 + "root.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] bchPriv = HashSHA256Util.sha256((baseB64 + "bch.key").getBytes(StandardCharsets.UTF_8));
|
||||
byte[] devPriv = HashSHA256Util.sha256((baseB64 + "dev.key").getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
|
||||
String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
|
||||
String devPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(devPriv));
|
||||
|
||||
return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
|
||||
}
|
||||
|
||||
private static final class UserKeys {
|
||||
final String solanaPublicB64;
|
||||
final String blockchainPublicB64;
|
||||
final String devicePublicB64;
|
||||
final byte[] blockchainPrivate32;
|
||||
|
||||
private UserKeys(String solanaPublicB64,
|
||||
String blockchainPublicB64,
|
||||
String devicePublicB64,
|
||||
byte[] blockchainPrivate32) {
|
||||
this.solanaPublicB64 = solanaPublicB64;
|
||||
this.blockchainPublicB64 = blockchainPublicB64;
|
||||
this.devicePublicB64 = devicePublicB64;
|
||||
this.blockchainPrivate32 = blockchainPrivate32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ public final class WsTestClient implements AutoCloseable {
|
||||
|
||||
private final WebSocket ws;
|
||||
private final Map<String, CompletableFuture<String>> pending = new ConcurrentHashMap<>();
|
||||
private final StringBuilder incomingTextBuffer = new StringBuilder();
|
||||
|
||||
public WsTestClient(String wsUri) {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
@ -24,11 +25,21 @@ public final class WsTestClient implements AutoCloseable {
|
||||
.buildAsync(URI.create(wsUri), new WebSocket.Listener() {
|
||||
@Override
|
||||
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||
String msg = data.toString();
|
||||
String requestId = extractRequestId(msg);
|
||||
if (requestId != null) {
|
||||
CompletableFuture<String> f = pending.remove(requestId);
|
||||
if (f != null) f.complete(msg);
|
||||
String msg = null;
|
||||
synchronized (incomingTextBuffer) {
|
||||
incomingTextBuffer.append(data);
|
||||
if (last) {
|
||||
msg = incomingTextBuffer.toString();
|
||||
incomingTextBuffer.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg != null) {
|
||||
String requestId = extractRequestId(msg);
|
||||
if (requestId != null) {
|
||||
CompletableFuture<String> f = pending.remove(requestId);
|
||||
if (f != null) f.complete(msg);
|
||||
}
|
||||
}
|
||||
webSocket.request(1);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
@ -73,4 +84,4 @@ public final class WsTestClient implements AutoCloseable {
|
||||
ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").join();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user