Browse Source

Metrics, tests

binwiederhier 2 years ago
parent
commit
113b7c8a08
4 changed files with 120 additions and 21 deletions
  1. 1 1
      server/server.go
  2. 20 0
      server/server_metrics.go
  3. 6 3
      server/server_twilio.go
  4. 93 17
      server/server_twilio_test.go

+ 1 - 1
server/server.go

@@ -98,7 +98,7 @@ var (
 	docsRegex                                            = regexp.MustCompile(`^/docs(|/.*)$`)
 	fileRegex                                            = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
 	urlRegex                                             = regexp.MustCompile(`^https?://`)
-	phoneNumberRegex                                     = regexp.MustCompile(`^\+\d{1,100}`)
+	phoneNumberRegex                                     = regexp.MustCompile(`^\+\d{1,100}$`)
 
 	//go:embed site
 	webFs       embed.FS

+ 20 - 0
server/server_metrics.go

@@ -15,6 +15,10 @@ var (
 	metricEmailsPublishedFailure       prometheus.Counter
 	metricEmailsReceivedSuccess        prometheus.Counter
 	metricEmailsReceivedFailure        prometheus.Counter
+	metricSMSSentSuccess               prometheus.Counter
+	metricSMSSentFailure               prometheus.Counter
+	metricCallsMadeSuccess             prometheus.Counter
+	metricCallsMadeFailure             prometheus.Counter
 	metricUnifiedPushPublishedSuccess  prometheus.Counter
 	metricMatrixPublishedSuccess       prometheus.Counter
 	metricMatrixPublishedFailure       prometheus.Counter
@@ -57,6 +61,18 @@ func initMetrics() {
 	metricEmailsReceivedFailure = prometheus.NewCounter(prometheus.CounterOpts{
 		Name: "ntfy_emails_received_failure",
 	})
+	metricSMSSentSuccess = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "ntfy_sms_sent_success",
+	})
+	metricSMSSentFailure = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "ntfy_sms_sent_failure",
+	})
+	metricCallsMadeSuccess = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "ntfy_calls_made_success",
+	})
+	metricCallsMadeFailure = prometheus.NewCounter(prometheus.CounterOpts{
+		Name: "ntfy_calls_made_failure",
+	})
 	metricUnifiedPushPublishedSuccess = prometheus.NewCounter(prometheus.CounterOpts{
 		Name: "ntfy_unifiedpush_published_success",
 	})
