telegrambots_test.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // telegrambots_test.go — pure-Go tests for the input validators
  2. // and the format helpers on the telegram_bots store. The
  3. // DB-backed paths (Create, Update, SetStatus, List, RotateToken)
  4. // are exercised by scripts/m13b_w3_smoke.sh against a real
  5. // Postgres.
  6. package authd
  7. import (
  8. "strings"
  9. "testing"
  10. )
  11. func TestValidTelegramBotID(t *testing.T) {
  12. cases := []struct {
  13. in string
  14. want bool
  15. }{
  16. // valid (same shape as auth.tenants.slug / source id)
  17. {"primary", true},
  18. {"ops-foo", true},
  19. {"a-b-c", true},
  20. {strings.Repeat("a", 64), true},
  21. // invalid
  22. {"", false},
  23. {"a", false}, // too short (1 char)
  24. {"A", false}, // uppercase
  25. {"-foo", false}, // leading dash
  26. {"foo-", false}, // trailing dash
  27. {"foo_bar", false}, // underscore
  28. {"foo bar", false}, // space
  29. {"foo.bar", false}, // dot
  30. {strings.Repeat("a", 65), false},
  31. }
  32. for _, c := range cases {
  33. if got := validTelegramBotID(c.in); got != c.want {
  34. t.Errorf("validTelegramBotID(%q) = %v, want %v", c.in, got, c.want)
  35. }
  36. }
  37. }
  38. func TestValidBotTokenFormat(t *testing.T) {
  39. goodSecret := strings.Repeat("a", 35)
  40. goodSecretWith := "abc-DEF_123" + strings.Repeat("a", 26)
  41. cases := []struct {
  42. in string
  43. want bool
  44. }{
  45. // valid
  46. {"12345678:" + goodSecret, true},
  47. {"1:" + goodSecret, true},
  48. {"1234567890:" + goodSecretWith, true},
  49. // invalid
  50. {"", false},
  51. {":", false},
  52. {":" + goodSecret, false}, // empty bot id
  53. {"12345678", false}, // no colon
  54. {"12345678:" + strings.Repeat("a", 34), false}, // secret too short
  55. {"12345678:" + strings.Repeat("a", 36), true}, // secret one over (still ok per spec)
  56. {"12345678:short", false}, // secret too short
  57. {"abc:" + goodSecret, false}, // non-digit bot id
  58. {"12345678:" + strings.Repeat("a", 35) + ":extra", false}, // extra colon
  59. {"12345678:" + goodSecret + "!", false}, // bad char in secret
  60. {"12345678:" + goodSecret + " with space", false},
  61. }
  62. for _, c := range cases {
  63. if got := validBotTokenFormat(c.in); got != c.want {
  64. t.Errorf("validBotTokenFormat(%q) = %v, want %v", c.in, got, c.want)
  65. }
  66. }
  67. }
  68. func TestCreateTelegramBotInput_Validate(t *testing.T) {
  69. goodToken := "12345678:" + strings.Repeat("a", 35)
  70. tooLongName := strings.Repeat("a", 201)
  71. tooLongWelcome := strings.Repeat("a", 4097)
  72. tooLongDesc := strings.Repeat("a", 501)
  73. cases := []struct {
  74. name string
  75. in CreateTelegramBotInput
  76. wantErr bool
  77. errSub string
  78. }{
  79. {
  80. name: "ok",
  81. in: CreateTelegramBotInput{
  82. ID: "primary", Name: "Primary", BotToken: goodToken,
  83. },
  84. wantErr: false,
  85. },
  86. {
  87. name: "ok with optional fields",
  88. in: CreateTelegramBotInput{
  89. ID: "primary", Name: "Primary", BotToken: goodToken,
  90. WelcomeMessage: "Welcome to Acme alerts!",
  91. DefaultSourceID: "primary",
  92. Description: "Main bot for ops",
  93. },
  94. wantErr: false,
  95. },
  96. {name: "bad id", in: CreateTelegramBotInput{ID: "Bad ID!", Name: "x", BotToken: goodToken}, wantErr: true, errSub: "id must match"},
  97. {name: "empty name", in: CreateTelegramBotInput{ID: "primary", Name: " ", BotToken: goodToken}, wantErr: true, errSub: "name is required"},
  98. {name: "name too long", in: CreateTelegramBotInput{ID: "primary", Name: tooLongName, BotToken: goodToken}, wantErr: true, errSub: "name must be"},
  99. {name: "bad token", in: CreateTelegramBotInput{ID: "primary", Name: "x", BotToken: "short"}, wantErr: true, errSub: "bot_token must match"},
  100. {name: "welcome too long", in: CreateTelegramBotInput{ID: "primary", Name: "x", BotToken: goodToken, WelcomeMessage: tooLongWelcome}, wantErr: true, errSub: "welcome_message must be"},
  101. {name: "default source id bad", in: CreateTelegramBotInput{ID: "primary", Name: "x", BotToken: goodToken, DefaultSourceID: "Bad ID!"}, wantErr: true, errSub: "default_source_id must match"},
  102. {name: "default source id empty ok", in: CreateTelegramBotInput{ID: "primary", Name: "x", BotToken: goodToken, DefaultSourceID: ""}, wantErr: false},
  103. {name: "description too long", in: CreateTelegramBotInput{ID: "primary", Name: "x", BotToken: goodToken, Description: tooLongDesc}, wantErr: true, errSub: "description must be"},
  104. }
  105. for _, c := range cases {
  106. t.Run(c.name, func(t *testing.T) {
  107. err := c.in.Validate()
  108. if c.wantErr {
  109. if err == nil {
  110. t.Fatalf("expected error containing %q, got nil", c.errSub)
  111. }
  112. if !strings.Contains(err.Error(), c.errSub) {
  113. t.Fatalf("expected error containing %q, got %q", c.errSub, err.Error())
  114. }
  115. } else if err != nil {
  116. t.Fatalf("unexpected error: %v", err)
  117. }
  118. })
  119. }
  120. }
  121. func TestUpdateTelegramBotInput_Validate(t *testing.T) {
  122. name := "Renamed"
  123. emptyName := " "
  124. welcome := "Hello there"
  125. tooLongWelcome := strings.Repeat("a", 4097)
  126. defaultSrc := "primary"
  127. badDefault := "Bad ID!"
  128. desc := "Some description"
  129. cases := []struct {
  130. name string
  131. in UpdateTelegramBotInput
  132. wantErr bool
  133. errSub string
  134. }{
  135. {name: "empty (no-op)", in: UpdateTelegramBotInput{}, wantErr: false},
  136. {name: "name change", in: UpdateTelegramBotInput{Name: &name}, wantErr: false},
  137. {name: "name empty", in: UpdateTelegramBotInput{Name: &emptyName}, wantErr: true, errSub: "name cannot be empty"},
  138. {name: "welcome ok", in: UpdateTelegramBotInput{WelcomeMessage: &welcome}, wantErr: false},
  139. {name: "welcome empty ok (clear)", in: UpdateTelegramBotInput{WelcomeMessage: ptr("")}, wantErr: false},
  140. {name: "welcome too long", in: UpdateTelegramBotInput{WelcomeMessage: &tooLongWelcome}, wantErr: true, errSub: "welcome_message must be"},
  141. {name: "default source ok", in: UpdateTelegramBotInput{DefaultSourceID: &defaultSrc}, wantErr: false},
  142. {name: "default source bad", in: UpdateTelegramBotInput{DefaultSourceID: &badDefault}, wantErr: true, errSub: "default_source_id must match"},
  143. {name: "description ok", in: UpdateTelegramBotInput{Description: &desc}, wantErr: false},
  144. }
  145. for _, c := range cases {
  146. t.Run(c.name, func(t *testing.T) {
  147. err := c.in.Validate()
  148. if c.wantErr {
  149. if err == nil || !strings.Contains(err.Error(), c.errSub) {
  150. t.Fatalf("expected error containing %q, got %v", c.errSub, err)
  151. }
  152. } else if err != nil {
  153. t.Fatalf("unexpected error: %v", err)
  154. }
  155. })
  156. }
  157. }
  158. func TestHashBotToken(t *testing.T) {
  159. plain := "12345678:" + strings.Repeat("a", 35)
  160. h, err := hashBotToken(plain)
  161. if err != nil {
  162. t.Fatalf("hashBotToken: %v", err)
  163. }
  164. if h == "" {
  165. t.Fatal("expected non-empty hash")
  166. }
  167. if h == plain {
  168. t.Fatal("hash equals plaintext (bcrypt not applied)")
  169. }
  170. if !strings.HasPrefix(h, "$2a$") && !strings.HasPrefix(h, "$2b$") {
  171. t.Errorf("expected bcrypt prefix ($2a$/$2b$), got %q", h[:10])
  172. }
  173. }