Добавлено тестовое заполнение и доработки входа

- Добавлено расширенное тестовое заполнение данных через Seed_TestDataPopulation и SeedDataPopulationHelper (включая базовую схему для пользователей 1, 2, 3, их связи и профили).

- Убраны лишние проверки из тестового заполнения: сид теперь только заполняет данные.

- Исправлен WsTestClient: корректная сборка фрагментированных WS-сообщений до полного JSON.

- На странице входа по логину добавлена подсказка про основные тестовые логины 1, 2, 3 (вход без пароля).
This commit is contained in:
AidarKC 2026-04-17 22:26:37 +03:00
parent 30fcde5744
commit ba3ee4290f
6 changed files with 589 additions and 248 deletions

View File

@ -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;
}

View File

@ -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');
});

View File

@ -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('Пользователь не найден');

View 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;
}
}
}

View File

@ -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
private static void addContacts(SeedDataPopulationHelper seed, String owner, String... contacts) {
for (String contact : contacts) {
seed.addContact(owner, contact);
}
}
""".formatted(
requestId,
login,
bch(login),
keys.solanaPublicB64,
keys.blockchainPublicB64,
keys.devicePublicB64
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;
}
}
}

View File

@ -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,12 +25,22 @@ 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 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);
}