@@ -95,6 +111,10 @@ func initMetrics() {
 		metricEmailsPublishedFailure,
 		metricEmailsReceivedSuccess,
 		metricEmailsReceivedFailure,
+		metricSMSSentSuccess,
+		metricSMSSentFailure,
+		metricCallsMadeSuccess,
+		metricCallsMadeFailure,
 		metricUnifiedPushPublishedSuccess,
 		metricMatrixPublishedSuccess,
 		metricMatrixPublishedFailure,

+ 6 - 3
server/server_twilio.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"encoding/xml"
 	"fmt"
+	"github.com/prometheus/client_golang/prometheus"
 	"heckel.io/ntfy/log"
 	"heckel.io/ntfy/util"
 	"io"
@@ -36,7 +37,7 @@ func (s *Server) sendSMS(v *visitor, r *http.Request, m *message, to string) {
 	data.Set("From", s.config.TwilioFromNumber)
 	data.Set("To", to)
 	data.Set("Body", body)
-	s.performTwilioRequest(v, r, m, twilioMessageEndpoint, to, body, data)
+	s.performTwilioRequest(v, r, m, metricSMSSentSuccess, metricSMSSentFailure, twilioMessageEndpoint, to, body, data)
 }
 
 func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
@@ -45,10 +46,10 @@ func (s *Server) callPhone(v *visitor, r *http.Request, m *message, to string) {
 	data.Set("From", s.config.TwilioFromNumber)
 	data.Set("To", to)
 	data.Set("Twiml", body)
-	s.performTwilioRequest(v, r, m, twilioCallEndpoint, to, body, data)
+	s.performTwilioRequest(v, r, m, metricCallsMadeSuccess, metricCallsMadeFailure, twilioCallEndpoint, to, body, data)
 }
 
-func (s *Server) performTwilioRequest(v *visitor, r *http.Request, m *message, endpoint, to, body string, data url.Values) {
+func (s *Server) performTwilioRequest(v *visitor, r *http.Request, m *message, msuccess, mfailure prometheus.Counter, endpoint, to, body string, data url.Values) {
 	logContext := log.Context{
 		"twilio_from": s.config.TwilioFromNumber,
 		"twilio_to":   to,
@@ -66,6 +67,7 @@ func (s *Server) performTwilioRequest(v *visitor, r *http.Request, m *message, e
 			Field("twilio_response", response).
 			Err(err).
 			Warn("Error sending Twilio request")
+		minc(mfailure)
 		return
 	}
 	if ev.IsTrace() {
@@ -73,6 +75,7 @@ func (s *Server) performTwilioRequest(v *visitor, r *http.Request, m *message, e
 	} else if ev.IsDebug() {
 		ev.Debug("Received successful Twilio response")
 	}
+	minc(msuccess)
 }
 
 func (s *Server) performTwilioRequestInternal(endpoint string, data url.Values) (string, error) {

+ 93 - 17
server/server_twilio_test.go

@@ -2,37 +2,113 @@ package server
 
 import (
 	"github.com/stretchr/testify/require"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"sync/atomic"
 	"testing"
 )
 
 func TestServer_Twilio_SMS(t *testing.T) {
+	var called atomic.Bool
+	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		require.Nil(t, err)
+		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Messages.json", r.URL.Path)
+		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
+		require.Equal(t, "Body=test%0A%0A--%0AThis+message+was+sent+by+9.9.9.9+via+ntfy.sh%2Fmytopic&From=%2B1234567890&To=%2B11122233344", string(body))
+		called.Store(true)
+	}))
+	defer twilioServer.Close()
+
 	c := newTestConfig(t)
-	c.TwilioBaseURL = "http://"
-	c.TwilioAccount = "AC123"
-	c.TwilioAuthToken = "secret-token"
-	c.TwilioFromNumber = "+123456789"
+	c.BaseURL = "https://ntfy.sh"
+	c.TwilioBaseURL = twilioServer.URL
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioFromNumber = "+1234567890"
 	s := newTestServer(t, c)
 
 	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
 		"SMS": "+11122233344",
 	})
-	require.Equal(t, 1, toMessage(t, response.Body.String()).Priority)
+	require.Equal(t, "test", toMessage(t, response.Body.String()).Message)
+	waitFor(t, func() bool {
+		return called.Load()
+	})
+}
+
+func TestServer_Twilio_Call(t *testing.T) {
+	var called atomic.Bool
+	twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		require.Nil(t, err)
+		require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
+		require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
+		require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EYou+have+a+message+from+notify+on+topic+mytopic.+Message%3A%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3Ethis+message+has%26%23xA%3Ba+new+line+and+%26lt%3Bbrackets%26gt%3B%21%26%23xA%3Band+%26%2334%3Bquotes+and+other+%26%2339%3Bquotes%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EEnd+message.%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay%3EThis+message+was+sent+by+9.9.9.9+via+127.0.0.1%3A12345%2Fmytopic%3C%2FSay%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%3C%2FResponse%3E", string(body))
+		called.Store(true)
+	}))
+	defer twilioServer.Close()
+
+	c := newTestConfig(t)
+	c.TwilioBaseURL = twilioServer.URL
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioFromNumber = "+1234567890"
+	s := newTestServer(t, c)
 
-	response = request(t, s, "GET", "/mytopic/send?priority=low", "test", nil)
-	require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
+	body := `this message has
+a new line and <brackets>!
+and "quotes and other 'quotes`
+	response := request(t, s, "POST", "/mytopic", body, map[string]string{
+		"x-call": "+11122233344",
+	})
+	require.Equal(t, "this message has\na new line and <brackets>!\nand \"quotes and other 'quotes", toMessage(t, response.Body.String()).Message)
+	waitFor(t, func() bool {
+		return called.Load()
+	})
+}
+
+func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
+	c := newTestConfig(t)
+	c.TwilioBaseURL = "https://127.0.0.1"
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioFromNumber = "+1234567890"
+	s := newTestServer(t, c)
 
-	response = request(t, s, "GET", "/mytopic/send?priority=default", "test", nil)
-	require.Equal(t, 3, toMessage(t, response.Body.String()).Priority)
+	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
+		"x-call": "+invalid",
+	})
+	require.Equal(t, 40031, toHTTPError(t, response.Body.String()).Code)
+}
 
-	response = request(t, s, "GET", "/mytopic/send?priority=high", "test", nil)
-	require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
+func TestServer_Twilio_SMS_InvalidNumber(t *testing.T) {
+	c := newTestConfig(t)
+	c.TwilioBaseURL = "https://127.0.0.1"
+	c.TwilioAccount = "AC1234567890"
+	c.TwilioAuthToken = "AAEAA1234567890"
+	c.TwilioFromNumber = "+1234567890"
+	s := newTestServer(t, c)
 
-	response = request(t, s, "GET", "/mytopic/send?priority=max", "test", nil)
-	require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
+	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
+		"x-sms": "+invalid",
+	})
+	require.Equal(t, 40031, toHTTPError(t, response.Body.String()).Code)
+}
 
-	response = request(t, s, "GET", "/mytopic/trigger?priority=urgent", "test", nil)
-	require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
+func TestServer_Twilio_SMS_Unconfigured(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
+		"x-sms": "+1234",
+	})
+	require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
+}
 
-	response = request(t, s, "GET", "/mytopic/trigger?priority=INVALID", "test", nil)
-	require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
+func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "POST", "/mytopic", "test", map[string]string{
+		"x-call": "+1234",
+	})
+	require.Equal(t, 40030, toHTTPError(t, response.Body.String()).Code)
 }