sultan token tenant update: Mandanten nachträglich ändern
Weitere Compose-Stacks eines Kunden sind der Regelfall; bisher ließen sich Domains und Routenlimit nur per delete + neu anlegen (und damit ohne Knoten) ändern. Die Änderung liegt bewusst nur auf dem Server: Ein Agent darf sich keine Domains selbst freigeben. - Domains und Features werden hinzugefügt/entfernt statt ersetzt; unbekannter Eintrag beim Entfernen ist ein Fehler, die letzte Domain bleibt - Verlängerung in die Zukunft setzt expired auf active (der Ablauf-Job setzt das nie zurück); Quarantäne und Zahlungsverzug bleiben - Wechsel auf kommerziell mit vergangenem valid_until verlangt --valid-until Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd93277750
commit
c2d6dd694f
7 files changed
+413
-25
No files matched your search
+158
-1
@@ -69,7 +69,7 @@ func TestToken_Usage(t *testing.T) {
|
||||
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"} {
|
||||
for _, want := range []string{"tenant add", "tenant update", "invite", "node list", "node maintenance", "node resume", "license issue", "purge"} {
|
||||
if !strings.Contains(stderr, want) {
|
||||
t.Errorf("Hilfe ohne %q:\n%s", want, stderr)
|
||||
}
|
||||
@@ -504,3 +504,160 @@ func TestToken_DataDirFromEnvironment(t *testing.T) {
|
||||
t.Errorf("code=%d stderr=%s", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToken_TenantUpdateDomains(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde")
|
||||
|
||||
// Normalisiert anfügen, Vorhandenes nicht doppelt, Reihenfolge bleibt.
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--add-domains= Neu.DE. ,kunde.de,*.neu.de")
|
||||
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if got := strings.Join(tn.AllowedDomains, ","); got != "kunde.de,*.kunde.de,neu.de,*.neu.de" {
|
||||
t.Errorf("nach add: %s", got)
|
||||
}
|
||||
|
||||
out := mustTok(t, dir, "tenant", "update", "--id=kunde", "--remove-domains=NEU.de,*.neu.de")
|
||||
tn, _ = openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if got := strings.Join(tn.AllowedDomains, ","); got != "kunde.de,*.kunde.de" {
|
||||
t.Errorf("nach remove: %s", got)
|
||||
}
|
||||
if !strings.Contains(out, "Routen") {
|
||||
t.Errorf("kein Hinweis auf wegfallende Routen:\n%s", out)
|
||||
}
|
||||
|
||||
// Tausch in einem Aufruf: erst entfernen, dann hinzufügen.
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--remove-domains=kunde.de,*.kunde.de", "--add-domains=anders.de")
|
||||
tn, _ = openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if got := strings.Join(tn.AllowedDomains, ","); got != "anders.de" {
|
||||
t.Errorf("nach Tausch: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Was nicht angegeben ist, bleibt – auch Status, offene Zahlung und Features.
|
||||
func TestToken_TenantUpdateKeepsOtherFields(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde", "--tier=pro", "--max-routes=20", "--valid-until=2099-12-31", "--features=websockets")
|
||||
mustTok(t, dir, "tenant", "payment", "--id=kunde", "--overdue-since=2026-01-15")
|
||||
mustTok(t, dir, "tenant", "set-status", "--id=kunde", "--status=suspended_payment")
|
||||
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--add-domains=neu.de")
|
||||
|
||||
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Name != "Kunde kunde" || tn.Email != "kunde@example.com" || tn.Tier != store.TierPro || tn.MaxRoutes != 20 ||
|
||||
tn.Status != store.TenantSuspendedPayment || tn.PaymentOverdueSince.IsZero() ||
|
||||
!tn.ValidUntil.Equal(time.Date(2099, 12, 31, 0, 0, 0, 0, time.UTC)) ||
|
||||
strings.Join(tn.Features, ",") != "websockets" {
|
||||
t.Errorf("Felder verändert: %+v", tn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToken_TenantUpdateFields(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde", "--features=websockets")
|
||||
out := mustTok(t, dir, "tenant", "update", "--id=kunde", "--name=Neu GmbH", "--email=neu@x.de", "--tier=pro",
|
||||
"--max-routes=-1", "--valid-until=2099-06-30", "--add-features=tcp_stream,websockets", "--remove-features=websockets")
|
||||
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Name != "Neu GmbH" || tn.Email != "neu@x.de" || tn.Tier != store.TierPro || tn.MaxRoutes != -1 ||
|
||||
!tn.ValidUntil.Equal(time.Date(2099, 6, 30, 0, 0, 0, 0, time.UTC)) {
|
||||
t.Errorf("gespeichert: %+v", tn)
|
||||
}
|
||||
if got := strings.Join(tn.Features, ","); got != "tcp_stream,websockets" {
|
||||
t.Errorf("Features: %s", got)
|
||||
}
|
||||
// Die Ausgabe zeigt den neuen Stand, auch das, was tenant list nicht zeigt.
|
||||
for _, want := range []string{"kunde", "pro", "2099-06-30", "unbegrenzt", "tcp_stream", "kunde.de"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("Ausgabe ohne %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
|
||||
// max-routes=0 ist ein gültiger Wert und nicht „nicht angegeben“.
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--max-routes=0")
|
||||
tn, _ = openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.MaxRoutes != 0 {
|
||||
t.Errorf("max_routes = %d", tn.MaxRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
// Verlängern nach Vertragsende: Sonst bliebe die Seite trotz neuem Datum auf
|
||||
// „Hosting abgelaufen“, denn der Job setzt expired nie zurück.
|
||||
func TestToken_TenantUpdateExtensionReactivates(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde", "--tier=pro", "--valid-until=2020-01-01")
|
||||
mustTok(t, dir, "tenant", "set-status", "--id=kunde", "--status=expired")
|
||||
|
||||
// Andere Änderungen und ein Datum in der Vergangenheit reaktivieren nicht.
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--add-domains=neu.de")
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--valid-until=2021-01-01")
|
||||
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Status != store.TenantExpired {
|
||||
t.Errorf("Status = %s, erwartet expired", tn.Status)
|
||||
}
|
||||
|
||||
out := mustTok(t, dir, "tenant", "update", "--id=kunde", "--valid-until=2099-01-01")
|
||||
tn, _ = openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Status != store.TenantActive {
|
||||
t.Errorf("Status = %s, erwartet active", tn.Status)
|
||||
}
|
||||
if !strings.Contains(out, "active") {
|
||||
t.Errorf("Reaktivierung nicht gemeldet:\n%s", out)
|
||||
}
|
||||
|
||||
// Quarantäne ist eine bewusste Sperre und bleibt.
|
||||
mustTok(t, dir, "tenant", "set-status", "--id=kunde", "--status=quarantined")
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--valid-until=2099-02-01")
|
||||
tn, _ = openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Status != store.TenantQuarantined {
|
||||
t.Errorf("Status = %s, erwartet quarantined", tn.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// Community prüft valid_until nicht; ein altes Datum würde beim Wechsel auf
|
||||
// ein kommerzielles Paket die Seiten sofort abschalten.
|
||||
func TestToken_TenantUpdateCommercialNeedsFutureDate(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde", "--valid-until=2020-01-01")
|
||||
|
||||
if code, _, stderr := tok(t, dir, "tenant", "update", "--id=kunde", "--tier=pro"); code != 1 || !strings.Contains(stderr, "--valid-until") {
|
||||
t.Errorf("code=%d stderr=%q", code, stderr)
|
||||
}
|
||||
tn, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if tn.Tier != store.TierCommunity {
|
||||
t.Errorf("Tier trotz Fehler geändert: %s", tn.Tier)
|
||||
}
|
||||
mustTok(t, dir, "tenant", "update", "--id=kunde", "--tier=pro", "--valid-until=2099-01-01")
|
||||
}
|
||||
|
||||
func TestToken_TenantUpdateErrors(t *testing.T) {
|
||||
dir := initDataDir(t)
|
||||
addTenant(t, dir, "kunde")
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
code int
|
||||
}{
|
||||
{"ID fehlt", []string{"--add-domains=a.de"}, 2},
|
||||
{"keine Änderung", []string{"--id=kunde"}, 2},
|
||||
{"unbekannter Mandant", []string{"--id=fehlt", "--add-domains=a.de"}, 1},
|
||||
{"Domain ungültig", []string{"--id=kunde", "--add-domains=*.de"}, 1},
|
||||
{"Tippfehler beim Entfernen", []string{"--id=kunde", "--remove-domains=kunde.dee"}, 1},
|
||||
{"letzte Domain", []string{"--id=kunde", "--remove-domains=kunde.de,*.kunde.de"}, 1},
|
||||
{"Feature unbekannt", []string{"--id=kunde", "--add-features=teleport"}, 1},
|
||||
{"Feature nicht vorhanden", []string{"--id=kunde", "--remove-features=websockets"}, 1},
|
||||
{"Tier ungültig", []string{"--id=kunde", "--tier=gold"}, 1},
|
||||
{"Routen keine Zahl", []string{"--id=kunde", "--max-routes=viele"}, 2},
|
||||
{"Routen zu klein", []string{"--id=kunde", "--max-routes=-2"}, 1},
|
||||
{"Datum ungültig", []string{"--id=kunde", "--valid-until=morgen"}, 2},
|
||||
}
|
||||
before, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
for _, tt := range tests {
|
||||
args := append([]string{"tenant", "update"}, tt.args...)
|
||||
if code, _, stderr := tok(t, dir, args...); code != tt.code {
|
||||
t.Errorf("%s: code=%d (erwartet %d) stderr=%q", tt.name, code, tt.code, stderr)
|
||||
}
|
||||
}
|
||||
after, _ := openStore(t, dir).GetTenant(context.Background(), "kunde")
|
||||
if strings.Join(after.AllowedDomains, ",") != strings.Join(before.AllowedDomains, ",") || after.MaxRoutes != before.MaxRoutes {
|
||||
t.Errorf("trotz Fehler geändert: %+v", after)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user