diff --git a/shine-UI/js/pages/login-password-view.js b/shine-UI/js/pages/login-password-view.js index 92e3d4a..0fce13b 100644 --- a/shine-UI/js/pages/login-password-view.js +++ b/shine-UI/js/pages/login-password-view.js @@ -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 = ` `; 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; } diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 11c3b32..d49be5d 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -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'); }); diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 5b8923e..bccd3c5 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -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('Пользователь не найден'); diff --git a/src/test/java/test/it/cases/SeedDataPopulationHelper.java b/src/test/java/test/it/cases/SeedDataPopulationHelper.java new file mode 100644 index 0000000..632fe50 --- /dev/null +++ b/src/test/java/test/it/cases/SeedDataPopulationHelper.java @@ -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 states = new LinkedHashMap<>(); + private final Map senders = new LinkedHashMap<>(); + private final Map 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 users) { + for (UserSpec user : users) createUserViaApi(user.login); + for (UserSpec user : users) initHeader(user.login); + } + + public void applyProfiles(List 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; + } + } +} diff --git a/src/test/java/test/it/cases/Seed_TestDataPopulation.java b/src/test/java/test/it/cases/Seed_TestDataPopulation.java index 3e02fd1..97d5d11 100644 --- a/src/test/java/test/it/cases/Seed_TestDataPopulation.java +++ b/src/test/java/test/it/cases/Seed_TestDataPopulation.java @@ -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 users = List.of("1", "2", "3", "4", "5", "6", "7", "8", "9"); + // ----------------------------------------------------------------- + // 1) Пользователи и профили + // ----------------------------------------------------------------- + List 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 states = new HashMap<>(); - Map senders = new HashMap<>(); - Map 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 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 senders, - Map states, - Map 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 senders, - Map states, - Map 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 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; - } } } diff --git a/src/test/java/test/it/utils/ws/WsTestClient.java b/src/test/java/test/it/utils/ws/WsTestClient.java index 9623fa8..d249d0e 100644 --- a/src/test/java/test/it/utils/ws/WsTestClient.java +++ b/src/test/java/test/it/utils/ws/WsTestClient.java @@ -16,6 +16,7 @@ public final class WsTestClient implements AutoCloseable { private final WebSocket ws; private final Map> 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 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 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) {} } -} \ No newline at end of file +}