publish_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. package cmd
  2. import (
  3. "fmt"
  4. "github.com/stretchr/testify/require"
  5. "heckel.io/ntfy/v2/test"
  6. "heckel.io/ntfy/v2/util"
  7. "net/http"
  8. "net/http/httptest"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "testing"
  15. "time"
  16. )
  17. func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
  18. t.Parallel()
  19. testMessage := util.RandomString(10)
  20. app, _, _, _ := newTestApp()
  21. require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
  22. _, err := util.Retry(func() (*int, error) {
  23. app2, _, stdout, _ := newTestApp()
  24. if err := app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}); err != nil {
  25. return nil, err
  26. }
  27. if !strings.Contains(stdout.String(), testMessage) {
  28. return nil, fmt.Errorf("test message %s not found in topic", testMessage)
  29. }
  30. return util.Int(1), nil
  31. }, time.Second, 2*time.Second, 5*time.Second) // Since #502, ntfy.sh writes messages to the cache asynchronously, after a timeout of ~1.5s
  32. require.Nil(t, err)
  33. }
  34. func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
  35. t.Parallel()
  36. s, port := test.StartServer(t)
  37. defer test.StopServer(t, s, port)
  38. topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
  39. app, _, stdout, _ := newTestApp()
  40. require.Nil(t, app.Run([]string{"ntfy", "publish", topic, "some message"}))
  41. m := toMessage(t, stdout.String())
  42. require.Equal(t, "some message", m.Message)
  43. app2, _, stdout, _ := newTestApp()
  44. require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", topic}))
  45. m = toMessage(t, stdout.String())
  46. require.Equal(t, "some message", m.Message)
  47. }
  48. func TestCLI_Publish_All_The_Things(t *testing.T) {
  49. t.Parallel()
  50. s, port := test.StartServer(t)
  51. defer test.StopServer(t, s, port)
  52. topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
  53. app, _, stdout, _ := newTestApp()
  54. require.Nil(t, app.Run([]string{
  55. "ntfy", "publish",
  56. "--title", "this is a title",
  57. "--priority", "high",
  58. "--tags", "tag1,tag2",
  59. // No --delay, --email
  60. "--click", "https://ntfy.sh",
  61. "--icon", "https://ntfy.sh/static/img/ntfy.png",
  62. "--attach", "https://f-droid.org/F-Droid.apk",
  63. "--filename", "fdroid.apk",
  64. "--no-cache",
  65. "--no-firebase",
  66. topic,
  67. "some message",
  68. }))
  69. m := toMessage(t, stdout.String())
  70. require.Equal(t, "message", m.Event)
  71. require.Equal(t, "mytopic", m.Topic)
  72. require.Equal(t, "some message", m.Message)
  73. require.Equal(t, "this is a title", m.Title)
  74. require.Equal(t, 4, m.Priority)
  75. require.Equal(t, []string{"tag1", "tag2"}, m.Tags)
  76. require.Equal(t, "https://ntfy.sh", m.Click)
  77. require.Equal(t, "https://f-droid.org/F-Droid.apk", m.Attachment.URL)
  78. require.Equal(t, "fdroid.apk", m.Attachment.Name)
  79. require.Equal(t, int64(0), m.Attachment.Size)
  80. require.Equal(t, "", m.Attachment.Owner)
  81. require.Equal(t, int64(0), m.Attachment.Expires)
  82. require.Equal(t, "", m.Attachment.Type)
  83. require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon)
  84. }
  85. func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
  86. s, port := test.StartServer(t)
  87. defer test.StopServer(t, s, port)
  88. topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
  89. // Test: sleep 0.5
  90. sleep := exec.Command("sleep", "0.5")
  91. require.Nil(t, sleep.Start())
  92. go sleep.Wait() // Must be called to release resources
  93. start := time.Now()
  94. app, _, stdout, _ := newTestApp()
  95. require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid), topic}))
  96. m := toMessage(t, stdout.String())
  97. require.True(t, time.Since(start) >= 500*time.Millisecond)
  98. require.Regexp(t, `Process with PID \d+ exited after `, m.Message)
  99. // Test: PID does not exist
  100. app, _, _, _ = newTestApp()
  101. err := app.Run([]string{"ntfy", "publish", "--wait-pid", "1234567", topic})
  102. require.Error(t, err)
  103. require.Equal(t, "process with PID 1234567 not running", err.Error())
  104. // Test: Successful command (exit 0)
  105. start = time.Now()
  106. app, _, stdout, _ = newTestApp()
  107. require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "sleep", "0.5"}))
  108. m = toMessage(t, stdout.String())
  109. require.True(t, time.Since(start) >= 500*time.Millisecond)
  110. require.Contains(t, m.Message, `Command succeeded after `)
  111. require.Contains(t, m.Message, `: sleep 0.5`)
  112. // Test: Failing command (exit 1)
  113. app, _, stdout, _ = newTestApp()
  114. require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "/bin/false", "false doesn't care about its args"}))
  115. m = toMessage(t, stdout.String())
  116. require.Contains(t, m.Message, `Command failed after `)
  117. require.Contains(t, m.Message, `(exit code 1): /bin/false "false doesn't care about its args"`, m.Message)
  118. // Test: Non-existing command (hard fail!)
  119. app, _, _, _ = newTestApp()
  120. err = app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "does-not-exist-no-really", "really though"})
  121. require.Error(t, err)
  122. require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
  123. // Tests with NTFY_TOPIC set ////
  124. t.Setenv("NTFY_TOPIC", topic)
  125. // Test: Successful command with NTFY_TOPIC
  126. app, _, stdout, _ = newTestApp()
  127. require.Nil(t, app.Run([]string{"ntfy", "publish", "--cmd", "echo", "hi there"}))
  128. m = toMessage(t, stdout.String())
  129. require.Equal(t, "mytopic", m.Topic)
  130. // Test: Successful --wait-pid with NTFY_TOPIC
  131. sleep = exec.Command("sleep", "0.2")
  132. require.Nil(t, sleep.Start())
  133. go sleep.Wait() // Must be called to release resources
  134. app, _, stdout, _ = newTestApp()
  135. require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
  136. m = toMessage(t, stdout.String())
  137. require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
  138. }
  139. func TestCLI_Publish_Default_UserPass(t *testing.T) {
  140. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  141. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  142. require.Equal(t, "/mytopic", r.URL.Path)
  143. require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
  144. w.WriteHeader(http.StatusOK)
  145. w.Write([]byte(message))
  146. }))
  147. defer server.Close()
  148. filename := filepath.Join(t.TempDir(), "client.yml")
  149. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  150. default-host: %s
  151. default-user: philipp
  152. default-password: mypass
  153. `, server.URL)), 0600))
  154. app, _, stdout, _ := newTestApp()
  155. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
  156. m := toMessage(t, stdout.String())
  157. require.Equal(t, "triggered", m.Message)
  158. }
  159. func TestCLI_Publish_Default_Token(t *testing.T) {
  160. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  161. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  162. require.Equal(t, "/mytopic", r.URL.Path)
  163. require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
  164. w.WriteHeader(http.StatusOK)
  165. w.Write([]byte(message))
  166. }))
  167. defer server.Close()
  168. filename := filepath.Join(t.TempDir(), "client.yml")
  169. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  170. default-host: %s
  171. default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
  172. `, server.URL)), 0600))
  173. app, _, stdout, _ := newTestApp()
  174. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
  175. m := toMessage(t, stdout.String())
  176. require.Equal(t, "triggered", m.Message)
  177. }
  178. func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
  179. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  180. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  181. require.Equal(t, "/mytopic", r.URL.Path)
  182. require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
  183. w.WriteHeader(http.StatusOK)
  184. w.Write([]byte(message))
  185. }))
  186. defer server.Close()
  187. filename := filepath.Join(t.TempDir(), "client.yml")
  188. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  189. default-host: %s
  190. default-user: philipp
  191. default-password: mypass
  192. `, server.URL)), 0600))
  193. app, _, stdout, _ := newTestApp()
  194. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
  195. m := toMessage(t, stdout.String())
  196. require.Equal(t, "triggered", m.Message)
  197. }
  198. func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
  199. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  200. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  201. require.Equal(t, "/mytopic", r.URL.Path)
  202. require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
  203. w.WriteHeader(http.StatusOK)
  204. w.Write([]byte(message))
  205. }))
  206. defer server.Close()
  207. filename := filepath.Join(t.TempDir(), "client.yml")
  208. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  209. default-host: %s
  210. default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
  211. `, server.URL)), 0600))
  212. app, _, stdout, _ := newTestApp()
  213. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
  214. m := toMessage(t, stdout.String())
  215. require.Equal(t, "triggered", m.Message)
  216. }
  217. func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
  218. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  219. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  220. require.Equal(t, "/mytopic", r.URL.Path)
  221. require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
  222. w.WriteHeader(http.StatusOK)
  223. w.Write([]byte(message))
  224. }))
  225. defer server.Close()
  226. filename := filepath.Join(t.TempDir(), "client.yml")
  227. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  228. default-host: %s
  229. default-token: tk_FAKETOKEN01234567890FAKETOKEN
  230. `, server.URL)), 0600))
  231. app, _, stdout, _ := newTestApp()
  232. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
  233. m := toMessage(t, stdout.String())
  234. require.Equal(t, "triggered", m.Message)
  235. }
  236. func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
  237. message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
  238. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  239. require.Equal(t, "/mytopic", r.URL.Path)
  240. require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
  241. w.WriteHeader(http.StatusOK)
  242. w.Write([]byte(message))
  243. }))
  244. defer server.Close()
  245. filename := filepath.Join(t.TempDir(), "client.yml")
  246. require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
  247. default-host: %s
  248. default-user: philipp
  249. default-password: fakepass
  250. `, server.URL)), 0600))
  251. app, _, stdout, _ := newTestApp()
  252. require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
  253. m := toMessage(t, stdout.String())
  254. require.Equal(t, "triggered", m.Message)
  255. }
  256. func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
  257. app, _, _, _ := newTestApp()
  258. err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
  259. require.Error(t, err)
  260. require.Equal(t, "cannot set both --user and --token", err.Error())
  261. }