sources_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. // sources_test.go — pure-Go tests for the input validators and
  2. // the format helpers on the sources store. The DB-backed paths
  3. // (Create, Update, SetStatus, List, RotateSecrets) are exercised
  4. // by scripts/m13b_w2_smoke.sh against a real Postgres.
  5. package authd
  6. import (
  7. "strings"
  8. "testing"
  9. )
  10. func TestValidSourceID(t *testing.T) {
  11. cases := []struct {
  12. in string
  13. want bool
  14. }{
  15. // valid (same shape as auth.tenants.slug)
  16. {"primary", true},
  17. {"ops-foo", true},
  18. {"a-b-c", true},
  19. {strings.Repeat("a", 64), true},
  20. // invalid
  21. {"", false},
  22. {"a", false}, // too short (1 char)
  23. {"A", false}, // uppercase
  24. {"-foo", false}, // leading dash
  25. {"foo-", false}, // trailing dash
  26. {"foo_bar", false}, // underscore
  27. {"foo bar", false}, // space
  28. {"foo.bar", false}, // dot
  29. {strings.Repeat("a", 65), false},
  30. }
  31. for _, c := range cases {
  32. if got := validSourceID(c.in); got != c.want {
  33. t.Errorf("validSourceID(%q) = %v, want %v", c.in, got, c.want)
  34. }
  35. }
  36. }
  37. func TestValidSecretFormat(t *testing.T) {
  38. cases := []struct {
  39. in string
  40. want bool
  41. }{
  42. {strings.Repeat("a", 32), true},
  43. {strings.Repeat("a", 64), true},
  44. {strings.Repeat("a", 128), true},
  45. {"abc-DEF_123" + strings.Repeat("a", 22), true},
  46. // invalid
  47. {"", false},
  48. {strings.Repeat("a", 31), false}, // too short
  49. {strings.Repeat("a", 129), false}, // too long
  50. {"with spaces inside", false},
  51. {"with!special", false},
  52. {"with.dot", false},
  53. {"with/slash", false},
  54. }
  55. for _, c := range cases {
  56. if got := validSecretFormat(c.in); got != c.want {
  57. t.Errorf("validSecretFormat(%q) = %v, want %v", c.in, got, c.want)
  58. }
  59. }
  60. }
  61. func TestValidAPIKeyFormat(t *testing.T) {
  62. cases := []struct {
  63. in string
  64. want bool
  65. }{
  66. {strings.Repeat("a", 16), true},
  67. {strings.Repeat("a", 48), true},
  68. {strings.Repeat("a", 128), true},
  69. // invalid
  70. {strings.Repeat("a", 15), false},
  71. {strings.Repeat("a", 129), false},
  72. {"with space", false},
  73. }
  74. for _, c := range cases {
  75. if got := validAPIKeyFormat(c.in); got != c.want {
  76. t.Errorf("validAPIKeyFormat(%q) = %v, want %v", c.in, got, c.want)
  77. }
  78. }
  79. }
  80. func TestCreateSourceInput_Validate(t *testing.T) {
  81. tooLongName := strings.Repeat("a", 201)
  82. cases := []struct {
  83. name string
  84. in CreateSourceInput
  85. wantErr bool
  86. errSub string
  87. }{
  88. {
  89. name: "ok",
  90. in: CreateSourceInput{
  91. ID: "primary", Name: "Primary", Type: "http", RateLimitPerSec: 100,
  92. },
  93. wantErr: false,
  94. },
  95. {
  96. name: "ok with optional secrets",
  97. in: CreateSourceInput{
  98. ID: "primary", Name: "Primary", Type: "http", RateLimitPerSec: 100,
  99. HMACSecret: strings.Repeat("a", 32),
  100. APIKey: strings.Repeat("a", 16),
  101. },
  102. wantErr: false,
  103. },
  104. {name: "bad id", in: CreateSourceInput{ID: "Bad ID!", Name: "x", Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "id must match"},
  105. {name: "empty name", in: CreateSourceInput{ID: "primary", Name: " ", Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "name is required"},
  106. {name: "name too long", in: CreateSourceInput{ID: "primary", Name: tooLongName, Type: "http", RateLimitPerSec: 1}, wantErr: true, errSub: "name must be"},
  107. {name: "bad type", in: CreateSourceInput{ID: "primary", Name: "x", Type: "smtp", RateLimitPerSec: 1}, wantErr: true, errSub: "type must be"},
  108. {name: "rate 0", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 0}, wantErr: true, errSub: "rate_limit_per_sec"},
  109. {name: "rate too high", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 2_000_000}, wantErr: true, errSub: "rate_limit_per_sec"},
  110. {name: "bad hmac", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, HMACSecret: "short"}, wantErr: true, errSub: "hmac_secret"},
  111. {name: "bad api_key", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, APIKey: "short"}, wantErr: true, errSub: "api_key"},
  112. {name: "bad allowed_targets json", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, AllowedTargets: []byte("{not-json")}, wantErr: true, errSub: "allowed_targets"},
  113. {name: "bad match_expr json", in: CreateSourceInput{ID: "primary", Name: "x", Type: "http", RateLimitPerSec: 1, MatchExpr: []byte("{not-json")}, wantErr: true, errSub: "match_expr"},
  114. }
  115. for _, c := range cases {
  116. t.Run(c.name, func(t *testing.T) {
  117. err := c.in.Validate()
  118. if c.wantErr {
  119. if err == nil {
  120. t.Fatalf("expected error containing %q, got nil", c.errSub)
  121. }
  122. if !strings.Contains(err.Error(), c.errSub) {
  123. t.Fatalf("expected error containing %q, got %q", c.errSub, err.Error())
  124. }
  125. } else if err != nil {
  126. t.Fatalf("unexpected error: %v", err)
  127. }
  128. })
  129. }
  130. }
  131. func TestUpdateSourceInput_Validate(t *testing.T) {
  132. name := "Renamed"
  133. badType := "smtp"
  134. rate := 50
  135. cases := []struct {
  136. name string
  137. in UpdateSourceInput
  138. wantErr bool
  139. errSub string
  140. }{
  141. {name: "empty (no-op)", in: UpdateSourceInput{}, wantErr: false},
  142. {name: "name change", in: UpdateSourceInput{Name: &name}, wantErr: false},
  143. {name: "type change", in: UpdateSourceInput{Type: &badType}, wantErr: true, errSub: "type must be"},
  144. {name: "rate change", in: UpdateSourceInput{RateLimitPerSec: &rate}, wantErr: false},
  145. {name: "empty name", in: UpdateSourceInput{Name: ptr(" ")}, wantErr: true, errSub: "name cannot be empty"},
  146. {name: "bad json", in: UpdateSourceInput{AllowedTargets: []byte("{nope")}, wantErr: true, errSub: "allowed_targets"},
  147. }
  148. for _, c := range cases {
  149. t.Run(c.name, func(t *testing.T) {
  150. err := c.in.Validate()
  151. if c.wantErr {
  152. if err == nil || !strings.Contains(err.Error(), c.errSub) {
  153. t.Fatalf("expected error containing %q, got %v", c.errSub, err)
  154. }
  155. } else if err != nil {
  156. t.Fatalf("unexpected error: %v", err)
  157. }
  158. })
  159. }
  160. }
  161. func TestGenerateSecret(t *testing.T) {
  162. a, err := generateSecret(16)
  163. if err != nil {
  164. t.Fatalf("generateSecret(16): %v", err)
  165. }
  166. if len(a) != 32 {
  167. // 16 bytes -> 32 hex chars
  168. t.Errorf("generateSecret(16) length = %d, want 32", len(a))
  169. }
  170. b, _ := generateSecret(16)
  171. if a == b {
  172. t.Errorf("generateSecret returned same value twice: %q", a)
  173. }
  174. }
  175. func ptr(s string) *string { return &s }