Files
br c8df9d7651 erste module drin.
zwischenstand: Architektur aktualisiert, TOFU implementiert,
2026-10-01 22:22:13 +02:00

254 lines
7.7 KiB
Go

package store
import (
"errors"
"reflect"
"testing"
"time"
)
func sampleTenant(id string) Tenant {
return Tenant{
ID: id,
Name: "Kunde " + id,
Email: id + "@example.com",
Tier: TierPro,
MaxRoutes: 10,
AllowedDomains: []string{"*.firma.de", "shop.de"},
ValidUntil: time.Date(2026, 12, 15, 0, 0, 0, 0, time.UTC),
Status: TenantActive,
}
}
func TestCreateTenant_RoundTrip(t *testing.T) {
s, clock := openTest(t)
in := sampleTenant("kunde_schmidt")
in.ValidUntil = time.Date(2026, 12, 15, 10, 30, 0, 987654321, time.FixedZone("CET", 3600))
created, err := s.CreateTenant(ctx, in)
if err != nil {
t.Fatalf("CreateTenant: %v", err)
}
if !created.CreatedAt.Equal(clock.now()) || !created.UpdatedAt.Equal(clock.now()) {
t.Errorf("Zeitstempel: created=%v updated=%v, erwartet %v", created.CreatedAt, created.UpdatedAt, clock.now())
}
if !created.ValidUntil.Equal(in.ValidUntil) || created.ValidUntil.Location() != time.UTC {
t.Errorf("ValidUntil = %v, erwartet %v in UTC", created.ValidUntil, in.ValidUntil)
}
got, err := s.GetTenant(ctx, "kunde_schmidt")
if err != nil {
t.Fatalf("GetTenant: %v", err)
}
if !reflect.DeepEqual(got, created) {
t.Errorf("gelesen weicht ab\n got: %+v\nwant: %+v", got, created)
}
}
func TestCreateTenant_Defaults(t *testing.T) {
s, _ := openTest(t)
in := sampleTenant("t1")
in.Tier, in.Status, in.AllowedDomains = "", "", nil
created, err := s.CreateTenant(ctx, in)
if err != nil {
t.Fatal(err)
}
if created.Tier != TierCommunity || created.Status != TenantActive {
t.Errorf("Defaults: tier=%q status=%q", created.Tier, created.Status)
}
if created.AllowedDomains == nil || len(created.AllowedDomains) != 0 {
t.Errorf("AllowedDomains = %#v, erwartet leere, nicht-nil Liste", created.AllowedDomains)
}
got, _ := s.GetTenant(ctx, "t1")
if !reflect.DeepEqual(got, created) {
t.Errorf("gelesen weicht ab\n got: %+v\nwant: %+v", got, created)
}
}
func TestCreateTenant_Duplicate(t *testing.T) {
s, _ := openTest(t)
if _, err := s.CreateTenant(ctx, sampleTenant("t1")); err != nil {
t.Fatal(err)
}
if _, err := s.CreateTenant(ctx, sampleTenant("t1")); !errors.Is(err, ErrExists) {
t.Errorf("erwartet ErrExists, bekommen %v", err)
}
}
func TestCreateTenant_Validation(t *testing.T) {
tests := map[string]func(*Tenant){
"ID leer": func(t *Tenant) { t.ID = "" },
"Name leer": func(t *Tenant) { t.Name = " " },
"E-Mail leer": func(t *Tenant) { t.Email = "" },
"unbekannter Tier": func(t *Tenant) { t.Tier = "gold" },
"unbekannter Status": func(t *Tenant) { t.Status = "pausiert" },
"MaxRoutes < -1": func(t *Tenant) { t.MaxRoutes = -2 },
"leere Domain": func(t *Tenant) { t.AllowedDomains = []string{"a.de", ""} },
"ValidUntil fehlt": func(t *Tenant) { t.ValidUntil = time.Time{} },
}
for name, mutate := range tests {
t.Run(name, func(t *testing.T) {
s, _ := openTest(t)
in := sampleTenant("t1")
mutate(&in)
if _, err := s.CreateTenant(ctx, in); !errors.Is(err, ErrInvalid) {
t.Errorf("erwartet ErrInvalid, bekommen %v", err)
}
})
}
}
func TestCreateTenant_AllStatusesAndTiers(t *testing.T) {
s, _ := openTest(t)
i := 0
for _, tier := range []Tier{TierCommunity, TierPro, TierEnterprise} {
for _, st := range []TenantStatus{TenantActive, TenantSuspendedPayment, TenantQuarantined, TenantExpired} {
in := sampleTenant(string(rune('a' + i)))
in.Tier, in.Status = tier, st
if _, err := s.CreateTenant(ctx, in); err != nil {
t.Errorf("tier=%s status=%s: %v", tier, st, err)
}
i++
}
}
}
func TestGetTenant_NotFound(t *testing.T) {
s, _ := openTest(t)
if _, err := s.GetTenant(ctx, "fehlt"); !errors.Is(err, ErrNotFound) {
t.Errorf("erwartet ErrNotFound, bekommen %v", err)
}
}
func TestListTenants(t *testing.T) {
s, _ := openTest(t)
got, err := s.ListTenants(ctx)
if err != nil || len(got) != 0 {
t.Fatalf("leere DB: %v, %v", got, err)
}
for _, id := range []string{"c", "a", "b"} {
if _, err := s.CreateTenant(ctx, sampleTenant(id)); err != nil {
t.Fatal(err)
}
}
got, err = s.ListTenants(ctx)
if err != nil {
t.Fatal(err)
}
var ids []string
for _, tn := range got {
ids = append(ids, tn.ID)
}
if !reflect.DeepEqual(ids, []string{"a", "b", "c"}) {
t.Errorf("IDs = %v, erwartet sortiert", ids)
}
}
func TestUpdateTenant(t *testing.T) {
s, clock := openTest(t)
created, err := s.CreateTenant(ctx, sampleTenant("t1"))
if err != nil {
t.Fatal(err)
}
clock.advance(time.Hour)
upd := created
upd.Name = "Neuer Name"
upd.Tier = TierEnterprise
upd.MaxRoutes = -1
upd.AllowedDomains = []string{"neu.de"}
upd.ValidUntil = created.ValidUntil.AddDate(1, 0, 0)
upd.CreatedAt = time.Time{} // darf nicht überschrieben werden
got, err := s.UpdateTenant(ctx, upd)
if err != nil {
t.Fatalf("UpdateTenant: %v", err)
}
if !got.CreatedAt.Equal(created.CreatedAt) {
t.Errorf("CreatedAt verändert: %v", got.CreatedAt)
}
if !got.UpdatedAt.Equal(clock.now()) {
t.Errorf("UpdatedAt = %v, erwartet %v", got.UpdatedAt, clock.now())
}
read, _ := s.GetTenant(ctx, "t1")
if !reflect.DeepEqual(read, got) {
t.Errorf("gelesen weicht ab\n got: %+v\nwant: %+v", read, got)
}
if read.Name != "Neuer Name" || read.MaxRoutes != -1 || read.Tier != TierEnterprise {
t.Errorf("Änderungen nicht gespeichert: %+v", read)
}
}
func TestUpdateTenant_Errors(t *testing.T) {
s, _ := openTest(t)
if _, err := s.UpdateTenant(ctx, sampleTenant("fehlt")); !errors.Is(err, ErrNotFound) {
t.Errorf("unbekannt: erwartet ErrNotFound, bekommen %v", err)
}
created, _ := s.CreateTenant(ctx, sampleTenant("t1"))
created.Email = ""
if _, err := s.UpdateTenant(ctx, created); !errors.Is(err, ErrInvalid) {
t.Errorf("ungültig: erwartet ErrInvalid, bekommen %v", err)
}
}
func TestSetTenantStatus(t *testing.T) {
s, clock := openTest(t)
if _, err := s.CreateTenant(ctx, sampleTenant("t1")); err != nil {
t.Fatal(err)
}
clock.advance(time.Minute)
if err := s.SetTenantStatus(ctx, "t1", TenantSuspendedPayment); err != nil {
t.Fatal(err)
}
got, _ := s.GetTenant(ctx, "t1")
if got.Status != TenantSuspendedPayment || !got.UpdatedAt.Equal(clock.now()) {
t.Errorf("status=%q updated=%v", got.Status, got.UpdatedAt)
}
if err := s.SetTenantStatus(ctx, "t1", "kaputt"); !errors.Is(err, ErrInvalid) {
t.Errorf("ungültig: erwartet ErrInvalid, bekommen %v", err)
}
if err := s.SetTenantStatus(ctx, "fehlt", TenantActive); !errors.Is(err, ErrNotFound) {
t.Errorf("unbekannt: erwartet ErrNotFound, bekommen %v", err)
}
}
func TestDeleteTenant_Cascades(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
mustTenant(t, s, "t2")
mustNode(t, s, "n1", "t1", "key1")
mustNode(t, s, "n2", "t2", "key2")
if err := s.AddEnrollmentToken(ctx, "tok1", "t1", clock.now().Add(time.Hour)); err != nil {
t.Fatal(err)
}
if err := s.DeleteTenant(ctx, "t1"); err != nil {
t.Fatalf("DeleteTenant: %v", err)
}
if _, err := s.GetTenant(ctx, "t1"); !errors.Is(err, ErrNotFound) {
t.Errorf("Mandant noch da: %v", err)
}
if _, err := s.GetNode(ctx, "n1"); !errors.Is(err, ErrNotFound) {
t.Errorf("Knoten des Mandanten nicht mitgelöscht: %v", err)
}
if _, err := s.Enroll(ctx, "tok1", sampleNode("n3", "key3")); !errors.Is(err, ErrNotFound) {
t.Errorf("Token des Mandanten nicht mitgelöscht: %v", err)
}
if _, err := s.GetNode(ctx, "n2"); err != nil {
t.Errorf("fremder Knoten gelöscht: %v", err)
}
if err := s.DeleteTenant(ctx, "t1"); !errors.Is(err, ErrNotFound) {
t.Errorf("zweites Löschen: erwartet ErrNotFound, bekommen %v", err)
}
}
func mustTenant(t *testing.T, s *Store, id string) Tenant {
t.Helper()
tn, err := s.CreateTenant(ctx, sampleTenant(id))
if err != nil {
t.Fatalf("CreateTenant(%s): %v", id, err)
}
return tn
}