// tenants_test.go — Pure-Go tests for the validation helpers and // the input structs on the tenants store. The DB-backed paths // (Create, Update, SetStatus, List) are tested in // tenants_pg_test.go under the 'postgres' build tag. package authd import ( "errors" "testing" ) func TestValidSlug(t *testing.T) { cases := []struct { in string want bool }{ // valid {"a", false}, {"ab", true}, {"acme", true}, {"acme-corp", true}, {"a-1-b-2", true}, {"x" + repeat("y", 62) + "z", true}, // exactly 64 chars // invalid {"", false}, {"A", false}, // uppercase {"-acme", false}, // leading dash {"acme-", false}, // trailing dash {"acme--corp", true}, // double dash is allowed by the regex {"acme_corp", false}, // underscore {"acme.corp", false}, // dot {"acme corp", false}, // space {repeat("a", 65), false}, // too long } for _, c := range cases { if got := validSlug(c.in); got != c.want { t.Errorf("validSlug(%q) = %v, want %v", c.in, got, c.want) } } } func TestLooksLikeEmail(t *testing.T) { cases := []struct { in string want bool }{ {"a@b.c", true}, {"ops@acme.test", true}, {"first.last@sub.acme.test", true}, {"a@b", false}, // no dot in domain {"@b.c", false}, // no local part {"a@.c", false}, // dot at start of domain {"a@b.", false}, // dot at end of domain {"", false}, {" a@b.c ", true}, // trimmed {repeat("a", 251) + "@b.c", false}, // 255 chars (RFC max 254) } for _, c := range cases { if got := looksLikeEmail(c.in); got != c.want { t.Errorf("looksLikeEmail(%q) = %v, want %v", c.in, got, c.want) } } } func TestCreateTenantInput_Validate(t *testing.T) { good := CreateTenantInput{ Slug: "acme", DisplayName: "Acme", ContactEmail: "ops@acme.test", RateLimitPerSec: 1000, FCMShared: nil, } if err := good.Validate(); err != nil { t.Errorf("good input rejected: %v", err) } cases := []struct { name string in CreateTenantInput ok bool }{ {"empty slug", CreateTenantInput{Slug: "", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false}, {"bad slug", CreateTenantInput{Slug: "Bad Slug", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false}, {"empty display name", CreateTenantInput{Slug: "acme", DisplayName: "", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false}, {"bad email", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "not-an-email", RateLimitPerSec: 100}, false}, {"rl=0", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 0}, false}, {"rl too high", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 2_000_000}, false}, {"rl at max", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 1_000_000}, true}, {"rl at min", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 1}, true}, } for _, c := range cases { err := c.in.Validate() if c.ok && err != nil { t.Errorf("%s: expected ok, got %v", c.name, err) } if !c.ok && err == nil { t.Errorf("%s: expected error, got nil", c.name) } if !c.ok && err != nil && !errors.Is(err, ErrTenantInvalid) { t.Errorf("%s: expected ErrTenantInvalid, got %v", c.name, err) } } } func TestUpdateTenantInput_Validate(t *testing.T) { // empty patch → ok empty := &UpdateTenantInput{} if err := empty.Validate(); err != nil { t.Errorf("empty patch rejected: %v", err) } dn := "Acme Corp" ce := "ops@acme.test" rl := 500 good := &UpdateTenantInput{DisplayName: &dn, ContactEmail: &ce, RateLimitPerSec: &rl} if err := good.Validate(); err != nil { t.Errorf("good patch rejected: %v", err) } badName := "" if err := (&UpdateTenantInput{DisplayName: &badName}).Validate(); err == nil { t.Error("empty display_name should fail") } badEmail := "not-an-email" if err := (&UpdateTenantInput{ContactEmail: &badEmail}).Validate(); err == nil { t.Error("bad contact_email should fail") } badRl := 0 if err := (&UpdateTenantInput{RateLimitPerSec: &badRl}).Validate(); err == nil { t.Error("rl=0 should fail") } } func repeat(s string, n int) string { out := make([]byte, 0, len(s)*n) for i := 0; i < n; i++ { out = append(out, s...) } return string(out) }