feat(ops): генератор нагрузки и подготовка аккаунтов для теста
- cmd/loadgen: держит заданное число WS-клиентов Gateway, шлёт сообщения с нужной частотой, измеряет задержку доставки (p50/p95/p99/max), ответы API, разрывы соединений и пишет отчёт в JSON; - glchat create-user (CLI + обёртка): создаёт аккаунты в обход выключенной регистрации, пакетно (--count/--prefix), при --sessions сразу выдаёт сессии и складывает учётные данные в файл 600 — иначе сотня клиентов не сможет войти из-за лимита попыток по IP; - auth: CreateUserByOperator и IssueSessionForOperator с тестом; - web/e2e/voice-ten.spec.ts: десять участников в одной голосовой комнате, проверка плиток, входящего аудио и слоя 1080p60 у клиента; сессии берутся из файла (без формы входа); - .gitignore: каталоги вывода Playwright test-results*.
This commit is contained in:
@@ -213,6 +213,61 @@ func (s *Service) Register(ctx context.Context, in RegisterInput) (*store.User,
|
||||
return user, token, session, nil
|
||||
}
|
||||
|
||||
// CreateUserByOperator создаёт аккаунт по команде обслуживания (CLI
|
||||
// create-user): регистрация на инстансе может быть выключена, а сессия
|
||||
// создателю не нужна — он действует от имени администратора. Используется в
|
||||
// том числе для подготовки нагрузки (AGENT.md 11.4).
|
||||
func (s *Service) CreateUserByOperator(ctx context.Context, in RegisterInput) (*store.User, error) {
|
||||
username := strings.TrimSpace(in.Username)
|
||||
if err := crypto.ValidateUsername(username); err != nil {
|
||||
return nil, ErrInvalidUsername
|
||||
}
|
||||
if err := s.policy.validate(in.Password); err != nil {
|
||||
return nil, ErrWeakPassword
|
||||
}
|
||||
email := crypto.NormalizeEmail(in.Email)
|
||||
if err := validateEmail(email); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwordHash, err := s.hasher.Hash(in.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
userID := s.store.NextID()
|
||||
emailEncrypted, err := s.encryptEmail(userID, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := s.store.CreateUser(ctx, store.CreateUserParams{
|
||||
ID: userID,
|
||||
Username: username,
|
||||
DisplayName: defaultDisplayName(in.DisplayName, username),
|
||||
EmailEnc: emailEncrypted,
|
||||
EmailIndex: s.masterKey.BlindIndex(email),
|
||||
PasswordHash: passwordHash,
|
||||
Locale: in.Locale,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrConflict) {
|
||||
if existing, lookupErr := s.store.GetUserByUsername(ctx, username); lookupErr == nil && existing != nil {
|
||||
return nil, ErrUsernameTaken
|
||||
}
|
||||
return nil, ErrEmailTaken
|
||||
}
|
||||
return nil, fmt.Errorf("create user: %w", err)
|
||||
}
|
||||
_ = s.store.RecordSecurityEvent(ctx, &user.ID, "created_by_operator", "", "", "")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IssueSessionForOperator выдаёт сессию по команде обслуживания: нужна для
|
||||
// нагрузочного теста (AGENT.md 11.4), где сотни клиентов не могут пройти вход
|
||||
// через /auth/login — лимит попыток по IP рассчитан на защиту от перебора.
|
||||
func (s *Service) IssueSessionForOperator(ctx context.Context, userID uint64, userAgent string) (string, error) {
|
||||
token, _, err := s.createSession(ctx, userID, userAgent, "")
|
||||
return token, err
|
||||
}
|
||||
|
||||
// UserByEmail ищет аккаунт по email через blind index (без расшифровки БД).
|
||||
func (s *Service) UserByEmail(ctx context.Context, email string) (*store.User, error) {
|
||||
return s.userByEmail(ctx, email)
|
||||
|
||||
Reference in New Issue
Block a user