Первичная настройка: ключ спрашивается один раз, затем обновления, модули и «Время и сеть»; DEPO одним шагом

- Первая страница — только администратор (и восстановление из копии); ключ продукта — после создания
  администратора, на шаге «Лицензия». Консоль первой загрузки ключ больше не спрашивает (для образа с
  комплектом bj).
- «Подготовка модулей» начинает с проверки обновлений: есть — ставит (bj перезапускается, страница ждёт и
  продолжает), нет — дальше; выпуск, который уже ставили, по кругу не ставится. Затем модули лицензии и
  переход на «Время и сеть» (шаг 12) — часовой пояс клиент выбирает сам, шаг больше не проскакивает.
- Учёт DEPO одним шагом: bj сам находит базу Fancy DEPO на сервере (по таблицам OD_FACES/OD_RESTS), имя
  базы не спрашивается. Один скрипт создаёт логин bj, таблицы обмена и права; по выбору — задание
  обработки в SQL Agent или обработка на сервере bj. Выполнить сразу (логин администратора СУБД не
  сохраняется), скачать скрипт для администратора или подключиться готовым логином.
- Восстановление из копии — только на первой странице, в мастере его больше нет.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 11:42:10 +03:00
parent 96976df549
commit 8905ebc6fb
12 changed files with 763 additions and 239 deletions
+42 -3
View File
@@ -307,14 +307,22 @@ func TestPrepareScreen(t *testing.T) {
t.Fatalf("нет %q", want)
}
}
for in, want := range map[string]string{"": "/admin/wizard", "/admin/setup?x=1": "/admin/setup?x=1", "https://evil": "/admin/wizard", "//evil/admin/": "/admin/wizard", "/admin/wizard\r\nX: y": "/admin/wizard"} {
const def = "/admin/wizard?step=12" // после ключа и модулей — «Время и сеть»
for in, want := range map[string]string{"": def, "/admin/setup?x=1": "/admin/setup?x=1", "https://evil": def, "//evil/admin/": def, "/admin/wizard\r\nX: y": def} {
if got := safeNext(in); got != want {
t.Errorf("safeNext(%q) = %q, ожидалось %q", in, got, want)
}
}
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
if items := prepareItems(rc, false, false, ""); len(items) != 1 || items[0].State != "ok" {
t.Fatalf("без лицензии — только ядро: %+v", items)
if items := prepareItems(rc, false, false, ""); len(items) != 2 || items[0].Title != "Обновление bj" || items[0].State != "wait" || items[1].State != "ok" {
t.Fatalf("без лицензии — обновление (ещё не проверялось) и ядро: %+v", items)
}
// выпуск, который подготовка уже ставила, после перезапуска bj не ставится по кругу
oldTried := prepareTriedFile
prepareTriedFile = t.TempDir() + "/tried"
defer func() { prepareTriedFile = oldTried }()
if prepareTried("2026.10.06.1") || !prepareTried("2026.10.06.1") || prepareTried("2026.10.06.2") {
t.Fatal("prepareTried: первый раз — ставить, второй раз тот же выпуск — нет, новый выпуск — ставить")
}
}
@@ -338,3 +346,34 @@ func TestFromLocalBundle(t *testing.T) {
t.Fatal("другой выпуск — качать с сервера, а не брать из образа")
}
}
// Один скрипт подготовки базы DEPO: сам ищет базу, создаёт логин, таблицы, права и (по выбору) задание.
func TestDepoSetupScript(t *testing.T) {
f := FancyMSSQLSettings{}
s := depoSetupScript(f, "", "bj_user", "Pass1word", true)
for _, want := range []string{"DECLARE @db sysname = N'';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''Pass1word''", "FTV_M2M_EVENTS", "db_datareader", "sp_add_job @job_name = N'bj2depo'", "@database_name = @db", "SQLAgentReaderRole"} {
if !strings.Contains(s, want) {
t.Fatalf("в скрипте нет %q", want)
}
}
if strings.Contains(s, "BJ_DEPO_DATA") || strings.Contains(s, "\nGO") {
t.Fatal("в скрипте — имя нашей базы или GO (переменные @db между пакетами не живут)")
}
// каждая строка EXEC(@in + N'…') — закрытый литерал: чётное число кавычек
for _, line := range strings.Split(s, "\n") {
if strings.HasPrefix(line, "EXEC(") && strings.HasSuffix(line, "');") && strings.Count(line, "'")%2 != 0 {
t.Fatalf("нечётные кавычки: %s", line)
}
}
bj := depoSetupScript(f, "DEPO_X", "bj_user", "Pass1word", false)
if strings.Contains(bj, "sp_add_job") || !strings.Contains(bj, "GRANT EXECUTE ON dbo.FTV_M2M_JOB") || !strings.Contains(bj, "N'DEPO_X'") {
t.Fatal("обработка силами bj: без задания, с правом на процедуру и указанной базой")
}
pg := depoSetupScript(FancyMSSQLSettings{DBType: "postgres"}, "depo", "bj_user", "p'w", false)
if !strings.Contains(pg, `\c "depo"`) || !strings.Contains(pg, "PASSWORD 'p''w'") {
t.Fatal("PostgreSQL: подключение к базе и экранирование пароля")
}
if p := depoGenPassword(); len(p) != 24 || !strings.ContainsAny(p, "ABCDEFGHJKLMNPQRSTUVWXYZ") || !strings.ContainsAny(p, "23456789") {
t.Fatalf("пароль: %q", p)
}
}