package license import ( "encoding/json" "errors" "testing" "time" ) var t0 = time.Date(2026, 10, 2, 12, 0, 0, 0, time.UTC) const day = 24 * time.Hour func noJitter(time.Duration) time.Duration { return 0 } func TestLeaseTerms(t *testing.T) { far := t0.AddDate(1, 0, 0) tests := []struct { name string tier Tier maintenance bool licenseUntil time.Time valid, renew time.Duration // relativ zu t0 ok bool }{ {"community", TierCommunity, false, far, 14 * day, 7 * day, true}, {"community ignoriert Vertragsende", TierCommunity, false, t0.Add(day), 14 * day, 7 * day, true}, {"pro", TierPro, false, far, 7 * day, 7 * day * 2 / 3, true}, {"pro in Wartung", TierPro, true, far, 10 * day, 10 * day * 2 / 3, true}, {"enterprise am Vertragsende gekappt", TierEnterprise, false, t0.Add(3 * day), 3 * day, 2 * day, true}, {"Vertrag abgelaufen", TierPro, false, t0, 0, 0, false}, } for _, tt := range tests { terms, ok := TermsFor(tt.tier, tt.maintenance, tt.licenseUntil, t0, 0, noJitter) if ok != tt.ok { t.Errorf("%s: ok=%v", tt.name, ok) continue } if !ok { continue } if !terms.ValidUntil.Equal(t0.Add(tt.valid)) || !terms.RenewAfter.Equal(t0.Add(tt.renew)) { t.Errorf("%s: gültig bis +%v, erneuern ab +%v; erwartet +%v / +%v", tt.name, terms.ValidUntil.Sub(t0), terms.RenewAfter.Sub(t0), tt.valid, tt.renew) } } } // Community-Erneuerung gestreut (± 12 h), damit nicht alle Agents gleichzeitig erneuern. func TestLeaseTerms_CommunityJitter(t *testing.T) { var maxSeen time.Duration jitter := func(max time.Duration) time.Duration { maxSeen = max; return -5 * time.Hour } terms, _ := TermsFor(TierCommunity, false, t0.AddDate(1, 0, 0), t0, 0, jitter) if maxSeen != 12*time.Hour { t.Errorf("Streuung ±%v, erwartet ±12h", maxSeen) } if !terms.RenewAfter.Equal(t0.Add(7*day - 5*time.Hour)) { t.Errorf("RenewAfter = +%v", terms.RenewAfter.Sub(t0)) } // Ohne explizite Funktion: echte Streuung, aber innerhalb der Grenzen. for range 100 { terms, _ := TermsFor(TierCommunity, false, t0.AddDate(1, 0, 0), t0, 0, nil) if d := terms.RenewAfter.Sub(t0); d < 7*day-12*time.Hour || d > 7*day+12*time.Hour { t.Fatalf("Streuung außerhalb: +%v", d) } } } // Override (Tests, Betrieb): feste Laufzeit, Erneuerung nach 2/3, Vertragsende gilt weiter. func TestLeaseTerms_Override(t *testing.T) { terms, ok := TermsFor(TierCommunity, false, t0.AddDate(1, 0, 0), t0, 300*time.Millisecond, noJitter) if !ok || !terms.ValidUntil.Equal(t0.Add(300*time.Millisecond)) || !terms.RenewAfter.Equal(t0.Add(200*time.Millisecond)) { t.Errorf("Override: %+v", terms) } } func TestIssueAndParseLease(t *testing.T) { iss, pub := newIssuer(t) lic := sample() terms := LeaseTerms{RenewAfter: t0.Add(4 * day), ValidUntil: t0.Add(7 * day)} l, err := iss.IssueLease(lic, "node-1", terms, t0) if err != nil { t.Fatalf("IssueLease: %v", err) } if l.NodeID != "node-1" || l.TenantID != lic.TenantID || !l.IssuedAt.Equal(t0) || !l.ValidUntil.Equal(terms.ValidUntil) || l.MaxRoutes != lic.MaxRoutes || len(l.AllowedDomains) != 2 { t.Errorf("Lease: %+v", l) } doc, _ := json.Marshal(l) parsed, err := ParseLease(doc, pub) if err != nil { t.Fatalf("ParseLease: %v", err) } if parsed.NodeID != "node-1" || !parsed.ValidUntil.Equal(terms.ValidUntil) { t.Errorf("geparst: %+v", parsed) } if !l.Expired(terms.ValidUntil) || l.Expired(terms.ValidUntil.Add(-time.Second)) { t.Error("Expired falsch") } for name, tamper := range map[string]func(*Lease){ "Knoten": func(l *Lease) { l.NodeID = "node-2" }, "Laufzeit": func(l *Lease) { l.ValidUntil = l.ValidUntil.Add(time.Hour) }, "Domains": func(l *Lease) { l.AllowedDomains = append([]string(nil), "bank.de") }, "Erneuerung": func(l *Lease) { l.RenewAfter = l.RenewAfter.Add(time.Hour) }, } { c := l tamper(&c) if err := c.Verify(pub); !errors.Is(err, ErrInvalidSignature) { t.Errorf("%s manipuliert: %v", name, err) } } } // Domänentrennung: Eine Lease ist keine Lizenz und umgekehrt. func TestLeaseIsNotALicense(t *testing.T) { iss, pub := newIssuer(t) lic, _ := iss.Issue(sample()) lease, _ := iss.IssueLease(sample(), "node-1", LeaseTerms{RenewAfter: t0.Add(day), ValidUntil: t0.Add(2 * day)}, t0) licDoc, _ := json.Marshal(lic) if _, err := ParseLease(licDoc, pub); err == nil { t.Error("Lizenz als Lease akzeptiert") } asLicense := License{ TenantID: lease.TenantID, Tier: lease.Tier, MaxRoutes: lease.MaxRoutes, AllowedDomains: lease.AllowedDomains, Features: lease.Features, ValidUntil: lease.ValidUntil, Signature: lease.Signature, } if err := asLicense.Verify(pub); !errors.Is(err, ErrInvalidSignature) { t.Error("Lease-Signatur als Lizenz-Signatur akzeptiert") } }