tenants_test.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. // tenants_test.go — Pure-Go tests for the validation helpers and
  2. // the input structs on the tenants store. The DB-backed paths
  3. // (Create, Update, SetStatus, List) are tested in
  4. // tenants_pg_test.go under the 'postgres' build tag.
  5. package authd
  6. import (
  7. "errors"
  8. "testing"
  9. )
  10. func TestValidSlug(t *testing.T) {
  11. cases := []struct {
  12. in string
  13. want bool
  14. }{
  15. // valid
  16. {"a", false},
  17. {"ab", true},
  18. {"acme", true},
  19. {"acme-corp", true},
  20. {"a-1-b-2", true},
  21. {"x" + repeat("y", 62) + "z", true}, // exactly 64 chars
  22. // invalid
  23. {"", false},
  24. {"A", false}, // uppercase
  25. {"-acme", false}, // leading dash
  26. {"acme-", false}, // trailing dash
  27. {"acme--corp", true}, // double dash is allowed by the regex
  28. {"acme_corp", false}, // underscore
  29. {"acme.corp", false}, // dot
  30. {"acme corp", false}, // space
  31. {repeat("a", 65), false}, // too long
  32. }
  33. for _, c := range cases {
  34. if got := validSlug(c.in); got != c.want {
  35. t.Errorf("validSlug(%q) = %v, want %v", c.in, got, c.want)
  36. }
  37. }
  38. }
  39. func TestLooksLikeEmail(t *testing.T) {
  40. cases := []struct {
  41. in string
  42. want bool
  43. }{
  44. {"a@b.c", true},
  45. {"ops@acme.test", true},
  46. {"first.last@sub.acme.test", true},
  47. {"a@b", false}, // no dot in domain
  48. {"@b.c", false}, // no local part
  49. {"a@.c", false}, // dot at start of domain
  50. {"a@b.", false}, // dot at end of domain
  51. {"", false},
  52. {" a@b.c ", true}, // trimmed
  53. {repeat("a", 251) + "@b.c", false}, // 255 chars (RFC max 254)
  54. }
  55. for _, c := range cases {
  56. if got := looksLikeEmail(c.in); got != c.want {
  57. t.Errorf("looksLikeEmail(%q) = %v, want %v", c.in, got, c.want)
  58. }
  59. }
  60. }
  61. func TestCreateTenantInput_Validate(t *testing.T) {
  62. good := CreateTenantInput{
  63. Slug: "acme", DisplayName: "Acme", ContactEmail: "ops@acme.test",
  64. RateLimitPerSec: 1000, FCMShared: nil,
  65. }
  66. if err := good.Validate(); err != nil {
  67. t.Errorf("good input rejected: %v", err)
  68. }
  69. cases := []struct {
  70. name string
  71. in CreateTenantInput
  72. ok bool
  73. }{
  74. {"empty slug", CreateTenantInput{Slug: "", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false},
  75. {"bad slug", CreateTenantInput{Slug: "Bad Slug", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false},
  76. {"empty display name", CreateTenantInput{Slug: "acme", DisplayName: "", ContactEmail: "a@b.c", RateLimitPerSec: 100}, false},
  77. {"bad email", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "not-an-email", RateLimitPerSec: 100}, false},
  78. {"rl=0", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 0}, false},
  79. {"rl too high", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 2_000_000}, false},
  80. {"rl at max", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 1_000_000}, true},
  81. {"rl at min", CreateTenantInput{Slug: "acme", DisplayName: "Acme", ContactEmail: "a@b.c", RateLimitPerSec: 1}, true},
  82. }
  83. for _, c := range cases {
  84. err := c.in.Validate()
  85. if c.ok && err != nil {
  86. t.Errorf("%s: expected ok, got %v", c.name, err)
  87. }
  88. if !c.ok && err == nil {
  89. t.Errorf("%s: expected error, got nil", c.name)
  90. }
  91. if !c.ok && err != nil && !errors.Is(err, ErrTenantInvalid) {
  92. t.Errorf("%s: expected ErrTenantInvalid, got %v", c.name, err)
  93. }
  94. }
  95. }
  96. func TestUpdateTenantInput_Validate(t *testing.T) {
  97. // empty patch → ok
  98. empty := &UpdateTenantInput{}
  99. if err := empty.Validate(); err != nil {
  100. t.Errorf("empty patch rejected: %v", err)
  101. }
  102. dn := "Acme Corp"
  103. ce := "ops@acme.test"
  104. rl := 500
  105. good := &UpdateTenantInput{DisplayName: &dn, ContactEmail: &ce, RateLimitPerSec: &rl}
  106. if err := good.Validate(); err != nil {
  107. t.Errorf("good patch rejected: %v", err)
  108. }
  109. badName := ""
  110. if err := (&UpdateTenantInput{DisplayName: &badName}).Validate(); err == nil {
  111. t.Error("empty display_name should fail")
  112. }
  113. badEmail := "not-an-email"
  114. if err := (&UpdateTenantInput{ContactEmail: &badEmail}).Validate(); err == nil {
  115. t.Error("bad contact_email should fail")
  116. }
  117. badRl := 0
  118. if err := (&UpdateTenantInput{RateLimitPerSec: &badRl}).Validate(); err == nil {
  119. t.Error("rl=0 should fail")
  120. }
  121. }
  122. func repeat(s string, n int) string {
  123. out := make([]byte, 0, len(s)*n)
  124. for i := 0; i < n; i++ {
  125. out = append(out, s...)
  126. }
  127. return string(out)
  128. }