507 lines
19 KiB
Go
507 lines
19 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sultan/pkg/auth"
|
|
"sultan/pkg/crypto"
|
|
"sultan/pkg/license"
|
|
"sultan/pkg/server"
|
|
"sultan/pkg/store"
|
|
)
|
|
|
|
// initDataDir initialisiert ein Datenverzeichnis so, wie es der erste
|
|
// Serverstart tut.
|
|
func initDataDir(t *testing.T) string {
|
|
t.Helper()
|
|
dir := filepath.Join(t.TempDir(), "suldata")
|
|
s, err := server.New(context.Background(), server.Config{DataDir: dir, Logger: slog.New(slog.DiscardHandler)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Shutdown(context.Background())
|
|
return dir
|
|
}
|
|
|
|
func openStore(t *testing.T, dir string) *store.Store {
|
|
t.Helper()
|
|
st, err := store.Open(context.Background(), filepath.Join(dir, "sultan.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
return st
|
|
}
|
|
|
|
// tok ruft `sultan token …` mit --data-dir auf.
|
|
func tok(t *testing.T, dir string, args ...string) (int, string, string) {
|
|
t.Helper()
|
|
full := append([]string{"token"}, args...)
|
|
// --data-dir hinter den Unterbefehl hängen (z. B. "tenant add --data-dir=…").
|
|
full = append(full, "--data-dir="+dir)
|
|
return run(context.Background(), full...)
|
|
}
|
|
|
|
func mustTok(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
code, stdout, stderr := tok(t, dir, args...)
|
|
if code != 0 {
|
|
t.Fatalf("sultan token %s: code=%d stderr=%s", strings.Join(args, " "), code, stderr)
|
|
}
|
|
return stdout
|
|
}
|
|
|
|
func addTenant(t *testing.T, dir, id string, extra ...string) {
|
|
t.Helper()
|
|
mustTok(t, dir, append([]string{"tenant", "add", "--id=" + id, "--name=Kunde " + id, "--email=" + id + "@example.com", "--domains=" + id + ".de,*." + id + ".de"}, extra...)...)
|
|
}
|
|
|
|
func TestToken_Usage(t *testing.T) {
|
|
code, _, stderr := run(context.Background(), "token")
|
|
if code != 2 {
|
|
t.Errorf("ohne Unterbefehl: code=%d", code)
|
|
}
|
|
for _, want := range []string{"tenant add", "invite", "node list", "node maintenance", "node resume", "license issue", "purge"} {
|
|
if !strings.Contains(stderr, want) {
|
|
t.Errorf("Hilfe ohne %q:\n%s", want, stderr)
|
|
}
|
|
}
|
|
if code, _, _ := run(context.Background(), "token", "zaubern"); code != 2 {
|
|
t.Errorf("unbekannter Unterbefehl: code=%d", code)
|
|
}
|
|
if code, _, _ := run(context.Background(), "token", "tenant"); code != 2 {
|
|
t.Errorf("tenant ohne Aktion: code=%d", code)
|
|
}
|
|
}
|
|
|
|
// Ein Tippfehler bei --data-dir darf keine neue Server-Identität erzeugen.
|
|
func TestToken_RequiresInitializedDataDir(t *testing.T) {
|
|
dir := filepath.Join(t.TempDir(), "vertippt")
|
|
code, _, stderr := tok(t, dir, "tenant", "list")
|
|
if code != 1 || !strings.Contains(stderr, "nicht initialisiert") {
|
|
t.Errorf("code=%d stderr=%q", code, stderr)
|
|
}
|
|
if _, err := os.Stat(dir); err == nil {
|
|
t.Error("Datenverzeichnis wurde angelegt")
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantAddAndList(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde", "--tier=pro", "--max-routes=20", "--valid-until=2027-12-31")
|
|
|
|
st := openStore(t, dir)
|
|
tn, err := st.GetTenant(context.Background(), "kunde")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tn.Tier != store.TierPro || tn.MaxRoutes != 20 || tn.Status != store.TenantActive ||
|
|
!tn.ValidUntil.Equal(time.Date(2027, 12, 31, 0, 0, 0, 0, time.UTC)) ||
|
|
strings.Join(tn.AllowedDomains, ",") != "kunde.de,*.kunde.de" {
|
|
t.Errorf("gespeichert: %+v", tn)
|
|
}
|
|
|
|
out := mustTok(t, dir, "tenant", "list")
|
|
for _, want := range []string{"ID", "kunde", "pro", "active", "2027-12-31", "*.kunde.de"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("Liste ohne %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantAddDefaults(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "homelab")
|
|
tn, _ := openStore(t, dir).GetTenant(context.Background(), "homelab")
|
|
if tn.Tier != store.TierCommunity || tn.MaxRoutes != 5 {
|
|
t.Errorf("Defaults: tier=%s max_routes=%d", tn.Tier, tn.MaxRoutes)
|
|
}
|
|
if d := time.Until(tn.ValidUntil); d < 364*24*time.Hour || d > 366*24*time.Hour {
|
|
t.Errorf("Default-Laufzeit: %v", d)
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantAddDomainsNormalized(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
mustTok(t, dir, "tenant", "add", "--id=k", "--name=K", "--email=k@x.de", "--domains= Shop.DE. , *.Firma.de")
|
|
tn, _ := openStore(t, dir).GetTenant(context.Background(), "k")
|
|
if strings.Join(tn.AllowedDomains, ",") != "shop.de,*.firma.de" {
|
|
t.Errorf("Domains: %v", tn.AllowedDomains)
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantAddErrors(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
code int
|
|
}{
|
|
{"doppelt", []string{"tenant", "add", "--id=kunde", "--name=K", "--email=k@x.de", "--domains=k.de"}, 1},
|
|
{"ID fehlt", []string{"tenant", "add", "--name=K", "--email=k@x.de", "--domains=k.de"}, 2},
|
|
{"Domains fehlen", []string{"tenant", "add", "--id=x", "--name=K", "--email=k@x.de"}, 2},
|
|
{"Domain ungültig", []string{"tenant", "add", "--id=x", "--name=K", "--email=k@x.de", "--domains=*.de"}, 1},
|
|
{"ID ungültig", []string{"tenant", "add", "--id=a__b", "--name=K", "--email=k@x.de", "--domains=k.de"}, 1},
|
|
{"Tier ungültig", []string{"tenant", "add", "--id=x", "--name=K", "--email=k@x.de", "--domains=k.de", "--tier=gold"}, 1},
|
|
{"Datum ungültig", []string{"tenant", "add", "--id=x", "--name=K", "--email=k@x.de", "--domains=k.de", "--valid-until=morgen"}, 2},
|
|
}
|
|
for _, tt := range tests {
|
|
if code, _, stderr := tok(t, dir, tt.args...); code != tt.code {
|
|
t.Errorf("%s: code=%d (erwartet %d) stderr=%q", tt.name, code, tt.code, stderr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantSetStatusAndDelete(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
mustTok(t, dir, "tenant", "set-status", "--id=kunde", "--status=suspended_payment")
|
|
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
|
if tn.Status != store.TenantSuspendedPayment {
|
|
t.Errorf("Status = %s", tn.Status)
|
|
}
|
|
if code, _, _ := tok(t, dir, "tenant", "set-status", "--id=kunde", "--status=pausiert"); code != 1 {
|
|
t.Errorf("ungültiger Status: code=%d", code)
|
|
}
|
|
if code, _, _ := tok(t, dir, "tenant", "set-status", "--id=fehlt", "--status=active"); code != 1 {
|
|
t.Errorf("unbekannter Mandant: code=%d", code)
|
|
}
|
|
|
|
mustTok(t, dir, "tenant", "delete", "--id=kunde")
|
|
if _, err := openStore(t, dir).GetTenant(context.Background(), "kunde"); err == nil {
|
|
t.Error("Mandant nicht gelöscht")
|
|
}
|
|
if code, _, _ := tok(t, dir, "tenant", "delete", "--id=kunde"); code != 1 {
|
|
t.Errorf("zweites Löschen: code=%d", code)
|
|
}
|
|
}
|
|
|
|
// Die Einladung muss den Fingerprint des echten Server-Zertifikats tragen
|
|
// und beim Server einlösbar sein.
|
|
func TestToken_Invite(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
|
|
code, stdout, stderr := tok(t, dir, "invite", "--tenant=kunde")
|
|
if code != 0 {
|
|
t.Fatalf("invite: code=%d stderr=%s", code, stderr)
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
|
if len(lines) != 1 {
|
|
t.Fatalf("stdout muss genau den Token enthalten: %q", stdout)
|
|
}
|
|
inv, err := crypto.ParseEnrollmentToken(lines[0])
|
|
if err != nil {
|
|
t.Fatalf("Token nicht lesbar: %v", err)
|
|
}
|
|
cert, _ := crypto.LoadOrCreateServerCert(dir)
|
|
if inv.Fingerprint != cert.Fingerprint {
|
|
t.Error("Token trägt nicht den Fingerprint des Server-Zertifikats")
|
|
}
|
|
if !strings.Contains(stderr, "kunde") {
|
|
t.Errorf("Hinweis auf stderr fehlt: %q", stderr)
|
|
}
|
|
|
|
st := openStore(t, dir)
|
|
if _, err := st.Enroll(context.Background(), inv.Secret, store.Node{ID: "n1", Hostname: "h", PublicKey: "k", IPAddress: "1.2.3.4"}); err != nil {
|
|
t.Errorf("Token nicht einlösbar: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestToken_InviteErrors(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
if code, _, _ := tok(t, dir, "invite", "--tenant=fehlt"); code != 1 {
|
|
t.Errorf("unbekannter Mandant: code=%d", code)
|
|
}
|
|
if code, _, _ := tok(t, dir, "invite"); code != 2 {
|
|
t.Errorf("ohne --tenant: code=%d", code)
|
|
}
|
|
addTenant(t, dir, "kunde")
|
|
for _, ttl := range []string{"--ttl=0s", "--ttl=-1h", "--ttl=bald"} {
|
|
if code, _, _ := tok(t, dir, "invite", "--tenant=kunde", ttl); code != 2 {
|
|
t.Errorf("%s: code=%d", ttl, code)
|
|
}
|
|
}
|
|
if code, _, _ := tok(t, dir, "invite", "--tenant=kunde", "--ttl=7d"); code != 0 {
|
|
t.Errorf("--ttl=7d (Tage) nicht akzeptiert: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_Nodes(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
st := openStore(t, dir)
|
|
if _, err := st.CreateNode(context.Background(), store.Node{ID: "node-1", TenantID: "kunde", Hostname: "pi", PublicKey: "k", IPAddress: "192.168.1.20"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
out := mustTok(t, dir, "node", "list")
|
|
for _, want := range []string{"ID", "node-1", "kunde", "pi", "online", "192.168.1.20"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("Liste ohne %q:\n%s", want, out)
|
|
}
|
|
}
|
|
if out := mustTok(t, dir, "node", "list", "--tenant=andere"); strings.Contains(out, "node-1") {
|
|
t.Errorf("Filter nach Mandant greift nicht:\n%s", out)
|
|
}
|
|
|
|
mustTok(t, dir, "node", "drain", "--id=node-1")
|
|
if n, _ := st.GetNode(context.Background(), "node-1"); n.Status != store.NodeDrained {
|
|
t.Errorf("drain: %s", n.Status)
|
|
}
|
|
mustTok(t, dir, "node", "resume", "--id=node-1")
|
|
if n, _ := st.GetNode(context.Background(), "node-1"); n.Status != store.NodeOffline {
|
|
t.Errorf("resume nach drain: %s (erwartet offline; online setzt erst der Stream)", n.Status)
|
|
}
|
|
mustTok(t, dir, "node", "delete", "--id=node-1")
|
|
if _, err := st.GetNode(context.Background(), "node-1"); err == nil {
|
|
t.Error("Knoten nicht gelöscht")
|
|
}
|
|
if code, _, _ := tok(t, dir, "node", "drain", "--id=fehlt"); code != 1 {
|
|
t.Errorf("unbekannter Knoten: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_LicenseIssue(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde", "--tier=enterprise", "--max-routes=-1", "--valid-until=2027-06-30")
|
|
|
|
stdout := mustTok(t, dir, "license", "issue", "--tenant=kunde")
|
|
master, err := auth.ReadPrivateKey(filepath.Join(dir, "master.key"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
l, err := license.Parse([]byte(stdout), master.Public().(ed25519.PublicKey))
|
|
if err != nil {
|
|
t.Fatalf("Lizenz ungültig: %v\n%s", err, stdout)
|
|
}
|
|
if l.TenantID != "kunde" || l.Tier != license.TierEnterprise || l.MaxRoutes != -1 || !l.AllowsDomain("www.kunde.de") {
|
|
t.Errorf("Lizenz: %+v", l)
|
|
}
|
|
if !json.Valid([]byte(stdout)) {
|
|
t.Error("stdout ist kein reines JSON")
|
|
}
|
|
if code, _, _ := tok(t, dir, "license", "issue", "--tenant=fehlt"); code != 1 {
|
|
t.Errorf("unbekannter Mandant: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_Purge(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
inv := strings.TrimSpace(mustTok(t, dir, "invite", "--tenant=kunde"))
|
|
parsed, _ := crypto.ParseEnrollmentToken(inv)
|
|
st := openStore(t, dir)
|
|
st.Enroll(context.Background(), parsed.Secret, store.Node{ID: "n1", Hostname: "h", PublicKey: "k", IPAddress: "1.2.3.4"})
|
|
|
|
out := mustTok(t, dir, "purge")
|
|
if !strings.Contains(out, "1") {
|
|
t.Errorf("purge-Ausgabe: %q", out)
|
|
}
|
|
}
|
|
|
|
// Funktioniert auch, während der Server läuft (gleiche SQLite-DB, WAL).
|
|
func TestToken_WhileServerRuns(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
s, err := server.New(context.Background(), server.Config{
|
|
DataDir: dir, AgentAddr: "127.0.0.1:0", ProviderAddr: "127.0.0.1:0", LanderAddr: "127.0.0.1:0", Logger: slog.New(slog.DiscardHandler),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Shutdown(context.Background())
|
|
|
|
addTenant(t, dir, "kunde")
|
|
inv := strings.TrimSpace(mustTok(t, dir, "invite", "--tenant=kunde"))
|
|
parsed, err := crypto.ParseEnrollmentToken(inv)
|
|
if err != nil || parsed.Fingerprint != s.Fingerprint() {
|
|
t.Errorf("Token passt nicht zum laufenden Server: %v", err)
|
|
}
|
|
}
|
|
|
|
func setPublicURL(t *testing.T, dir, url string) {
|
|
t.Helper()
|
|
s, err := server.New(context.Background(), server.Config{DataDir: dir, PublicURLs: []string{url, "https://100.64.0.1:8443"}, Logger: slog.New(slog.DiscardHandler)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Shutdown(context.Background())
|
|
}
|
|
|
|
func TestToken_InviteEmbedsPublicURL(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
|
|
// Ohne öffentliche Adresse: Token ohne URL, Hinweis auf --public-url.
|
|
code, stdout, stderr := tok(t, dir, "invite", "--tenant=kunde")
|
|
inv, _ := crypto.ParseEnrollmentToken(strings.TrimSpace(stdout))
|
|
if code != 0 || inv.ServerURL != "" || !strings.Contains(stderr, "--public-url") {
|
|
t.Errorf("ohne Adresse: code=%d url=%q stderr=%q", code, inv.ServerURL, stderr)
|
|
}
|
|
|
|
setPublicURL(t, dir, "https://ingress.example:8443")
|
|
code, stdout, stderr = tok(t, dir, "invite", "--tenant=kunde")
|
|
inv, err := crypto.ParseEnrollmentToken(strings.TrimSpace(stdout))
|
|
if code != 0 || err != nil || inv.ServerURL != "https://ingress.example:8443" {
|
|
t.Errorf("mit gemerkter Adresse: code=%d %+v %v", code, inv, err)
|
|
}
|
|
if !strings.Contains(stderr, "SULTAN_TOKEN=") {
|
|
t.Errorf("Hinweis ohne fertige Befehlszeile: %q", stderr)
|
|
}
|
|
|
|
// Pro Einladung überschreibbar, z. B. NetBird-Adresse für diesen Knoten.
|
|
_, stdout, _ = tok(t, dir, "invite", "--tenant=kunde", "--server-url=https://100.64.0.1:8443")
|
|
if inv, _ := crypto.ParseEnrollmentToken(strings.TrimSpace(stdout)); inv.ServerURL != "https://100.64.0.1:8443" {
|
|
t.Errorf("--server-url: %+v", inv)
|
|
}
|
|
if code, _, _ := tok(t, dir, "invite", "--tenant=kunde", "--server-url=http://x:8443"); code != 2 {
|
|
t.Errorf("ungültige --server-url: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_NodeMaintenance(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
st := openStore(t, dir)
|
|
st.CreateNode(context.Background(), store.Node{ID: "pi", TenantID: "kunde", Hostname: "pi", PublicKey: "k", IPAddress: "1.2.3.4"})
|
|
|
|
out := mustTok(t, dir, "node", "maintenance", "--id=pi", "--for=2h", "--reason=Kernel-Update", "--public-reason=Sicherheitsupdate")
|
|
if !strings.Contains(out, "pi") {
|
|
t.Errorf("Ausgabe: %q", out)
|
|
}
|
|
n, _ := st.GetNode(context.Background(), "pi")
|
|
if m := n.Maintenance; m == nil || m.Reason != "Kernel-Update" || m.PublicReason != "Sicherheitsupdate" ||
|
|
time.Until(m.Until) < 119*time.Minute || time.Until(m.Until) > 121*time.Minute {
|
|
t.Errorf("gespeichert: %+v", n.Maintenance)
|
|
}
|
|
if list := mustTok(t, dir, "node", "list"); !strings.Contains(list, "MAINTENANCE") || !strings.Contains(list, "bis ") {
|
|
t.Errorf("Liste ohne Wartung:\n%s", list)
|
|
}
|
|
|
|
mustTok(t, dir, "node", "maintenance", "--id=pi") // Default 4h
|
|
if n, _ := st.GetNode(context.Background(), "pi"); time.Until(n.Maintenance.Until) < 239*time.Minute {
|
|
t.Errorf("Default-Dauer: %v", time.Until(n.Maintenance.Until))
|
|
}
|
|
mustTok(t, dir, "node", "maintenance", "--id=pi", "--for=0")
|
|
if n, _ := st.GetNode(context.Background(), "pi"); !n.Maintenance.Until.IsZero() {
|
|
t.Error("--for=0 nicht unbefristet")
|
|
}
|
|
if list := mustTok(t, dir, "node", "list"); !strings.Contains(list, "unbefristet") {
|
|
t.Errorf("Liste:\n%s", list)
|
|
}
|
|
|
|
mustTok(t, dir, "node", "resume", "--id=pi")
|
|
if n, _ := st.GetNode(context.Background(), "pi"); n.Maintenance != nil {
|
|
t.Error("resume beendet die Wartung nicht")
|
|
}
|
|
for _, args := range [][]string{
|
|
{"node", "maintenance", "--id=pi", "--for=bald"},
|
|
{"node", "maintenance", "--id=pi", "--for=-1h"},
|
|
{"node", "maintenance"},
|
|
} {
|
|
if code, _, _ := tok(t, dir, args...); code != 2 {
|
|
t.Errorf("%v: code=%d", args, code)
|
|
}
|
|
}
|
|
if code, _, _ := tok(t, dir, "node", "maintenance", "--id=fehlt"); code != 1 {
|
|
t.Errorf("unbekannter Knoten: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_NodeCommand(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
st := openStore(t, dir)
|
|
st.CreateNode(context.Background(), store.Node{ID: "pi", TenantID: "kunde", Hostname: "pi", PublicKey: "k", IPAddress: "1.2.3.4"})
|
|
|
|
mustTok(t, dir, "node", "command", "--id=pi", "--cmd=rotate_key", "--reason=Routine")
|
|
mustTok(t, dir, "node", "drain", "--id=pi") // erzeugt zusätzlich einen Hinweis an den Agent
|
|
cmds, _ := st.PendingNodeCommands(context.Background(), "pi")
|
|
if len(cmds) != 2 || cmds[0].Command != "rotate_key" || cmds[0].Reason != "Routine" || cmds[1].Command != "drain" {
|
|
t.Errorf("Warteschlange: %+v", cmds)
|
|
}
|
|
if code, _, _ := tok(t, dir, "node", "command", "--id=pi", "--cmd=rm -rf /"); code != 2 {
|
|
t.Errorf("beliebiger Befehl: code=%d", code)
|
|
}
|
|
if code, _, _ := tok(t, dir, "node", "command", "--id=fehlt", "--cmd=resync"); code != 1 {
|
|
t.Errorf("unbekannter Knoten: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_NodeListShowsLease(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde")
|
|
st := openStore(t, dir)
|
|
st.CreateNode(context.Background(), store.Node{ID: "pi", TenantID: "kunde", Hostname: "pi", PublicKey: "k", IPAddress: "1.2.3.4"})
|
|
if list := mustTok(t, dir, "node", "list"); !strings.Contains(list, "LEASE") {
|
|
t.Errorf("Spalte LEASE fehlt:\n%s", list)
|
|
}
|
|
st.PutLease(context.Background(), store.Lease{NodeID: "pi", IssuedAt: time.Now(), RenewAfter: time.Now().Add(time.Hour),
|
|
ValidUntil: time.Date(2030, 1, 2, 3, 4, 0, 0, time.Local), Document: "{}"})
|
|
if list := mustTok(t, dir, "node", "list"); !strings.Contains(list, "2030-01-02 03:04") {
|
|
t.Errorf("Lease-Ende fehlt:\n%s", list)
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantAddFeatures(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde", "--features=custom_middleware,websockets")
|
|
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
|
if strings.Join(tn.Features, ",") != "custom_middleware,websockets" {
|
|
t.Errorf("Features: %v", tn.Features)
|
|
}
|
|
if code, _, _ := tok(t, dir, "tenant", "add", "--id=x", "--name=x", "--email=x@x.de", "--domains=x.de", "--features=teleport"); code != 1 {
|
|
t.Errorf("unbekanntes Feature: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_TenantPayment(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
addTenant(t, dir, "kunde", "--tier=pro")
|
|
st := openStore(t, dir)
|
|
|
|
mustTok(t, dir, "tenant", "payment", "--id=kunde", "--overdue-since=2026-09-01")
|
|
if tn, _ := st.GetTenant(context.Background(), "kunde"); !tn.PaymentOverdueSince.Equal(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)) {
|
|
t.Errorf("offen seit: %v", tn.PaymentOverdueSince)
|
|
}
|
|
if list := mustTok(t, dir, "tenant", "list"); !strings.Contains(list, "PAYMENT") || !strings.Contains(list, "offen seit 2026-09-01") {
|
|
t.Errorf("Liste ohne Zahlungsstatus:\n%s", list)
|
|
}
|
|
mustTok(t, dir, "tenant", "payment", "--id=kunde", "--paid")
|
|
if tn, _ := st.GetTenant(context.Background(), "kunde"); !tn.PaymentOverdueSince.IsZero() {
|
|
t.Error("--paid setzt nicht zurück")
|
|
}
|
|
for _, args := range [][]string{
|
|
{"tenant", "payment", "--id=kunde"},
|
|
{"tenant", "payment", "--id=kunde", "--paid", "--overdue-since=2026-09-01"},
|
|
{"tenant", "payment", "--id=kunde", "--overdue-since=gestern"},
|
|
} {
|
|
if code, _, _ := tok(t, dir, args...); code != 2 {
|
|
t.Errorf("%v: code=%d", args, code)
|
|
}
|
|
}
|
|
if code, _, _ := tok(t, dir, "tenant", "payment", "--id=fehlt", "--paid"); code != 1 {
|
|
t.Errorf("unbekannter Mandant: code=%d", code)
|
|
}
|
|
}
|
|
|
|
func TestToken_DataDirFromEnvironment(t *testing.T) {
|
|
dir := initDataDir(t)
|
|
t.Setenv("SULTAN_DATA_DIR", dir)
|
|
if code, _, stderr := run(context.Background(), "token", "tenant", "list"); code != 0 {
|
|
t.Errorf("code=%d stderr=%s", code, stderr)
|
|
}
|
|
}
|