server_twilio_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package server
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "sync/atomic"
  7. "testing"
  8. "text/template"
  9. "github.com/stretchr/testify/require"
  10. "heckel.io/ntfy/v2/user"
  11. "heckel.io/ntfy/v2/util"
  12. )
  13. func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
  14. var called, verified atomic.Bool
  15. var code atomic.Pointer[string]
  16. twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  17. body, err := io.ReadAll(r.Body)
  18. require.Nil(t, err)
  19. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  20. if r.URL.Path == "/v2/Services/VA1234567890/Verifications" {
  21. if code.Load() != nil {
  22. t.Fatal("Should be only called once")
  23. }
  24. require.Equal(t, "Channel=sms&To=%2B12223334444", string(body))
  25. code.Store(util.String("123456"))
  26. } else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" {
  27. if verified.Load() {
  28. t.Fatal("Should be only called once")
  29. }
  30. require.Equal(t, "Code=123456&To=%2B12223334444", string(body))
  31. verified.Store(true)
  32. } else {
  33. t.Fatal("Unexpected path:", r.URL.Path)
  34. }
  35. }))
  36. defer twilioVerifyServer.Close()
  37. twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  38. if called.Load() {
  39. t.Fatal("Should be only called once")
  40. }
  41. body, err := io.ReadAll(r.Body)
  42. require.Nil(t, err)
  43. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  44. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  45. 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))
  46. called.Store(true)
  47. }))
  48. defer twilioCallsServer.Close()
  49. c := newTestConfigWithAuthFile(t)
  50. c.TwilioVerifyBaseURL = twilioVerifyServer.URL
  51. c.TwilioCallsBaseURL = twilioCallsServer.URL
  52. c.TwilioAccount = "AC1234567890"
  53. c.TwilioAuthToken = "AAEAA1234567890"
  54. c.TwilioPhoneNumber = "+1234567890"
  55. c.TwilioVerifyService = "VA1234567890"
  56. s := newTestServer(t, c)
  57. // Add tier and user
  58. require.Nil(t, s.userManager.AddTier(&user.Tier{
  59. Code: "pro",
  60. MessageLimit: 10,
  61. CallLimit: 1,
  62. }))
  63. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  64. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  65. u, err := s.userManager.User("phil")
  66. require.Nil(t, err)
  67. // Send verification code for phone number
  68. response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{
  69. "authorization": util.BasicAuth("phil", "phil"),
  70. })
  71. require.Equal(t, 200, response.Code)
  72. waitFor(t, func() bool {
  73. return *code.Load() == "123456"
  74. })
  75. // Add phone number with code
  76. response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{
  77. "authorization": util.BasicAuth("phil", "phil"),
  78. })
  79. require.Equal(t, 200, response.Code)
  80. waitFor(t, func() bool {
  81. return verified.Load()
  82. })
  83. phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
  84. require.Nil(t, err)
  85. require.Equal(t, 1, len(phoneNumbers))
  86. require.Equal(t, "+12223334444", phoneNumbers[0])
  87. // Do the thing
  88. response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  89. "authorization": util.BasicAuth("phil", "phil"),
  90. "x-call": "yes",
  91. })
  92. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  93. waitFor(t, func() bool {
  94. return called.Load()
  95. })
  96. // Remove the phone number
  97. response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{
  98. "authorization": util.BasicAuth("phil", "phil"),
  99. })
  100. require.Equal(t, 200, response.Code)
  101. // Verify the phone number is gone from the DB
  102. phoneNumbers, err = s.userManager.PhoneNumbers(u.ID)
  103. require.Nil(t, err)
  104. require.Equal(t, 0, len(phoneNumbers))
  105. }
  106. func TestServer_Twilio_Call_Success(t *testing.T) {
  107. var called atomic.Bool
  108. twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  109. if called.Load() {
  110. t.Fatal("Should be only called once")
  111. }
  112. body, err := io.ReadAll(r.Body)
  113. require.Nil(t, err)
  114. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  115. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  116. 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))
  117. called.Store(true)
  118. }))
  119. defer twilioServer.Close()
  120. c := newTestConfigWithAuthFile(t)
  121. c.TwilioCallsBaseURL = twilioServer.URL
  122. c.TwilioAccount = "AC1234567890"
  123. c.TwilioAuthToken = "AAEAA1234567890"
  124. c.TwilioPhoneNumber = "+1234567890"
  125. s := newTestServer(t, c)
  126. // Add tier and user
  127. require.Nil(t, s.userManager.AddTier(&user.Tier{
  128. Code: "pro",
  129. MessageLimit: 10,
  130. CallLimit: 1,
  131. }))
  132. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  133. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  134. u, err := s.userManager.User("phil")
  135. require.Nil(t, err)
  136. require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
  137. // Do the thing
  138. response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  139. "authorization": util.BasicAuth("phil", "phil"),
  140. "x-call": "+11122233344",
  141. })
  142. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  143. waitFor(t, func() bool {
  144. return called.Load()
  145. })
  146. }
  147. func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
  148. var called atomic.Bool
  149. twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  150. if called.Load() {
  151. t.Fatal("Should be only called once")
  152. }
  153. body, err := io.ReadAll(r.Body)
  154. require.Nil(t, err)
  155. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  156. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  157. 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))
  158. called.Store(true)
  159. }))
  160. defer twilioServer.Close()
  161. c := newTestConfigWithAuthFile(t)
  162. c.TwilioCallsBaseURL = twilioServer.URL
  163. c.TwilioAccount = "AC1234567890"
  164. c.TwilioAuthToken = "AAEAA1234567890"
  165. c.TwilioPhoneNumber = "+1234567890"
  166. s := newTestServer(t, c)
  167. // Add tier and user
  168. require.Nil(t, s.userManager.AddTier(&user.Tier{
  169. Code: "pro",
  170. MessageLimit: 10,
  171. CallLimit: 1,
  172. }))
  173. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  174. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  175. u, err := s.userManager.User("phil")
  176. require.Nil(t, err)
  177. require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
  178. // Do the thing
  179. response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  180. "authorization": util.BasicAuth("phil", "phil"),
  181. "x-call": "yes", // <<<------
  182. })
  183. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  184. waitFor(t, func() bool {
  185. return called.Load()
  186. })
  187. }
  188. func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
  189. var called atomic.Bool
  190. twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  191. if called.Load() {
  192. t.Fatal("Should be only called once")
  193. }
  194. body, err := io.ReadAll(r.Body)
  195. require.Nil(t, err)
  196. require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
  197. require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
  198. require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+language%3D%22de-DE%22+loop%3D%223%22%3E%0A%09%09Du+hast+eine+Nachricht+von+notify+im+Thema+mytopic.+Nachricht%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%09Ende+der+Nachricht.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Diese+Nachricht+wurde+von+Benutzer+phil+gesendet.+Sie+wird+drei+Mal+wiederholt.%0A%09%09Um+dich+von+Anrufen+wie+diesen+abzumelden%2C+entferne+deine+Telefonnummer+in+der+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay+language%3D%22de-DE%22%3EAuf+Wiederh%C3%B6ren.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
  199. called.Store(true)
  200. }))
  201. defer twilioServer.Close()
  202. c := newTestConfigWithAuthFile(t)
  203. c.TwilioCallsBaseURL = twilioServer.URL
  204. c.TwilioAccount = "AC1234567890"
  205. c.TwilioAuthToken = "AAEAA1234567890"
  206. c.TwilioPhoneNumber = "+1234567890"
  207. c.TwilioCallFormat = template.Must(template.New("twiml").Parse(`
  208. <Response>
  209. <Pause length="1"/>
  210. <Say language="de-DE" loop="3">
  211. Du hast eine Nachricht von notify im Thema {{.Topic}}. Nachricht:
  212. <break time="1s"/>
  213. {{.Message}}
  214. <break time="1s"/>
  215. Ende der Nachricht.
  216. <break time="1s"/>
  217. Diese Nachricht wurde von Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
  218. Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
  219. <break time="3s"/>
  220. </Say>
  221. <Say language="de-DE">Auf Wiederhören.</Say>
  222. </Response>`))
  223. s := newTestServer(t, c)
  224. // Add tier and user
  225. require.Nil(t, s.userManager.AddTier(&user.Tier{
  226. Code: "pro",
  227. MessageLimit: 10,
  228. CallLimit: 1,
  229. }))
  230. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  231. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  232. u, err := s.userManager.User("phil")
  233. require.Nil(t, err)
  234. require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))
  235. // Do the thing
  236. response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
  237. "authorization": util.BasicAuth("phil", "phil"),
  238. "x-call": "+11122233344",
  239. })
  240. require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
  241. waitFor(t, func() bool {
  242. return called.Load()
  243. })
  244. }
  245. func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
  246. c := newTestConfigWithAuthFile(t)
  247. c.TwilioCallsBaseURL = "http://dummy.invalid"
  248. c.TwilioAccount = "AC1234567890"
  249. c.TwilioAuthToken = "AAEAA1234567890"
  250. c.TwilioPhoneNumber = "+1234567890"
  251. s := newTestServer(t, c)
  252. // Add tier and user
  253. require.Nil(t, s.userManager.AddTier(&user.Tier{
  254. Code: "pro",
  255. MessageLimit: 10,
  256. CallLimit: 1,
  257. }))
  258. require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
  259. require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
  260. // Do the thing
  261. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  262. "authorization": util.BasicAuth("phil", "phil"),
  263. "x-call": "+11122233344",
  264. })
  265. require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code)
  266. }
  267. func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
  268. c := newTestConfigWithAuthFile(t)
  269. c.TwilioCallsBaseURL = "https://127.0.0.1"
  270. c.TwilioAccount = "AC1234567890"
  271. c.TwilioAuthToken = "AAEAA1234567890"
  272. c.TwilioPhoneNumber = "+1234567890"
  273. s := newTestServer(t, c)
  274. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  275. "x-call": "+invalid",
  276. })
  277. require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code)
  278. }
  279. func TestServer_Twilio_Call_Anonymous(t *testing.T) {
  280. c := newTestConfigWithAuthFile(t)
  281. c.TwilioCallsBaseURL = "https://127.0.0.1"
  282. c.TwilioAccount = "AC1234567890"
  283. c.TwilioAuthToken = "AAEAA1234567890"
  284. c.TwilioPhoneNumber = "+1234567890"
  285. s := newTestServer(t, c)
  286. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  287. "x-call": "+123123",
  288. })
  289. require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code)
  290. }
  291. func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
  292. s := newTestServer(t, newTestConfig(t))
  293. response := request(t, s, "POST", "/mytopic", "test", map[string]string{
  294. "x-call": "+1234",
  295. })
  296. require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code)
  297. }