fd238c1078
В режиме разработки новая страница /admin/emulators с двумя клавишами: - «Эмулятор НРД» (флаг LK.EmulateNSD): заявки ЛК и ручного ввода идут через внутренний имитатор, в НРД ничего не отправляется. Настройки: задержка решения (0–300 с), исход (подтвердить/отклонить/не отвечать), код отказа M2M01–M2M09/18. Сендер перенастраивается в рантайме через потокобезопасный mock.Sender.Reconfigure — без перезапуска. - «Эмулятор МНП QUIK» (флаг QuikMNP.Emulate): шаги цикла без записи в боевую БД МНП. Настройки: задержка шага (0–60 с), точечные отказы по шагам (регистрация/подпись/отказ/зачисление) — эмулированный отказ ведёт себя как боевой: шаг OK=false, состояние не продвигается, повтор на следующем проходе. Настройки хранятся в setup.json (nsd_emu/mnp_emu), обратная совместимость — дефолты. Баннер «Режим эмуляции» показывается и при ручном включении эмуляторов (раньше зависел только от отсутствия ИШ). Пункт меню «Эмуляторы» и две плитки на дашборде — только в dev. Пути занесены в devOnlyPath. Попутно: исправлен race в TestLicenseActivateAndRenew (cleanup восстанавливал DefaultLicensePublicKey без moduleEnforceMu — падал под -race и на чистом HEAD).
144 lines
5.1 KiB
Go
144 lines
5.1 KiB
Go
package lkgateway
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license"
|
|
)
|
|
|
|
// testLicenseKey подменяет зашитый ключ проверки тестовой парой.
|
|
func testLicenseKey(t *testing.T) ed25519.PrivateKey {
|
|
t.Helper()
|
|
pub, priv, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
prev := DefaultLicensePublicKey
|
|
DefaultLicensePublicKey = base64.StdEncoding.EncodeToString(pub)
|
|
t.Cleanup(func() {
|
|
// Ждём возможный фоновый enforceModuleLicense (он читает ключ в licenseStatus),
|
|
// иначе восстановление ключа гоняется с ним под -race.
|
|
moduleEnforceMu.Lock()
|
|
defer moduleEnforceMu.Unlock()
|
|
DefaultLicensePublicKey = prev
|
|
})
|
|
return priv
|
|
}
|
|
|
|
func testRC(t *testing.T) *RuntimeConfig {
|
|
t.Helper()
|
|
rc, err := NewRuntimeConfig(filepath.Join(t.TempDir(), "setup.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return rc
|
|
}
|
|
|
|
func signTestLicense(t *testing.T, priv ed25519.PrivateKey, l *license.License) string {
|
|
t.Helper()
|
|
tok, err := license.Sign(l, priv, "main")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tok.Encode()
|
|
}
|
|
|
|
func TestLicenseActivateAndRenew(t *testing.T) {
|
|
priv := testLicenseKey(t)
|
|
rc := testRC(t)
|
|
var gotInstall string
|
|
renewed := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var in map[string]string
|
|
_ = json.NewDecoder(r.Body).Decode(&in)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.URL.Path == "/v1/activate" && in["code"] != "BJ-TEST-CODE-0001" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
_, _ = w.Write([]byte(`{"error":"unknown_code","message":"код активации не найден"}`))
|
|
return
|
|
}
|
|
if r.URL.Path == "/v1/renew" {
|
|
renewed++
|
|
}
|
|
gotInstall = in["install_id"]
|
|
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(60 * 24 * time.Hour), Features: []string{"npr"}, InstallID: in["install_id"], ContractUntil: now.AddDate(1, 0, 0)})
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"token": tok, "license_id": "lic-1", "tenant": "Тест"})
|
|
}))
|
|
defer srv.Close()
|
|
prev := licenseServer
|
|
licenseServer = srv.URL
|
|
t.Cleanup(func() { licenseServer = prev })
|
|
ctx := context.Background()
|
|
|
|
if err := LicenseActivate(ctx, rc, "BJ-XXXX-XXXX-XXXX"); err == nil || !strings.Contains(err.Error(), "не найден") {
|
|
t.Fatalf("неверный код: ожидали отказ сервера, получили %v", err)
|
|
}
|
|
if licenseStatus(rc).Present {
|
|
t.Fatal("после отказа ключ не должен сохраняться")
|
|
}
|
|
if err := LicenseActivate(ctx, rc, "BJ-TEST-CODE-0001"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st := licenseStatus(rc)
|
|
if !st.Valid || !st.Online || st.Tenant != "Тест" {
|
|
t.Fatalf("после активации: %+v", st)
|
|
}
|
|
if gotInstall == "" || gotInstall != rc.Snapshot().License.InstallID {
|
|
t.Fatalf("install_id в запросе %q, в настройках %q", gotInstall, rc.Snapshot().License.InstallID)
|
|
}
|
|
if !moduleAllowed(rc, "npr") || moduleAllowed(rc, "quik-mnp") {
|
|
t.Fatal("модули должны следовать признакам лицензии pro")
|
|
}
|
|
if licenseNeedsRenew(rc) {
|
|
t.Fatal("сразу после активации продлевать не нужно")
|
|
}
|
|
if err := LicenseRenew(ctx, rc); err != nil || renewed != 1 {
|
|
t.Fatalf("продление: %v, обращений %d", err, renewed)
|
|
}
|
|
|
|
s := rc.Snapshot().License
|
|
s.InstallID = "00000000-0000-4000-8000-000000000000"
|
|
_ = rc.SaveLicense(s)
|
|
if st := licenseStatus(rc); st.Valid || !strings.Contains(st.Message, "другой установке") {
|
|
t.Fatalf("ключ чужой установки должен быть недействителен: %+v", st)
|
|
}
|
|
}
|
|
|
|
func TestLicenseOfflineEnterpriseAndForeignKey(t *testing.T) {
|
|
priv := testLicenseKey(t)
|
|
rc := testRC(t)
|
|
now := time.Now().UTC()
|
|
s := rc.Snapshot().License
|
|
s.Key = signTestLicense(t, priv, &license.License{Schema: 1, ID: "lic-2", Tenant: "Офлайн", Product: "bj-server", Plan: license.PlanEnterprise,
|
|
IssuedAt: now, ExpiresAt: now.AddDate(1, 0, 0)})
|
|
_ = rc.SaveLicense(s)
|
|
st := licenseStatus(rc)
|
|
if !st.Valid || st.Online {
|
|
t.Fatalf("офлайн-ключ: %+v", st)
|
|
}
|
|
if !moduleAllowed(rc, "quik-export") || !moduleAllowed(rc, "npr") {
|
|
t.Fatal("enterprise должен открывать все модули")
|
|
}
|
|
if err := LicenseRenew(context.Background(), rc); err != errLicenseOffline {
|
|
t.Fatalf("офлайн-ключ не продлевается: %v", err)
|
|
}
|
|
|
|
_, other, _ := ed25519.GenerateKey(nil)
|
|
s.Key = signTestLicense(t, other, &license.License{Schema: 1, ID: "x", Plan: license.PlanEnterprise, IssuedAt: now, ExpiresAt: now.AddDate(1, 0, 0)})
|
|
_ = rc.SaveLicense(s)
|
|
if licenseStatus(rc).Valid || moduleAllowed(rc, "npr") {
|
|
t.Fatal("ключ, подписанный чужим ключом, не должен приниматься")
|
|
}
|
|
}
|