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
+}