240 lines
7.2 KiB
Go
240 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|