package services import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "lokistack/config" "lokistack/models" "lokistack/repository" ) func withTempDB(t *testing.T) { t.Helper() t.Setenv("DATA_DIR", t.TempDir()) t.Setenv("JWT_SECRET", "test-jwt-secret-with-at-least-32-characters") t.Setenv("ENCRYPTION_KEY", "test-encryption-key-with-at-least-32-characters") if err := repository.InitDB(); err != nil { t.Fatalf("InitDB: %v", err) } t.Cleanup(func() { _ = repository.Shutdown() }) } func TestDeactivateLicenseHMACClearsLocal(t *testing.T) { withTempDB(t) key, err := GenerateLicenseKey("pro", "personal", "c", 1, 1, 0) if err != nil { t.Fatal(err) } if _, err := ActivateLicense(key); err != nil { t.Fatal(err) } if CurrentLicense() == nil { t.Fatal("expected pro before deactivate") } res, err := DeactivateLicense() if err != nil { t.Fatal(err) } if res.OfflineLocalOnly { t.Fatal("HMAC should not be offlineLocalOnly") } if CurrentLicense() != nil || StoredLicense() != nil { t.Fatal("expected local license cleared") } } func TestDeactivateLicenseOfflineLocalOnly(t *testing.T) { withTempDB(t) instanceID := GetOrCreateInstanceID() // 无法签发真 LICOFF- 无私钥;直接写入库存模拟离线证 db := repository.GetDB() db.Lock() db.License = &models.License{ Plan: "pro", Source: "opsx", LicenseKey: "LICOFF-TEST-FAKE", InstanceID: instanceID, ActivatedAt: time.Now(), IssuedAt: time.Now(), } db.RequestSave() db.Unlock() res, err := DeactivateLicense() if err != nil { t.Fatal(err) } if !res.OfflineLocalOnly { t.Fatal("expected offlineLocalOnly") } if !strings.Contains(res.Message, "后台") && !strings.Contains(strings.ToLower(res.Message), "ops") { // 中文提示 if res.Message == "" { t.Fatal("expected offline note message") } } if StoredLicense() != nil { t.Fatal("expected cleared") } } func TestDeactivateLicenseOnlineKeepsStateOnRemoteFailure(t *testing.T) { withTempDB(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{"code": 1, "message": "解绑校验失败"}) })) defer srv.Close() cfg := config.GetConfig() prev := cfg.LicenseAPIBase cfg.LicenseAPIBase = srv.URL defer func() { cfg.LicenseAPIBase = prev }() db := repository.GetDB() db.Lock() db.License = &models.License{ Plan: "pro", Source: "opsx", LicenseKey: "LIC-KEEP-ON-FAIL", InstanceID: GetOrCreateInstanceID(), ActivationToken: "tok", ActivatedAt: time.Now(), IssuedAt: time.Now(), LastVerifiedAt: time.Now().Unix(), } db.RequestSave() db.Unlock() _, err := DeactivateLicense() if err == nil { t.Fatal("expected remote failure") } if !strings.Contains(err.Error(), "远端解绑失败") { t.Fatalf("unexpected err: %v", err) } stored := StoredLicense() if stored == nil || stored.LicenseKey != "LIC-KEEP-ON-FAIL" || stored.Plan != "pro" { t.Fatalf("must keep local state on failure: %+v", stored) } } func TestDeactivateLicenseOnlineSuccessClears(t *testing.T) { withTempDB(t) var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/license/deactivate" { t.Errorf("path %s", r.URL.Path) } _ = json.NewDecoder(r.Body).Decode(&gotBody) _ = json.NewEncoder(w).Encode(map[string]any{"code": 0, "message": "ok"}) })) defer srv.Close() cfg := config.GetConfig() prev := cfg.LicenseAPIBase cfg.LicenseAPIBase = srv.URL defer func() { cfg.LicenseAPIBase = prev }() id := GetOrCreateInstanceID() db := repository.GetDB() db.Lock() db.License = &models.License{ Plan: "pro", Source: "opsx", LicenseKey: "LIC-OK", InstanceID: id, ActivationToken: "act-token", ActivatedAt: time.Now(), IssuedAt: time.Now(), LastVerifiedAt: time.Now().Unix(), } db.RequestSave() db.Unlock() res, err := DeactivateLicense() if err != nil { t.Fatal(err) } if res.OfflineLocalOnly { t.Fatal("online should not set offlineLocalOnly") } if gotBody["license_key"] != "LIC-OK" || gotBody["device_id"] != id || gotBody["activation_token"] != "act-token" { t.Fatalf("bad deactivate body: %#v", gotBody) } if StoredLicense() != nil { t.Fatal("expected cleared after success") } } func TestDeactivateLicenseEmpty(t *testing.T) { withTempDB(t) if _, err := DeactivateLicense(); err == nil { t.Fatal("expected error when no license") } }