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:
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
@@ -33,6 +35,8 @@ func runCLI(args []string) int {
|
||||
return cliTOTPSetup(args[1:])
|
||||
case "totp-reset":
|
||||
return cliTOTPReset(args[1:])
|
||||
case "create-user":
|
||||
return cliCreateUser(args[1:])
|
||||
case "cleanup":
|
||||
return cliCleanup(args[1:])
|
||||
case "reindex":
|
||||
@@ -273,6 +277,107 @@ func cliSetInstanceAdmin(command string, args []string) int {
|
||||
|
||||
// cliCleanup запускает чистку вручную: сессии, аудит и файлы без ссылок
|
||||
// (AGENT.md 6.4, 10.7).
|
||||
// cliCreateUser создаёт аккаунты по команде обслуживания: регистрация на
|
||||
// инстансе может быть выключена, а для нагрузочного теста (AGENT.md 11.4)
|
||||
// нужны десятки пользователей. Пароль задаёт оператор, аккаунты создаются без
|
||||
// сессий; список логинов и паролей можно выгрузить в файл для генератора
|
||||
// нагрузки (--out).
|
||||
func cliCreateUser(args []string) int {
|
||||
flags := flag.NewFlagSet("create-user", flag.ContinueOnError)
|
||||
email := flags.String("email", "", "email пользователя")
|
||||
username := flags.String("username", "", "username (по умолчанию — часть email)")
|
||||
password := flags.String("password", "", "пароль")
|
||||
count := flags.Int("count", 1, "сколько аккаунтов создать (для нагрузки)")
|
||||
prefix := flags.String("prefix", "", "префикс логина в пакетном режиме, например loadtest")
|
||||
domain := flags.String("domain", "loadtest.local", "домен email в пакетном режиме")
|
||||
out := flags.String("out", "", "файл с созданными учётными данными (JSON)")
|
||||
sessions := flags.Bool("sessions", false, "выдать каждому аккаунту сессию (для нагрузочного теста)")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *password == "" {
|
||||
fmt.Fprintln(os.Stderr, "glchat create-user: укажите --password")
|
||||
return 2
|
||||
}
|
||||
if *count > 1 && *prefix == "" {
|
||||
fmt.Fprintln(os.Stderr, "glchat create-user: для --count > 1 нужен --prefix")
|
||||
return 2
|
||||
}
|
||||
if *count <= 1 && *email == "" {
|
||||
fmt.Fprintln(os.Stderr, "glchat create-user: укажите --email")
|
||||
return 2
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
created := make([]credentials, 0, *count)
|
||||
|
||||
code := withServices(func(ctx context.Context, _ *store.Store, authService *auth.Service, _ config.Config) error {
|
||||
for index := 1; index <= *count; index += 1 {
|
||||
name := *username
|
||||
address := *email
|
||||
if *count > 1 {
|
||||
name = fmt.Sprintf("%s%d", *prefix, index)
|
||||
address = fmt.Sprintf("%s%d@%s", *prefix, index, *domain)
|
||||
}
|
||||
user, err := authService.CreateUserByOperator(ctx, auth.RegisterInput{
|
||||
Username: name,
|
||||
Email: address,
|
||||
Password: *password,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.ErrUsernameTaken) || errors.Is(err, auth.ErrEmailTaken) {
|
||||
fmt.Printf("пропущен (уже существует): %s\n", address)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("создать %s: %w", address, err)
|
||||
}
|
||||
entry := credentials{
|
||||
ID: strconv.FormatUint(user.ID, 10),
|
||||
Username: user.Username,
|
||||
Email: address,
|
||||
Password: *password,
|
||||
}
|
||||
if *sessions {
|
||||
token, err := authService.IssueSessionForOperator(ctx, user.ID, "glchat-cli loadtest")
|
||||
if err != nil {
|
||||
return fmt.Errorf("сессия для %s: %w", address, err)
|
||||
}
|
||||
entry.Token = token
|
||||
}
|
||||
created = append(created, entry)
|
||||
if *count == 1 || index%25 == 0 || index == *count {
|
||||
fmt.Printf("создано %d из %d\n", index, *count)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if code != 0 {
|
||||
return code
|
||||
}
|
||||
if *out != "" {
|
||||
// #nosec G117 -- файл с тестовыми паролями создаётся намеренно, права 600.
|
||||
encoded, err := json.MarshalIndent(created, "", " ")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "glchat create-user: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
// Файл содержит пароли тестовых аккаунтов: права 600.
|
||||
if err := os.WriteFile(*out, encoded, 0o600); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "glchat create-user: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("учётные данные: %s (%d)\n", *out, len(created))
|
||||
}
|
||||
fmt.Printf("создано аккаунтов: %d\n", len(created))
|
||||
return 0
|
||||
}
|
||||
|
||||
func cliCleanup(args []string) int {
|
||||
flags := flag.NewFlagSet("cleanup", flag.ContinueOnError)
|
||||
auditDays := flags.Int("audit-days", -1, "сколько дней хранить журнал аудита (по умолчанию из конфигурации)")
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "bootstrap-admin", "reset-password", "make-admin", "remove-admin", "totp-setup", "totp-reset",
|
||||
"cleanup", "reindex":
|
||||
"create-user", "cleanup", "reindex":
|
||||
os.Exit(runCLI(os.Args[1:]))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user