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("ключ, подписанный чужим ключом, не должен приниматься") } }