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 }