package license import ( "crypto/ed25519" "crypto/rand" "encoding/json" "errors" "reflect" "strings" "testing" "time" ) func newIssuer(t *testing.T) (*Issuer, ed25519.PublicKey) { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } return &Issuer{Key: priv}, pub } func sample() License { return License{ TenantID: "kunde_schmidt", Tier: TierPro, MaxRoutes: 10, AllowedDomains: []string{"*.firma.de", "shop.de"}, Features: []Feature{FeatureWebsockets, FeatureTCPStream}, ValidUntil: time.Date(2026, 12, 15, 0, 0, 0, 0, time.UTC), } } func TestIssueAndVerify(t *testing.T) { iss, pub := newIssuer(t) l, err := iss.Issue(sample()) if err != nil { t.Fatalf("Issue: %v", err) } if l.Signature == "" { t.Fatal("keine Signatur gesetzt") } if err := l.Verify(pub); err != nil { t.Errorf("Verify: %v", err) } } func TestIssue_NormalizesAndDoesNotMutateInput(t *testing.T) { iss, _ := newIssuer(t) in := sample() in.ValidUntil = time.Date(2026, 12, 15, 1, 30, 0, 999_000_000, time.FixedZone("CET", 3600)) in.Signature = "alt" l, err := iss.Issue(in) if err != nil { t.Fatal(err) } want := time.Date(2026, 12, 15, 0, 30, 0, 0, time.UTC) if !l.ValidUntil.Equal(want) || l.ValidUntil.Location() != time.UTC { t.Errorf("ValidUntil = %v, erwartet %v (UTC, ganze Sekunden)", l.ValidUntil, want) } if in.Signature != "alt" || in.ValidUntil.Location() == time.UTC { t.Error("Eingabe wurde verändert") } } // Feldnamen gemäß Spec Abschnitt 4.1. func TestJSONFormat(t *testing.T) { iss, _ := newIssuer(t) l, _ := iss.Issue(sample()) b, err := json.Marshal(l) if err != nil { t.Fatal(err) } var m map[string]any json.Unmarshal(b, &m) for _, key := range []string{"tenant_id", "tier", "max_routes", "allowed_domains", "features", "valid_until", "signature"} { if _, ok := m[key]; !ok { t.Errorf("Feld %q fehlt in %s", key, b) } } if len(m) != 7 { t.Errorf("unerwartete Felder: %s", b) } if m["valid_until"] != "2026-12-15T00:00:00Z" { t.Errorf("valid_until = %v, erwartet RFC 3339", m["valid_until"]) } } func TestParse_RoundTrip(t *testing.T) { iss, pub := newIssuer(t) l, _ := iss.Issue(sample()) b, _ := json.MarshalIndent(l, "", " ") got, err := Parse(b, pub) if err != nil { t.Fatalf("Parse: %v", err) } if !reflect.DeepEqual(got, l) { t.Errorf("Round-Trip weicht ab\n got: %+v\nwant: %+v", got, l) } } func TestParse_Errors(t *testing.T) { iss, pub := newIssuer(t) l, _ := iss.Issue(sample()) good, _ := json.Marshal(l) otherPub, _, _ := ed25519.GenerateKey(rand.Reader) if _, err := Parse(good, otherPub); !errors.Is(err, ErrInvalidSignature) { t.Errorf("fremder Schlüssel: erwartet ErrInvalidSignature, bekommen %v", err) } tests := map[string]string{ "kein JSON": "lizenz", "unbekanntes Feld": strings.Replace(string(good), `"tier"`, `"admin":true,"tier"`, 1), "Signatur fehlt": strings.Replace(string(good), `"signature":"`+l.Signature+`"`, `"signature":""`, 1), "Pflichtfeld fehlt": strings.Replace(string(good), `"tenant_id":"kunde_schmidt",`, ``, 1), } for name, data := range tests { t.Run(name, func(t *testing.T) { _, err := Parse([]byte(data), pub) if !errors.Is(err, ErrInvalid) && !errors.Is(err, ErrInvalidSignature) { t.Errorf("erwartet ErrInvalid oder ErrInvalidSignature, bekommen %v", err) } }) } } func TestVerify_Tampering(t *testing.T) { tests := map[string]func(*License){ "Mandant": func(l *License) { l.TenantID = "fremd" }, "Tier": func(l *License) { l.Tier = TierEnterprise }, "MaxRoutes": func(l *License) { l.MaxRoutes = -1 }, "Domain ergänzt": func(l *License) { l.AllowedDomains = append(l.AllowedDomains, "bank.de") }, "Domains vertauscht": func(l *License) { l.AllowedDomains[0], l.AllowedDomains[1] = l.AllowedDomains[1], l.AllowedDomains[0] }, "Feature ergänzt": func(l *License) { l.Features = append(l.Features, FeatureCustomMiddleware) }, "Ablauf +1s": func(l *License) { l.ValidUntil = l.ValidUntil.Add(time.Second) }, "Signatur Müll": func(l *License) { l.Signature = "!!!" }, "Signatur leer": func(l *License) { l.Signature = "" }, } for name, tamper := range tests { t.Run(name, func(t *testing.T) { iss, pub := newIssuer(t) l, _ := iss.Issue(sample()) l.AllowedDomains = append([]string(nil), l.AllowedDomains...) tamper(&l) if err := l.Verify(pub); !errors.Is(err, ErrInvalidSignature) { t.Errorf("erwartet ErrInvalidSignature, bekommen %v", err) } }) } } func TestIssue_Validation(t *testing.T) { tests := map[string]func(*License){ "Mandant leer": func(l *License) { l.TenantID = " " }, "unbekannter Tier": func(l *License) { l.Tier = "gold" }, "MaxRoutes < -1": func(l *License) { l.MaxRoutes = -2 }, "keine Domains": func(l *License) { l.AllowedDomains = nil }, "Domain ungültig": func(l *License) { l.AllowedDomains = []string{"foo bar.de"} }, "Wildcard allein": func(l *License) { l.AllowedDomains = []string{"*"} }, "Wildcard auf TLD": func(l *License) { l.AllowedDomains = []string{"*.de"} }, "Wildcard in der Mitte": func(l *License) { l.AllowedDomains = []string{"a.*.de"} }, "unbekanntes Feature": func(l *License) { l.Features = []Feature{"teleport"} }, "ValidUntil fehlt": func(l *License) { l.ValidUntil = time.Time{} }, } for name, mutate := range tests { t.Run(name, func(t *testing.T) { iss, _ := newIssuer(t) in := sample() mutate(&in) if _, err := iss.Issue(in); !errors.Is(err, ErrInvalid) { t.Errorf("erwartet ErrInvalid, bekommen %v", err) } }) } if _, err := (&Issuer{}).Issue(sample()); err == nil { t.Error("Issuer ohne Schlüssel: erwartet Fehler") } } func TestIssue_DomainsNormalized(t *testing.T) { iss, _ := newIssuer(t) in := sample() in.AllowedDomains = []string{"Shop.DE.", " *.Firma.de "} l, err := iss.Issue(in) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(l.AllowedDomains, []string{"shop.de", "*.firma.de"}) { t.Errorf("AllowedDomains = %v", l.AllowedDomains) } } func TestExpiry(t *testing.T) { l := sample() // gültig bis 2026-12-15T00:00:00Z tests := []struct { now time.Time expired bool daysLeft int }{ {l.ValidUntil.AddDate(0, 0, -74), false, 74}, {l.ValidUntil.Add(-12 * time.Hour), false, 0}, {l.ValidUntil, true, 0}, {l.ValidUntil.Add(time.Hour), true, -1}, {l.ValidUntil.Add(25 * time.Hour), true, -2}, } for _, tt := range tests { if got := l.Expired(tt.now); got != tt.expired { t.Errorf("Expired(%v) = %v", tt.now, got) } if got := l.DaysLeft(tt.now); got != tt.daysLeft { t.Errorf("DaysLeft(%v) = %d, erwartet %d", tt.now, got, tt.daysLeft) } } } func TestAllowsRoutes(t *testing.T) { tests := []struct { max, n int want bool }{ {-1, 0, true}, {-1, 10_000, true}, {0, 0, true}, {0, 1, false}, {5, 5, true}, {5, 6, false}, } for _, tt := range tests { l := License{MaxRoutes: tt.max} if got := l.AllowsRoutes(tt.n); got != tt.want { t.Errorf("max=%d AllowsRoutes(%d) = %v", tt.max, tt.n, got) } } } func TestHasFeature(t *testing.T) { l := sample() if !l.HasFeature(FeatureTCPStream) || l.HasFeature(FeatureCustomMiddleware) { t.Errorf("HasFeature falsch für %v", l.Features) } }