server_twilio_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. package server
  2. import (
  3. "github.com/stretchr/testify/require"
  4. "heckel.io/ntfy/v2/user"
  5. "heckel.io/ntfy/v2/util"
  6. "io"
  7. "net/http"
  8. "net/http/httptest"
  9. "sync/atomic"
  10. "testing"
  11. )
  12. func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
  13. var called, verified atomic.Bool
  14. var code atomic.Pointer[string]
  15. twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  16. body, err := io.ReadAll(r.Body)
  17. require.Nil(t, err)
  18. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  19. if r.URL.Path == "/v2/Services/VA1234567890/Verifications" {
  20. if code.Load() != nil {
  21. t.Fatal("Should be only called once")
  22. }
  23. require.Equal(t, "Channel=sms&To=%2B12223334444", string(body))
  24. code.Store(util.String("123456"))
  25. } else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" {
  26. if verified.Load() {
  27. t.Fatal("Should be only called once")
  28. }
  29. require.Equal(t, "Code=123456&To=%2B12223334444", string(body))
  30. verified.Store(true)
  31. } else {
  32. t.Fatal("Unexpected path:", r.URL.Path)
  33. }
  34. }))
  35. defer twilioVerifyServer.Close()
  36. twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  37. if called.Load() {
  38. t.Fatal("Should be only called once")
  39. }
  40. body, err := io.ReadAll(r.Body)
  41. require.Nil(t, err)
  42. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  43. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  44. require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
  45. called.Store(true)
  46. }))
  47. defer twilioCallsServer.Close()
  48. c := newTestConfigWithAuthFile(t)
  49. c.TwilioVerifyBaseURL = twilioVerifyServer.URL
  50. c.TwilioCallsBaseURL = twilioCallsServer.URL
  51. c.TwilioAccount = "AC1234567890"
  52. c.TwilioAuthToken = "AAEAA1234567890"
  53. c.TwilioPhoneNumber = "+1234567890"
  54. c.TwilioVerifyService = "VA1234567890"
  55. s := newTestServer(t, c)
  56. // Add tier and user
  57. require.Nil(t, s.userManager.AddTier(&user.Tier{
  58. Code: "pro",
  59. MessageLimit: 10,
  60. CallLimit: 1,
  61. }))
  62. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
  63. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  64. u, err := s.userManager.User("phil")
  65. require.Nil(t, err)
  66. // Send verification code for phone number
  67. response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{
  68. "authorization": util.BasicAuth("phil", "phil"),
  69. })
  70. require.Equal(t, 200, response.Code)
  71. waitFor(t, func() bool {
  72. return *code.Load() == "123456"
  73. })
  74. // Add phone number with code
  75. response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{
  76. "authorization": util.BasicAuth("phil", "phil"),
  77. })
  78. require.Equal(t, 200, response.Code)
  79. waitFor(t, func() bool {
  80. return verified.Load()
  81. })
  82. phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
  83. require.Nil(t, err)
  84. require.Equal(t, 1, len(phoneNumbers))
  85. require.Equal(t, "+12223334444", phoneNumbers[0])
  86. // Do the thing
  87. response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  88. "authorization": util.BasicAuth("phil", "phil"),
  89. "x-call": "yes",
  90. })
  91. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  92. waitFor(t, func() bool {
  93. return called.Load()
  94. })
  95. // Remove the phone number
  96. response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{
  97. "authorization": util.BasicAuth("phil", "phil"),
  98. })
  99. require.Equal(t, 200, response.Code)
  100. // Verify the phone number is gone from the DB
  101. phoneNumbers, err = s.userManager.PhoneNumbers(u.ID)
  102. require.Nil(t, err)
  103. require.Equal(t, 0, len(phoneNumbers))
  104. }
  105. func TestServer_Twilio_Call_Success(t *testing.T) {
  106. var called atomic.Bool
  107. twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  108. if called.Load() {
  109. t.Fatal("Should be only called once")
  110. }
  111. body, err := io.ReadAll(r.Body)
  112. require.Nil(t, err)
  113. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  114. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  115. require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
  116. called.Store(true)
  117. }))
  118. defer twilioServer.Close()
  119. c := newTestConfigWithAuthFile(t)
  120. c.TwilioCallsBaseURL = twilioServer.URL
  121. c.TwilioAccount = "AC1234567890"
  122. c.TwilioAuthToken = "AAEAA1234567890"
  123. c.TwilioPhoneNumber = "+1234567890"
  124. s := newTestServer(t, c)
  125. // Add tier and user
  126. require.Nil(t, s.userManager.AddTier(&user.Tier{
  127. Code: "pro",
  128. MessageLimit: 10,
  129. CallLimit: 1,
  130. }))
  131. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
  132. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  133. u, err := s.userManager.User("phil")
  134. require.Nil(t, err)
  135. require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
  136. // Do the thing
  137. response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  138. "authorization": util.BasicAuth("phil", "phil"),
  139. "x-call": "+11122233344",
  140. })
  141. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  142. waitFor(t, func() bool {
  143. return called.Load()
  144. })
  145. }
  146. func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
  147. var called atomic.Bool
  148. twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  149. if called.Load() {
  150. t.Fatal("Should be only called once")
  151. }
  152. body, err := io.ReadAll(r.Body)
  153. require.Nil(t, err)
  154. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  155. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  156. require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
  157. called.Store(true)
  158. }))
  159. defer twilioServer.Close()
  160. c := newTestConfigWithAuthFile(t)
  161. c.TwilioCallsBaseURL = twilioServer.URL
  162. c.TwilioAccount = "AC1234567890"
  163. c.TwilioAuthToken = "AAEAA1234567890"
  164. c.TwilioPhoneNumber = "+1234567890"
  165. s := newTestServer(t, c)
  166. // Add tier and user
  167. require.Nil(t, s.userManager.AddTier(&user.Tier{
  168. Code: "pro",
  169. MessageLimit: 10,
  170. CallLimit: 1,
  171. }))
  172. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
  173. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  174. u, err := s.userManager.User("phil")
  175. require.Nil(t, err)
  176. require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
  177. // Do the thing
  178. response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  179. "authorization": util.BasicAuth("phil", "phil"),
  180. "x-call": "yes", // <<<------
  181. })
  182. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  183. waitFor(t, func() bool {
  184. return called.Load()
  185. })
  186. }
  187. func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
  188. c := newTestConfigWithAuthFile(t)
  189. c.TwilioCallsBaseURL = "http://dummy.invalid"
  190. c.TwilioAccount = "AC1234567890"
  191. c.TwilioAuthToken = "AAEAA1234567890"
  192. c.TwilioPhoneNumber = "+1234567890"
  193. s := newTestServer(t, c)
  194. // Add tier and user
  195. require.Nil(t, s.userManager.AddTier(&user.Tier{
  196. Code: "pro",
  197. MessageLimit: 10,
  198. CallLimit: 1,
  199. }))
  200. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
  201. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  202. // Do the thing
  203. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  204. "authorization": util.BasicAuth("phil", "phil"),
  205. "x-call": "+11122233344",
  206. })
  207. require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code)
  208. }
  209. func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
  210. c := newTestConfigWithAuthFile(t)
  211. c.TwilioCallsBaseURL = "https://127.0.0.1"
  212. c.TwilioAccount = "AC1234567890"
  213. c.TwilioAuthToken = "AAEAA1234567890"
  214. c.TwilioPhoneNumber = "+1234567890"
  215. s := newTestServer(t, c)
  216. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  217. "x-call": "+invalid",
  218. })
  219. require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code)
  220. }
  221. func TestServer_Twilio_Call_Anonymous(t *testing.T) {
  222. c := newTestConfigWithAuthFile(t)
  223. c.TwilioCallsBaseURL = "https://127.0.0.1"
  224. c.TwilioAccount = "AC1234567890"
  225. c.TwilioAuthToken = "AAEAA1234567890"
  226. c.TwilioPhoneNumber = "+1234567890"
  227. s := newTestServer(t, c)
  228. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  229. "x-call": "+123123",
  230. })
  231. require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code)
  232. }
  233. func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
  234. s := newTestServer(t, newTestConfig(t))
  235. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  236. "x-call": "+1234",
  237. })
  238. require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code)
  239. }