«Подготовка модулей» между ключом продукта и мастером: bj узнаёт состав лицензии, ставит модули, которые в неё входят (из комплекта образа, если там тот же выпуск, — без скачивания), запускает их службы и открывает мастер; после активации ключа и при изменении состава лицензии модули ставятся сразу, а не при следующем запуске 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-05 19:25:17 +03:00
parent 64eb63efc2
commit 0dd38bbae4
8 changed files with 413 additions and 9 deletions
+65
View File
@@ -12,6 +12,9 @@ import (
"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) {
@@ -273,3 +276,65 @@ func TestNotifyKindsByLicense(t *testing.T) {
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)
}
}
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"} {
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)
}
}
// Модуль из комплекта образа берётся без скачивания, только если это тот же файл, что в выпуске.
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("другой выпуск — качать с сервера, а не брать из образа")
}
}