Files
Bridge-and-Join-s/internal/lkgateway/onboarding_test.go
T
zuevav 35f70a1b49 Учёт DEPO: имя базы обязательно, bj базу по серверу не ищет
Имя базы DEPO задаёт клиент — скрипт и подготовка логином администратора
работают только с ней. Автопоиск по серверу убран (экран, скрипт, подготовка,
проверка подключения, старый экран conn), выбор из нескольких найденных баз —
тоже. Вместо поиска — защита от опечатки: скрипт (SQL Server и PostgreSQL),
подготовка и проверка подключения сначала убеждаются, что в названной базе
есть учётные таблицы OD_FACES и OD_RESTS, иначе останавливаются, ничего не
меняя.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 15:18:15 +03:00

521 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
func TestAccPrefix(t *testing.T) {
for in, want := range map[string]string{"ВТНКЦ": "ВТНКЦ", " БР ": "БР", "AB_1": "AB_1", "a'b": "", "x%": "", "": ""} {
if got := cleanAccPrefix(in); got != want {
t.Errorf("cleanAccPrefix(%q)=%q, want %q", in, got, want)
}
}
if q := accSQL("LIKE N'{ACC}%'"); !strings.Contains(q, "N'ВТНКЦ%'") {
t.Errorf("accSQL по умолчанию: %s", q)
}
}
func TestDoneNSDUnknownProfile(t *testing.T) {
var s Settings
s.NSD.IGWBaseURL, s.NSD.Profile = "http://localhost:8090", "prod"
if doneNSD(s) {
t.Fatal("профиль prod неизвестен bj — шаг не должен считаться выполненным")
}
s.NSD.Profile = "prod-gost"
if !doneNSD(s) {
t.Fatal("prod-gost — известный профиль")
}
}
func TestStoreCertKindAndExpiry(t *testing.T) {
bundle := map[string]bool{}
own := NSDStoreCert{Kind: "x509", Subject: "O=Предприятие 1046,CN=Сотрудник 1046,INN=078000001046", SubjectCN: "Сотрудник 1046", IssuerCN: "Тестовый УЦ", Serial: "AA01"}
nsd := NSDStoreCert{Kind: "x509", Subject: "O=НКО АО НРД,CN=Петров,INN=007702165310", SubjectCN: "Петров", IssuerCN: "Тестовый УЦ", Serial: "BB02"}
ca := NSDStoreCert{Kind: "x509", SubjectCN: "Тестовый УЦ", SelfSigned: true}
for c, want := range map[*NSDStoreCert]string{&own: "own", &nsd: "counterpart", &ca: "ca"} {
if got := storeCertKind(*c, bundle); got != want {
t.Errorf("%s: %s, want %s", c.SubjectCN, got, want)
}
}
if subjectINN(nsd.Subject) != "7702165310" {
t.Errorf("subjectINN: %q", subjectINN(nsd.Subject))
}
// уведомления: за 20 дней — одно (отметка 30), повторно не шлётся; за 5 дней — следующее (7)
dir := t.TempDir()
certExpiry.file = filepath.Join(dir, "cert-expiry.json")
rc, err := NewRuntimeConfig(filepath.Join(dir, "setup.json"))
if err != nil {
t.Fatal(err)
}
own.NotAfter = time.Now().AddDate(0, 0, 20).Format("02.01.2006")
st := rc.Snapshot().NSDCerts
st.LastStore = []NSDStoreCert{own, nsd, ca}
_ = rc.UpdateNSDCerts(st)
count := func() int { return len(rc.Snapshot().News.Items) }
certExpiryTick(rc)
n1 := count()
certExpiryTick(rc)
if n1 != 1 || count() != 1 {
t.Fatalf("за 20 дней: ожидалось одно уведомление, было %d, затем %d", n1, count())
}
own.NotAfter = time.Now().AddDate(0, 0, 5).Format("02.01.2006")
st.LastStore = []NSDStoreCert{own}
_ = rc.UpdateNSDCerts(st)
certExpiryTick(rc)
if count() != 2 {
t.Fatalf("за 5 дней: ожидалось второе уведомление, всего %d", count())
}
raw, _ := os.ReadFile(certExpiry.file)
if !strings.Contains(string(raw), "7") {
t.Fatalf("отметка 7 не сохранена: %s", raw)
}
}
func TestSafeReturnPathKeepsStep(t *testing.T) {
for in, want := range map[string]string{
"/admin/wizard?step=4": "/admin/wizard?step=4",
"/admin/wizard?step=3&sub=done&flash=x": "/admin/wizard?step=3&sub=done",
"/admin/setup#sec-org": "/admin/setup",
"/admin/wizard?step=4%0d%0aX": "/admin/wizard",
"//evil.example/admin": "",
"/admin/x?a=<script>": "/admin/x",
} {
if got := safeReturnPath(in); got != want {
t.Errorf("safeReturnPath(%q)=%q, want %q", in, got, want)
}
}
}
func TestFirstRunRestoreNeedsCode(t *testing.T) {
if !authExempt("/first-run/backup/list") || authExempt("/first-runx") {
t.Fatal("пути восстановления при первичной настройке должны быть без входа, но не шире")
}
mux := http.NewServeMux()
registerFirstRunRestore(mux)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/first-run/backup/restore", strings.NewReader("code=1234&name=2026-10-05_1445")))
if !strings.Contains(rec.Body.String(), `"ok":false`) {
t.Fatalf("без ожидающей первичной настройки восстановление не должно запускаться: %s", rec.Body.String())
}
}
func TestHTTPSSelfSignedAndRedirect(t *testing.T) {
old := tlsDir
tlsDir = t.TempDir()
defer func() { tlsDir = old }()
if err := ensureSelfSigned(false); err != nil {
t.Fatal(err)
}
if cn, until, err := tlsCertInfo(); err != nil || cn == "" || certDaysLeft(until) < 1000 {
t.Fatalf("сертификат: %q %v %v", cn, until, err)
}
if _, err := loadTLSCert(nil); err != nil {
t.Fatal(err)
}
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
_ = rc.UpdateHTTPS(HTTPSSettings{Redirect: true})
h := httpsRedirect(rc, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) }))
cases := []struct {
path, remote string
want int
}{
{"/admin/setup", "10.10.10.5:5000", http.StatusFound},
{"/api/v1/lk/x", "10.10.10.5:5000", http.StatusTeapot},
{"/healthz", "10.10.10.5:5000", http.StatusTeapot},
{"/admin/setup", "127.0.0.1:5000", http.StatusTeapot},
}
for _, c := range cases {
req := httptest.NewRequest(http.MethodGet, "http://10.10.10.6:8080"+c.path, nil)
req.RemoteAddr = c.remote
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != c.want {
t.Errorf("%s с %s: %d, ожидалось %d", c.path, c.remote, rec.Code, c.want)
}
if c.want == http.StatusFound && rec.Header().Get("Location") != "https://10.10.10.6:8443/admin/setup" {
t.Errorf("перенаправление: %s", rec.Header().Get("Location"))
}
}
}
func TestReceiverUnknownRejected(t *testing.T) {
if !handbookLoaded() {
t.Skip("справочник участников не вшит")
}
svc := &Service{defaultSender: "MC0413600000", defaultReceiver: "MC0010300000"}
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{ReceivingDepositoryINN: "0000000001"}); err == nil || !errors.Is(err, errReceiverUnknown) {
t.Fatalf("неизвестный депозитарий получателя должен давать errReceiverUnknown, а не чужой код: %v", err)
}
}
func TestBuildRequestNeedsOrganization(t *testing.T) {
svc := &Service{}
if _, err := svc.BuildRequest(context.Background(), CreateClaimRequest{}); err == nil || !strings.Contains(err.Error(), "организация не задана") {
t.Fatalf("без кода отправителя перевод не должен формироваться: %v", err)
}
}
func TestKeyArchiveContour(t *testing.T) {
dir := t.TempDir()
raw, err := nsdCertsFS.ReadFile("nsd-certs/nsd-test-gost-petrov-20240924.cer")
if err != nil {
t.Skip(err)
}
_ = os.MkdirAll(filepath.Join(dir, "spr1"), 0o755)
_ = os.WriteFile(filepath.Join(dir, "spr1", "user.cer"), raw, 0o644)
if c, iss := keyArchiveContour(dir); c != "test3" || iss == "" {
t.Fatalf("тестовый сертификат: %q %q", c, iss)
}
raw, _ = nsdCertsFS.ReadFile("nsd-certs/nsd-prod-gost-shlyappo-20260821.cer")
dir2 := t.TempDir()
_ = os.WriteFile(filepath.Join(dir2, "user.cer"), raw, 0o644)
if c, _ := keyArchiveContour(dir2); c != "prod" {
t.Fatalf("боевой сертификат: %q", c)
}
}
func TestWizardChaptersWithoutLicense(t *testing.T) {
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
done := map[int]bool{2: true, 6: true}
path, chapters := wizPath(rc, done, 4)
want := []int{1, 12, 4, 7, 8, 9, 10} // без лицензии нет учёта DEPO, QUIK и модулей; база и Валидата — сделаны
if fmt.Sprint(path) != fmt.Sprint(want) {
t.Fatalf("путь %v, ожидался %v", path, want)
}
if len(chapters) != 4 || !chapters[1].Current || chapters[1].Steps[0].N != 4 {
t.Fatalf("главы: %+v", chapters)
}
if p, n := wizNeighbours(path, 4); p != 12 || n != 7 {
t.Fatalf("соседи шага 4: %d %d", p, n)
}
if d := wizDefaultStep(path, map[int]bool{1: true, 4: false}); d != 4 {
t.Fatalf("по умолчанию: %d", d)
}
all := map[int]bool{}
for _, n := range path {
all[n] = true
}
if d := wizDefaultStep(path, all); d != 10 {
t.Fatalf("всё сделано — проверка, а не %d", d)
}
if len(wizModuleViews(rc, rc.Snapshot())) != 0 {
t.Fatal("без лицензии модулей в мастере быть не должно")
}
if chapters[1].Title != "Организация" {
t.Fatalf("без учёта DEPO и QUIK глава 2 — «Организация», а не %q", chapters[1].Title)
}
if l := wizChaptersLine(chapters); l != "Четыре главы: сервер, организация, НРД, запуск." {
t.Fatalf("подзаголовок: %q", l)
}
}
func TestLettersFill(t *testing.T) {
var s Settings
s.NSD.OrgName, s.NSD.OrgINN, s.NSD.OrgCode = "АО «Тест»", "7700000001", "MC0000000001"
s.NSD.DeponentCode, s.NSD.AccountID, s.NSD.SectionID = "MC0000000001", "HL000000001C", "36MC0000000001F00"
s.Fancy.Server = "sql.example"
for _, k := range []string{"nsd-test", "nsd-prod", "arqa", "dba"} {
l, ok := letterFor(s, k)
if !ok || !strings.Contains(l.Body, "АО «Тест»") || l.Subject == "" {
t.Errorf("%s: %+v", k, l)
}
}
l, _ := letterFor(s, "nsd-test")
if !strings.Contains(l.Body, "HL000000001C") || !strings.Contains(l.Body, "ГОСТ") {
t.Error("в письме НРД нет реквизитов")
}
s.Fancy.DBType = "postgres"
if l, _ := letterFor(s, "dba"); strings.Contains(l.Body, "SQL Server Agent") || !strings.Contains(l.Body, "postgres") {
t.Error("письмо администратору PostgreSQL не должно говорить о SQL Agent")
}
if _, ok := letterFor(s, "nope"); ok {
t.Error("неизвестное письмо")
}
}
func TestRuPlural(t *testing.T) {
for n, want := range map[int]string{1: "участник", 2: "участника", 4: "участника", 5: "участников", 11: "участников", 12: "участников", 21: "участник", 162: "участника", 111: "участников", 0: "участников"} {
if got := ruPlural(n, "участник", "участника", "участников"); got != want {
t.Errorf("%d: %q, ожидалось %q", n, got, want)
}
}
}
// Без лицензии QUIK МНП маршрута «ошибки QUIK» в форме нет, а сохранение формы не стирает его прежнее значение.
func TestNotifyKindsByLicense(t *testing.T) {
rc, _ := NewRuntimeConfig(t.TempDir() + "/setup.json")
for _, k := range visibleNotifyKinds(rc) {
if k.Key == "quik" {
t.Fatal("без лицензии QUIK МНП маршрута QUIK быть не должно")
}
}
if len(visibleNotifyKinds(rc)) != len(notifyKinds)-1 {
t.Fatalf("типы: %+v", visibleNotifyKinds(rc))
}
h := &setupHandlers{rc: rc}
form := url.Values{"enabled": {"on"}, "emails": {"depo@example.ru"}, "route_incoming_depo": {"1"}}
req := httptest.NewRequest(http.MethodPost, "/admin/setup/incoming-notify", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
h.saveIncomingNotify(httptest.NewRecorder(), req)
r := rc.Snapshot().Incoming.Routes
if r["incoming"] != "depo" || r["quik"] != defaultNotifyRoutes["quik"] {
t.Fatalf("маршруты после сохранения: %+v", r)
}
}
// Модули ставятся сразу, когда состав лицензии изменился (план или признаки), а не при каждом продлении.
func TestLicenseScopeChanged(t *testing.T) {
a := &license.License{Plan: "pro", Features: []string{"updates", "quik-mnp"}}
same := &license.License{Plan: "pro", Features: []string{"quik-mnp", "updates"}}
more := &license.License{Plan: "pro", Features: []string{"updates", "quik-mnp", "npr"}}
other := &license.License{Plan: "enterprise", Features: []string{"updates", "quik-mnp"}}
if licenseScopeChanged(a, same) {
t.Fatal("тот же состав в другом порядке — не изменение")
}
if !licenseScopeChanged(a, more) || !licenseScopeChanged(a, other) || !licenseScopeChanged(nil, a) {
t.Fatal("новый модуль или план — изменение")
}
}
// «Подготовка модулей»: страница собирается, переход — только на свои страницы, без лицензии — одно ядро.
func TestPrepareScreen(t *testing.T) {
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
var b strings.Builder
d := PrepareData{page: page{Title: "Подготовка модулей", Active: "wizard"}, Tenant: "ТЕСТ ZETIT", Plan: "pro", Until: "активна до 04.12.2026", Next: "/admin/wizard", Flash: "Администратор создан, ключ продукта принят."}
if err := a.prepare.ExecuteTemplate(&b, "layout", d); err != nil {
t.Fatal(err)
}
for _, want := range []string{"Подготовка модулей", "ТЕСТ ZETIT", "/admin/setup/prepare/status", "Перейти к настройке"} {
if !strings.Contains(b.String(), want) {
t.Fatalf("нет %q", want)
}
}
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) != 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: первый раз — ставить, второй раз тот же выпуск — нет, новый выпуск — ставить")
}
}
// Модуль из комплекта образа берётся без скачивания, только если это тот же файл, что в выпуске.
func TestFromLocalBundle(t *testing.T) {
dir, staging := t.TempDir(), t.TempDir()
old := localBundleDir
localBundleDir = dir
defer func() { localBundleDir = old }()
if err := os.WriteFile(filepath.Join(dir, "crypto-service.jar"), []byte("jar-2026.09.25.14"), 0o644); err != nil {
t.Fatal(err)
}
sum, _, _ := release.HashFile(filepath.Join(dir, "crypto-service.jar"))
if !fromLocalBundle(release.Artifact{Name: "crypto-service.jar", File: "crypto-service.jar", SHA256: sum}, staging) {
t.Fatal("тот же файл — должен взяться из образа")
}
if b, _ := os.ReadFile(filepath.Join(staging, "crypto-service.jar")); string(b) != "jar-2026.09.25.14" {
t.Fatalf("скопировано: %q", b)
}
if fromLocalBundle(release.Artifact{Name: "crypto-service.jar", File: "crypto-service.jar", SHA256: strings.Repeat("0", 64)}, t.TempDir()) {
t.Fatal("другой выпуск — качать с сервера, а не брать из образа")
}
}
// Один скрипт подготовки базы DEPO: в названной базе (сам не ищет, но проверяет, что это DEPO) создаёт логин,
// таблицы, права и (по выбору) задание.
func TestDepoSetupScript(t *testing.T) {
f := FancyMSSQLSettings{}
s := depoSetupScript(f, "DEPO_DATA", "bj_user", "Pass1word", true)
for _, want := range []string{"DECLARE @db sysname = N'DEPO_DATA';", "RAISERROR(N'Базы %s на этом сервере нет.", "OBJECT_ID(QUOTENAME(@db) + N'.dbo.OD_FACES', N'U') IS NULL", "DECLARE @pass nvarchar(128) = N'Pass1word';", "OD_FACES", "CREATE LOGIN [bj_user] WITH PASSWORD = N''' + REPLACE(@pass, N'''', N'''''')", "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") || strings.Contains(s, "sys.databases") || strings.Contains(s, "%!") {
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'") || !strings.Contains(pg, "RAISE EXCEPTION 'В базе % нет учётных таблиц DEPO") {
t.Fatal("PostgreSQL: подключение к базе и экранирование пароля")
}
if p := depoGenPassword(); len(p) != 24 || !strings.ContainsAny(p, "ABCDEFGHJKLMNPQRSTUVWXYZ") || !strings.ContainsAny(p, "23456789") {
t.Fatalf("пароль: %q", p)
}
}
// Шаги мастера отрисовываются без ошибок шаблона: лицензия, учёт DEPO одним шагом, время и сеть, проверка.
func TestWizardStepsRender(t *testing.T) {
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
priv := testLicenseKey(t)
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
IssuedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}}
for step, want := range map[string]string{"1": "QUIK: неторговые поручения", "3": "wiz-depo", "5": "Сохранить и проверить", "12": "wiz-tz-suggest", "10": "Проверка"} {
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, "/admin/wizard?step="+step, nil))
body := w.Body.String()
if d := os.Getenv("BJ_DUMP_PAGES"); d != "" {
_ = os.WriteFile(filepath.Join(d, "wizard-"+step+".html"), []byte(body), 0o644)
}
if w.Code != http.StatusOK || !strings.Contains(body, want) {
i := strings.Index(body, "<main")
t.Fatalf("шаг %s: код %d, нет %q\n%s", step, w.Code, want, body[max(i, 0):min(len(body), max(i, 0)+1500)])
}
if step == "3" && (strings.Contains(body, "BJ_DEPO_DATA") || !strings.Contains(body, "depo_job")) {
t.Fatal("шаг DEPO: имя нашей базы в форме или нет выбора, где работает задание")
}
if strings.Contains(body, "wiz-restore") {
t.Fatal("восстановление из копии — только на первой странице, не в мастере")
}
}
}
// Снимки страниц первичной настройки для проверки глазами: BJ_DUMP_PAGES=<каталог> go test -run TestDumpOnboardingPages.
func TestDumpOnboardingPages(t *testing.T) {
dir := os.Getenv("BJ_DUMP_PAGES")
if dir == "" {
t.Skip("BJ_DUMP_PAGES не задан")
}
priv := testLicenseKey(t)
a, err := newAdmin()
if err != nil {
t.Fatal(err)
}
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "АО «Пример Инвест»", Product: "bj-server", Plan: license.PlanEnterprise,
IssuedAt: now, ExpiresAt: now.Add(365 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp", "web-cabinet", "npr", "quik-export"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
h := &setupHandlers{rc: rc, tpl: &adminTemplates{a: a}}
for step := 1; step <= wizardSteps; step++ {
w := httptest.NewRecorder()
h.renderWizard(w, httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/wizard?step=%d", step), nil))
_ = os.WriteFile(filepath.Join(dir, fmt.Sprintf("wizard-%02d.html", step)), w.Body.Bytes(), 0o644)
}
var b strings.Builder
d := PrepareData{page: page{Title: "Подготовка модулей", Active: "wizard"}, Tenant: "АО «Пример Инвест»", Plan: "enterprise", Until: "поддержка до 06.10.2027", Next: "/admin/wizard?step=12", Flash: "Администратор создан."}
if err := a.prepare.ExecuteTemplate(&b, "layout", d); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "prepare.html"), []byte(b.String()), 0o644)
_ = os.WriteFile(filepath.Join(dir, "depo-script.sql"), []byte(depoSetupScript(FancyMSSQLSettings{}, "DEPO_DATA", "bj_user", "Xk7pQ2mN9vR4tL8wZ3hB6cJd", true)), 0o644)
}
// Первая страница (администратор) — для снимка в TestDumpOnboardingPages.
func TestDumpFirstRunPage(t *testing.T) {
dir := os.Getenv("BJ_DUMP_PAGES")
if dir == "" {
t.Skip("BJ_DUMP_PAGES не задан")
}
var b strings.Builder
if err := firstRunTpl.Execute(&b, firstRunData{NeedKey: true}); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(dir, "first-run.html"), []byte(b.String()), 0o644)
}
// «Отдать скрипт администратору»: скрипт показывается на странице с базой и паролем с экрана, пароль
// запоминается (с ним bj потом проверяет подключение); короткий пароль не принимается.
func TestDepoScriptView(t *testing.T) {
priv := testLicenseKey(t)
rc := testRC(t)
now := time.Now().UTC()
tok := signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-1", Tenant: "Тест", Product: "bj-server", Plan: license.PlanPro,
IssuedAt: now, ExpiresAt: now.Add(30 * 24 * time.Hour), ContractUntil: now.AddDate(1, 0, 0), Features: []string{"updates", "quik-mnp"}})
if err := rc.SaveLicense(LicenseSettings{Key: tok}); err != nil {
t.Fatal(err)
}
old := globalRC
globalRC = rc
defer func() { globalRC = old }()
h := &setupHandlers{rc: rc}
post := func(form url.Values) *httptest.ResponseRecorder {
r := httptest.NewRequest(http.MethodPost, "/admin/setup/wizard-depo/script.sql", strings.NewReader(form.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
h.wizardDepoScript(w, r)
return w
}
form := url.Values{"server": {"10.0.0.5"}, "port": {"1433"}, "db_type": {"mssql"}, "depo_database": {"DEPO_X"},
"mode": {"script"}, "bj_user": {"bj_user"}, "bj_password": {"Secret123x"}, "depo_job": {"agent"}, "view": {"1"}}
w := post(form)
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "DECLARE @db sysname = N'DEPO_X';") || !strings.Contains(w.Body.String(), "N'Secret123x';") {
t.Fatalf("показ скрипта: %d %s", w.Code, w.Body.String()[:min(300, w.Body.Len())])
}
if f := rc.Snapshot().Fancy; f.User != "bj_user" || f.Password != "Secret123x" || f.DepoTablesMode != "script" {
t.Fatalf("логин и пароль bj не запомнились: %+v", f.User)
}
form.Set("bj_password", "short")
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "8 знаков") {
t.Fatalf("короткий пароль: %d %s", w.Code, w.Body.String())
}
// экран пересобирает скрипт на ходу — и до того, как вписан адрес сервера; без адреса ничего не сохраняется
form.Set("bj_password", "Other123x")
form.Set("server", "")
if w := post(form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "N'Other123x';") {
t.Fatalf("скрипт без адреса сервера: %d %s", w.Code, w.Body.String())
}
if f := rc.Snapshot().Fancy; f.Server != "10.0.0.5" || f.Password != "Secret123x" {
t.Fatalf("без адреса сервера настройки не должны меняться: %q", f.Server)
}
// имя базы обязательно (bj по серверу не ищет) — без него вместо скрипта понятная причина
form.Set("depo_database", "")
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
t.Fatalf("SQL Server без базы: %d %s", w.Code, w.Body.String())
}
form.Set("db_type", "postgres")
if w := post(form); w.Code != http.StatusBadRequest || !strings.Contains(w.Body.String(), "имя базы") {
t.Fatalf("PostgreSQL без базы: %d %s", w.Code, w.Body.String())
}
form.Set("depo_database", "depo")
if w := post(form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "PostgreSQL") {
t.Fatalf("PostgreSQL: %d %s", w.Code, w.Body.String())
}
}