server läuft
This commit is contained in:
1 parent
c8df9d7651
commit
42f0c0accb
21 files changed
+4185
-10
No files matched your search
@@ -0,0 +1,68 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Ein DNS-Label: 1–63 Zeichen, a–z, 0–9 und Bindestrich, nicht am Rand.
|
||||
// Internationalisierte Domains werden in Punycode (xn--…) angegeben.
|
||||
var labelRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$`)
|
||||
|
||||
// splitHost normalisiert host (klein, ohne Punkt am Ende) und prüft, dass er
|
||||
// aus mindestens zwei gültigen Labels besteht.
|
||||
func splitHost(host string) (string, bool) {
|
||||
h := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")
|
||||
if len(h) > 253 {
|
||||
return "", false
|
||||
}
|
||||
labels := strings.Split(h, ".")
|
||||
if len(labels) < 2 {
|
||||
return "", false
|
||||
}
|
||||
for _, l := range labels {
|
||||
if !labelRe.MatchString(l) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
// normalizePattern prüft einen Eintrag aus allowed_domains: entweder eine
|
||||
// Domain ("shop.de") oder eine Wildcard ganz vorne ("*.firma.de"). Eine
|
||||
// Wildcard direkt auf einer TLD ("*.de") ist nicht erlaubt.
|
||||
func normalizePattern(p string) (string, error) {
|
||||
p = strings.TrimSpace(p)
|
||||
rest, wildcard := strings.CutPrefix(p, "*.")
|
||||
h, ok := splitHost(rest)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: ungültiges Domain-Muster %q", ErrInvalid, p)
|
||||
}
|
||||
if wildcard {
|
||||
return "*." + h, nil
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// AllowsDomain meldet, ob host durch allowed_domains gedeckt ist:
|
||||
// - "shop.de" deckt genau shop.de (keine Subdomains),
|
||||
// - "*.firma.de" deckt jede Subdomain beliebiger Tiefe, nicht firma.de selbst.
|
||||
//
|
||||
// Groß-/Kleinschreibung und ein Punkt am Ende werden ignoriert.
|
||||
func (l License) AllowsDomain(host string) bool {
|
||||
h, ok := splitHost(host)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, p := range l.AllowedDomains {
|
||||
if base, wildcard := strings.CutPrefix(p, "*."); wildcard {
|
||||
if strings.HasSuffix(h, "."+base) {
|
||||
return true
|
||||
}
|
||||
} else if h == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package license
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAllowsDomain(t *testing.T) {
|
||||
l := License{AllowedDomains: []string{"*.firma.de", "shop.de"}}
|
||||
tests := map[string]bool{
|
||||
"shop.de": true,
|
||||
"SHOP.de": true,
|
||||
"shop.de.": true,
|
||||
"www.shop.de": false, // exakter Eintrag deckt keine Subdomains
|
||||
"a.firma.de": true,
|
||||
"a.b.firma.de": true, // Wildcard deckt alle Subdomain-Ebenen
|
||||
"firma.de": false, // Wildcard deckt nicht die Domain selbst
|
||||
"evilfirma.de": false,
|
||||
"firma.de.evil.com": false,
|
||||
"a.firma.de.evil.com": false,
|
||||
"": false,
|
||||
".firma.de": false,
|
||||
"a..firma.de": false,
|
||||
}
|
||||
for host, want := range tests {
|
||||
if got := l.AllowsDomain(host); got != want {
|
||||
t.Errorf("AllowsDomain(%q) = %v, erwartet %v", host, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDomainPattern(t *testing.T) {
|
||||
valid := map[string]string{
|
||||
"firma.de": "firma.de",
|
||||
"Firma.DE.": "firma.de",
|
||||
"*.firma.de": "*.firma.de",
|
||||
"xn--mnchen-3ya.de": "xn--mnchen-3ya.de",
|
||||
"a-b.c1.de": "a-b.c1.de",
|
||||
}
|
||||
for in, want := range valid {
|
||||
got, err := normalizePattern(in)
|
||||
if err != nil || got != want {
|
||||
t.Errorf("normalizePattern(%q) = %q, %v; erwartet %q", in, got, err, want)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "*", "*.de", "de", "a.*.de", "**.de", "-a.de", "a-.de", "a_b.de", "münchen.de", "a..de", "foo bar.de", strings63() + "x.de"} {
|
||||
if _, err := normalizePattern(bad); err == nil {
|
||||
t.Errorf("normalizePattern(%q): erwartet Fehler", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// strings63 liefert ein Label mit 63 Zeichen (Maximum); mit "x" wird es zu lang.
|
||||
func strings63() string {
|
||||
b := make([]byte, 63)
|
||||
for i := range b {
|
||||
b[i] = 'a'
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Package license stellt Mandanten-Lizenzen (Capabilities, Spec Abschnitt
|
||||
// 4.1) aus und prüft sie.
|
||||
//
|
||||
// Eine Lizenz legt fest, was ein Mandant darf: Tier, maximale Routenzahl,
|
||||
// erlaubte Domains, Features und Laufzeit. Sie ist mit dem Ed25519-
|
||||
// Master-Key des Servers signiert; jede Änderung an einem Feld macht die
|
||||
// Signatur ungültig.
|
||||
package license
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalid: Die Lizenz verletzt eine Formatregel.
|
||||
ErrInvalid = errors.New("license: ungültige Lizenz")
|
||||
// ErrInvalidSignature: Signatur fehlt, ist beschädigt oder passt nicht.
|
||||
ErrInvalidSignature = errors.New("license: Signatur ungültig")
|
||||
)
|
||||
|
||||
// Tier ist die Lizenzstufe.
|
||||
type Tier string
|
||||
|
||||
const (
|
||||
TierCommunity Tier = "community"
|
||||
TierPro Tier = "pro"
|
||||
TierEnterprise Tier = "enterprise"
|
||||
)
|
||||
|
||||
// Feature ist eine optional freigeschaltete Fähigkeit.
|
||||
type Feature string
|
||||
|
||||
const (
|
||||
FeatureWebsockets Feature = "websockets"
|
||||
FeatureTCPStream Feature = "tcp_stream"
|
||||
FeatureCustomMiddleware Feature = "custom_middleware"
|
||||
)
|
||||
|
||||
// License entspricht license.json (Spec 4.1).
|
||||
type License struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Tier Tier `json:"tier"`
|
||||
MaxRoutes int `json:"max_routes"` // -1 = unbegrenzt
|
||||
AllowedDomains []string `json:"allowed_domains"`
|
||||
Features []Feature `json:"features"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// signatureVersion trennt Lizenz-Signaturen von anderen Nutzungen desselben
|
||||
// Master-Keys (z. B. künftig signierten Server-Events).
|
||||
const signatureVersion = "SULTAN-LICENSE-V1\n"
|
||||
|
||||
// signedPayload legt Reihenfolge und Format der signierten Felder fest.
|
||||
type signedPayload struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
Tier Tier `json:"tier"`
|
||||
MaxRoutes int `json:"max_routes"`
|
||||
AllowedDomains []string `json:"allowed_domains"`
|
||||
Features []Feature `json:"features"`
|
||||
ValidUntil string `json:"valid_until"`
|
||||
}
|
||||
|
||||
func (l License) message() []byte {
|
||||
b, _ := json.Marshal(signedPayload{
|
||||
TenantID: l.TenantID,
|
||||
Tier: l.Tier,
|
||||
MaxRoutes: l.MaxRoutes,
|
||||
AllowedDomains: l.AllowedDomains,
|
||||
Features: l.Features,
|
||||
ValidUntil: l.ValidUntil.UTC().Format(time.RFC3339),
|
||||
})
|
||||
return append([]byte(signatureVersion), b...)
|
||||
}
|
||||
|
||||
// Issuer stellt Lizenzen mit dem Master-Key des Servers aus.
|
||||
type Issuer struct {
|
||||
Key ed25519.PrivateKey
|
||||
}
|
||||
|
||||
// Issue prüft und normalisiert l (Domains klein und ohne Punkt am Ende,
|
||||
// ValidUntil in UTC auf ganze Sekunden) und signiert sie. Die Eingabe wird
|
||||
// nicht verändert.
|
||||
func (i *Issuer) Issue(l License) (License, error) {
|
||||
if len(i.Key) != ed25519.PrivateKeySize {
|
||||
return License{}, errors.New("license: Issuer ohne gültigen Master-Key")
|
||||
}
|
||||
out := l
|
||||
out.Signature = ""
|
||||
out.ValidUntil = l.ValidUntil.UTC().Truncate(time.Second)
|
||||
out.AllowedDomains = make([]string, len(l.AllowedDomains))
|
||||
for n, d := range l.AllowedDomains {
|
||||
p, err := normalizePattern(d)
|
||||
if err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
out.AllowedDomains[n] = p
|
||||
}
|
||||
out.Features = append([]Feature{}, l.Features...)
|
||||
if err := out.validate(); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
out.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(i.Key, out.message()))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (l License) validate() error {
|
||||
switch {
|
||||
case strings.TrimSpace(l.TenantID) == "":
|
||||
return fmt.Errorf("%w: tenant_id fehlt", ErrInvalid)
|
||||
case l.Tier != TierCommunity && l.Tier != TierPro && l.Tier != TierEnterprise:
|
||||
return fmt.Errorf("%w: unbekannter Tier %q", ErrInvalid, l.Tier)
|
||||
case l.MaxRoutes < -1:
|
||||
return fmt.Errorf("%w: max_routes %d (erlaubt: -1 oder ≥ 0)", ErrInvalid, l.MaxRoutes)
|
||||
case len(l.AllowedDomains) == 0:
|
||||
return fmt.Errorf("%w: allowed_domains leer", ErrInvalid)
|
||||
case l.ValidUntil.IsZero():
|
||||
return fmt.Errorf("%w: valid_until fehlt", ErrInvalid)
|
||||
}
|
||||
for _, d := range l.AllowedDomains {
|
||||
if p, err := normalizePattern(d); err != nil || p != d {
|
||||
return fmt.Errorf("%w: Domain %q nicht normalisiert", ErrInvalid, d)
|
||||
}
|
||||
}
|
||||
for _, f := range l.Features {
|
||||
if f != FeatureWebsockets && f != FeatureTCPStream && f != FeatureCustomMiddleware {
|
||||
return fmt.Errorf("%w: unbekanntes Feature %q", ErrInvalid, f)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify prüft die Signatur mit dem Public Key des Master-Keys.
|
||||
func (l License) Verify(pub ed25519.PublicKey) error {
|
||||
if len(pub) != ed25519.PublicKeySize {
|
||||
return fmt.Errorf("%w: kein gültiger Public Key", ErrInvalidSignature)
|
||||
}
|
||||
sig, err := base64.StdEncoding.DecodeString(l.Signature)
|
||||
if err != nil || len(sig) != ed25519.SignatureSize {
|
||||
return fmt.Errorf("%w: Signatur fehlt oder ist beschädigt", ErrInvalidSignature)
|
||||
}
|
||||
if !ed25519.Verify(pub, l.message(), sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse liest license.json, lehnt unbekannte Felder ab und prüft Format und
|
||||
// Signatur.
|
||||
func Parse(data []byte, pub ed25519.PublicKey) (License, error) {
|
||||
dec := json.NewDecoder(strings.NewReader(string(data)))
|
||||
dec.DisallowUnknownFields()
|
||||
var l License
|
||||
if err := dec.Decode(&l); err != nil {
|
||||
return License{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
l.ValidUntil = l.ValidUntil.UTC()
|
||||
if l.Features == nil {
|
||||
l.Features = []Feature{}
|
||||
}
|
||||
if err := l.validate(); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
if err := l.Verify(pub); err != nil {
|
||||
return License{}, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Expired meldet, ob die Lizenz zum Zeitpunkt now abgelaufen ist (der
|
||||
// Ablaufzeitpunkt selbst zählt als abgelaufen).
|
||||
func (l License) Expired(now time.Time) bool { return !now.Before(l.ValidUntil) }
|
||||
|
||||
// DaysLeft liefert die verbleibenden vollen Tage (abgerundet); nach Ablauf
|
||||
// negativ. Grundlage für die Checkmk-Schwellen (30/14 Tage).
|
||||
func (l License) DaysLeft(now time.Time) int {
|
||||
return int(math.Floor(l.ValidUntil.Sub(now).Hours() / 24))
|
||||
}
|
||||
|
||||
// AllowsRoutes meldet, ob n Routen im Limit liegen.
|
||||
func (l License) AllowsRoutes(n int) bool { return l.MaxRoutes == -1 || n <= l.MaxRoutes }
|
||||
|
||||
// HasFeature meldet, ob f freigeschaltet ist.
|
||||
func (l License) HasFeature(f Feature) bool { return slices.Contains(l.Features, f) }
|
||||
@@ -0,0 +1,239 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user