package license import ( "encoding/json" "os" "path/filepath" "strings" "testing" "time" "aijianzhan/platform/internal/config" ) func testCfg(dir, seed string) config.LicenseConf { return config.LicenseConf{ Enabled: true, LeaseDir: dir, SeedNotAfter: seed, Customer: "t", ControlSecret: "test-license-secret", SignSecret: "test-license-secret", } } func countLeaseFiles(dir string) int { ents, err := os.ReadDir(dir) if err != nil { return 0 } n := 0 for _, e := range ents { if !e.IsDir() && strings.HasPrefix(e.Name(), "lease-") && strings.HasSuffix(e.Name(), ".json") { n++ } } return n } func TestEachOpWritesNewFile(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, time.Now().UTC().AddDate(0, 0, 10).Format("2006-01-02"))) if err != nil { t.Fatal(err) } if n := countLeaseFiles(dir); n != 1 { t.Fatalf("bootstrap files=%d", n) } if _, err := m.Extend(7, "support", "e1"); err != nil { t.Fatal(err) } if _, err := m.Renew(time.Now().UTC().AddDate(1, 0, 0), "billing", "y1"); err != nil { t.Fatal(err) } if n := countLeaseFiles(dir); n != 3 { t.Fatalf("want 3 lease files, got %d", n) } st := m.Status(time.Now()) if st.LeaseCount != 3 || st.ActiveFile == "" || st.Expired { t.Fatalf("%+v", st) } } func TestExtendLimits(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, time.Now().UTC().AddDate(0, 0, 10).Format("2006-01-02"))) if err != nil { t.Fatal(err) } for i := 0; i < DefaultMaxExtensions; i++ { if _, err := m.Extend(DefaultExtensionMaxDays, "test", ""); err != nil { t.Fatalf("extend %d: %v", i+1, err) } } if _, err := m.Extend(1, "test", ""); err == nil { t.Fatal("expected max extensions exceeded") } } func TestTamperDoesNotOverrideValidHistory(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, time.Now().UTC().AddDate(0, 0, 30).Format("2006-01-02"))) if err != nil { t.Fatal(err) } good := m.Status(time.Now()).ActiveFile // 写入一份篡改文件 bad := `{ "id": "deadbeef", "customer": "hack", "not_after": "2099-12-31T00:00:00Z", "extensions_used": 0, "extensions_max": 5, "extension_max_days": 30, "updated_at": "2099-01-01T00:00:00Z", "updated_by": "hacker", "note": "", "signature": "00" }` if err := os.WriteFile(filepath.Join(dir, "lease-20990101T000000Z-hacker-deadbeef.json"), []byte(bad), 0o644); err != nil { t.Fatal(err) } st := m.Status(time.Now()) if st.Expired || !st.SignatureOK || st.ActiveFile != good { t.Fatalf("tampered file must be ignored: %+v want active=%s", st, good) } } func TestTamperActiveRejectedWhenAlone(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, time.Now().UTC().AddDate(0, 0, 30).Format("2006-01-02"))) if err != nil { t.Fatal(err) } active := m.Status(time.Now()).ActiveFile b, _ := os.ReadFile(filepath.Join(dir, active)) var raw map[string]any _ = json.Unmarshal(b, &raw) raw["not_after"] = "2099-12-31T00:00:00Z" out, _ := json.MarshalIndent(raw, "", " ") _ = os.WriteFile(filepath.Join(dir, active), out, 0o644) st := m.Status(time.Now()) if !st.Expired || st.SignatureOK { t.Fatalf("tampered sole lease must fail: %+v", st) } } func TestWatchdogViaAPI(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, "2020-01-01")) if err != nil { t.Fatal(err) } before := countLeaseFiles(dir) lease, err := m.PutLease(&Lease{ Customer: "A", NotAfter: time.Date(2099, 12, 31, 0, 0, 0, 0, time.UTC), }, "watchdog") if err != nil { t.Fatal(err) } if lease.FileName == "" || lease.ID == "" { t.Fatalf("%+v", lease) } if countLeaseFiles(dir) != before+1 { t.Fatal("put should add a new file") } } func TestOfflineSameFileOnce(t *testing.T) { dir := t.TempDir() m, err := NewManager(testCfg(dir, time.Now().UTC().AddDate(0, 0, 5).Format("2006-01-02"))) if err != nil { t.Fatal(err) } _ = m.Status(time.Now()) // 初始化消费账本 cur := m.Status(time.Now()) pkg, err := m.MintSignedJSON(&Lease{ Customer: "t", NotAfter: time.Now().UTC().AddDate(0, 0, 35), ExtensionsUsed: 1, ExtensionsMax: 1, ExtensionMaxDays: 30, UpdatedAt: time.Now().UTC(), UpdatedBy: "offline-pack", Note: "usb", }) if err != nil { t.Fatal(err) } if _, err := m.ImportSigned(pkg, "usb"); err != nil { t.Fatal(err) } if m.Status(time.Now()).Expired { t.Fatal("after import should work") } // 同一份再导入 → 拒绝 if _, err := m.ImportSigned(pkg, "usb"); err == nil { t.Fatal("same file must not import twice") } // 正式续费后,再丢回这份旧包仍应拒绝 if _, err := m.Renew(time.Now().UTC().AddDate(1, 0, 0), "billing", ""); err != nil { t.Fatal(err) } if _, err := m.ImportSigned(pkg, "usb"); err == nil { t.Fatal("consumed id must stay rejected after renew") } _ = cur } func TestClearConsumedFailsClosed(t *testing.T) { dir := t.TempDir() leaseDir := filepath.Join(dir, "leases") stateDir := filepath.Join(dir, "state") cfg := testCfg(leaseDir, time.Now().UTC().AddDate(0, 0, 20).Format("2006-01-02")) cfg.StateDir = stateDir m, err := NewManager(cfg) if err != nil { t.Fatal(err) } _ = m.Status(time.Now()) if err := os.WriteFile(filepath.Join(stateDir, "_consumed.json"), []byte(`{"active_id":"x","ids":[],"updated_at":"2020-01-01T00:00:00Z","signature":"00"}`), 0o644); err != nil { t.Fatal(err) } st := m.Status(time.Now()) if !st.Expired { t.Fatalf("tampered consumed ledger must fail closed: %+v", st) } } func TestWipeLeasesKeepsState(t *testing.T) { dir := t.TempDir() leaseDir := filepath.Join(dir, "leases") stateDir := filepath.Join(dir, "state") cfg := testCfg(leaseDir, time.Now().UTC().AddDate(0, 0, 5).Format("2006-01-02")) cfg.StateDir = stateDir m, err := NewManager(cfg) if err != nil { t.Fatal(err) } _ = m.Status(time.Now()) pkg, err := m.MintSignedJSON(&Lease{ Customer: "t", NotAfter: time.Now().UTC().AddDate(0, 0, 40), ExtensionsUsed: 1, ExtensionsMax: 1, ExtensionMaxDays: 30, UpdatedAt: time.Now().UTC(), UpdatedBy: "pack", }) if err != nil { t.Fatal(err) } if _, err := m.ImportSigned(pkg, "usb"); err != nil { t.Fatal(err) } // 删光租约文件,保留 state ents, _ := os.ReadDir(leaseDir) for _, e := range ents { _ = os.Remove(filepath.Join(leaseDir, e.Name())) } if _, err := m.ImportSigned(pkg, "usb"); err == nil { t.Fatal("after wiping leases, same pack must still be rejected via state") } } func TestDisabled(t *testing.T) { m, err := NewManager(config.LicenseConf{Enabled: false}) if err != nil { t.Fatal(err) } st := m.Status(time.Now()) if st.Expired || st.Enabled { t.Fatalf("%+v", st) } }