From 9e4a48b058b6ffde0d2ec9104d16b1a69af3c5df Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Thu, 19 Feb 2026 20:48:01 -0500 Subject: [PATCH] Make server tests also run against postgres --- message/store.go | 12 +- message/store_postgres.go | 6 +- message/store_sqlite.go | 6 +- server/server_account_test.go | 1192 ++++--- server/server_admin_test.go | 830 ++--- server/server_manager_test.go | 32 +- server/server_payments_test.go | 1254 +++---- server/server_test.go | 6035 +++++++++++++++++--------------- server/server_twilio_test.go | 510 +-- server/server_webpush_test.go | 332 +- 10 files changed, 5379 insertions(+), 4830 deletions(-) diff --git a/message/store.go b/message/store.go index c51f732f..05ab31d0 100644 --- a/message/store.go +++ b/message/store.go @@ -26,12 +26,13 @@ type Store interface { AddMessage(m *model.Message) error AddMessages(ms []*model.Message) error DB() *sql.DB + Message(id string) (*model.Message, error) + MessageCounts() (map[string]int, error) Messages(topic string, since model.SinceMarker, scheduled bool) ([]*model.Message, error) MessagesDue() ([]*model.Message, error) MessagesExpired() ([]string, error) - Message(id string) (*model.Message, error) MarkPublished(m *model.Message) error - MessageCounts() (map[string]int, error) + UpdateMessageTime(messageID string, timestamp int64) error Topics() ([]string, error) DeleteMessages(ids ...string) error DeleteScheduledBySequenceID(topic, sequenceID string) ([]string, error) @@ -71,6 +72,7 @@ type storeQueries struct { selectAttachmentsSizeByUserID string selectStats string updateStats string + updateMessageTime string } // commonStore implements store operations that are identical across database backends @@ -302,6 +304,12 @@ func (c *commonStore) Message(id string) (*model.Message, error) { return readMessage(rows) } +// UpdateMessageTime updates the time column for a message by ID. This is only used for testing. +func (c *commonStore) UpdateMessageTime(messageID string, timestamp int64) error { + _, err := c.db.Exec(c.queries.updateMessageTime, timestamp, messageID) + return err +} + func (c *commonStore) MarkPublished(m *model.Message) error { c.mu.Lock() defer c.mu.Unlock() diff --git a/message/store_postgres.go b/message/store_postgres.go index 3efab244..46c26bad 100644 --- a/message/store_postgres.go +++ b/message/store_postgres.go @@ -71,8 +71,9 @@ const ( pgSelectAttachmentsSizeBySenderQuery = `SELECT COALESCE(SUM(attachment_size), 0) FROM message WHERE user_id = '' AND sender = $1 AND attachment_expires >= $2` pgSelectAttachmentsSizeByUserIDQuery = `SELECT COALESCE(SUM(attachment_size), 0) FROM message WHERE user_id = $1 AND attachment_expires >= $2` - pgSelectStatsQuery = `SELECT value FROM message_stats WHERE key = 'messages'` - pgUpdateStatsQuery = `UPDATE message_stats SET value = $1 WHERE key = 'messages'` + pgSelectStatsQuery = `SELECT value FROM message_stats WHERE key = 'messages'` + pgUpdateStatsQuery = `UPDATE message_stats SET value = $1 WHERE key = 'messages'` + pgUpdateMessageTimesQuery = `UPDATE message SET time = $1 WHERE mid = $2` ) var pgQueries = storeQueries{ @@ -100,6 +101,7 @@ var pgQueries = storeQueries{ selectAttachmentsSizeByUserID: pgSelectAttachmentsSizeByUserIDQuery, selectStats: pgSelectStatsQuery, updateStats: pgUpdateStatsQuery, + updateMessageTime: pgUpdateMessageTimesQuery, } // NewPostgresStore creates a new PostgreSQL-backed message cache store. diff --git a/message/store_sqlite.go b/message/store_sqlite.go index 275fa43d..566682b1 100644 --- a/message/store_sqlite.go +++ b/message/store_sqlite.go @@ -74,8 +74,9 @@ const ( sqliteSelectAttachmentsSizeBySenderQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = '' AND sender = ? AND attachment_expires >= ?` sqliteSelectAttachmentsSizeByUserIDQuery = `SELECT IFNULL(SUM(attachment_size), 0) FROM messages WHERE user = ? AND attachment_expires >= ?` - sqliteSelectStatsQuery = `SELECT value FROM stats WHERE key = 'messages'` - sqliteUpdateStatsQuery = `UPDATE stats SET value = ? WHERE key = 'messages'` + sqliteSelectStatsQuery = `SELECT value FROM stats WHERE key = 'messages'` + sqliteUpdateStatsQuery = `UPDATE stats SET value = ? WHERE key = 'messages'` + sqliteUpdateMessageTimeQuery = `UPDATE messages SET time = ? WHERE mid = ?` ) var sqliteQueries = storeQueries{ @@ -103,6 +104,7 @@ var sqliteQueries = storeQueries{ selectAttachmentsSizeByUserID: sqliteSelectAttachmentsSizeByUserIDQuery, selectStats: sqliteSelectStatsQuery, updateStats: sqliteUpdateStatsQuery, + updateMessageTime: sqliteUpdateMessageTimeQuery, } // NewSQLiteStore creates a SQLite file-backed cache diff --git a/server/server_account_test.go b/server/server_account_test.go index 409ccfcf..d573e06e 100644 --- a/server/server_account_test.go +++ b/server/server_account_test.go @@ -15,706 +15,748 @@ import ( ) func TestAccount_Signup_Success(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) + defer s.closeDatabases() - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + token, _ := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) + require.NotEmpty(t, token.Token) + require.True(t, time.Now().Add(71*time.Hour).Unix() < token.Expires) + require.True(t, strings.HasPrefix(token.Token, "tk_")) + require.Equal(t, "9.9.9.9", token.LastOrigin) + require.True(t, token.LastAccess > time.Now().Unix()-2) + require.True(t, token.LastAccess < time.Now().Unix()+2) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BearerAuth(token.Token), + }) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, "phil", account.Username) + require.Equal(t, "user", account.Role) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("", token.Token), // We allow a fake basic auth to make curl-ing easier (curl -u :) + }) + require.Equal(t, 200, rr.Code) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, "phil", account.Username) }) - require.Equal(t, 200, rr.Code) - token, _ := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) - require.NotEmpty(t, token.Token) - require.True(t, time.Now().Add(71*time.Hour).Unix() < token.Expires) - require.True(t, strings.HasPrefix(token.Token, "tk_")) - require.Equal(t, "9.9.9.9", token.LastOrigin) - require.True(t, token.LastAccess > time.Now().Unix()-2) - require.True(t, token.LastAccess < time.Now().Unix()+2) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BearerAuth(token.Token), - }) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, "phil", account.Username) - require.Equal(t, "user", account.Role) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("", token.Token), // We allow a fake basic auth to make curl-ing easier (curl -u :) - }) - require.Equal(t, 200, rr.Code) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, "phil", account.Username) } func TestAccount_Signup_UserExists(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) + defer s.closeDatabases() - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 409, rr.Code) - require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) + rr = request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 409, rr.Code) + require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) + }) } func TestAccount_Signup_LimitReached(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) + defer s.closeDatabases() - for i := 0; i < 3; i++ { - rr := request(t, s, "POST", "/v1/account", fmt.Sprintf(`{"username":"phil%d", "password":"mypass"}`, i), nil) - require.Equal(t, 200, rr.Code) - } - rr := request(t, s, "POST", "/v1/account", `{"username":"thiswontwork", "password":"mypass"}`, nil) - require.Equal(t, 429, rr.Code) - require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code) + for i := 0; i < 3; i++ { + rr := request(t, s, "POST", "/v1/account", fmt.Sprintf(`{"username":"phil%d", "password":"mypass"}`, i), nil) + require.Equal(t, 200, rr.Code) + } + rr := request(t, s, "POST", "/v1/account", `{"username":"thiswontwork", "password":"mypass"}`, nil) + require.Equal(t, 429, rr.Code) + require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code) + }) } func TestAccount_Signup_AsUser(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) + defer s.closeDatabases() - log.Info("1") - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - log.Info("2") - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - log.Info("3") - rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + log.Info("1") + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + log.Info("2") + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + log.Info("3") + rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + log.Info("4") + rr = request(t, s, "POST", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) }) - require.Equal(t, 200, rr.Code) - log.Info("4") - rr = request(t, s, "POST", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) } func TestAccount_Signup_Disabled(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = false - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = false + s := newTestServer(t, conf) + defer s.closeDatabases() - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40022, toHTTPError(t, rr.Body.String()).Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40022, toHTTPError(t, rr.Body.String()).Code) + }) } func TestAccount_Signup_Rate_Limit(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) - for i := 0; i < 3; i++ { - rr := request(t, s, "POST", "/v1/account", fmt.Sprintf(`{"username":"phil%d", "password":"mypass"}`, i), nil) - require.Equal(t, 200, rr.Code, "failed on iteration %d", i) - } - rr := request(t, s, "POST", "/v1/account", `{"username":"notallowed", "password":"mypass"}`, nil) - require.Equal(t, 429, rr.Code) - require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code) + for i := 0; i < 3; i++ { + rr := request(t, s, "POST", "/v1/account", fmt.Sprintf(`{"username":"phil%d", "password":"mypass"}`, i), nil) + require.Equal(t, 200, rr.Code, "failed on iteration %d", i) + } + rr := request(t, s, "POST", "/v1/account", `{"username":"notallowed", "password":"mypass"}`, nil) + require.Equal(t, 429, rr.Code) + require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code) + }) } func TestAccount_Get_Anonymous(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.VisitorRequestLimitReplenish = 86 * time.Second - conf.VisitorEmailLimitReplenish = time.Hour - conf.VisitorAttachmentTotalSizeLimit = 5123 - conf.AttachmentFileSizeLimit = 512 - s := newTestServer(t, conf) - s.smtpSender = &testMailer{} - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.VisitorRequestLimitReplenish = 86 * time.Second + conf.VisitorEmailLimitReplenish = time.Hour + conf.VisitorAttachmentTotalSizeLimit = 5123 + conf.AttachmentFileSizeLimit = 512 + s := newTestServer(t, conf) + s.smtpSender = &testMailer{} + defer s.closeDatabases() - rr := request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, "*", account.Username) - require.Equal(t, string(user.RoleAnonymous), account.Role) - require.Equal(t, "ip", account.Limits.Basis) - require.Equal(t, int64(1004), account.Limits.Messages) // I hate this - require.Equal(t, int64(24), account.Limits.Emails) // I hate this - require.Equal(t, int64(5123), account.Limits.AttachmentTotalSize) - require.Equal(t, int64(512), account.Limits.AttachmentFileSize) - require.Equal(t, int64(0), account.Stats.Messages) - require.Equal(t, int64(1004), account.Stats.MessagesRemaining) - require.Equal(t, int64(0), account.Stats.Emails) - require.Equal(t, int64(24), account.Stats.EmailsRemaining) - require.Equal(t, int64(0), account.Stats.Calls) - require.Equal(t, int64(0), account.Stats.CallsRemaining) + rr := request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, "*", account.Username) + require.Equal(t, string(user.RoleAnonymous), account.Role) + require.Equal(t, "ip", account.Limits.Basis) + require.Equal(t, int64(1004), account.Limits.Messages) // I hate this + require.Equal(t, int64(24), account.Limits.Emails) // I hate this + require.Equal(t, int64(5123), account.Limits.AttachmentTotalSize) + require.Equal(t, int64(512), account.Limits.AttachmentFileSize) + require.Equal(t, int64(0), account.Stats.Messages) + require.Equal(t, int64(1004), account.Stats.MessagesRemaining) + require.Equal(t, int64(0), account.Stats.Emails) + require.Equal(t, int64(24), account.Stats.EmailsRemaining) + require.Equal(t, int64(0), account.Stats.Calls) + require.Equal(t, int64(0), account.Stats.CallsRemaining) - rr = request(t, s, "POST", "/mytopic", "", nil) - require.Equal(t, 200, rr.Code) - rr = request(t, s, "POST", "/mytopic", "", map[string]string{ - "Email": "phil@ntfy.sh", + rr = request(t, s, "POST", "/mytopic", "", nil) + require.Equal(t, 200, rr.Code) + rr = request(t, s, "POST", "/mytopic", "", map[string]string{ + "Email": "phil@ntfy.sh", + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, rr.Code) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, int64(2), account.Stats.Messages) + require.Equal(t, int64(1002), account.Stats.MessagesRemaining) + require.Equal(t, int64(1), account.Stats.Emails) + require.Equal(t, int64(23), account.Stats.EmailsRemaining) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, rr.Code) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, int64(2), account.Stats.Messages) - require.Equal(t, int64(1002), account.Stats.MessagesRemaining) - require.Equal(t, int64(1), account.Stats.Emails) - require.Equal(t, int64(23), account.Stats.EmailsRemaining) } func TestAccount_ChangeSettings(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - u, _ := s.userManager.User("phil") - token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified(), false) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + u, _ := s.userManager.User("phil") + token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified(), false) - rr := request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"sound": "juntos"},"ignored": true}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + rr := request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"sound": "juntos"},"ignored": true}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"delete_after": 86400}, "language": "de"}`, map[string]string{ + "Authorization": util.BearerAuth(token.Value), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ + "Authorization": util.BearerAuth(token.Value), + }) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, "de", account.Language) + require.Equal(t, util.Int(86400), account.Notification.DeleteAfter) + require.Equal(t, util.String("juntos"), account.Notification.Sound) + require.Nil(t, account.Notification.MinPriority) // Not set }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "PATCH", "/v1/account/settings", `{"notification": {"delete_after": 86400}, "language": "de"}`, map[string]string{ - "Authorization": util.BearerAuth(token.Value), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", `{"username":"marian", "password":"marian"}`, map[string]string{ - "Authorization": util.BearerAuth(token.Value), - }) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, "de", account.Language) - require.Equal(t, util.Int(86400), account.Notification.DeleteAfter) - require.Equal(t, util.String("juntos"), account.Notification.Sound) - require.Nil(t, account.Notification.MinPriority) // Not set } func TestAccount_Subscription_AddUpdateDelete(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, 1, len(account.Subscriptions)) + require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) + require.Equal(t, "def", account.Subscriptions[0].Topic) + require.Nil(t, account.Subscriptions[0].DisplayName) + + rr = request(t, s, "PATCH", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def", "display_name": "ding dong"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, 1, len(account.Subscriptions)) + require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) + require.Equal(t, "def", account.Subscriptions[0].Topic) + require.Equal(t, util.String("ding dong"), account.Subscriptions[0].DisplayName) + + rr = request(t, s, "DELETE", "/v1/account/subscription", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + "X-BaseURL": "http://abc.com", + "X-Topic": "def", + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, 0, len(account.Subscriptions)) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, 1, len(account.Subscriptions)) - require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) - require.Equal(t, "def", account.Subscriptions[0].Topic) - require.Nil(t, account.Subscriptions[0].DisplayName) - - rr = request(t, s, "PATCH", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def", "display_name": "ding dong"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, 1, len(account.Subscriptions)) - require.Equal(t, "http://abc.com", account.Subscriptions[0].BaseURL) - require.Equal(t, "def", account.Subscriptions[0].Topic) - require.Equal(t, util.String("ding dong"), account.Subscriptions[0].DisplayName) - - rr = request(t, s, "DELETE", "/v1/account/subscription", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - "X-BaseURL": "http://abc.com", - "X-Topic": "def", - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, 0, len(account.Subscriptions)) } func TestAccount_ChangePassword(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.AuthUsers = []*user.User{ - {Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass - } - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.AuthUsers = []*user.User{ + {Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass + } + s := newTestServer(t, conf) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) + + rr = request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": "new password"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code) + + rr = request(t, s, "POST", "/v1/account/password", `{"password": "phil", "new_password": "new password"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 401, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "new password"), + }) + require.Equal(t, 200, rr.Code) + + // Cannot change password of provisioned user + rr = request(t, s, "POST", "/v1/account/password", `{"password": "philpass", "new_password": "new password"}`, map[string]string{ + "Authorization": util.BasicAuth("philuser", "philpass"), + }) + require.Equal(t, 409, rr.Code) }) - require.Equal(t, 400, rr.Code) - - rr = request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": "new password"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code) - - rr = request(t, s, "POST", "/v1/account/password", `{"password": "phil", "new_password": "new password"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 401, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "new password"), - }) - require.Equal(t, 200, rr.Code) - - // Cannot change password of provisioned user - rr = request(t, s, "POST", "/v1/account/password", `{"password": "philpass", "new_password": "new password"}`, map[string]string{ - "Authorization": util.BasicAuth("philuser", "philpass"), - }) - require.Equal(t, 409, rr.Code) } func TestAccount_ChangePassword_NoAccount(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - rr := request(t, s, "POST", "/v1/account/password", `{"password": "new password"}`, nil) - require.Equal(t, 401, rr.Code) + rr := request(t, s, "POST", "/v1/account/password", `{"password": "new password"}`, nil) + require.Equal(t, 401, rr.Code) + }) } func TestAccount_ExtendToken(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + token, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + + time.Sleep(time.Second) + + rr = request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ + "Authorization": util.BearerAuth(token.Token), + }) + require.Equal(t, 200, rr.Code) + extendedToken, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.Equal(t, token.Token, extendedToken.Token) + require.True(t, token.Expires < extendedToken.Expires) + + expires := time.Now().Add(999 * time.Hour) + body := fmt.Sprintf(`{"token":"%s", "label":"some label", "expires": %d}`, token.Token, expires.Unix()) + rr = request(t, s, "PATCH", "/v1/account/token", body, map[string]string{ + "Authorization": util.BearerAuth(token.Token), + }) + require.Equal(t, 200, rr.Code) + token, err = util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.Equal(t, "some label", token.Label) + require.Equal(t, expires.Unix(), token.Expires) }) - require.Equal(t, 200, rr.Code) - token, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - - time.Sleep(time.Second) - - rr = request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ - "Authorization": util.BearerAuth(token.Token), - }) - require.Equal(t, 200, rr.Code) - extendedToken, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.Equal(t, token.Token, extendedToken.Token) - require.True(t, token.Expires < extendedToken.Expires) - - expires := time.Now().Add(999 * time.Hour) - body := fmt.Sprintf(`{"token":"%s", "label":"some label", "expires": %d}`, token.Token, expires.Unix()) - rr = request(t, s, "PATCH", "/v1/account/token", body, map[string]string{ - "Authorization": util.BearerAuth(token.Token), - }) - require.Equal(t, 200, rr.Code) - token, err = util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.Equal(t, "some label", token.Label) - require.Equal(t, expires.Unix(), token.Expires) } func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! + rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! + }) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40023, toHTTPError(t, rr.Body.String()).Code) }) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40023, toHTTPError(t, rr.Body.String()).Code) } func TestAccount_DeleteToken(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + token, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.True(t, token.Expires > time.Now().Add(71*time.Hour).Unix()) + + // Delete token failure (using basic auth) + rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! + }) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40023, toHTTPError(t, rr.Body.String()).Code) + + // Delete token with wrong token + rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ + "Authorization": util.BearerAuth("invalidtoken"), + }) + require.Equal(t, 401, rr.Code) + + // Delete token with correct token + rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ + "Authorization": util.BearerAuth(token.Token), + }) + require.Equal(t, 200, rr.Code) + + // Cannot get account anymore + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BearerAuth(token.Token), + }) + require.Equal(t, 401, rr.Code) }) - require.Equal(t, 200, rr.Code) - token, err := util.UnmarshalJSON[apiAccountTokenResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.True(t, token.Expires > time.Now().Add(71*time.Hour).Unix()) - - // Delete token failure (using basic auth) - rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), // Not Bearer! - }) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40023, toHTTPError(t, rr.Body.String()).Code) - - // Delete token with wrong token - rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ - "Authorization": util.BearerAuth("invalidtoken"), - }) - require.Equal(t, 401, rr.Code) - - // Delete token with correct token - rr = request(t, s, "DELETE", "/v1/account/token", "", map[string]string{ - "Authorization": util.BearerAuth(token.Token), - }) - require.Equal(t, 200, rr.Code) - - // Cannot get account anymore - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BearerAuth(token.Token), - }) - require.Equal(t, 401, rr.Code) } func TestAccount_Delete_Success(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "DELETE", "/v1/account", `{"password":"mypass"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Account was marked deleted + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 401, rr.Code) + + // Cannot re-create account, since still exists + rr = request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 409, rr.Code) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "DELETE", "/v1/account", `{"password":"mypass"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - // Account was marked deleted - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 401, rr.Code) - - // Cannot re-create account, since still exists - rr = request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 409, rr.Code) } func TestAccount_Delete_Not_Allowed(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "DELETE", "/v1/account", "", nil) - require.Equal(t, 401, rr.Code) + rr = request(t, s, "DELETE", "/v1/account", "", nil) + require.Equal(t, 401, rr.Code) - rr = request(t, s, "DELETE", "/v1/account", `{"password":"mypass"}`, nil) - require.Equal(t, 401, rr.Code) + rr = request(t, s, "DELETE", "/v1/account", `{"password":"mypass"}`, nil) + require.Equal(t, 401, rr.Code) - rr = request(t, s, "DELETE", "/v1/account", `{"password":"INCORRECT"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + rr = request(t, s, "DELETE", "/v1/account", `{"password":"INCORRECT"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code) }) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40026, toHTTPError(t, rr.Body.String()).Code) } func TestAccount_Reservation_AddWithoutTierFails(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 401, rr.Code) }) - require.Equal(t, 401, rr.Code) } func TestAccount_Reservation_AddAdminSuccess(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + s := newTestServer(t, conf) - // A user, an admin, and a reservation walk into a bar - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - ReservationLimit: 2, - })) - require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro")) - require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll)) + // A user, an admin, and a reservation walk into a bar + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + ReservationLimit: 2, + })) + require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro")) + require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll)) - require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro")) + require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro")) - require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false)) - // Admin can reserve topic - rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "adminpass"), + // Admin can reserve topic + rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "adminpass"), + }) + require.Equal(t, 200, rr.Code) + + // User cannot reserve already reserved topic + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic","everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("noadmin2", "pass"), + }) + require.Equal(t, 409, rr.Code) + + // Admin cannot reserve already reserved topic + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic","everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "adminpass"), + }) + require.Equal(t, 409, rr.Code) + + reservations, err := s.userManager.Reservations("phil") + require.Nil(t, err) + require.Equal(t, 1, len(reservations)) + require.Equal(t, "sometopic", reservations[0].Topic) + + reservations, err = s.userManager.Reservations("noadmin1") + require.Nil(t, err) + require.Equal(t, 1, len(reservations)) + require.Equal(t, "mytopic", reservations[0].Topic) + + reservations, err = s.userManager.Reservations("noadmin2") + require.Nil(t, err) + require.Equal(t, 0, len(reservations)) }) - require.Equal(t, 200, rr.Code) - - // User cannot reserve already reserved topic - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic","everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("noadmin2", "pass"), - }) - require.Equal(t, 409, rr.Code) - - // Admin cannot reserve already reserved topic - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic":"mytopic","everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "adminpass"), - }) - require.Equal(t, 409, rr.Code) - - reservations, err := s.userManager.Reservations("phil") - require.Nil(t, err) - require.Equal(t, 1, len(reservations)) - require.Equal(t, "sometopic", reservations[0].Topic) - - reservations, err = s.userManager.Reservations("noadmin1") - require.Nil(t, err) - require.Equal(t, 1, len(reservations)) - require.Equal(t, "mytopic", reservations[0].Topic) - - reservations, err = s.userManager.Reservations("noadmin2") - require.Nil(t, err) - require.Equal(t, 0, len(reservations)) } func TestAccount_Reservation_AddRemoveUserWithTierSuccess(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.EnableSignup = true - conf.EnableReservations = true - conf.TwilioAccount = "dummy" - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.EnableSignup = true + conf.EnableReservations = true + conf.TwilioAccount = "dummy" + s := newTestServer(t, conf) - // Create user - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + // Create user + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - // Create a tier - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 123, - MessageExpiryDuration: 86400 * time.Second, - EmailLimit: 32, - CallLimit: 10, - ReservationLimit: 2, - AttachmentFileSizeLimit: 1231231, - AttachmentTotalSizeLimit: 123123, - AttachmentExpiryDuration: 10800 * time.Second, - AttachmentBandwidthLimit: 21474836480, - })) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + // Create a tier + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 123, + MessageExpiryDuration: 86400 * time.Second, + EmailLimit: 32, + CallLimit: 10, + ReservationLimit: 2, + AttachmentFileSizeLimit: 1231231, + AttachmentTotalSizeLimit: 123123, + AttachmentExpiryDuration: 10800 * time.Second, + AttachmentBandwidthLimit: 21474836480, + })) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - // Reserve two topics - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + // Reserve two topics + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "another", "everyone":"read-only"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Trying to reserve a third should fail + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "yet-another", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 429, rr.Code) + + // Modify existing should still work + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "another", "everyone":"write-only"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Check account result + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, "pro", account.Tier.Code) + require.Equal(t, int64(123), account.Limits.Messages) + require.Equal(t, int64(86400), account.Limits.MessagesExpiryDuration) + require.Equal(t, int64(32), account.Limits.Emails) + require.Equal(t, int64(10), account.Limits.Calls) + require.Equal(t, int64(2), account.Limits.Reservations) + require.Equal(t, int64(1231231), account.Limits.AttachmentFileSize) + require.Equal(t, int64(123123), account.Limits.AttachmentTotalSize) + require.Equal(t, int64(10800), account.Limits.AttachmentExpiryDuration) + require.Equal(t, int64(21474836480), account.Limits.AttachmentBandwidth) + require.Equal(t, 2, len(account.Reservations)) + require.Equal(t, "another", account.Reservations[0].Topic) + require.Equal(t, "write-only", account.Reservations[0].Everyone) + require.Equal(t, "mytopic", account.Reservations[1].Topic) + require.Equal(t, "deny-all", account.Reservations[1].Everyone) + + // Delete and re-check + rr = request(t, s, "DELETE", "/v1/account/reservation/another", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, 1, len(account.Reservations)) + require.Equal(t, "mytopic", account.Reservations[0].Topic) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "another", "everyone":"read-only"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - // Trying to reserve a third should fail - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "yet-another", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 429, rr.Code) - - // Modify existing should still work - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "another", "everyone":"write-only"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - // Check account result - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, "pro", account.Tier.Code) - require.Equal(t, int64(123), account.Limits.Messages) - require.Equal(t, int64(86400), account.Limits.MessagesExpiryDuration) - require.Equal(t, int64(32), account.Limits.Emails) - require.Equal(t, int64(10), account.Limits.Calls) - require.Equal(t, int64(2), account.Limits.Reservations) - require.Equal(t, int64(1231231), account.Limits.AttachmentFileSize) - require.Equal(t, int64(123123), account.Limits.AttachmentTotalSize) - require.Equal(t, int64(10800), account.Limits.AttachmentExpiryDuration) - require.Equal(t, int64(21474836480), account.Limits.AttachmentBandwidth) - require.Equal(t, 2, len(account.Reservations)) - require.Equal(t, "another", account.Reservations[0].Topic) - require.Equal(t, "write-only", account.Reservations[0].Everyone) - require.Equal(t, "mytopic", account.Reservations[1].Topic) - require.Equal(t, "deny-all", account.Reservations[1].Everyone) - - // Delete and re-check - rr = request(t, s, "DELETE", "/v1/account/reservation/another", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, 1, len(account.Reservations)) - require.Equal(t, "mytopic", account.Reservations[0].Topic) } func TestAccount_Reservation_PublishByAnonymousFails(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - conf.AuthDefault = user.PermissionReadWrite - conf.EnableSignup = true - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + conf.AuthDefault = user.PermissionReadWrite + conf.EnableSignup = true + s := newTestServer(t, conf) - // Create user with tier - rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) - require.Equal(t, 200, rr.Code) + // Create user with tier + rr := request(t, s, "POST", "/v1/account", `{"username":"phil", "password":"mypass"}`, nil) + require.Equal(t, 200, rr.Code) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 20, - ReservationLimit: 2, - })) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 20, + ReservationLimit: 2, + })) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - // Reserve a topic - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), + // Reserve a topic + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Publish a message + rr = request(t, s, "POST", "/mytopic", `Howdy`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Publish a message (as anonymous) + rr = request(t, s, "POST", "/mytopic", `Howdy`, nil) + require.Equal(t, 403, rr.Code) }) - require.Equal(t, 200, rr.Code) - - // Publish a message - rr = request(t, s, "POST", "/mytopic", `Howdy`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - // Publish a message (as anonymous) - rr = request(t, s, "POST", "/mytopic", `Howdy`, nil) - require.Equal(t, 403, rr.Code) } func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) { - t.Parallel() - conf := newTestConfigWithAuthFile(t) - conf.AuthDefault = user.PermissionReadWrite - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + conf := newTestConfigWithAuthFile(t) + conf.AuthDefault = user.PermissionReadWrite + s := newTestServer(t, conf) - // Create user with tier - require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false)) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 20, - MessageExpiryDuration: time.Hour, - ReservationLimit: 2, - AttachmentTotalSizeLimit: 10000, - AttachmentFileSizeLimit: 10000, - AttachmentExpiryDuration: time.Hour, - AttachmentBandwidthLimit: 10000, - })) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + // Create user with tier + require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 20, + MessageExpiryDuration: time.Hour, + ReservationLimit: 2, + AttachmentTotalSizeLimit: 10000, + AttachmentFileSizeLimit: 10000, + AttachmentExpiryDuration: time.Hour, + AttachmentBandwidthLimit: 10000, + })) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - // Reserve two topics "mytopic1" and "mytopic2" - rr := request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic1", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) + // Reserve two topics "mytopic1" and "mytopic2" + rr := request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic1", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic2", "everyone":"deny-all"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) + rr = request(t, s, "POST", "/v1/account/reservation", `{"topic": "mytopic2", "everyone":"deny-all"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) - // Publish a message with attachment to each topic - rr = request(t, s, "POST", "/mytopic1?f=attach.txt", `Howdy`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - m1 := toMessage(t, rr.Body.String()) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) + // Publish a message with attachment to each topic + rr = request(t, s, "POST", "/mytopic1?f=attach.txt", `Howdy`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + m1 := toMessage(t, rr.Body.String()) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) - rr = request(t, s, "POST", "/mytopic2?f=attach.txt", `Howdy`, map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - m2 := toMessage(t, rr.Body.String()) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) + rr = request(t, s, "POST", "/mytopic2?f=attach.txt", `Howdy`, map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + m2 := toMessage(t, rr.Body.String()) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) - // Pre-verify message count and file - ms, err := s.messageCache.Messages("mytopic1", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 1, len(ms)) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) - - ms, err = s.messageCache.Messages("mytopic2", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 1, len(ms)) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) - - // Delete reservation - rr = request(t, s, "DELETE", "/v1/account/reservation/mytopic1", ``, map[string]string{ - "X-Delete-Messages": "true", - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "DELETE", "/v1/account/reservation/mytopic2", ``, map[string]string{ - "X-Delete-Messages": "false", - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 200, rr.Code) - - // Verify that messages and attachments were deleted - // This does not explicitly call the manager! - waitFor(t, func() bool { + // Pre-verify message count and file ms, err := s.messageCache.Messages("mytopic1", sinceAllMessages, false) require.Nil(t, err) - return len(ms) == 0 && !util.FileExists(filepath.Join(s.config.AttachmentCacheDir, m1.ID)) + require.Equal(t, 1, len(ms)) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) + + ms, err = s.messageCache.Messages("mytopic2", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 1, len(ms)) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) + + // Delete reservation + rr = request(t, s, "DELETE", "/v1/account/reservation/mytopic1", ``, map[string]string{ + "X-Delete-Messages": "true", + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "DELETE", "/v1/account/reservation/mytopic2", ``, map[string]string{ + "X-Delete-Messages": "false", + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 200, rr.Code) + + // Verify that messages and attachments were deleted + // This does not explicitly call the manager! + waitFor(t, func() bool { + ms, err := s.messageCache.Messages("mytopic1", sinceAllMessages, false) + require.Nil(t, err) + return len(ms) == 0 && !util.FileExists(filepath.Join(s.config.AttachmentCacheDir, m1.ID)) + }) + + ms, err = s.messageCache.Messages("mytopic1", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 0, len(ms)) + require.NoFileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) + + ms, err = s.messageCache.Messages("mytopic2", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 1, len(ms)) + require.Equal(t, m2.ID, ms[0].ID) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) }) - - ms, err = s.messageCache.Messages("mytopic1", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 0, len(ms)) - require.NoFileExists(t, filepath.Join(s.config.AttachmentCacheDir, m1.ID)) - - ms, err = s.messageCache.Messages("mytopic2", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 1, len(ms)) - require.Equal(t, m2.ID, ms[0].ID) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, m2.ID)) } /*func TestAccount_Persist_UserStats_After_Tier_Change(t *testing.T) { diff --git a/server/server_admin_test.go b/server/server_admin_test.go index cedaed87..131cd145 100644 --- a/server/server_admin_test.go +++ b/server/server_admin_test.go @@ -11,427 +11,451 @@ import ( ) func TestVersion_Admin(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.BuildVersion = "1.2.3" - c.BuildCommit = "abcdef0" - c.BuildDate = "2026-02-08T00:00:00Z" - s := newTestServer(t, c) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.BuildVersion = "1.2.3" + c.BuildCommit = "abcdef0" + c.BuildDate = "2026-02-08T00:00:00Z" + s := newTestServer(t, c) + defer s.closeDatabases() - // Create admin and regular user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + // Create admin and regular user + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - // Admin can access /v1/version - rr := request(t, s, "GET", "/v1/version", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Admin can access /v1/version + rr := request(t, s, "GET", "/v1/version", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + var versionResponse apiVersionResponse + require.Nil(t, json.NewDecoder(rr.Body).Decode(&versionResponse)) + require.Equal(t, "1.2.3", versionResponse.Version) + require.Equal(t, "abcdef0", versionResponse.Commit) + require.Equal(t, "2026-02-08T00:00:00Z", versionResponse.Date) + + // Non-admin user cannot access /v1/version + rr = request(t, s, "GET", "/v1/version", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Unauthenticated user cannot access /v1/version + rr = request(t, s, "GET", "/v1/version", "", nil) + require.Equal(t, 401, rr.Code) }) - require.Equal(t, 200, rr.Code) - - var versionResponse apiVersionResponse - require.Nil(t, json.NewDecoder(rr.Body).Decode(&versionResponse)) - require.Equal(t, "1.2.3", versionResponse.Version) - require.Equal(t, "abcdef0", versionResponse.Commit) - require.Equal(t, "2026-02-08T00:00:00Z", versionResponse.Date) - - // Non-admin user cannot access /v1/version - rr = request(t, s, "GET", "/v1/version", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) - - // Unauthenticated user cannot access /v1/version - rr = request(t, s, "GET", "/v1/version", "", nil) - require.Equal(t, 401, rr.Code) } func TestUser_AddRemove(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "tier1", - })) + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) - // Create user via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Create user with tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 4, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Nil(t, users[1].Tier) + require.Equal(t, "emma", users[2].Name) + require.Equal(t, user.RoleUser, users[2].Role) + require.Equal(t, "tier1", users[2].Tier.Code) + require.Equal(t, user.Everyone, users[3].Name) + + // Delete user via API + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check user was deleted + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "emma", users[1].Name) + require.Equal(t, user.Everyone, users[2].Name) + + // Reject invalid user change + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) }) - require.Equal(t, 200, rr.Code) - - // Create user with tier via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "tier1"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Check users - users, err := s.userManager.Users() - require.Nil(t, err) - require.Equal(t, 4, len(users)) - require.Equal(t, "phil", users[0].Name) - require.Equal(t, "ben", users[1].Name) - require.Equal(t, user.RoleUser, users[1].Role) - require.Nil(t, users[1].Tier) - require.Equal(t, "emma", users[2].Name) - require.Equal(t, user.RoleUser, users[2].Role) - require.Equal(t, "tier1", users[2].Tier.Code) - require.Equal(t, user.Everyone, users[3].Name) - - // Delete user via API - rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Check user was deleted - users, err = s.userManager.Users() - require.Nil(t, err) - require.Equal(t, 3, len(users)) - require.Equal(t, "phil", users[0].Name) - require.Equal(t, "emma", users[1].Name) - require.Equal(t, user.Everyone, users[2].Name) - - // Reject invalid user change - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 400, rr.Code) } func TestUser_AddWithPasswordHash(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() - // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - // Create user via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) - // Check that user can login with password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 200, rr.Code) - - // Check users - users, err := s.userManager.Users() - require.Nil(t, err) - require.Equal(t, 3, len(users)) - require.Equal(t, "phil", users[0].Name) - require.Equal(t, user.RoleAdmin, users[0].Role) - require.Equal(t, "ben", users[1].Name) - require.Equal(t, user.RoleUser, users[1].Role) -} - -func TestUser_ChangeUserPassword(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - - // Create user via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Try to login with first password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 200, rr.Code) - - // Change password via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Make sure first password fails - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) - - // Try to login with second password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben-two"), - }) - require.Equal(t, 200, rr.Code) -} - -func TestUser_ChangeUserTier(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "tier1", - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "tier2", - })) - - // Create user with tier via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Check users - users, err := s.userManager.Users() - require.Nil(t, err) - require.Equal(t, 3, len(users)) - require.Equal(t, "phil", users[0].Name) - require.Equal(t, "ben", users[1].Name) - require.Equal(t, user.RoleUser, users[1].Role) - require.Equal(t, "tier1", users[1].Tier.Code) - - // Change user tier via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Check users again - users, err = s.userManager.Users() - require.Nil(t, err) - require.Equal(t, "tier2", users[1].Tier.Code) -} - -func TestUser_ChangeUserPasswordAndTier(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin, tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "tier1", - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "tier2", - })) - - // Create user with tier via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Check users - users, err := s.userManager.Users() - require.Nil(t, err) - require.Equal(t, 3, len(users)) - require.Equal(t, "phil", users[0].Name) - require.Equal(t, "ben", users[1].Name) - require.Equal(t, user.RoleUser, users[1].Role) - require.Equal(t, "tier1", users[1].Tier.Code) - - // Change user password and tier via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Make sure first password fails - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) - - // Try to login with second password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben-two"), - }) - require.Equal(t, 200, rr.Code) - - // Check new tier - users, err = s.userManager.Users() - require.Nil(t, err) - require.Equal(t, "tier2", users[1].Tier.Code) -} - -func TestUser_ChangeUserPasswordWithHash(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - - // Create user with tier via API - rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Try to login with first password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "not-ben"), - }) - require.Equal(t, 200, rr.Code) - - // Change user password and tier via API - rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Try to login with second password - rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 200, rr.Code) -} - -func TestUser_DontChangeAdminPassword(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false)) - - // Try to change password via API - rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 403, rr.Code) -} - -func TestUser_AddRemove_Failures(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) - defer s.closeDatabases() - - // Create admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - - // Cannot create user with invalid username - rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 400, rr.Code) - - // Cannot create user if user already exists - rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) - - // Cannot create user with invalid tier - rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code) - - // Cannot delete user as non-admin - rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) - - // Delete user via API - rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) -} - -func TestAccess_AllowReset(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) - defer s.closeDatabases() - - // User and admin - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - - // Subscribing not allowed - rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 403, rr.Code) - - // Grant access - rr = request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Now subscribing is allowed - rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 200, rr.Code) - - // Reset access - rr = request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gold"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Subscribing not allowed (again) - rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 403, rr.Code) -} - -func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) - defer s.closeDatabases() - - // User - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - - // Grant access fails, because non-admin - rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 401, rr.Code) -} - -func TestAccess_AllowReset_KillConnection(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) - defer s.closeDatabases() - - // User and admin, grant access to "gol*" topics - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard! - - start, timeTaken := time.Now(), atomic.Int64{} - go func() { - rr := request(t, s, "GET", "/gold/json", "", map[string]string{ + // Check that user can login with password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ "Authorization": util.BasicAuth("ben", "ben"), }) require.Equal(t, 200, rr.Code) - timeTaken.Store(time.Since(start).Milliseconds()) - }() - time.Sleep(500 * time.Millisecond) - // Reset access - rr := request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gol*"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - // Wait for connection to be killed; this will fail if the connection is never killed - waitFor(t, func() bool { - return timeTaken.Load() >= 500 + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, user.RoleAdmin, users[0].Role) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + }) +} + +func TestUser_ChangeUserPassword(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change password via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) + }) +} + +func TestUser_ChangeUserTier(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users again + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) + }) +} + +func TestUser_ChangeUserPasswordAndTier(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin, tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier1", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "tier2", + })) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Check users + users, err := s.userManager.Users() + require.Nil(t, err) + require.Equal(t, 3, len(users)) + require.Equal(t, "phil", users[0].Name) + require.Equal(t, "ben", users[1].Name) + require.Equal(t, user.RoleUser, users[1].Role) + require.Equal(t, "tier1", users[1].Tier.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Make sure first password fails + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben-two"), + }) + require.Equal(t, 200, rr.Code) + + // Check new tier + users, err = s.userManager.Users() + require.Nil(t, err) + require.Equal(t, "tier2", users[1].Tier.Code) + }) +} + +func TestUser_ChangeUserPasswordWithHash(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + + // Create user with tier via API + rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with first password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "not-ben"), + }) + require.Equal(t, 200, rr.Code) + + // Change user password and tier via API + rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Try to login with second password + rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + }) +} + +func TestUser_DontChangeAdminPassword(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false)) + + // Try to change password via API + rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 403, rr.Code) + }) +} + +func TestUser_AddRemove_Failures(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) + defer s.closeDatabases() + + // Create admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + + // Cannot create user with invalid username + rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 400, rr.Code) + + // Cannot create user if user already exists + rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code) + + // Cannot create user with invalid tier + rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code) + + // Cannot delete user as non-admin + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + + // Delete user via API + rr = request(t, s, "DELETE", "/v1/users", `{"username": "ben"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + }) +} + +func TestAccess_AllowReset(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User and admin + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + + // Subscribing not allowed + rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, rr.Code) + + // Grant access + rr = request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Now subscribing is allowed + rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + + // Reset access + rr = request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gold"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Subscribing not allowed (again) + rr = request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, rr.Code) + }) +} + +func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + + // Grant access fails, because non-admin + rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 401, rr.Code) + }) +} + +func TestAccess_AllowReset_KillConnection(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) + defer s.closeDatabases() + + // User and admin, grant access to "gol*" topics + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard! + + start, timeTaken := time.Now(), atomic.Int64{} + go func() { + rr := request(t, s, "GET", "/gold/json", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, rr.Code) + timeTaken.Store(time.Since(start).Milliseconds()) + }() + time.Sleep(500 * time.Millisecond) + + // Reset access + rr := request(t, s, "DELETE", "/v1/users/access", `{"username": "ben", "topic":"gol*"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // Wait for connection to be killed; this will fail if the connection is never killed + waitFor(t, func() bool { + return timeTaken.Load() >= 500 + }) }) } diff --git a/server/server_manager_test.go b/server/server_manager_test.go index f17d583f..1760737b 100644 --- a/server/server_manager_test.go +++ b/server/server_manager_test.go @@ -6,23 +6,25 @@ import ( ) func TestServer_Manager_Prune_Messages_Without_Attachments_DoesNotPanic(t *testing.T) { - // Tests that the manager runs without attachment-cache-dir set, see #617 - c := newTestConfig(t) - c.AttachmentCacheDir = "" - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + // Tests that the manager runs without attachment-cache-dir set, see #617 + c := newTestConfig(t) + c.AttachmentCacheDir = "" + s := newTestServer(t, c) - // Publish a message - rr := request(t, s, "POST", "/mytopic", "hi", nil) - require.Equal(t, 200, rr.Code) - m := toMessage(t, rr.Body.String()) + // Publish a message + rr := request(t, s, "POST", "/mytopic", "hi", nil) + require.Equal(t, 200, rr.Code) + m := toMessage(t, rr.Body.String()) - // Expire message - require.Nil(t, s.messageCache.ExpireMessages("mytopic")) + // Expire message + require.Nil(t, s.messageCache.ExpireMessages("mytopic")) - // Does not panic - s.pruneMessages() + // Does not panic + s.pruneMessages() - // Actually deleted - _, err := s.messageCache.Message(m.ID) - require.Equal(t, errMessageNotFound, err) + // Actually deleted + _, err := s.messageCache.Message(m.ID) + require.Equal(t, errMessageNotFound, err) + }) } diff --git a/server/server_payments_test.go b/server/server_payments_test.go index 5dd75921..10c2a9b7 100644 --- a/server/server_payments_test.go +++ b/server/server_payments_test.go @@ -21,725 +21,745 @@ import ( ) func TestPayments_Tiers(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - c.VisitorRequestLimitReplenish = 12 * time.Hour - c.CacheDuration = 13 * time.Hour - c.AttachmentFileSizeLimit = 111 - c.VisitorAttachmentTotalSizeLimit = 222 - c.AttachmentExpiryDuration = 123 * time.Second - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + c.VisitorRequestLimitReplenish = 12 * time.Hour + c.CacheDuration = 13 * time.Hour + c.AttachmentFileSizeLimit = 111 + c.VisitorAttachmentTotalSizeLimit = 222 + c.AttachmentExpiryDuration = 123 * time.Second + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("ListPrices", mock.Anything). - Return([]*stripe.Price{ - {ID: "price_123", UnitAmount: 500}, - {ID: "price_124", UnitAmount: 5000}, - {ID: "price_456", UnitAmount: 1000}, - {ID: "price_457", UnitAmount: 10000}, - {ID: "price_999", UnitAmount: 9999}, - }, nil) + // Define how the mock should react + stripeMock. + On("ListPrices", mock.Anything). + Return([]*stripe.Price{ + {ID: "price_123", UnitAmount: 500}, + {ID: "price_124", UnitAmount: 5000}, + {ID: "price_456", UnitAmount: 1000}, + {ID: "price_457", UnitAmount: 10000}, + {ID: "price_999", UnitAmount: 9999}, + }, nil) - // Create tiers - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_1", - Code: "admin", - Name: "Admin", - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "pro", - Name: "Pro", - MessageLimit: 1000, - MessageExpiryDuration: time.Hour, - EmailLimit: 123, - ReservationLimit: 777, - AttachmentFileSizeLimit: 999, - AttachmentTotalSizeLimit: 888, - AttachmentExpiryDuration: time.Minute, - StripeMonthlyPriceID: "price_123", - StripeYearlyPriceID: "price_124", - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_444", - Code: "business", - Name: "Business", - MessageLimit: 2000, - MessageExpiryDuration: 10 * time.Hour, - EmailLimit: 123123, - ReservationLimit: 777333, - AttachmentFileSizeLimit: 999111, - AttachmentTotalSizeLimit: 888111, - AttachmentExpiryDuration: time.Hour, - StripeMonthlyPriceID: "price_456", - StripeYearlyPriceID: "price_457", - })) - response := request(t, s, "GET", "/v1/tiers", "", nil) - require.Equal(t, 200, response.Code) - var tiers []apiAccountBillingTier - require.Nil(t, json.NewDecoder(response.Body).Decode(&tiers)) - require.Equal(t, 3, len(tiers)) + // Create tiers + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_1", + Code: "admin", + Name: "Admin", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "pro", + Name: "Pro", + MessageLimit: 1000, + MessageExpiryDuration: time.Hour, + EmailLimit: 123, + ReservationLimit: 777, + AttachmentFileSizeLimit: 999, + AttachmentTotalSizeLimit: 888, + AttachmentExpiryDuration: time.Minute, + StripeMonthlyPriceID: "price_123", + StripeYearlyPriceID: "price_124", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_444", + Code: "business", + Name: "Business", + MessageLimit: 2000, + MessageExpiryDuration: 10 * time.Hour, + EmailLimit: 123123, + ReservationLimit: 777333, + AttachmentFileSizeLimit: 999111, + AttachmentTotalSizeLimit: 888111, + AttachmentExpiryDuration: time.Hour, + StripeMonthlyPriceID: "price_456", + StripeYearlyPriceID: "price_457", + })) + response := request(t, s, "GET", "/v1/tiers", "", nil) + require.Equal(t, 200, response.Code) + var tiers []apiAccountBillingTier + require.Nil(t, json.NewDecoder(response.Body).Decode(&tiers)) + require.Equal(t, 3, len(tiers)) - // Free tier - tier := tiers[0] - require.Equal(t, "", tier.Code) - require.Equal(t, "", tier.Name) - require.Equal(t, "ip", tier.Limits.Basis) - require.Equal(t, int64(0), tier.Limits.Reservations) - require.Equal(t, int64(2), tier.Limits.Messages) // :-( - require.Equal(t, int64(13*3600), tier.Limits.MessagesExpiryDuration) - require.Equal(t, int64(24), tier.Limits.Emails) - require.Equal(t, int64(111), tier.Limits.AttachmentFileSize) - require.Equal(t, int64(222), tier.Limits.AttachmentTotalSize) - require.Equal(t, int64(123), tier.Limits.AttachmentExpiryDuration) + // Free tier + tier := tiers[0] + require.Equal(t, "", tier.Code) + require.Equal(t, "", tier.Name) + require.Equal(t, "ip", tier.Limits.Basis) + require.Equal(t, int64(0), tier.Limits.Reservations) + require.Equal(t, int64(2), tier.Limits.Messages) // :-( + require.Equal(t, int64(13*3600), tier.Limits.MessagesExpiryDuration) + require.Equal(t, int64(24), tier.Limits.Emails) + require.Equal(t, int64(111), tier.Limits.AttachmentFileSize) + require.Equal(t, int64(222), tier.Limits.AttachmentTotalSize) + require.Equal(t, int64(123), tier.Limits.AttachmentExpiryDuration) - // Admin tier is not included, because it is not paid! + // Admin tier is not included, because it is not paid! - tier = tiers[1] - require.Equal(t, "pro", tier.Code) - require.Equal(t, "Pro", tier.Name) - require.Equal(t, "tier", tier.Limits.Basis) - require.Equal(t, int64(500), tier.Prices.Month) - require.Equal(t, int64(5000), tier.Prices.Year) - require.Equal(t, int64(777), tier.Limits.Reservations) - require.Equal(t, int64(1000), tier.Limits.Messages) - require.Equal(t, int64(3600), tier.Limits.MessagesExpiryDuration) - require.Equal(t, int64(123), tier.Limits.Emails) - require.Equal(t, int64(999), tier.Limits.AttachmentFileSize) - require.Equal(t, int64(888), tier.Limits.AttachmentTotalSize) - require.Equal(t, int64(60), tier.Limits.AttachmentExpiryDuration) + tier = tiers[1] + require.Equal(t, "pro", tier.Code) + require.Equal(t, "Pro", tier.Name) + require.Equal(t, "tier", tier.Limits.Basis) + require.Equal(t, int64(500), tier.Prices.Month) + require.Equal(t, int64(5000), tier.Prices.Year) + require.Equal(t, int64(777), tier.Limits.Reservations) + require.Equal(t, int64(1000), tier.Limits.Messages) + require.Equal(t, int64(3600), tier.Limits.MessagesExpiryDuration) + require.Equal(t, int64(123), tier.Limits.Emails) + require.Equal(t, int64(999), tier.Limits.AttachmentFileSize) + require.Equal(t, int64(888), tier.Limits.AttachmentTotalSize) + require.Equal(t, int64(60), tier.Limits.AttachmentExpiryDuration) - tier = tiers[2] - require.Equal(t, "business", tier.Code) - require.Equal(t, "Business", tier.Name) - require.Equal(t, int64(1000), tier.Prices.Month) - require.Equal(t, int64(10000), tier.Prices.Year) - require.Equal(t, "tier", tier.Limits.Basis) - require.Equal(t, int64(777333), tier.Limits.Reservations) - require.Equal(t, int64(2000), tier.Limits.Messages) - require.Equal(t, int64(36000), tier.Limits.MessagesExpiryDuration) - require.Equal(t, int64(123123), tier.Limits.Emails) - require.Equal(t, int64(999111), tier.Limits.AttachmentFileSize) - require.Equal(t, int64(888111), tier.Limits.AttachmentTotalSize) - require.Equal(t, int64(3600), tier.Limits.AttachmentExpiryDuration) + tier = tiers[2] + require.Equal(t, "business", tier.Code) + require.Equal(t, "Business", tier.Name) + require.Equal(t, int64(1000), tier.Prices.Month) + require.Equal(t, int64(10000), tier.Prices.Year) + require.Equal(t, "tier", tier.Limits.Basis) + require.Equal(t, int64(777333), tier.Limits.Reservations) + require.Equal(t, int64(2000), tier.Limits.Messages) + require.Equal(t, int64(36000), tier.Limits.MessagesExpiryDuration) + require.Equal(t, int64(123123), tier.Limits.Emails) + require.Equal(t, int64(999111), tier.Limits.AttachmentFileSize) + require.Equal(t, int64(888111), tier.Limits.AttachmentTotalSize) + require.Equal(t, int64(3600), tier.Limits.AttachmentExpiryDuration) + }) } func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("NewCheckoutSession", mock.Anything). - Return(&stripe.CheckoutSession{URL: "https://billing.stripe.com/abc/def"}, nil) + // Define how the mock should react + stripeMock. + On("NewCheckoutSession", mock.Anything). + Return(&stripe.CheckoutSession{URL: "https://billing.stripe.com/abc/def"}, nil) - // Create tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "pro", - StripeMonthlyPriceID: "price_123", - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + // Create tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "pro", + StripeMonthlyPriceID: "price_123", + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - // Create subscription - response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Create subscription + response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + redirectResponse, err := util.UnmarshalJSON[apiAccountBillingSubscriptionCreateResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, "https://billing.stripe.com/abc/def", redirectResponse.RedirectURL) }) - require.Equal(t, 200, response.Code) - redirectResponse, err := util.UnmarshalJSON[apiAccountBillingSubscriptionCreateResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, "https://billing.stripe.com/abc/def", redirectResponse.RedirectURL) } func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("GetCustomer", "acct_123"). - Return(&stripe.Customer{Subscriptions: &stripe.SubscriptionList{}}, nil) - stripeMock. - On("NewCheckoutSession", mock.Anything). - Return(&stripe.CheckoutSession{URL: "https://billing.stripe.com/abc/def"}, nil) + // Define how the mock should react + stripeMock. + On("GetCustomer", "acct_123"). + Return(&stripe.Customer{Subscriptions: &stripe.SubscriptionList{}}, nil) + stripeMock. + On("NewCheckoutSession", mock.Anything). + Return(&stripe.CheckoutSession{URL: "https://billing.stripe.com/abc/def"}, nil) - // Create tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "pro", - StripeMonthlyPriceID: "price_123", - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + // Create tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "pro", + StripeMonthlyPriceID: "price_123", + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - u, err := s.userManager.User("phil") - require.Nil(t, err) + u, err := s.userManager.User("phil") + require.Nil(t, err) - billing := &user.Billing{ - StripeCustomerID: "acct_123", - } - require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) + billing := &user.Billing{ + StripeCustomerID: "acct_123", + } + require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) - // Create subscription - response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Create subscription + response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + redirectResponse, err := util.UnmarshalJSON[apiAccountBillingSubscriptionCreateResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, "https://billing.stripe.com/abc/def", redirectResponse.RedirectURL) }) - require.Equal(t, 200, response.Code) - redirectResponse, err := util.UnmarshalJSON[apiAccountBillingSubscriptionCreateResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, "https://billing.stripe.com/abc/def", redirectResponse.RedirectURL) } func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.EnableSignup = true - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.EnableSignup = true + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("CancelSubscription", "sub_123"). - Return(&stripe.Subscription{}, nil) + // Define how the mock should react + stripeMock. + On("CancelSubscription", "sub_123"). + Return(&stripe.Subscription{}, nil) - // Create tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "pro", - StripeMonthlyPriceID: "price_123", - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + // Create tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "pro", + StripeMonthlyPriceID: "price_123", + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - u, err := s.userManager.User("phil") - require.Nil(t, err) + u, err := s.userManager.User("phil") + require.Nil(t, err) - billing := &user.Billing{ - StripeCustomerID: "acct_123", - StripeSubscriptionID: "sub_123", - } - require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) + billing := &user.Billing{ + StripeCustomerID: "acct_123", + StripeSubscriptionID: "sub_123", + } + require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) - // Delete account - rr := request(t, s, "DELETE", "/v1/account", `{"password": "phil"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Delete account + rr := request(t, s, "DELETE", "/v1/account", `{"password": "phil"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "mypass"), + }) + require.Equal(t, 401, rr.Code) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "mypass"), - }) - require.Equal(t, 401, rr.Code) } func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *testing.T) { - // This test is too overloaded, but it's also a great end-to-end a test. - // - // It tests: - // - A successful checkout flow (not a paying customer -> paying customer) - // - Tier-changes reset the rate limits for the user - // - The request limits for tier-less user and a tier-user - // - The message limits for a tier-user + forEachBackend(t, func(t *testing.T) { + // This test is too overloaded, but it's also a great end-to-end a test. + // + // It tests: + // - A successful checkout flow (not a paying customer -> paying customer) + // - Tier-changes reset the rate limits for the user + // - The request limits for tier-less user and a tier-user + // - The message limits for a tier-user - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - c.VisitorRequestLimitBurst = 5 - c.VisitorRequestLimitReplenish = time.Hour - c.CacheBatchSize = 500 - c.CacheBatchTimeout = time.Second - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + c.VisitorRequestLimitBurst = 5 + c.VisitorRequestLimitReplenish = time.Hour + c.CacheBatchSize = 500 + c.CacheBatchTimeout = time.Second + s := newTestServer(t, c) + s.stripe = stripeMock - // Create a user with a Stripe subscription and 3 reservations - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "starter", - StripeMonthlyPriceID: "price_1234", - ReservationLimit: 1, - MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in - MessageExpiryDuration: time.Hour, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier - u, err := s.userManager.User("phil") - require.Nil(t, err) + // Create a user with a Stripe subscription and 3 reservations + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "starter", + StripeMonthlyPriceID: "price_1234", + ReservationLimit: 1, + MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in + MessageExpiryDuration: time.Hour, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier + u, err := s.userManager.User("phil") + require.Nil(t, err) - // Define how the mock should react - stripeMock. - On("GetSession", "SOMETOKEN"). - Return(&stripe.CheckoutSession{ - ClientReferenceID: u.ID, // ntfy user ID - Customer: &stripe.Customer{ - ID: "acct_5555", - }, - Subscription: &stripe.Subscription{ - ID: "sub_1234", - }, - }, nil) - stripeMock. - On("GetSubscription", "sub_1234"). - Return(&stripe.Subscription{ - ID: "sub_1234", - Status: stripe.SubscriptionStatusActive, - CurrentPeriodEnd: 123456789, - CancelAt: 0, - Items: &stripe.SubscriptionItemList{ - Data: []*stripe.SubscriptionItem{ - { - Price: &stripe.Price{ - ID: "price_1234", - Recurring: &stripe.PriceRecurring{ - Interval: stripe.PriceRecurringIntervalMonth, + // Define how the mock should react + stripeMock. + On("GetSession", "SOMETOKEN"). + Return(&stripe.CheckoutSession{ + ClientReferenceID: u.ID, // ntfy user ID + Customer: &stripe.Customer{ + ID: "acct_5555", + }, + Subscription: &stripe.Subscription{ + ID: "sub_1234", + }, + }, nil) + stripeMock. + On("GetSubscription", "sub_1234"). + Return(&stripe.Subscription{ + ID: "sub_1234", + Status: stripe.SubscriptionStatusActive, + CurrentPeriodEnd: 123456789, + CancelAt: 0, + Items: &stripe.SubscriptionItemList{ + Data: []*stripe.SubscriptionItem{ + { + Price: &stripe.Price{ + ID: "price_1234", + Recurring: &stripe.PriceRecurring{ + Interval: stripe.PriceRecurringIntervalMonth, + }, }, }, }, }, - }, - }, nil) - stripeMock. - On("UpdateCustomer", "acct_5555", &stripe.CustomerParams{ - Params: stripe.Params{ - Metadata: map[string]string{ - "user_id": u.ID, - "user_name": u.Name, + }, nil) + stripeMock. + On("UpdateCustomer", "acct_5555", &stripe.CustomerParams{ + Params: stripe.Params{ + Metadata: map[string]string{ + "user_id": u.ID, + "user_name": u.Name, + }, }, - }, - }). - Return(&stripe.Customer{}, nil) + }). + Return(&stripe.Customer{}, nil) - // Send messages until rate limit of free tier is hit - for i := 0; i < 5; i++ { - rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - } - rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 429, rr.Code) - - // Verify some "before-stats" - u, err = s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, u.Tier) - require.Equal(t, "", u.Billing.StripeCustomerID) - require.Equal(t, "", u.Billing.StripeSubscriptionID) - require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) - require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval) - require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) - require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) - require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users! - require.Equal(t, int64(0), u.Stats.Emails) - - // Simulate Stripe success return URL call (no user context) - rr = request(t, s, "GET", "/v1/account/billing/subscription/success/SOMETOKEN", "", nil) - require.Equal(t, 303, rr.Code) - - // Verify that database columns were updated - u, err = s.userManager.User("phil") - require.Nil(t, err) - require.Equal(t, "starter", u.Tier.Code) // Not "pro" - require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) - require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) - require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) - require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval) - require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix()) - require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) - require.Equal(t, int64(0), u.Stats.Messages) - require.Equal(t, int64(0), u.Stats.Emails) - - // Now for the fun part: Verify that new rate limits are immediately applied - // This only tests the request limiter, which kicks in before the message limiter. - for i := 0; i < 11; i++ { - rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code, "failed on iteration %d", i) - } - rr = request(t, s, "PUT", "/mytopic", "some message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 429, rr.Code) - - // Now let's test the message limiter by faking a ridiculously generous rate limiter - v := s.visitor(netip.MustParseAddr("9.9.9.9"), u) - v.requestLimiter = rate.NewLimiter(rate.Every(time.Millisecond), 1000000) - - var wg sync.WaitGroup - for i := 0; i < 209; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() + // Send messages until rate limit of free tier is hit + for i := 0; i < 5; i++ { rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) - require.Equal(t, 200, rr.Code, "Failed on %d", i) - }(i) - } - wg.Wait() - rr = request(t, s, "PUT", "/mytopic", "some message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 429, rr.Code) + require.Equal(t, 200, rr.Code) + } + rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 429, rr.Code) - // And now let's cross-check that the stats are correct too - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Verify some "before-stats" + u, err = s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, u.Tier) + require.Equal(t, "", u.Billing.StripeCustomerID) + require.Equal(t, "", u.Billing.StripeSubscriptionID) + require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) + require.Equal(t, payments.PriceRecurringInterval(""), u.Billing.StripeSubscriptionInterval) + require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) + require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) + require.Equal(t, int64(0), u.Stats.Messages) // Messages and emails are not persisted for no-tier users! + require.Equal(t, int64(0), u.Stats.Emails) + + // Simulate Stripe success return URL call (no user context) + rr = request(t, s, "GET", "/v1/account/billing/subscription/success/SOMETOKEN", "", nil) + require.Equal(t, 303, rr.Code) + + // Verify that database columns were updated + u, err = s.userManager.User("phil") + require.Nil(t, err) + require.Equal(t, "starter", u.Tier.Code) // Not "pro" + require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) + require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) + require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) + require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), u.Billing.StripeSubscriptionInterval) + require.Equal(t, int64(123456789), u.Billing.StripeSubscriptionPaidUntil.Unix()) + require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) + require.Equal(t, int64(0), u.Stats.Messages) + require.Equal(t, int64(0), u.Stats.Emails) + + // Now for the fun part: Verify that new rate limits are immediately applied + // This only tests the request limiter, which kicks in before the message limiter. + for i := 0; i < 11; i++ { + rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code, "failed on iteration %d", i) + } + rr = request(t, s, "PUT", "/mytopic", "some message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 429, rr.Code) + + // Now let's test the message limiter by faking a ridiculously generous rate limiter + v := s.visitor(netip.MustParseAddr("9.9.9.9"), u) + v.requestLimiter = rate.NewLimiter(rate.Every(time.Millisecond), 1000000) + + var wg sync.WaitGroup + for i := 0; i < 209; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + rr := request(t, s, "PUT", "/mytopic", "some message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code, "Failed on %d", i) + }(i) + } + wg.Wait() + rr = request(t, s, "PUT", "/mytopic", "some message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 429, rr.Code) + + // And now let's cross-check that the stats are correct too + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, int64(220), account.Limits.Messages) + require.Equal(t, int64(220), account.Stats.Messages) + require.Equal(t, int64(0), account.Stats.MessagesRemaining) }) - require.Equal(t, 200, rr.Code) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, int64(220), account.Limits.Messages) - require.Equal(t, int64(220), account.Stats.Messages) - require.Equal(t, int64(0), account.Stats.MessagesRemaining) } func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(t *testing.T) { - t.Parallel() + forEachBackend(t, func(t *testing.T) { + t.Parallel() - // This tests incoming webhooks from Stripe to update a subscription: - // - All Stripe columns are updated in the user table - // - When downgrading, excess reservations are deleted, including messages and attachments in - // the corresponding topics + // This tests incoming webhooks from Stripe to update a subscription: + // - All Stripe columns are updated in the user table + // - When downgrading, excess reservations are deleted, including messages and attachments in + // the corresponding topics - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("ConstructWebhookEvent", mock.Anything, "stripe signature", "webhook key"). - Return(jsonToStripeEvent(t, subscriptionUpdatedEventJSON), nil) + // Define how the mock should react + stripeMock. + On("ConstructWebhookEvent", mock.Anything, "stripe signature", "webhook key"). + Return(jsonToStripeEvent(t, subscriptionUpdatedEventJSON), nil) - // Create a user with a Stripe subscription and 3 reservations - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_1", - Code: "starter", - StripeMonthlyPriceID: "price_1234", // ! - ReservationLimit: 1, // ! - MessageLimit: 100, - MessageExpiryDuration: time.Hour, - AttachmentExpiryDuration: time.Hour, - AttachmentFileSizeLimit: 1000000, - AttachmentTotalSizeLimit: 1000000, - AttachmentBandwidthLimit: 1000000, - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_2", - Code: "pro", - StripeMonthlyPriceID: "price_1111", // ! - ReservationLimit: 3, // ! - MessageLimit: 200, - MessageExpiryDuration: time.Hour, - AttachmentExpiryDuration: time.Hour, - AttachmentFileSizeLimit: 1000000, - AttachmentTotalSizeLimit: 1000000, - AttachmentBandwidthLimit: 1000000, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) - require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll)) + // Create a user with a Stripe subscription and 3 reservations + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_1", + Code: "starter", + StripeMonthlyPriceID: "price_1234", // ! + ReservationLimit: 1, // ! + MessageLimit: 100, + MessageExpiryDuration: time.Hour, + AttachmentExpiryDuration: time.Hour, + AttachmentFileSizeLimit: 1000000, + AttachmentTotalSizeLimit: 1000000, + AttachmentBandwidthLimit: 1000000, + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_2", + Code: "pro", + StripeMonthlyPriceID: "price_1111", // ! + ReservationLimit: 3, // ! + MessageLimit: 200, + MessageExpiryDuration: time.Hour, + AttachmentExpiryDuration: time.Hour, + AttachmentFileSizeLimit: 1000000, + AttachmentTotalSizeLimit: 1000000, + AttachmentBandwidthLimit: 1000000, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) + require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll)) - // Add billing details - u, err := s.userManager.User("phil") - require.Nil(t, err) + // Add billing details + u, err := s.userManager.User("phil") + require.Nil(t, err) - billing := &user.Billing{ - StripeCustomerID: "acct_5555", - StripeSubscriptionID: "sub_1234", - StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue), - StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), - StripeSubscriptionPaidUntil: time.Unix(123, 0), - StripeSubscriptionCancelAt: time.Unix(456, 0), - } - require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) + billing := &user.Billing{ + StripeCustomerID: "acct_5555", + StripeSubscriptionID: "sub_1234", + StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue), + StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), + StripeSubscriptionPaidUntil: time.Unix(123, 0), + StripeSubscriptionCancelAt: time.Unix(456, 0), + } + require.Nil(t, s.userManager.ChangeBilling(u.Name, billing)) - // Add some messages to "atopic" and "ztopic", everything in "ztopic" will be deleted - rr := request(t, s, "PUT", "/atopic", "some aaa message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Add some messages to "atopic" and "ztopic", everything in "ztopic" will be deleted + rr := request(t, s, "PUT", "/atopic", "some aaa message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "PUT", "/atopic", strings.Repeat("a", 5000), map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + a2 := toMessage(t, rr.Body.String()) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, a2.ID)) + + rr = request(t, s, "PUT", "/ztopic", "some zzz message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + rr = request(t, s, "PUT", "/ztopic", strings.Repeat("z", 5000), map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + z2 := toMessage(t, rr.Body.String()) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, z2.ID)) + + // Call the webhook: This does all the magic + rr = request(t, s, "POST", "/v1/account/billing/webhook", "dummy", map[string]string{ + "Stripe-Signature": "stripe signature", + }) + require.Equal(t, 200, rr.Code) + + // Verify that database columns were updated + u, err = s.userManager.User("phil") + require.Nil(t, err) + require.Equal(t, "starter", u.Tier.Code) // Not "pro" + require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) + require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) + require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due" + require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month" + require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated + require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated + + // Verify that reservations were deleted + r, err := s.userManager.Reservations("phil") + require.Nil(t, err) + require.Equal(t, 1, len(r)) // "ztopic" reservation was deleted + require.Equal(t, "atopic", r[0].Topic) + + // Verify that messages and attachments were deleted + time.Sleep(time.Second) + s.execManager() + + ms, err := s.messageCache.Messages("atopic", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 2, len(ms)) + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, a2.ID)) + + ms, err = s.messageCache.Messages("ztopic", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 0, len(ms)) + require.NoFileExists(t, filepath.Join(s.config.AttachmentCacheDir, z2.ID)) }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "PUT", "/atopic", strings.Repeat("a", 5000), map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - a2 := toMessage(t, rr.Body.String()) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, a2.ID)) - - rr = request(t, s, "PUT", "/ztopic", "some zzz message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - - rr = request(t, s, "PUT", "/ztopic", strings.Repeat("z", 5000), map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - z2 := toMessage(t, rr.Body.String()) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, z2.ID)) - - // Call the webhook: This does all the magic - rr = request(t, s, "POST", "/v1/account/billing/webhook", "dummy", map[string]string{ - "Stripe-Signature": "stripe signature", - }) - require.Equal(t, 200, rr.Code) - - // Verify that database columns were updated - u, err = s.userManager.User("phil") - require.Nil(t, err) - require.Equal(t, "starter", u.Tier.Code) // Not "pro" - require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) - require.Equal(t, "sub_1234", u.Billing.StripeSubscriptionID) - require.Equal(t, payments.SubscriptionStatus(stripe.SubscriptionStatusActive), u.Billing.StripeSubscriptionStatus) // Not "past_due" - require.Equal(t, payments.PriceRecurringInterval(stripe.PriceRecurringIntervalYear), u.Billing.StripeSubscriptionInterval) // Not "month" - require.Equal(t, int64(1674268231), u.Billing.StripeSubscriptionPaidUntil.Unix()) // Updated - require.Equal(t, int64(1674299999), u.Billing.StripeSubscriptionCancelAt.Unix()) // Updated - - // Verify that reservations were deleted - r, err := s.userManager.Reservations("phil") - require.Nil(t, err) - require.Equal(t, 1, len(r)) // "ztopic" reservation was deleted - require.Equal(t, "atopic", r[0].Topic) - - // Verify that messages and attachments were deleted - time.Sleep(time.Second) - s.execManager() - - ms, err := s.messageCache.Messages("atopic", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 2, len(ms)) - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, a2.ID)) - - ms, err = s.messageCache.Messages("ztopic", sinceAllMessages, false) - require.Nil(t, err) - require.Equal(t, 0, len(ms)) - require.NoFileExists(t, filepath.Join(s.config.AttachmentCacheDir, z2.ID)) } func TestPayments_Webhook_Subscription_Deleted(t *testing.T) { - // This tests incoming webhooks from Stripe to delete a subscription. It verifies that the database is - // updated (all Stripe fields are deleted, and the tier is removed). - // - // It doesn't fully test the message/attachment deletion. That is tested above in the subscription update call. + forEachBackend(t, func(t *testing.T) { + // This tests incoming webhooks from Stripe to delete a subscription. It verifies that the database is + // updated (all Stripe fields are deleted, and the tier is removed). + // + // It doesn't fully test the message/attachment deletion. That is tested above in the subscription update call. - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("ConstructWebhookEvent", mock.Anything, "stripe signature", "webhook key"). - Return(jsonToStripeEvent(t, subscriptionDeletedEventJSON), nil) + // Define how the mock should react + stripeMock. + On("ConstructWebhookEvent", mock.Anything, "stripe signature", "webhook key"). + Return(jsonToStripeEvent(t, subscriptionDeletedEventJSON), nil) - // Create a user with a Stripe subscription and 3 reservations - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_1", - Code: "pro", - StripeMonthlyPriceID: "price_1234", - ReservationLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) + // Create a user with a Stripe subscription and 3 reservations + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_1", + Code: "pro", + StripeMonthlyPriceID: "price_1234", + ReservationLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll)) - // Add billing details - u, err := s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{ - StripeCustomerID: "acct_5555", - StripeSubscriptionID: "sub_1234", - StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue), - StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), - StripeSubscriptionPaidUntil: time.Unix(123, 0), - StripeSubscriptionCancelAt: time.Unix(0, 0), - })) + // Add billing details + u, err := s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, s.userManager.ChangeBilling(u.Name, &user.Billing{ + StripeCustomerID: "acct_5555", + StripeSubscriptionID: "sub_1234", + StripeSubscriptionStatus: payments.SubscriptionStatus(stripe.SubscriptionStatusPastDue), + StripeSubscriptionInterval: payments.PriceRecurringInterval(stripe.PriceRecurringIntervalMonth), + StripeSubscriptionPaidUntil: time.Unix(123, 0), + StripeSubscriptionCancelAt: time.Unix(0, 0), + })) - // Call the webhook: This does all the magic - rr := request(t, s, "POST", "/v1/account/billing/webhook", "dummy", map[string]string{ - "Stripe-Signature": "stripe signature", + // Call the webhook: This does all the magic + rr := request(t, s, "POST", "/v1/account/billing/webhook", "dummy", map[string]string{ + "Stripe-Signature": "stripe signature", + }) + require.Equal(t, 200, rr.Code) + + // Verify that database columns were updated + u, err = s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, u.Tier) + require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) + require.Equal(t, "", u.Billing.StripeSubscriptionID) + require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) + require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) + require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) + + // Verify that reservations were deleted + r, err := s.userManager.Reservations("phil") + require.Nil(t, err) + require.Equal(t, 0, len(r)) }) - require.Equal(t, 200, rr.Code) - - // Verify that database columns were updated - u, err = s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, u.Tier) - require.Equal(t, "acct_5555", u.Billing.StripeCustomerID) - require.Equal(t, "", u.Billing.StripeSubscriptionID) - require.Equal(t, payments.SubscriptionStatus(""), u.Billing.StripeSubscriptionStatus) - require.Equal(t, int64(0), u.Billing.StripeSubscriptionPaidUntil.Unix()) - require.Equal(t, int64(0), u.Billing.StripeSubscriptionCancelAt.Unix()) - - // Verify that reservations were deleted - r, err := s.userManager.Reservations("phil") - require.Nil(t, err) - require.Equal(t, 0, len(r)) } func TestPayments_Subscription_Update_Different_Tier(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("GetSubscription", "sub_123"). - Return(&stripe.Subscription{ - ID: "sub_123", - Items: &stripe.SubscriptionItemList{ - Data: []*stripe.SubscriptionItem{ - { - ID: "someid_123", - Price: &stripe.Price{ID: "price_123"}, + // Define how the mock should react + stripeMock. + On("GetSubscription", "sub_123"). + Return(&stripe.Subscription{ + ID: "sub_123", + Items: &stripe.SubscriptionItemList{ + Data: []*stripe.SubscriptionItem{ + { + ID: "someid_123", + Price: &stripe.Price{ID: "price_123"}, + }, }, }, - }, - }, nil) - stripeMock. - On("UpdateSubscription", "sub_123", &stripe.SubscriptionParams{ - CancelAtPeriodEnd: stripe.Bool(false), - ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorAlwaysInvoice)), - Items: []*stripe.SubscriptionItemsParams{ - { - ID: stripe.String("someid_123"), - Price: stripe.String("price_457"), + }, nil) + stripeMock. + On("UpdateSubscription", "sub_123", &stripe.SubscriptionParams{ + CancelAtPeriodEnd: stripe.Bool(false), + ProrationBehavior: stripe.String(string(stripe.SubscriptionSchedulePhaseProrationBehaviorAlwaysInvoice)), + Items: []*stripe.SubscriptionItemsParams{ + { + ID: stripe.String("someid_123"), + Price: stripe.String("price_457"), + }, }, - }, - }). - Return(&stripe.Subscription{}, nil) + }). + Return(&stripe.Subscription{}, nil) - // Create tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_123", - Code: "pro", - StripeMonthlyPriceID: "price_123", - StripeYearlyPriceID: "price_124", - })) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - ID: "ti_456", - Code: "business", - StripeMonthlyPriceID: "price_456", - StripeYearlyPriceID: "price_457", - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ - StripeCustomerID: "acct_123", - StripeSubscriptionID: "sub_123", - })) + // Create tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_123", + Code: "pro", + StripeMonthlyPriceID: "price_123", + StripeYearlyPriceID: "price_124", + })) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + ID: "ti_456", + Code: "business", + StripeMonthlyPriceID: "price_456", + StripeYearlyPriceID: "price_457", + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ + StripeCustomerID: "acct_123", + StripeSubscriptionID: "sub_123", + })) - // Call endpoint to change subscription - rr := request(t, s, "PUT", "/v1/account/billing/subscription", `{"tier":"business","interval":"year"}`, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Call endpoint to change subscription + rr := request(t, s, "PUT", "/v1/account/billing/subscription", `{"tier":"business","interval":"year"}`, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) }) - require.Equal(t, 200, rr.Code) } func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("UpdateSubscription", "sub_123", mock.MatchedBy(func(s *stripe.SubscriptionParams) bool { - return *s.CancelAtPeriodEnd // Is true - })). - Return(&stripe.Subscription{}, nil) + // Define how the mock should react + stripeMock. + On("UpdateSubscription", "sub_123", mock.MatchedBy(func(s *stripe.SubscriptionParams) bool { + return *s.CancelAtPeriodEnd // Is true + })). + Return(&stripe.Subscription{}, nil) - // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ - StripeCustomerID: "acct_123", - StripeSubscriptionID: "sub_123", - })) + // Create user + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ + StripeCustomerID: "acct_123", + StripeSubscriptionID: "sub_123", + })) - // Delete subscription - rr := request(t, s, "DELETE", "/v1/account/billing/subscription", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Delete subscription + rr := request(t, s, "DELETE", "/v1/account/billing/subscription", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) }) - require.Equal(t, 200, rr.Code) } func TestPayments_CreatePortalSession(t *testing.T) { - stripeMock := &testStripeAPI{} - defer stripeMock.AssertExpectations(t) + forEachBackend(t, func(t *testing.T) { + stripeMock := &testStripeAPI{} + defer stripeMock.AssertExpectations(t) - c := newTestConfigWithAuthFile(t) - c.StripeSecretKey = "secret key" - c.StripeWebhookKey = "webhook key" - s := newTestServer(t, c) - s.stripe = stripeMock + c := newTestConfigWithAuthFile(t) + c.StripeSecretKey = "secret key" + c.StripeWebhookKey = "webhook key" + s := newTestServer(t, c) + s.stripe = stripeMock - // Define how the mock should react - stripeMock. - On("NewPortalSession", &stripe.BillingPortalSessionParams{ - Customer: stripe.String("acct_123"), - ReturnURL: stripe.String(s.config.BaseURL), - }). - Return(&stripe.BillingPortalSession{ - URL: "https://billing.stripe.com/blablabla", - }, nil) + // Define how the mock should react + stripeMock. + On("NewPortalSession", &stripe.BillingPortalSessionParams{ + Customer: stripe.String("acct_123"), + ReturnURL: stripe.String(s.config.BaseURL), + }). + Return(&stripe.BillingPortalSession{ + URL: "https://billing.stripe.com/blablabla", + }, nil) - // Create user - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ - StripeCustomerID: "acct_123", - StripeSubscriptionID: "sub_123", - })) + // Create user + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{ + StripeCustomerID: "acct_123", + StripeSubscriptionID: "sub_123", + })) - // Create portal session - rr := request(t, s, "POST", "/v1/account/billing/portal", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Create portal session + rr := request(t, s, "POST", "/v1/account/billing/portal", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + ps, _ := util.UnmarshalJSON[apiAccountBillingPortalRedirectResponse](io.NopCloser(rr.Body)) + require.Equal(t, "https://billing.stripe.com/blablabla", ps.RedirectURL) }) - require.Equal(t, 200, rr.Code) - ps, _ := util.UnmarshalJSON[apiAccountBillingPortalRedirectResponse](io.NopCloser(rr.Body)) - require.Equal(t, "https://billing.stripe.com/blablabla", ps.RedirectURL) } type testStripeAPI struct { diff --git a/server/server_test.go b/server/server_test.go index 23f72d1c..2a8930c9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "crypto/rand" + "database/sql" _ "embed" "encoding/base64" "encoding/json" @@ -13,6 +14,7 @@ import ( "net/http" "net/http/httptest" "net/netip" + "net/url" "os" "path/filepath" "runtime/debug" @@ -22,6 +24,7 @@ import ( "testing" "time" + _ "github.com/jackc/pgx/v5/stdlib" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" "heckel.io/ntfy/v2/log" @@ -37,77 +40,84 @@ func TestMain(m *testing.M) { } func TestServer_PublishAndPoll(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response1 := request(t, s, "PUT", "/mytopic", "my first message", nil) - msg1 := toMessage(t, response1.Body.String()) - require.NotEmpty(t, msg1.ID) - require.Equal(t, "my first message", msg1.Message) + response1 := request(t, s, "PUT", "/mytopic", "my first message", nil) + msg1 := toMessage(t, response1.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) - response2 := request(t, s, "PUT", "/mytopic", "my second\n\nmessage", nil) - msg2 := toMessage(t, response2.Body.String()) - require.NotEqual(t, msg1.ID, msg2.ID) - require.NotEmpty(t, msg2.ID) - require.Equal(t, "my second\n\nmessage", msg2.Message) + response2 := request(t, s, "PUT", "/mytopic", "my second\n\nmessage", nil) + msg2 := toMessage(t, response2.Body.String()) + require.NotEqual(t, msg1.ID, msg2.ID) + require.NotEmpty(t, msg2.ID) + require.Equal(t, "my second\n\nmessage", msg2.Message) - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 2, len(messages)) - require.Equal(t, "my first message", messages[0].Message) - require.Equal(t, "my second\n\nmessage", messages[1].Message) + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 2, len(messages)) + require.Equal(t, "my first message", messages[0].Message) + require.Equal(t, "my second\n\nmessage", messages[1].Message) - response = request(t, s, "GET", "/mytopic/sse?poll=1&since=all", "", nil) - lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 3, len(lines)) - require.Equal(t, "my first message", toMessage(t, strings.TrimPrefix(lines[0], "data: ")).Message) - require.Equal(t, "", lines[1]) - require.Equal(t, "my second\n\nmessage", toMessage(t, strings.TrimPrefix(lines[2], "data: ")).Message) + response = request(t, s, "GET", "/mytopic/sse?poll=1&since=all", "", nil) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 3, len(lines)) + require.Equal(t, "my first message", toMessage(t, strings.TrimPrefix(lines[0], "data: ")).Message) + require.Equal(t, "", lines[1]) + require.Equal(t, "my second\n\nmessage", toMessage(t, strings.TrimPrefix(lines[2], "data: ")).Message) - response = request(t, s, "GET", "/mytopic/raw?poll=1", "", nil) - lines = strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 2, len(lines)) - require.Equal(t, "my first message", lines[0]) - require.Equal(t, "my second message", lines[1]) // \n -> " " + response = request(t, s, "GET", "/mytopic/raw?poll=1", "", nil) + lines = strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) + require.Equal(t, "my first message", lines[0]) + require.Equal(t, "my second message", lines[1]) // \n -> " " + }) } func TestServer_PublishWithFirebase(t *testing.T) { - sender := newTestFirebaseSender(10) - s := newTestServer(t, newTestConfig(t)) - s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + forEachBackend(t, func(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) - response := request(t, s, "PUT", "/mytopic", "my first message", nil) - msg1 := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg1.ID) - require.Equal(t, "my first message", msg1.Message) + response := request(t, s, "PUT", "/mytopic", "my first message", nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) - time.Sleep(100 * time.Millisecond) // Firebase publishing happens - require.Equal(t, 1, len(sender.Messages())) - require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) - require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) - require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) + }) } func TestServer_PublishWithoutFirebase(t *testing.T) { - sender := newTestFirebaseSender(10) - s := newTestServer(t, newTestConfig(t)) - s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + forEachBackend(t, func(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) - response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{ - "firebase": "no", + response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{ + "firebase": "no", + }) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) + + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) }) - msg1 := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg1.ID) - require.Equal(t, "my first message", msg1.Message) - - time.Sleep(100 * time.Millisecond) // Firebase publishing happens - require.Equal(t, 0, len(sender.Messages())) } func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) { - // This tests issue #641, which used to panic before the fix + forEachBackend(t, func(t *testing.T) { + // This tests issue #641, which used to panic before the fix - firebaseKeyFile := filepath.Join(t.TempDir(), "firebase.json") - contents := `{ + firebaseKeyFile := filepath.Join(t.TempDir(), "firebase.json") + contents := `{ "type": "service_account", "project_id": "ntfy-test", "private_key_id": "fsfhskjdfhskdhfskdjfhsdf", @@ -120,482 +130,529 @@ func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) { "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-muv04%40ntfy-test.iam.gserviceaccount.com" } ` - require.Nil(t, os.WriteFile(firebaseKeyFile, []byte(contents), 0600)) - c := newTestConfig(t) - c.FirebaseKeyFile = firebaseKeyFile - s := newTestServer(t, c) + require.Nil(t, os.WriteFile(firebaseKeyFile, []byte(contents), 0600)) + c := newTestConfig(t) + c.FirebaseKeyFile = firebaseKeyFile + s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", "my first message", nil) - require.Equal(t, "my first message", toMessage(t, response.Body.String()).Message) + response := request(t, s, "PUT", "/mytopic", "my first message", nil) + require.Equal(t, "my first message", toMessage(t, response.Body.String()).Message) + }) } func TestServer_SubscribeOpenAndKeepalive(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.KeepaliveInterval = time.Second - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.KeepaliveInterval = time.Second + s := newTestServer(t, c) - rr := httptest.NewRecorder() - ctx, cancel := context.WithCancel(context.Background()) - req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil) - if err != nil { - t.Fatal(err) - } - doneChan := make(chan bool) - go func() { - s.handle(rr, req) - doneChan <- true - }() - time.Sleep(1300 * time.Millisecond) - cancel() - <-doneChan + rr := httptest.NewRecorder() + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil) + if err != nil { + t.Fatal(err) + } + doneChan := make(chan bool) + go func() { + s.handle(rr, req) + doneChan <- true + }() + time.Sleep(1300 * time.Millisecond) + cancel() + <-doneChan - messages := toMessages(t, rr.Body.String()) - require.Equal(t, 2, len(messages)) + messages := toMessages(t, rr.Body.String()) + require.Equal(t, 2, len(messages)) - require.Equal(t, openEvent, messages[0].Event) - require.Equal(t, "mytopic", messages[0].Topic) - require.Equal(t, "", messages[0].Message) - require.Equal(t, "", messages[0].Title) - require.Equal(t, 0, messages[0].Priority) - require.Nil(t, messages[0].Tags) + require.Equal(t, openEvent, messages[0].Event) + require.Equal(t, "mytopic", messages[0].Topic) + require.Equal(t, "", messages[0].Message) + require.Equal(t, "", messages[0].Title) + require.Equal(t, 0, messages[0].Priority) + require.Nil(t, messages[0].Tags) - require.Equal(t, keepaliveEvent, messages[1].Event) - require.Equal(t, "mytopic", messages[1].Topic) - require.Equal(t, "", messages[1].Message) - require.Equal(t, "", messages[1].Title) - require.Equal(t, 0, messages[1].Priority) - require.Nil(t, messages[1].Tags) + require.Equal(t, keepaliveEvent, messages[1].Event) + require.Equal(t, "mytopic", messages[1].Topic) + require.Equal(t, "", messages[1].Message) + require.Equal(t, "", messages[1].Title) + require.Equal(t, 0, messages[1].Priority) + require.Nil(t, messages[1].Tags) + }) } func TestServer_PublishAndSubscribe(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - subscribeRR := httptest.NewRecorder() - subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR) + subscribeRR := httptest.NewRecorder() + subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR) - publishFirstRR := request(t, s, "PUT", "/mytopic", "my first message", nil) - require.Equal(t, 200, publishFirstRR.Code) - time.Sleep(500 * time.Millisecond) // Publishing is done asynchronously, this avoids races + publishFirstRR := request(t, s, "PUT", "/mytopic", "my first message", nil) + require.Equal(t, 200, publishFirstRR.Code) + time.Sleep(500 * time.Millisecond) // Publishing is done asynchronously, this avoids races - publishSecondRR := request(t, s, "PUT", "/mytopic", "my other message", map[string]string{ - "Title": " This is a title ", - "X-Tags": "tag1,tag 2, tag3", - "p": "1", + publishSecondRR := request(t, s, "PUT", "/mytopic", "my other message", map[string]string{ + "Title": " This is a title ", + "X-Tags": "tag1,tag 2, tag3", + "p": "1", + }) + require.Equal(t, 200, publishSecondRR.Code) + + subscribeCancel() + messages := toMessages(t, subscribeRR.Body.String()) + require.Equal(t, 3, len(messages)) + require.Equal(t, openEvent, messages[0].Event) + + require.Equal(t, messageEvent, messages[1].Event) + require.Equal(t, "mytopic", messages[1].Topic) + require.Equal(t, "my first message", messages[1].Message) + require.Equal(t, "", messages[1].Title) + require.Equal(t, 0, messages[1].Priority) + require.Nil(t, messages[1].Tags) + require.True(t, time.Now().Add(12*time.Hour-5*time.Second).Unix() < messages[1].Expires) + require.True(t, time.Now().Add(12*time.Hour+5*time.Second).Unix() > messages[1].Expires) + + require.Equal(t, messageEvent, messages[2].Event) + require.Equal(t, "mytopic", messages[2].Topic) + require.Equal(t, "my other message", messages[2].Message) + require.Equal(t, "This is a title", messages[2].Title) + require.Equal(t, 1, messages[2].Priority) + require.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags) }) - require.Equal(t, 200, publishSecondRR.Code) - - subscribeCancel() - messages := toMessages(t, subscribeRR.Body.String()) - require.Equal(t, 3, len(messages)) - require.Equal(t, openEvent, messages[0].Event) - - require.Equal(t, messageEvent, messages[1].Event) - require.Equal(t, "mytopic", messages[1].Topic) - require.Equal(t, "my first message", messages[1].Message) - require.Equal(t, "", messages[1].Title) - require.Equal(t, 0, messages[1].Priority) - require.Nil(t, messages[1].Tags) - require.True(t, time.Now().Add(12*time.Hour-5*time.Second).Unix() < messages[1].Expires) - require.True(t, time.Now().Add(12*time.Hour+5*time.Second).Unix() > messages[1].Expires) - - require.Equal(t, messageEvent, messages[2].Event) - require.Equal(t, "mytopic", messages[2].Topic) - require.Equal(t, "my other message", messages[2].Message) - require.Equal(t, "This is a title", messages[2].Title) - require.Equal(t, 1, messages[2].Priority) - require.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags) } func TestServer_Publish_Disallowed_Topic(t *testing.T) { - c := newTestConfig(t) - c.DisallowedTopics = []string{"about", "time", "this", "got", "added"} - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.DisallowedTopics = []string{"about", "time", "this", "got", "added"} + s := newTestServer(t, c) - rr := request(t, s, "PUT", "/mytopic", "my first message", nil) - require.Equal(t, 200, rr.Code) + rr := request(t, s, "PUT", "/mytopic", "my first message", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "PUT", "/about", "another message", nil) - require.Equal(t, 400, rr.Code) - require.Equal(t, 40010, toHTTPError(t, rr.Body.String()).Code) + rr = request(t, s, "PUT", "/about", "another message", nil) + require.Equal(t, 400, rr.Code) + require.Equal(t, 40010, toHTTPError(t, rr.Body.String()).Code) + }) } func TestServer_StaticSites(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - rr := request(t, s, "GET", "/", "", nil) - require.Equal(t, 200, rr.Code) - require.Contains(t, rr.Body.String(), "") + rr := request(t, s, "GET", "/", "", nil) + require.Equal(t, 200, rr.Code) + require.Contains(t, rr.Body.String(), "") - rr = request(t, s, "HEAD", "/", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s, "HEAD", "/", "", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "OPTIONS", "/", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s, "OPTIONS", "/", "", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s, "GET", "/does-not-exist.txt", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/does-not-exist.txt", "", nil) + require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/mytopic", "", nil) - require.Equal(t, 200, rr.Code) - require.Contains(t, rr.Body.String(), ``) + rr = request(t, s, "GET", "/mytopic", "", nil) + require.Equal(t, 200, rr.Code) + require.Contains(t, rr.Body.String(), ``) - rr = request(t, s, "GET", "/docs", "", nil) - require.Equal(t, 301, rr.Code) + rr = request(t, s, "GET", "/docs", "", nil) + require.Equal(t, 301, rr.Code) - // Docs test removed, it was failing annoyingly. + // Docs test removed, it was failing annoyingly. + }) } func TestServer_WebEnabled(t *testing.T) { - conf := newTestConfig(t) - conf.WebRoot = "" // Disable web app - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfig(t) + conf.WebRoot = "" // Disable web app + s := newTestServer(t, conf) - rr := request(t, s, "GET", "/", "", nil) - require.Equal(t, 404, rr.Code) + rr := request(t, s, "GET", "/", "", nil) + require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/config.js", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/config.js", "", nil) + require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/sw.js", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/sw.js", "", nil) + require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/app.html", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/app.html", "", nil) + require.Equal(t, 404, rr.Code) - rr = request(t, s, "GET", "/static/css/home.css", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s, "GET", "/static/css/home.css", "", nil) + require.Equal(t, 404, rr.Code) - conf2 := newTestConfig(t) - conf2.WebRoot = "/" - s2 := newTestServer(t, conf2) + conf2 := newTestConfig(t) + conf2.WebRoot = "/" + s2 := newTestServer(t, conf2) - rr = request(t, s2, "GET", "/", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s2, "GET", "/", "", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s2, "GET", "/config.js", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s2, "GET", "/config.js", "", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s2, "GET", "/sw.js", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s2, "GET", "/sw.js", "", nil) + require.Equal(t, 200, rr.Code) - rr = request(t, s2, "GET", "/app.html", "", nil) - require.Equal(t, 200, rr.Code) + rr = request(t, s2, "GET", "/app.html", "", nil) + require.Equal(t, 200, rr.Code) + }) } func TestServer_PublishLargeMessage(t *testing.T) { - c := newTestConfig(t) - c.AttachmentCacheDir = "" // Disable attachments - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.AttachmentCacheDir = "" // Disable attachments + s := newTestServer(t, c) - body := strings.Repeat("this is a large message", 5000) - response := request(t, s, "PUT", "/mytopic", body, nil) - require.Equal(t, 400, response.Code) + body := strings.Repeat("this is a large message", 5000) + response := request(t, s, "PUT", "/mytopic", body, nil) + require.Equal(t, 400, response.Code) + }) } func TestServer_PublishPriority(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - for prio := 1; prio <= 5; prio++ { - response := request(t, s, "GET", fmt.Sprintf("/mytopic/publish?priority=%d", prio), fmt.Sprintf("priority %d", prio), nil) - msg := toMessage(t, response.Body.String()) - require.Equal(t, prio, msg.Priority) - } + for prio := 1; prio <= 5; prio++ { + response := request(t, s, "GET", fmt.Sprintf("/mytopic/publish?priority=%d", prio), fmt.Sprintf("priority %d", prio), nil) + msg := toMessage(t, response.Body.String()) + require.Equal(t, prio, msg.Priority) + } - response := request(t, s, "GET", "/mytopic/publish?priority=min", "test", nil) - require.Equal(t, 1, toMessage(t, response.Body.String()).Priority) + response := request(t, s, "GET", "/mytopic/publish?priority=min", "test", nil) + require.Equal(t, 1, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "GET", "/mytopic/send?priority=low", "test", nil) - require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) + response = request(t, s, "GET", "/mytopic/send?priority=low", "test", nil) + require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) - 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, "GET", "/mytopic/send?priority=default", "test", nil) + require.Equal(t, 3, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "GET", "/mytopic/send?priority=high", "test", nil) - require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) + response = request(t, s, "GET", "/mytopic/send?priority=high", "test", nil) + require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) - 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, "GET", "/mytopic/send?priority=max", "test", nil) + require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "GET", "/mytopic/trigger?priority=urgent", "test", nil) - require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) + response = request(t, s, "GET", "/mytopic/trigger?priority=urgent", "test", nil) + require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "GET", "/mytopic/trigger?priority=INVALID", "test", nil) - require.Equal(t, 40007, 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_PublishPriority_SpecialHTTPHeader(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "Priority": "u=4", - "X-Priority": "5", - }) - require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "Priority": "u=4", + "X-Priority": "5", + }) + require.Equal(t, 5, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{ - "Priority": "u=9", - }) - require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) + response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{ + "Priority": "u=9", + }) + require.Equal(t, 4, toMessage(t, response.Body.String()).Priority) - response = request(t, s, "POST", "/mytopic", "test", map[string]string{ - "p": "2", - "priority": "u=9, i", + response = request(t, s, "POST", "/mytopic", "test", map[string]string{ + "p": "2", + "priority": "u=9, i", + }) + require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) }) - require.Equal(t, 2, toMessage(t, response.Body.String()).Priority) } func TestServer_PublishGETOnlyOneTopic(t *testing.T) { - // This tests a bug that allowed publishing topics with a comma in the name (no ticket) + forEachBackend(t, func(t *testing.T) { + // This tests a bug that allowed publishing topics with a comma in the name (no ticket) - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/mytopic,mytopic2/publish?m=hi", "", nil) - require.Equal(t, 404, response.Code) + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "GET", "/mytopic,mytopic2/publish?m=hi", "", nil) + require.Equal(t, 404, response.Code) + }) } func TestServer_PublishNoCache(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "this message is not cached", map[string]string{ - "Cache": "no", + response := request(t, s, "PUT", "/mytopic", "this message is not cached", map[string]string{ + "Cache": "no", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "this message is not cached", msg.Message) + require.Equal(t, int64(0), msg.Expires) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Empty(t, messages) }) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "this message is not cached", msg.Message) - require.Equal(t, int64(0), msg.Expires) - - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Empty(t, messages) } func TestServer_PublishAt(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "In": "1h", + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "In": "1h", + }) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 0, len(messages)) + + // Update message time to the past + fakeTime := time.Now().Add(-10 * time.Second).Unix() + require.Nil(t, s.messageCache.UpdateMessageTime(msg.ID, fakeTime)) + + // Trigger delayed message sending + require.Nil(t, s.sendDelayedMessages()) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "a message", messages[0].Message) + require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender! + + var err error + messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true) + require.Nil(t, err) + require.Equal(t, 1, len(messages)) + require.Equal(t, "a message", messages[0].Message) + require.Equal(t, "9.9.9.9", messages[0].Sender.String()) // It's stored in the DB though! }) - require.Equal(t, 200, response.Code) - - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 0, len(messages)) - - // Update message time to the past - fakeTime := time.Now().Add(-10 * time.Second).Unix() - _, err := s.messageCache.DB().Exec(`UPDATE messages SET time=?`, fakeTime) - require.Nil(t, err) - - // Trigger delayed message sending - require.Nil(t, s.sendDelayedMessages()) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "a message", messages[0].Message) - require.Equal(t, netip.Addr{}, messages[0].Sender) // Never return the sender! - - messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true) - require.Nil(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, "a message", messages[0].Message) - require.Equal(t, "9.9.9.9", messages[0].Sender.String()) // It's stored in the DB though! } func TestServer_PublishAt_FromUser(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfigWithAuthFile(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfigWithAuthFile(t)) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - "In": "1h", + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + "In": "1h", + }) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + + // Message doesn't show up immediately + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 0, len(messages)) + + // Update message time to the past + fakeTime := time.Now().Add(-10 * time.Second).Unix() + require.Nil(t, s.messageCache.UpdateMessageTime(msg.ID, fakeTime)) + + // Trigger delayed message sending + require.Nil(t, s.sendDelayedMessages()) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, fakeTime, messages[0].Time) + require.Equal(t, "a message", messages[0].Message) + + var err error + messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true) + require.Nil(t, err) + require.Equal(t, 1, len(messages)) + require.Equal(t, "a message", messages[0].Message) + require.True(t, strings.HasPrefix(messages[0].User, "u_")) }) - require.Equal(t, 200, response.Code) - - // Message doesn't show up immediately - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 0, len(messages)) - - // Update message time to the past - fakeTime := time.Now().Add(-10 * time.Second).Unix() - _, err := s.messageCache.DB().Exec(`UPDATE messages SET time=?`, fakeTime) - require.Nil(t, err) - - // Trigger delayed message sending - require.Nil(t, s.sendDelayedMessages()) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, fakeTime, messages[0].Time) - require.Equal(t, "a message", messages[0].Message) - - messages, err = s.messageCache.Messages("mytopic", sinceAllMessages, true) - require.Nil(t, err) - require.Equal(t, 1, len(messages)) - require.Equal(t, "a message", messages[0].Message) - require.True(t, strings.HasPrefix(messages[0].User, "u_")) } func TestServer_PublishAt_Expires(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "In": "2 days", + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "In": "2 days", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.True(t, m.Expires > time.Now().Add(12*time.Hour+48*time.Hour-time.Minute).Unix()) + require.True(t, m.Expires < time.Now().Add(12*time.Hour+48*time.Hour+time.Minute).Unix()) }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.True(t, m.Expires > time.Now().Add(12*time.Hour+48*time.Hour-time.Minute).Unix()) - require.True(t, m.Expires < time.Now().Add(12*time.Hour+48*time.Hour+time.Minute).Unix()) } func TestServer_PublishAtWithCacheError(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "Cache": "no", - "In": "30 min", + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "Cache": "no", + "In": "30 min", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, errHTTPBadRequestDelayNoCache, toHTTPError(t, response.Body.String())) }) - require.Equal(t, 400, response.Code) - require.Equal(t, errHTTPBadRequestDelayNoCache, toHTTPError(t, response.Body.String())) } func TestServer_PublishAtTooShortDelay(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "In": "1s", + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "In": "1s", + }) + require.Equal(t, 400, response.Code) }) - require.Equal(t, 400, response.Code) } func TestServer_PublishAtTooLongDelay(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "In": "99999999h", + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "In": "99999999h", + }) + require.Equal(t, 400, response.Code) }) - require.Equal(t, 400, response.Code) } func TestServer_PublishAtInvalidDelay(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?delay=INVALID", "a message", nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 400, response.Code) - require.Equal(t, 40004, err.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?delay=INVALID", "a message", nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 400, response.Code) + require.Equal(t, 40004, err.Code) + }) } func TestServer_PublishAtTooLarge(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?x-in=99999h", "a message", nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 400, response.Code) - require.Equal(t, 40006, err.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?x-in=99999h", "a message", nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 400, response.Code) + require.Equal(t, 40006, err.Code) + }) } func TestServer_PublishAtAndPrune(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ - "In": "1h", + response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{ + "In": "1h", + }) + require.Equal(t, 200, response.Code) + s.execManager() // Fire pruning + + response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) // Not affected by pruning + require.Equal(t, "a message", messages[0].Message) + + time.Sleep(time.Second) // FIXME CI failing not sure why }) - require.Equal(t, 200, response.Code) - s.execManager() // Fire pruning - - response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) // Not affected by pruning - require.Equal(t, "a message", messages[0].Message) - - time.Sleep(time.Second) // FIXME CI failing not sure why } func TestServer_PublishAndMultiPoll(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic1", "message 1", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "mytopic1", msg.Topic) - require.Equal(t, "message 1", msg.Message) + response := request(t, s, "PUT", "/mytopic1", "message 1", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "mytopic1", msg.Topic) + require.Equal(t, "message 1", msg.Message) - response = request(t, s, "PUT", "/mytopic2", "message 2", nil) - msg = toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "mytopic2", msg.Topic) - require.Equal(t, "message 2", msg.Message) + response = request(t, s, "PUT", "/mytopic2", "message 2", nil) + msg = toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "mytopic2", msg.Topic) + require.Equal(t, "message 2", msg.Message) - response = request(t, s, "GET", "/mytopic1/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "mytopic1", messages[0].Topic) - require.Equal(t, "message 1", messages[0].Message) + response = request(t, s, "GET", "/mytopic1/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "mytopic1", messages[0].Topic) + require.Equal(t, "message 1", messages[0].Message) - response = request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 2, len(messages)) - require.Equal(t, "mytopic1", messages[0].Topic) - require.Equal(t, "message 1", messages[0].Message) - require.Equal(t, "mytopic2", messages[1].Topic) - require.Equal(t, "message 2", messages[1].Message) + response = request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 2, len(messages)) + require.Equal(t, "mytopic1", messages[0].Topic) + require.Equal(t, "message 1", messages[0].Message) + require.Equal(t, "mytopic2", messages[1].Topic) + require.Equal(t, "message 2", messages[1].Message) + }) } func TestServer_PublishWithNopCache(t *testing.T) { - c := newTestConfig(t) - c.CacheDuration = 0 - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.CacheDuration = 0 + s := newTestServer(t, c) - subscribeRR := httptest.NewRecorder() - subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR) + subscribeRR := httptest.NewRecorder() + subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR) - publishRR := request(t, s, "PUT", "/mytopic", "my first message", nil) - require.Equal(t, 200, publishRR.Code) + publishRR := request(t, s, "PUT", "/mytopic", "my first message", nil) + require.Equal(t, 200, publishRR.Code) - subscribeCancel() - messages := toMessages(t, subscribeRR.Body.String()) - require.Equal(t, 2, len(messages)) - require.Equal(t, openEvent, messages[0].Event) - require.Equal(t, messageEvent, messages[1].Event) - require.Equal(t, "my first message", messages[1].Message) + subscribeCancel() + messages := toMessages(t, subscribeRR.Body.String()) + require.Equal(t, 2, len(messages)) + require.Equal(t, openEvent, messages[0].Event) + require.Equal(t, messageEvent, messages[1].Event) + require.Equal(t, "my first message", messages[1].Message) - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages = toMessages(t, response.Body.String()) - require.Empty(t, messages) + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages = toMessages(t, response.Body.String()) + require.Empty(t, messages) + }) } func TestServer_PublishAndPollSince(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - request(t, s, "PUT", "/mytopic", "test 1", nil) - time.Sleep(1100 * time.Millisecond) + request(t, s, "PUT", "/mytopic", "test 1", nil) + time.Sleep(1100 * time.Millisecond) - since := time.Now().Unix() - request(t, s, "PUT", "/mytopic", "test 2", nil) + since := time.Now().Unix() + request(t, s, "PUT", "/mytopic", "test 2", nil) - response := request(t, s, "GET", fmt.Sprintf("/mytopic/json?poll=1&since=%d", since), "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "test 2", messages[0].Message) + response := request(t, s, "GET", fmt.Sprintf("/mytopic/json?poll=1&since=%d", since), "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) - response = request(t, s, "GET", "/mytopic/json?poll=1&since=10s", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 2, len(messages)) - require.Equal(t, "test 1", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=10s", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 2, len(messages)) + require.Equal(t, "test 1", messages[0].Message) - response = request(t, s, "GET", "/mytopic/json?poll=1&since=100ms", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=100ms", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) - response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "test 2", messages[0].Message) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=latest", "", nil) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "test 2", messages[0].Message) - response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil) - require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) + response = request(t, s, "GET", "/mytopic/json?poll=1&since=INVALID", "", nil) + require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) + }) } func newMessageWithTimestamp(topic, msg string, timestamp int64) *model.Message { @@ -605,605 +662,658 @@ func newMessageWithTimestamp(topic, msg string, timestamp int64) *model.Message } func TestServer_PollSinceID_MultipleTopics(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 1", 1655740277))) - markerMessage := newMessageWithTimestamp("mytopic2", "test 2", 1655740283) - require.Nil(t, s.messageCache.AddMessage(markerMessage)) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 1", 1655740277))) + markerMessage := newMessageWithTimestamp("mytopic2", "test 2", 1655740283) + require.Nil(t, s.messageCache.AddMessage(markerMessage)) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303))) - response := request(t, s, "GET", fmt.Sprintf("/mytopic1,mytopic2/json?poll=1&since=%s", markerMessage.ID), "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 4, len(messages)) - require.Equal(t, "test 3", messages[0].Message) - require.Equal(t, "mytopic1", messages[0].Topic) - require.Equal(t, "test 4", messages[1].Message) - require.Equal(t, "mytopic2", messages[1].Topic) - require.Equal(t, "test 5", messages[2].Message) - require.Equal(t, "mytopic1", messages[2].Topic) - require.Equal(t, "test 6", messages[3].Message) - require.Equal(t, "mytopic2", messages[3].Topic) + response := request(t, s, "GET", fmt.Sprintf("/mytopic1,mytopic2/json?poll=1&since=%s", markerMessage.ID), "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 4, len(messages)) + require.Equal(t, "test 3", messages[0].Message) + require.Equal(t, "mytopic1", messages[0].Topic) + require.Equal(t, "test 4", messages[1].Message) + require.Equal(t, "mytopic2", messages[1].Topic) + require.Equal(t, "test 5", messages[2].Message) + require.Equal(t, "mytopic1", messages[2].Topic) + require.Equal(t, "test 6", messages[3].Message) + require.Equal(t, "mytopic2", messages[3].Topic) + }) } func TestServer_PollSinceID_MultipleTopics_IDDoesNotMatch(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297))) - require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297))) + require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303))) - response := request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1&since=NoMatchForID", "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 4, len(messages)) - require.Equal(t, "test 3", messages[0].Message) - require.Equal(t, "test 4", messages[1].Message) - require.Equal(t, "test 5", messages[2].Message) - require.Equal(t, "test 6", messages[3].Message) + response := request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1&since=NoMatchForID", "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 4, len(messages)) + require.Equal(t, "test 3", messages[0].Message) + require.Equal(t, "test 4", messages[1].Message) + require.Equal(t, "test 5", messages[2].Message) + require.Equal(t, "test 6", messages[3].Message) + }) } func TestServer_PublishViaGET(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/mytopic/trigger", "", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "triggered", msg.Message) + response := request(t, s, "GET", "/mytopic/trigger", "", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "triggered", msg.Message) - response = request(t, s, "GET", "/mytopic/send?message=This+is+a+test&t=This+is+a+title&tags=skull&x-priority=5&delay=24h", "", nil) - msg = toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "This is a test", msg.Message) - require.Equal(t, "This is a title", msg.Title) - require.Equal(t, []string{"skull"}, msg.Tags) - require.Equal(t, 5, msg.Priority) - require.Greater(t, msg.Time, time.Now().Add(23*time.Hour).Unix()) + response = request(t, s, "GET", "/mytopic/send?message=This+is+a+test&t=This+is+a+title&tags=skull&x-priority=5&delay=24h", "", nil) + msg = toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "This is a test", msg.Message) + require.Equal(t, "This is a title", msg.Title) + require.Equal(t, []string{"skull"}, msg.Tags) + require.Equal(t, 5, msg.Priority) + require.Greater(t, msg.Time, time.Now().Add(23*time.Hour).Unix()) + }) } func TestServer_PublishMessageInHeaderWithNewlines(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "", map[string]string{ - "Message": "Line 1\\nLine 2", + response := request(t, s, "PUT", "/mytopic", "", map[string]string{ + "Message": "Line 1\\nLine 2", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "Line 1\nLine 2", msg.Message) // \\n -> \n ! }) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "Line 1\nLine 2", msg.Message) // \\n -> \n ! } func TestServer_PublishInvalidTopic(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - s.smtpSender = &testMailer{} - response := request(t, s, "PUT", "/docs", "fail", nil) - require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + s.smtpSender = &testMailer{} + response := request(t, s, "PUT", "/docs", "fail", nil) + require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_PublishWithSIDInPath(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic/sid", "message", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SequenceID) + response := request(t, s, "POST", "/mytopic/sid", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid", msg.SequenceID) + }) } func TestServer_PublishWithSIDInHeader(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", "message", map[string]string{ - "sid": "sid", + response := request(t, s, "POST", "/mytopic", "message", map[string]string{ + "sid": "sid", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid", msg.SequenceID) }) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "sid", msg.SequenceID) } func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{ - "sid": "sid2", + response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{ + "sid": "sid2", + }) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header }) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SequenceID) // Sequence ID in path has priority over header } func TestServer_PublishWithSIDInQuery(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SequenceID) + response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SequenceID) + }) } func TestServer_PublishWithSIDViaGet(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "sid1", msg.SequenceID) + response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "sid1", msg.SequenceID) + }) } func TestServer_PublishAsJSON_WithSequenceID(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic","message":"A message","sequence_id":"my-sequence-123"}` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 200, response.Code) + body := `{"topic":"mytopic","message":"A message","sequence_id":"my-sequence-123"}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "my-sequence-123", msg.SequenceID) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "my-sequence-123", msg.SequenceID) + }) } func TestServer_PublishWithInvalidSIDInPath(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic/.", "message", nil) + response := request(t, s, "POST", "/mytopic/.", "message", nil) - require.Equal(t, 404, response.Code) + require.Equal(t, 404, response.Code) + }) } func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", "message", map[string]string{ - "X-Sequence-ID": "*&?", + response := request(t, s, "POST", "/mytopic", "message", map[string]string{ + "X-Sequence-ID": "*&?", + }) + + require.Equal(t, 400, response.Code) + require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code) }) - - require.Equal(t, 400, response.Code) - require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code) } func TestServer_PollWithQueryFilters(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?priority=1&tags=tag1,tag2", "my first message", nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) + response := request(t, s, "PUT", "/mytopic?priority=1&tags=tag1,tag2", "my first message", nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) - response = request(t, s, "PUT", "/mytopic?title=a+title", "my second message", map[string]string{ - "Tags": "tag2,tag3", + response = request(t, s, "PUT", "/mytopic?title=a+title", "my second message", map[string]string{ + "Tags": "tag2,tag3", + }) + msg = toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + + queriesThatShouldReturnMessageOne := []string{ + "/mytopic/json?poll=1&priority=1", + "/mytopic/json?poll=1&priority=min", + "/mytopic/json?poll=1&priority=min,low", + "/mytopic/json?poll=1&priority=1,2", + "/mytopic/json?poll=1&p=2,min", + "/mytopic/json?poll=1&tags=tag1", + "/mytopic/json?poll=1&tags=tag1,tag2", + "/mytopic/json?poll=1&message=my+first+message", + } + for _, query := range queriesThatShouldReturnMessageOne { + response = request(t, s, "GET", query, "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages), "Query failed: "+query) + require.Equal(t, "my first message", messages[0].Message, "Query failed: "+query) + } + + queriesThatShouldReturnMessageTwo := []string{ + "/mytopic/json?poll=1&x-priority=3", // ! + "/mytopic/json?poll=1&priority=3", + "/mytopic/json?poll=1&priority=default", + "/mytopic/json?poll=1&p=3", + "/mytopic/json?poll=1&x-tags=tag2,tag3", + "/mytopic/json?poll=1&tags=tag2,tag3", + "/mytopic/json?poll=1&tag=tag2,tag3", + "/mytopic/json?poll=1&ta=tag2,tag3", + "/mytopic/json?poll=1&x-title=a+title", + "/mytopic/json?poll=1&title=a+title", + "/mytopic/json?poll=1&t=a+title", + "/mytopic/json?poll=1&x-message=my+second+message", + "/mytopic/json?poll=1&message=my+second+message", + "/mytopic/json?poll=1&m=my+second+message", + "/mytopic/json?x-poll=1&m=my+second+message", + "/mytopic/json?po=1&m=my+second+message", + } + for _, query := range queriesThatShouldReturnMessageTwo { + response = request(t, s, "GET", query, "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages), "Query failed: "+query) + require.Equal(t, "my second message", messages[0].Message, "Query failed: "+query) + } + + queriesThatShouldReturnNoMessages := []string{ + "/mytopic/json?poll=1&priority=4", + "/mytopic/json?poll=1&tags=tag1,tag2,tag3", + "/mytopic/json?poll=1&title=another+title", + "/mytopic/json?poll=1&message=my+third+message", + "/mytopic/json?poll=1&message=my+third+message", + } + for _, query := range queriesThatShouldReturnNoMessages { + response = request(t, s, "GET", query, "", nil) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 0, len(messages), "Query failed: "+query) + } }) - msg = toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - - queriesThatShouldReturnMessageOne := []string{ - "/mytopic/json?poll=1&priority=1", - "/mytopic/json?poll=1&priority=min", - "/mytopic/json?poll=1&priority=min,low", - "/mytopic/json?poll=1&priority=1,2", - "/mytopic/json?poll=1&p=2,min", - "/mytopic/json?poll=1&tags=tag1", - "/mytopic/json?poll=1&tags=tag1,tag2", - "/mytopic/json?poll=1&message=my+first+message", - } - for _, query := range queriesThatShouldReturnMessageOne { - response = request(t, s, "GET", query, "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages), "Query failed: "+query) - require.Equal(t, "my first message", messages[0].Message, "Query failed: "+query) - } - - queriesThatShouldReturnMessageTwo := []string{ - "/mytopic/json?poll=1&x-priority=3", // ! - "/mytopic/json?poll=1&priority=3", - "/mytopic/json?poll=1&priority=default", - "/mytopic/json?poll=1&p=3", - "/mytopic/json?poll=1&x-tags=tag2,tag3", - "/mytopic/json?poll=1&tags=tag2,tag3", - "/mytopic/json?poll=1&tag=tag2,tag3", - "/mytopic/json?poll=1&ta=tag2,tag3", - "/mytopic/json?poll=1&x-title=a+title", - "/mytopic/json?poll=1&title=a+title", - "/mytopic/json?poll=1&t=a+title", - "/mytopic/json?poll=1&x-message=my+second+message", - "/mytopic/json?poll=1&message=my+second+message", - "/mytopic/json?poll=1&m=my+second+message", - "/mytopic/json?x-poll=1&m=my+second+message", - "/mytopic/json?po=1&m=my+second+message", - } - for _, query := range queriesThatShouldReturnMessageTwo { - response = request(t, s, "GET", query, "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages), "Query failed: "+query) - require.Equal(t, "my second message", messages[0].Message, "Query failed: "+query) - } - - queriesThatShouldReturnNoMessages := []string{ - "/mytopic/json?poll=1&priority=4", - "/mytopic/json?poll=1&tags=tag1,tag2,tag3", - "/mytopic/json?poll=1&title=another+title", - "/mytopic/json?poll=1&message=my+third+message", - "/mytopic/json?poll=1&message=my+third+message", - } - for _, query := range queriesThatShouldReturnNoMessages { - response = request(t, s, "GET", query, "", nil) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 0, len(messages), "Query failed: "+query) - } } func TestServer_SubscribeWithQueryFilters(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.KeepaliveInterval = 800 * time.Millisecond - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.KeepaliveInterval = 800 * time.Millisecond + s := newTestServer(t, c) - subscribeResponse := httptest.NewRecorder() - subscribeCancel := subscribe(t, s, "/mytopic/json?tags=zfs-issue", subscribeResponse) + subscribeResponse := httptest.NewRecorder() + subscribeCancel := subscribe(t, s, "/mytopic/json?tags=zfs-issue", subscribeResponse) - response := request(t, s, "PUT", "/mytopic", "my first message", nil) - require.Equal(t, 200, response.Code) - response = request(t, s, "PUT", "/mytopic", "ZFS scrub failed", map[string]string{ - "Tags": "zfs-issue,zfs-scrub", + response := request(t, s, "PUT", "/mytopic", "my first message", nil) + require.Equal(t, 200, response.Code) + response = request(t, s, "PUT", "/mytopic", "ZFS scrub failed", map[string]string{ + "Tags": "zfs-issue,zfs-scrub", + }) + require.Equal(t, 200, response.Code) + + time.Sleep(850 * time.Millisecond) + subscribeCancel() + + messages := toMessages(t, subscribeResponse.Body.String()) + require.Equal(t, 3, len(messages)) + require.Equal(t, openEvent, messages[0].Event) + require.Equal(t, messageEvent, messages[1].Event) + require.Equal(t, "ZFS scrub failed", messages[1].Message) + require.Equal(t, keepaliveEvent, messages[2].Event) }) - require.Equal(t, 200, response.Code) - - time.Sleep(850 * time.Millisecond) - subscribeCancel() - - messages := toMessages(t, subscribeResponse.Body.String()) - require.Equal(t, 3, len(messages)) - require.Equal(t, openEvent, messages[0].Event) - require.Equal(t, messageEvent, messages[1].Event) - require.Equal(t, "ZFS scrub failed", messages[1].Message) - require.Equal(t, keepaliveEvent, messages[2].Event) } func TestServer_Auth_Success_Admin(t *testing.T) { - c := newTestConfigWithAuthFile(t) - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) }) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) } func TestServer_Auth_Success_User(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) - response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), + response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, response.Code) }) - require.Equal(t, 200, response.Code) } func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) - require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite)) + require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite)) - response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), + response := request(t, s, "GET", "/mytopic,anothertopic/auth", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, response.Code) + + response = request(t, s, "GET", "/mytopic,anothertopic,NOT-THIS-ONE/auth", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, response.Code) }) - require.Equal(t, 200, response.Code) - - response = request(t, s, "GET", "/mytopic,anothertopic,NOT-THIS-ONE/auth", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - require.Equal(t, 403, response.Code) } func TestServer_Auth_Fail_InvalidPass(t *testing.T) { - c := newTestConfig(t) - c.AuthFile = filepath.Join(t.TempDir(), "user.db") - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "INVALID"), + response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "INVALID"), + }) + require.Equal(t, 401, response.Code) }) - require.Equal(t, 401, response.Code) } func TestServer_Auth_Fail_Unauthorized(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic! + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic! - response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), + response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 403, response.Code) }) - require.Equal(t, 403, response.Code) } func TestServer_Auth_Fail_CannotPublish(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionReadWrite // Open by default - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionReadWrite // Open by default + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) - require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll)) - require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll)) + require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) - response := request(t, s, "PUT", "/mytopic", "test", nil) - require.Equal(t, 200, response.Code) + response := request(t, s, "PUT", "/mytopic", "test", nil) + require.Equal(t, 200, response.Code) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - response = request(t, s, "PUT", "/announcements", "test", nil) - require.Equal(t, 403, response.Code) // Cannot write as anonymous + response = request(t, s, "PUT", "/announcements", "test", nil) + require.Equal(t, 403, response.Code) // Cannot write as anonymous - response = request(t, s, "PUT", "/announcements", "test", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + response = request(t, s, "PUT", "/announcements", "test", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + + response = request(t, s, "GET", "/announcements/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) // Anonymous read allowed + + response = request(t, s, "GET", "/private/json?poll=1", "", nil) + require.Equal(t, 403, response.Code) // Anonymous read not allowed }) - require.Equal(t, 200, response.Code) - - response = request(t, s, "GET", "/announcements/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) // Anonymous read allowed - - response = request(t, s, "GET", "/private/json?poll=1", "", nil) - require.Equal(t, 403, response.Code) // Anonymous read not allowed } func TestServer_Auth_Fail_Rate_Limiting(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.VisitorAuthFailureLimitBurst = 10 - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.VisitorAuthFailureLimitBurst = 10 + s := newTestServer(t, c) + + for i := 0; i < 10; i++ { + response := request(t, s, "PUT", "/announcements", "test", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 401, response.Code) + } - for i := 0; i < 10; i++ { response := request(t, s, "PUT", "/announcements", "test", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) - require.Equal(t, 401, response.Code) - } - - response := request(t, s, "PUT", "/announcements", "test", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + require.Equal(t, 429, response.Code) + require.Equal(t, 42909, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 429, response.Code) - require.Equal(t, 42909, toHTTPError(t, response.Body.String()).Code) } func TestServer_Auth_ViaQuery(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, c) - require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false)) + require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false)) - u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass")))) - response := request(t, s, "GET", u, "", nil) - require.Equal(t, 200, response.Code) + u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass")))) + response := request(t, s, "GET", u, "", nil) + require.Equal(t, 200, response.Code) - u = fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "WRONNNGGGG")))) - response = request(t, s, "GET", u, "", nil) - require.Equal(t, 401, response.Code) + u = fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "WRONNNGGGG")))) + response = request(t, s, "GET", u, "", nil) + require.Equal(t, 401, response.Code) + }) } func TestServer_Auth_NonBasicHeader(t *testing.T) { - s := newTestServer(t, newTestConfigWithAuthFile(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithAuthFile(t)) - response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ - "Authorization": "WebPush not-supported", - }) - require.Equal(t, 200, response.Code) + response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Authorization": "WebPush not-supported", + }) + require.Equal(t, 200, response.Code) - response = request(t, s, "PUT", "/mytopic", "test", map[string]string{ - "Authorization": "Bearer supported", - }) - require.Equal(t, 401, response.Code) + response = request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Authorization": "Bearer supported", + }) + require.Equal(t, 401, response.Code) - response = request(t, s, "PUT", "/mytopic", "test", map[string]string{ - "Authorization": "basic supported", + response = request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Authorization": "basic supported", + }) + require.Equal(t, 401, response.Code) }) - require.Equal(t, 401, response.Code) } func TestServer_StatsResetter(t *testing.T) { - t.Parallel() - // This tests the stats resetter for - // - an anonymous user - // - a user without a tier (treated like the same as the anonymous user) - // - a user with a tier + forEachBackend(t, func(t *testing.T) { + t.Parallel() + // This tests the stats resetter for + // - an anonymous user + // - a user without a tier (treated like the same as the anonymous user) + // - a user with a tier - c := newTestConfigWithAuthFile(t) - c.VisitorStatsResetTime = time.Now().Add(2 * time.Second) - s := newTestServer(t, c) - go s.runStatsResetter() + c := newTestConfigWithAuthFile(t) + c.VisitorStatsResetTime = time.Now().Add(2 * time.Second) + s := newTestServer(t, c) + go s.runStatsResetter() - // Create user with tier (tieruser) and user without tier (phil) - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - MessageLimit: 5, - MessageExpiryDuration: -5 * time.Second, // Second, what a hack! - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("tieruser", "test")) + // Create user with tier (tieruser) and user without tier (phil) + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + MessageLimit: 5, + MessageExpiryDuration: -5 * time.Second, // Second, what a hack! + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("tieruser", "test")) - // Send an anonymous message - response := request(t, s, "PUT", "/mytopic", "test", nil) - require.Equal(t, 200, response.Code) + // Send an anonymous message + response := request(t, s, "PUT", "/mytopic", "test", nil) + require.Equal(t, 200, response.Code) - // Send messages from user without tier (phil) - for i := 0; i < 5; i++ { - response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + // Send messages from user without tier (phil) + for i := 0; i < 5; i++ { + response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + } + + // Send messages from user with tier + for i := 0; i < 2; i++ { + response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Authorization": util.BasicAuth("tieruser", "tieruser"), + }) + require.Equal(t, 200, response.Code) + } + + // User stats show 6 messages (for user without tier) + response = request(t, s, "GET", "/v1/account", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, response.Code) - } + account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(6), account.Stats.Messages) - // Send messages from user with tier - for i := 0; i < 2; i++ { - response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + // User stats show 6 messages (for anonymous visitor) + response = request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, response.Code) + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(6), account.Stats.Messages) + + // User stats show 2 messages (for user with tier) + response = request(t, s, "GET", "/v1/account", "", map[string]string{ "Authorization": util.BasicAuth("tieruser", "tieruser"), }) require.Equal(t, 200, response.Code) - } + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(2), account.Stats.Messages) - // User stats show 6 messages (for user without tier) - response = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(6), account.Stats.Messages) + // Wait for stats resetter to run + waitFor(t, func() bool { + response = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + return account.Stats.Messages == 0 + }) - // User stats show 6 messages (for anonymous visitor) - response = request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, response.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(6), account.Stats.Messages) - - // User stats show 2 messages (for user with tier) - response = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("tieruser", "tieruser"), - }) - require.Equal(t, 200, response.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(2), account.Stats.Messages) - - // Wait for stats resetter to run - waitFor(t, func() bool { + // User stats show 0 messages now! response = request(t, s, "GET", "/v1/account", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, response.Code) account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) require.Nil(t, err) - return account.Stats.Messages == 0 - }) + require.Equal(t, int64(0), account.Stats.Messages) - // User stats show 0 messages now! - response = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(0), account.Stats.Messages) + // Since this is a user without a tier, the anonymous user should have the same stats + response = request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, response.Code) + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(0), account.Stats.Messages) - // Since this is a user without a tier, the anonymous user should have the same stats - response = request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, response.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(0), account.Stats.Messages) - - // User stats show 0 messages (for user with tier) - response = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("tieruser", "tieruser"), + // User stats show 0 messages (for user with tier) + response = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("tieruser", "tieruser"), + }) + require.Equal(t, 200, response.Code) + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(0), account.Stats.Messages) }) - require.Equal(t, 200, response.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(0), account.Stats.Messages) } func TestServer_StatsResetter_MessageLimiter_EmailsLimiter(t *testing.T) { - // This tests that the messageLimiter (the only fixed limiter) and the emailsLimiter (token bucket) - // is reset by the stats resetter + forEachBackend(t, func(t *testing.T) { + // This tests that the messageLimiter (the only fixed limiter) and the emailsLimiter (token bucket) + // is reset by the stats resetter - c := newTestConfigWithAuthFile(t) - s := newTestServer(t, c) - s.smtpSender = &testMailer{} + c := newTestConfigWithAuthFile(t) + s := newTestServer(t, c) + s.smtpSender = &testMailer{} - // Publish some messages, and check stats - for i := 0; i < 3; i++ { - response := request(t, s, "PUT", "/mytopic", "test", nil) + // Publish some messages, and check stats + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/mytopic", "test", nil) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ + "Email": "test@email.com", + }) require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "test", map[string]string{ - "Email": "test@email.com", + + rr := request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, rr.Code) + account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.Equal(t, int64(4), account.Stats.Messages) + require.Equal(t, int64(1), account.Stats.Emails) + v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil) + require.Equal(t, int64(4), v.Stats().Messages) + require.Equal(t, int64(4), v.messagesLimiter.Value()) + require.Equal(t, int64(1), v.Stats().Emails) + require.Equal(t, int64(1), v.emailsLimiter.Value()) + + // Reset stats and check again + s.resetStats() + rr = request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, rr.Code) + account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.Equal(t, int64(0), account.Stats.Messages) + require.Equal(t, int64(0), account.Stats.Emails) + v = s.visitor(netip.MustParseAddr("9.9.9.9"), nil) + require.Equal(t, int64(0), v.Stats().Messages) + require.Equal(t, int64(0), v.messagesLimiter.Value()) + require.Equal(t, int64(0), v.Stats().Emails) + require.Equal(t, int64(0), v.emailsLimiter.Value()) }) - require.Equal(t, 200, response.Code) - - rr := request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, rr.Code) - account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.Equal(t, int64(4), account.Stats.Messages) - require.Equal(t, int64(1), account.Stats.Emails) - v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil) - require.Equal(t, int64(4), v.Stats().Messages) - require.Equal(t, int64(4), v.messagesLimiter.Value()) - require.Equal(t, int64(1), v.Stats().Emails) - require.Equal(t, int64(1), v.emailsLimiter.Value()) - - // Reset stats and check again - s.resetStats() - rr = request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, rr.Code) - account, err = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.Equal(t, int64(0), account.Stats.Messages) - require.Equal(t, int64(0), account.Stats.Emails) - v = s.visitor(netip.MustParseAddr("9.9.9.9"), nil) - require.Equal(t, int64(0), v.Stats().Messages) - require.Equal(t, int64(0), v.messagesLimiter.Value()) - require.Equal(t, int64(0), v.Stats().Emails) - require.Equal(t, int64(0), v.emailsLimiter.Value()) } func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) { - t.Parallel() + forEachBackend(t, func(t *testing.T) { + t.Parallel() - // This tests that the daily message quota is prefilled originally from the database, - // if the visitor is unknown + // This tests that the daily message quota is prefilled originally from the database, + // if the visitor is unknown - c := newTestConfigWithAuthFile(t) - c.AuthStatsQueueWriterInterval = 100 * time.Millisecond - s := newTestServer(t, c) + c := newTestConfigWithAuthFile(t) + c.AuthStatsQueueWriterInterval = 100 * time.Millisecond + s := newTestServer(t, c) - // Create user, and update it with some message and email stats - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "test")) + // Create user, and update it with some message and email stats + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "test")) - u, err := s.userManager.User("phil") - require.Nil(t, err) - s.userManager.EnqueueUserStats(u.ID, &user.Stats{ - Messages: 123456, - Emails: 999, + u, err := s.userManager.User("phil") + require.Nil(t, err) + s.userManager.EnqueueUserStats(u.ID, &user.Stats{ + Messages: 123456, + Emails: 999, + }) + time.Sleep(400 * time.Millisecond) + + // Get account and verify stats are read from the DB, and that the visitor also has these stats + rr := request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Nil(t, err) + require.Equal(t, int64(123456), account.Stats.Messages) + require.Equal(t, int64(999), account.Stats.Emails) + v := s.visitor(netip.MustParseAddr("9.9.9.9"), u) + require.Equal(t, int64(123456), v.Stats().Messages) + require.Equal(t, int64(123456), v.messagesLimiter.Value()) + require.Equal(t, int64(999), v.Stats().Emails) + require.Equal(t, int64(999), v.emailsLimiter.Value()) }) - time.Sleep(400 * time.Millisecond) - - // Get account and verify stats are read from the DB, and that the visitor also has these stats - rr := request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Nil(t, err) - require.Equal(t, int64(123456), account.Stats.Messages) - require.Equal(t, int64(999), account.Stats.Emails) - v := s.visitor(netip.MustParseAddr("9.9.9.9"), u) - require.Equal(t, int64(123456), v.Stats().Messages) - require.Equal(t, int64(123456), v.messagesLimiter.Value()) - require.Equal(t, int64(999), v.Stats().Emails) - require.Equal(t, int64(999), v.emailsLimiter.Value()) } type testMailer struct { @@ -1229,561 +1339,630 @@ func (t *testMailer) Count() int { } func TestServer_PublishTooManyRequests_Defaults(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - for i := 0; i < 60; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "message", nil) - require.Equal(t, 429, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + for i := 0; i < 60; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil) + require.Equal(t, 429, response.Code) + }) } func TestServer_PublishTooManyRequests_Defaults_IPv6(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - overrideRemoteAddr1 := func(r *http.Request) { - r.RemoteAddr = "[2001:db8:9999:8888:1::1]:1234" - } - overrideRemoteAddr2 := func(r *http.Request) { - r.RemoteAddr = "[2001:db8:9999:8888:2::1]:1234" // Same /64 - } - for i := 0; i < 30; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) - require.Equal(t, 200, response.Code) - } - for i := 0; i < 30; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) - require.Equal(t, 429, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + overrideRemoteAddr1 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999:8888:1::1]:1234" + } + overrideRemoteAddr2 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999:8888:2::1]:1234" // Same /64 + } + for i := 0; i < 30; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) + require.Equal(t, 200, response.Code) + } + for i := 0; i < 30; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) + require.Equal(t, 429, response.Code) + }) } func TestServer_PublishTooManyRequests_IPv6_Slash48(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 6 - c.VisitorPrefixBitsIPv6 = 48 // Use /48 for IPv6 prefixes - s := newTestServer(t, c) - overrideRemoteAddr1 := func(r *http.Request) { - r.RemoteAddr = "[2001:db8:9999::1]:1234" - } - overrideRemoteAddr2 := func(r *http.Request) { - r.RemoteAddr = "[2001:db8:9999::2]:1234" // Same /48 - } - for i := 0; i < 3; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) - require.Equal(t, 200, response.Code) - } - for i := 0; i < 3; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) - require.Equal(t, 429, response.Code) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 6 + c.VisitorPrefixBitsIPv6 = 48 // Use /48 for IPv6 prefixes + s := newTestServer(t, c) + overrideRemoteAddr1 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::1]:1234" + } + overrideRemoteAddr2 := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::2]:1234" // Same /48 + } + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr1) + require.Equal(t, 200, response.Code) + } + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr2) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil, overrideRemoteAddr1) + require.Equal(t, 429, response.Code) + }) } func TestServer_PublishTooManyRequests_Defaults_ExemptHosts(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() - s := newTestServer(t, c) - for i := 0; i < 5; i++ { // > 3 - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) - require.Equal(t, 200, response.Code) - } + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() + s := newTestServer(t, c) + for i := 0; i < 5; i++ { // > 3 + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) + require.Equal(t, 200, response.Code) + } + }) } func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_IPv6(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")} - s := newTestServer(t, c) - overrideRemoteAddr := func(r *http.Request) { - r.RemoteAddr = "[2001:db8:9999::1]:1234" - } - for i := 0; i < 5; i++ { // > 3 - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr) - require.Equal(t, 200, response.Code) - } + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:9999::/48")} + s := newTestServer(t, c) + overrideRemoteAddr := func(r *http.Request) { + r.RemoteAddr = "[2001:db8:9999::1]:1234" + } + for i := 0; i < 5; i++ { // > 3 + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil, overrideRemoteAddr) + require.Equal(t, 200, response.Code) + } + }) } func TestServer_PublishTooManyRequests_Defaults_ExemptHosts_MessageDailyLimit(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 10 - c.VisitorMessageDailyLimit = 4 - c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() - s := newTestServer(t, c) - for i := 0; i < 8; i++ { // 4 - response := request(t, s, "PUT", "/mytopic", "message", nil) - require.Equal(t, 200, response.Code) - } + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 10 + c.VisitorMessageDailyLimit = 4 + c.VisitorRequestExemptPrefixes = []netip.Prefix{netip.MustParsePrefix("9.9.9.9/32")} // see request() + s := newTestServer(t, c) + for i := 0; i < 8; i++ { // 4 + response := request(t, s, "PUT", "/mytopic", "message", nil) + require.Equal(t, 200, response.Code) + } + }) } func TestServer_PublishTooManyRequests_ShortReplenish(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 60 - c.VisitorRequestLimitReplenish = time.Second - s := newTestServer(t, c) - for i := 0; i < 60; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "message", nil) - require.Equal(t, 429, response.Code) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 60 + c.VisitorRequestLimitReplenish = time.Second + s := newTestServer(t, c) + for i := 0; i < 60; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), nil) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "message", nil) + require.Equal(t, 429, response.Code) - time.Sleep(1020 * time.Millisecond) - response = request(t, s, "PUT", "/mytopic", "message", nil) - require.Equal(t, 200, response.Code) + time.Sleep(1020 * time.Millisecond) + response = request(t, s, "PUT", "/mytopic", "message", nil) + require.Equal(t, 200, response.Code) + }) } func TestServer_PublishTooManyEmails_Defaults(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - s.smtpSender = &testMailer{} - for i := 0; i < 16; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + s.smtpSender = &testMailer{} + for i := 0; i < 16; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ + "E-Mail": "test@example.com", + }) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{ "E-Mail": "test@example.com", }) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{ - "E-Mail": "test@example.com", + require.Equal(t, 429, response.Code) }) - require.Equal(t, 429, response.Code) } func TestServer_PublishTooManyEmails_Replenish(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.VisitorEmailLimitReplenish = 500 * time.Millisecond - s := newTestServer(t, c) - s.smtpSender = &testMailer{} - for i := 0; i < 16; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.VisitorEmailLimitReplenish = 500 * time.Millisecond + s := newTestServer(t, c) + s.smtpSender = &testMailer{} + for i := 0; i < 16; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{ + "E-Mail": "test@example.com", + }) + require.Equal(t, 200, response.Code) + } + response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{ + "E-Mail": "test@example.com", + }) + require.Equal(t, 429, response.Code) + + time.Sleep(510 * time.Millisecond) + response = request(t, s, "PUT", "/mytopic", "this should be okay again too many", map[string]string{ "E-Mail": "test@example.com", }) require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{ - "E-Mail": "test@example.com", - }) - require.Equal(t, 429, response.Code) - time.Sleep(510 * time.Millisecond) - response = request(t, s, "PUT", "/mytopic", "this should be okay again too many", map[string]string{ - "E-Mail": "test@example.com", + response = request(t, s, "PUT", "/mytopic", "and bad again", map[string]string{ + "E-Mail": "test@example.com", + }) + require.Equal(t, 429, response.Code) }) - require.Equal(t, 200, response.Code) - - response = request(t, s, "PUT", "/mytopic", "and bad again", map[string]string{ - "E-Mail": "test@example.com", - }) - require.Equal(t, 429, response.Code) } func TestServer_PublishDelayedEmail_Fail(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - s.smtpSender = &testMailer{} - response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ - "E-Mail": "test@example.com", - "Delay": "20 min", + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + s.smtpSender = &testMailer{} + response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ + "E-Mail": "test@example.com", + "Delay": "20 min", + }) + require.Equal(t, 40003, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40003, toHTTPError(t, response.Body.String()).Code) } func TestServer_PublishDelayedCall_Fail(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ - "Call": "yes", - "Delay": "20 min", + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ + "Call": "yes", + "Delay": "20 min", + }) + require.Equal(t, 40037, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40037, toHTTPError(t, response.Body.String()).Code) } func TestServer_PublishEmailNoMailer_Fail(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ - "E-Mail": "test@example.com", + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{ + "E-Mail": "test@example.com", + }) + require.Equal(t, 400, response.Code) }) - require.Equal(t, 400, response.Code) } func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - defer s.messageCache.Close() + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + defer s.messageCache.Close() - subFn := func(v *visitor, msg *model.Message) error { - return nil - } - - // Publish and check last access - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "Cache": "no", - }) - require.Equal(t, 200, response.Code) - waitFor(t, func() bool { - s.mu.Lock() - tp, exists := s.topics["mytopic"] - s.mu.Unlock() - if !exists { - return false + subFn := func(v *visitor, msg *model.Message) error { + return nil } - // .lastAccess set in t.Publish() -> t.Keepalive() in Goroutine - tp.mu.RLock() - defer tp.mu.RUnlock() - return tp.lastAccess.Unix() >= time.Now().Unix()-2 && - tp.lastAccess.Unix() <= time.Now().Unix()+2 + + // Publish and check last access + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "Cache": "no", + }) + require.Equal(t, 200, response.Code) + waitFor(t, func() bool { + s.mu.Lock() + tp, exists := s.topics["mytopic"] + s.mu.Unlock() + if !exists { + return false + } + // .lastAccess set in t.Publish() -> t.Keepalive() in Goroutine + tp.mu.RLock() + defer tp.mu.RUnlock() + return tp.lastAccess.Unix() >= time.Now().Unix()-2 && + tp.lastAccess.Unix() <= time.Now().Unix()+2 + }) + + // Hack! + time.Sleep(time.Second) + + // Topic won't get pruned + s.execManager() + require.NotNil(t, s.topics["mytopic"]) + + // Fudge with last access, but subscribe, and see that it won't get pruned (because of subscriber) + subID := s.topics["mytopic"].Subscribe(subFn, "", func() {}) + s.topics["mytopic"].mu.Lock() + s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) + s.topics["mytopic"].mu.Unlock() + s.execManager() + require.NotNil(t, s.topics["mytopic"]) + + // It'll finally get pruned now that there are no subscribers and last access is 17 hours ago + s.topics["mytopic"].Unsubscribe(subID) + s.execManager() + require.Nil(t, s.topics["mytopic"]) }) - - // Hack! - time.Sleep(time.Second) - - // Topic won't get pruned - s.execManager() - require.NotNil(t, s.topics["mytopic"]) - - // Fudge with last access, but subscribe, and see that it won't get pruned (because of subscriber) - subID := s.topics["mytopic"].Subscribe(subFn, "", func() {}) - s.topics["mytopic"].mu.Lock() - s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) - s.topics["mytopic"].mu.Unlock() - s.execManager() - require.NotNil(t, s.topics["mytopic"]) - - // It'll finally get pruned now that there are no subscribers and last access is 17 hours ago - s.topics["mytopic"].Unsubscribe(subID) - s.execManager() - require.Nil(t, s.topics["mytopic"]) } func TestServer_TopicKeepaliveOnPoll(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Create topic by polling once - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + // Create topic by polling once + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - // Mess with last access time - s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) + // Mess with last access time + s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) - // Poll again and check keepalive time - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - require.True(t, s.topics["mytopic"].lastAccess.Unix() >= time.Now().Unix()-2) - require.True(t, s.topics["mytopic"].lastAccess.Unix() <= time.Now().Unix()+2) + // Poll again and check keepalive time + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + require.True(t, s.topics["mytopic"].lastAccess.Unix() >= time.Now().Unix()-2) + require.True(t, s.topics["mytopic"].lastAccess.Unix() <= time.Now().Unix()+2) + }) } func TestServer_UnifiedPushDiscovery(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/mytopic?up=1", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "GET", "/mytopic?up=1", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) + }) } func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) { - b := make([]byte, 12) // Max length - _, err := rand.Read(b) - require.Nil(t, err) + forEachBackend(t, func(t *testing.T) { + b := make([]byte, 12) // Max length + _, err := rand.Read(b) + require.Nil(t, err) - s := newTestServer(t, newTestConfig(t)) + s := newTestServer(t, newTestConfig(t)) - // Register a UnifiedPush subscriber - response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + // Register a UnifiedPush subscriber + response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - // Publish message to topic - response = request(t, s, "PUT", "/up123456789012?up=1", string(b), nil) - require.Equal(t, 200, response.Code) + // Publish message to topic + response = request(t, s, "PUT", "/up123456789012?up=1", string(b), nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "base64", m.Encoding) - b2, err := base64.StdEncoding.DecodeString(m.Message) - require.Nil(t, err) - require.Equal(t, b, b2) + m := toMessage(t, response.Body.String()) + require.Equal(t, "base64", m.Encoding) + b2, err := base64.StdEncoding.DecodeString(m.Message) + require.Nil(t, err) + require.Equal(t, b, b2) - // Retrieve and check published message - response = request(t, s, "GET", "/up123456789012/json?poll=1", string(b), nil) - require.Equal(t, 200, response.Code) - m = toMessage(t, response.Body.String()) - require.Equal(t, "base64", m.Encoding) - b2, err = base64.StdEncoding.DecodeString(m.Message) - require.Nil(t, err) - require.Equal(t, b, b2) + // Retrieve and check published message + response = request(t, s, "GET", "/up123456789012/json?poll=1", string(b), nil) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, "base64", m.Encoding) + b2, err = base64.StdEncoding.DecodeString(m.Message) + require.Nil(t, err) + require.Equal(t, b, b2) + }) } func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) { - b := make([]byte, 5000) // Longer than max length - _, err := rand.Read(b) - require.Nil(t, err) + forEachBackend(t, func(t *testing.T) { + b := make([]byte, 5000) // Longer than max length + _, err := rand.Read(b) + require.Nil(t, err) - s := newTestServer(t, newTestConfig(t)) + s := newTestServer(t, newTestConfig(t)) - // Register a UnifiedPush subscriber - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + // Register a UnifiedPush subscriber + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - // Publish message to topic - response = request(t, s, "PUT", "/mytopic?up=1", string(b), nil) - require.Equal(t, 200, response.Code) + // Publish message to topic + response = request(t, s, "PUT", "/mytopic?up=1", string(b), nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "base64", m.Encoding) - b2, err := base64.StdEncoding.DecodeString(m.Message) - require.Nil(t, err) - require.Equal(t, 4096, len(b2)) - require.Equal(t, b[:4096], b2) + m := toMessage(t, response.Body.String()) + require.Equal(t, "base64", m.Encoding) + b2, err := base64.StdEncoding.DecodeString(m.Message) + require.Nil(t, err) + require.Equal(t, 4096, len(b2)) + require.Equal(t, b[:4096], b2) + }) } func TestServer_PublishUnifiedPushText(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - // Register a UnifiedPush subscriber - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + // Register a UnifiedPush subscriber + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - // Publish UnifiedPush text message - response = request(t, s, "PUT", "/mytopic?up=1", "this is a unifiedpush text message", nil) - require.Equal(t, 200, response.Code) + // Publish UnifiedPush text message + response = request(t, s, "PUT", "/mytopic?up=1", "this is a unifiedpush text message", nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "", m.Encoding) - require.Equal(t, "this is a unifiedpush text message", m.Message) + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.Encoding) + require.Equal(t, "this is a unifiedpush text message", m.Message) + }) } func TestServer_MatrixGateway_Discovery_Success(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"unifiedpush":{"gateway":"matrix"}}`+"\n", response.Body.String()) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"unifiedpush":{"gateway":"matrix"}}`+"\n", response.Body.String()) + }) } func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing.T) { - c := newTestConfig(t) - c.BaseURL = "" - s := newTestServer(t, c) - response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil) - require.Equal(t, 500, response.Code) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 50003, err.Code) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BaseURL = "" + s := newTestServer(t, c) + response := request(t, s, "GET", "/_matrix/push/v1/notify", "", nil) + require.Equal(t, 500, response.Code) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 50003, err.Code) + }) } func TestServer_MatrixGateway_Push_Success(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) + response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) - notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` - response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String()) + notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` + response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String()) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, notification, m.Message) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, notification, m.Message) + }) } func TestServer_MatrixGateway_Push_Failure_NoSubscriber(t *testing.T) { - c := newTestConfig(t) - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) - notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 507, response.Code) - require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) + notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 507, response.Code) + require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MatrixGateway_Push_Failure_NoSubscriber_After13Hours(t *testing.T) { - c := newTestConfig(t) - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) - notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) + notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` - // No success if no rate visitor set (this also creates the topic in memory) - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 507, response.Code) - require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code) - require.Nil(t, s.topics["mytopic"].rateVisitor) + // No success if no rate visitor set (this also creates the topic in memory) + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 507, response.Code) + require.Equal(t, 50701, toHTTPError(t, response.Body.String()).Code) + require.Nil(t, s.topics["mytopic"].rateVisitor) - // Fake: This topic has been around for 13 hours without a rate visitor - s.topics["mytopic"].lastAccess = time.Now().Add(-13 * time.Hour) + // Fake: This topic has been around for 13 hours without a rate visitor + s.topics["mytopic"].lastAccess = time.Now().Add(-13 * time.Hour) - // Same request should now return HTTP 200 with a rejected pushkey - response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"rejected":["http://127.0.0.1:12345/mytopic?up=1"]}`, strings.TrimSpace(response.Body.String())) + // Same request should now return HTTP 200 with a rejected pushkey + response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":["http://127.0.0.1:12345/mytopic?up=1"]}`, strings.TrimSpace(response.Body.String())) - // Slightly unrelated: Test that topic is pruned after 16 hours - s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) - s.execManager() - require.Nil(t, s.topics["mytopic"]) + // Slightly unrelated: Test that topic is pruned after 16 hours + s.topics["mytopic"].lastAccess = time.Now().Add(-17 * time.Hour) + s.execManager() + require.Nil(t, s.topics["mytopic"]) + }) } func TestServer_MatrixGateway_Push_Failure_InvalidPushkey(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - notification := `{"notification":{"devices":[{"pushkey":"http://wrong-base-url.com/mytopic?up=1"}]}}` - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"rejected":["http://wrong-base-url.com/mytopic?up=1"]}`+"\n", response.Body.String()) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + notification := `{"notification":{"devices":[{"pushkey":"http://wrong-base-url.com/mytopic?up=1"}]}}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":["http://wrong-base-url.com/mytopic?up=1"]}`+"\n", response.Body.String()) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, "", response.Body.String()) // Empty! + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, "", response.Body.String()) // Empty! + }) } func TestServer_MatrixGateway_Push_Failure_EverythingIsWrong(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - notification := `{"message":"this is not really a Matrix message"}` - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 400, response.Code) - require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + notification := `{"message":"this is not really a Matrix message"}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 400, response.Code) + require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code) - notification = `this isn't even JSON'` - response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 400, response.Code) - require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code) + notification = `this isn't even JSON'` + response = request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 400, response.Code) + require.Equal(t, 40019, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MatrixGateway_Push_Failure_Unconfigured(t *testing.T) { - c := newTestConfig(t) - c.BaseURL = "" - s := newTestServer(t, c) - notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 500, response.Code) - require.Equal(t, 50003, toHTTPError(t, response.Body.String()).Code) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BaseURL = "" + s := newTestServer(t, c) + notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}` + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 500, response.Code) + require.Equal(t, 50003, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_PublishActions_AndPoll(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "my message", map[string]string{ - "Actions": "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65", - }) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "my message", map[string]string{ + "Actions": "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65", + }) + require.Equal(t, 200, response.Code) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, 2, len(m.Actions)) - require.Equal(t, "view", m.Actions[0].Action) - require.Equal(t, "Open portal", m.Actions[0].Label) - require.Equal(t, "https://home.nest.com/", m.Actions[0].URL) - require.Equal(t, "http", m.Actions[1].Action) - require.Equal(t, "Turn down", m.Actions[1].Label) - require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL) - require.Equal(t, "target_temp_f=65", m.Actions[1].Body) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, 2, len(m.Actions)) + require.Equal(t, "view", m.Actions[0].Action) + require.Equal(t, "Open portal", m.Actions[0].Label) + require.Equal(t, "https://home.nest.com/", m.Actions[0].URL) + require.Equal(t, "http", m.Actions[1].Action) + require.Equal(t, "Turn down", m.Actions[1].Label) + require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL) + require.Equal(t, "target_temp_f=65", m.Actions[1].Body) + }) } func TestServer_PublishMarkdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ - "Content-Type": "text/markdown", - }) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ + "Content-Type": "text/markdown", + }) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "**make this bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) + m := toMessage(t, response.Body.String()) + require.Equal(t, "**make this bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) + }) } func TestServer_PublishMarkdown_QueryParam(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?md=1", "**make this bold**", nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "**make this bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) + m := toMessage(t, response.Body.String()) + require.Equal(t, "**make this bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) + }) } func TestServer_PublishMarkdown_NotMarkdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ - "Content-Type": "not-markdown", - }) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "**make this bold**", map[string]string{ + "Content-Type": "not-markdown", + }) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "", m.ContentType) + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.ContentType) + }) } func TestServer_PublishAsJSON(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + - `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` + - `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` + + `"not-a-thing":"ok", "attach":"http://google.com","filename":"google.pdf", "click":"http://ntfy.sh","priority":4,` + + `"icon":"https://ntfy.sh/static/img/ntfy.png", "delay":"30min"}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "A message", m.Message) - require.Equal(t, "a title\nwith lines", m.Title) - require.Equal(t, []string{"tag1", "tag 2"}, m.Tags) - require.Equal(t, "http://google.com", m.Attachment.URL) - require.Equal(t, "google.pdf", m.Attachment.Name) - require.Equal(t, "http://ntfy.sh", m.Click) - require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) - require.Equal(t, "", m.ContentType) + m := toMessage(t, response.Body.String()) + require.Equal(t, "mytopic", m.Topic) + require.Equal(t, "A message", m.Message) + require.Equal(t, "a title\nwith lines", m.Title) + require.Equal(t, []string{"tag1", "tag 2"}, m.Tags) + require.Equal(t, "http://google.com", m.Attachment.URL) + require.Equal(t, "google.pdf", m.Attachment.Name) + require.Equal(t, "http://ntfy.sh", m.Click) + require.Equal(t, "https://ntfy.sh/static/img/ntfy.png", m.Icon) + require.Equal(t, "", m.ContentType) - require.Equal(t, 4, m.Priority) - require.True(t, m.Time > time.Now().Unix()+29*60) - require.True(t, m.Time < time.Now().Unix()+31*60) + require.Equal(t, 4, m.Priority) + require.True(t, m.Time > time.Now().Unix()+29*60) + require.True(t, m.Time < time.Now().Unix()+31*60) + }) } func TestServer_PublishAsJSON_Markdown(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 200, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message":"**This is bold**","markdown":true}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "**This is bold**", m.Message) - require.Equal(t, "text/markdown", m.ContentType) + m := toMessage(t, response.Body.String()) + require.Equal(t, "mytopic", m.Topic) + require.Equal(t, "**This is bold**", m.Message) + require.Equal(t, "text/markdown", m.ContentType) + }) } func TestServer_PublishAsJSON_RateLimit_MessageDailyLimit(t *testing.T) { - // Publishing as JSON follows a different path. This ensures that rate - // limiting works for this endpoint as well - c := newTestConfig(t) - c.VisitorMessageDailyLimit = 3 - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + // Publishing as JSON follows a different path. This ensures that rate + // limiting works for this endpoint as well + c := newTestConfig(t) + c.VisitorMessageDailyLimit = 3 + s := newTestServer(t, c) - for i := 0; i < 3; i++ { + for i := 0; i < 3; i++ { + response := request(t, s, "PUT", "/", `{"topic":"mytopic","message":"A message"}`, nil) + require.Equal(t, 200, response.Code) + } response := request(t, s, "PUT", "/", `{"topic":"mytopic","message":"A message"}`, nil) - require.Equal(t, 200, response.Code) - } - response := request(t, s, "PUT", "/", `{"topic":"mytopic","message":"A message"}`, nil) - require.Equal(t, 429, response.Code) - require.Equal(t, 42908, toHTTPError(t, response.Body.String()).Code) + require.Equal(t, 429, response.Code) + require.Equal(t, 42908, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_PublishAsJSON_WithEmail(t *testing.T) { - t.Parallel() - mailer := &testMailer{} - s := newTestServer(t, newTestConfig(t)) - s.smtpSender = mailer - body := `{"topic":"mytopic","message":"A message","email":"phil@example.com"}` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 200, response.Code) - time.Sleep(100 * time.Millisecond) // E-Mail publishing happens in a Go routine + forEachBackend(t, func(t *testing.T) { + t.Parallel() + mailer := &testMailer{} + s := newTestServer(t, newTestConfig(t)) + s.smtpSender = mailer + body := `{"topic":"mytopic","message":"A message","email":"phil@example.com"}` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 200, response.Code) + time.Sleep(100 * time.Millisecond) // E-Mail publishing happens in a Go routine - m := toMessage(t, response.Body.String()) - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "A message", m.Message) - require.Equal(t, 1, mailer.Count()) + m := toMessage(t, response.Body.String()) + require.Equal(t, "mytopic", m.Topic) + require.Equal(t, "A message", m.Message) + require.Equal(t, 1, mailer.Count()) + }) } func TestServer_PublishAsJSON_WithActions(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{ + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{ "topic":"mytopic", "message":"A message", "actions": [ @@ -1800,1225 +1979,1333 @@ func TestServer_PublishAsJSON_WithActions(t *testing.T) { } ] }` - response := request(t, s, "POST", "/", body, nil) - require.Equal(t, 200, response.Code) + response := request(t, s, "POST", "/", body, nil) + require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "mytopic", m.Topic) - require.Equal(t, "A message", m.Message) - require.Equal(t, 2, len(m.Actions)) - require.Equal(t, "view", m.Actions[0].Action) - require.Equal(t, "Open portal", m.Actions[0].Label) - require.Equal(t, "https://home.nest.com/", m.Actions[0].URL) - require.Equal(t, "http", m.Actions[1].Action) - require.Equal(t, "Turn down", m.Actions[1].Label) - require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL) - require.Equal(t, "target_temp_f=65", m.Actions[1].Body) + m := toMessage(t, response.Body.String()) + require.Equal(t, "mytopic", m.Topic) + require.Equal(t, "A message", m.Message) + require.Equal(t, 2, len(m.Actions)) + require.Equal(t, "view", m.Actions[0].Action) + require.Equal(t, "Open portal", m.Actions[0].Label) + require.Equal(t, "https://home.nest.com/", m.Actions[0].URL) + require.Equal(t, "http", m.Actions[1].Action) + require.Equal(t, "Turn down", m.Actions[1].Label) + require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL) + require.Equal(t, "target_temp_f=65", m.Actions[1].Body) + }) } func TestServer_PublishAsJSON_NoCache(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}` - response := request(t, s, "PUT", "/", body, nil) - msg := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg.ID) - require.Equal(t, "this message is not cached", msg.Message) - require.Equal(t, int64(0), msg.Expires) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg.ID) + require.Equal(t, "this message is not cached", msg.Message) + require.Equal(t, int64(0), msg.Expires) - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - messages := toMessages(t, response.Body.String()) - require.Empty(t, messages) + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + messages := toMessages(t, response.Body.String()) + require.Empty(t, messages) + }) } func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) { - sender := newTestFirebaseSender(10) - s := newTestServer(t, newTestConfig(t)) - s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + forEachBackend(t, func(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) - body := `{"topic":"mytopic","message": "my first message","firebase":"no"}` - response := request(t, s, "PUT", "/", body, nil) - msg1 := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg1.ID) - require.Equal(t, "my first message", msg1.Message) + body := `{"topic":"mytopic","message": "my first message","firebase":"no"}` + response := request(t, s, "PUT", "/", body, nil) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Equal(t, "my first message", msg1.Message) - time.Sleep(100 * time.Millisecond) // Firebase publishing happens - require.Equal(t, 0, len(sender.Messages())) + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 0, len(sender.Messages())) + }) } func TestServer_PublishAsJSON_Invalid(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - body := `{"topic":"mytopic",INVALID` - response := request(t, s, "PUT", "/", body, nil) - require.Equal(t, 400, response.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + body := `{"topic":"mytopic",INVALID` + response := request(t, s, "PUT", "/", body, nil) + require.Equal(t, 400, response.Code) + }) } func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) { - c := newTestConfigWithAuthFile(t) - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + s := newTestServer(t, c) - // Create tier with certain limits - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - MessageLimit: 5, - MessageExpiryDuration: -5 * time.Second, // Second, what a hack! - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "test")) + // Create tier with certain limits + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + MessageLimit: 5, + MessageExpiryDuration: -5 * time.Second, // Second, what a hack! + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "test")) - // Publish to reach message limit - for i := 0; i < 5; i++ { - response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("this is message %d", i+1), map[string]string{ + // Publish to reach message limit + for i := 0; i < 5; i++ { + response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("this is message %d", i+1), map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.True(t, msg.Expires < time.Now().Unix()+5) + } + response := request(t, s, "PUT", "/mytopic", "this is too much", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 429, response.Code) + + // Run pruning and see if they are gone + s.execManager() + response = request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{ "Authorization": util.BasicAuth("phil", "phil"), }) require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.True(t, msg.Expires < time.Now().Unix()+5) - } - response := request(t, s, "PUT", "/mytopic", "this is too much", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + require.Empty(t, response.Body) }) - require.Equal(t, 429, response.Code) - - // Run pruning and see if they are gone - s.execManager() - response = request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - require.Empty(t, response.Body) } func TestServer_PublishAttachment(t *testing.T) { - content := "text file!" + util.RandomString(4990) // > 4096 - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", content, nil) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "attachment.txt", msg.Attachment.Name) - require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) - require.Equal(t, int64(5000), msg.Attachment.Size) - require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) + forEachBackend(t, func(t *testing.T) { + content := "text file!" + util.RandomString(4990) // > 4096 + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", content, nil) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "attachment.txt", msg.Attachment.Name) + require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) + require.Equal(t, int64(5000), msg.Attachment.Size) + require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(179*time.Minute).Unix()) // Almost 3 hours + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) - // GET - path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, "5000", response.Header().Get("Content-Length")) - require.Equal(t, content, response.Body.String()) + // GET + path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, "5000", response.Header().Get("Content-Length")) + require.Equal(t, content, response.Body.String()) - // HEAD - response = request(t, s, "HEAD", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, "5000", response.Header().Get("Content-Length")) - require.Equal(t, "", response.Body.String()) + // HEAD + response = request(t, s, "HEAD", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, "5000", response.Header().Get("Content-Length")) + require.Equal(t, "", response.Body.String()) - // Slightly unrelated cross-test: make sure we add an owner for internal attachments - size, err := s.messageCache.AttachmentBytesUsedBySender("9.9.9.9") // See request() - require.Nil(t, err) - require.Equal(t, int64(5000), size) + // Slightly unrelated cross-test: make sure we add an owner for internal attachments + size, err := s.messageCache.AttachmentBytesUsedBySender("9.9.9.9") // See request() + require.Nil(t, err) + require.Equal(t, int64(5000), size) + }) } func TestServer_PublishAttachmentShortWithFilename(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - s := newTestServer(t, c) - content := "this is an ATTACHMENT" - response := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, map[string]string{ - "X-Forwarded-For": "1.2.3.4", + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + s := newTestServer(t, c) + content := "this is an ATTACHMENT" + response := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, map[string]string{ + "X-Forwarded-For": "1.2.3.4", + }) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "myfile.txt", msg.Attachment.Name) + require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) + require.Equal(t, int64(21), msg.Attachment.Size) + require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) + + path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, "21", response.Header().Get("Content-Length")) + require.Equal(t, content, response.Body.String()) + + // Slightly unrelated cross-test: make sure we add an owner for internal attachments + size, err := s.messageCache.AttachmentBytesUsedBySender("1.2.3.4") + require.Nil(t, err) + require.Equal(t, int64(21), size) }) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "myfile.txt", msg.Attachment.Name) - require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) - require.Equal(t, int64(21), msg.Attachment.Size) - require.GreaterOrEqual(t, msg.Attachment.Expires, time.Now().Add(3*time.Hour).Unix()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.Equal(t, netip.Addr{}, msg.Sender) // Should never be returned - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) - - path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, "21", response.Header().Get("Content-Length")) - require.Equal(t, content, response.Body.String()) - - // Slightly unrelated cross-test: make sure we add an owner for internal attachments - size, err := s.messageCache.AttachmentBytesUsedBySender("1.2.3.4") - require.Nil(t, err) - require.Equal(t, int64(21), size) } func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "", map[string]string{ - "Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", - }) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "You received a file: Pink_flower.jpg", msg.Message) - require.Equal(t, "Pink_flower.jpg", msg.Attachment.Name) - require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL) - require.Equal(t, "", msg.Attachment.Type) - require.Equal(t, int64(0), msg.Attachment.Size) - require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, netip.Addr{}, msg.Sender) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "", map[string]string{ + "Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", + }) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "You received a file: Pink_flower.jpg", msg.Message) + require.Equal(t, "Pink_flower.jpg", msg.Attachment.Name) + require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL) + require.Equal(t, "", msg.Attachment.Type) + require.Equal(t, int64(0), msg.Attachment.Size) + require.Equal(t, int64(0), msg.Attachment.Expires) + require.Equal(t, netip.Addr{}, msg.Sender) - // Slightly unrelated cross-test: make sure we don't add an owner for external attachments - size, err := s.messageCache.AttachmentBytesUsedBySender("127.0.0.1") - require.Nil(t, err) - require.Equal(t, int64(0), size) + // Slightly unrelated cross-test: make sure we don't add an owner for external attachments + size, err := s.messageCache.AttachmentBytesUsedBySender("127.0.0.1") + require.Nil(t, err) + require.Equal(t, int64(0), size) + }) } func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", "This is a custom message", map[string]string{ - "X-Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", - "File": "some file.jpg", + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", "This is a custom message", map[string]string{ + "X-Attach": "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", + "File": "some file.jpg", + }) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "This is a custom message", msg.Message) + require.Equal(t, "some file.jpg", msg.Attachment.Name) + require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL) + require.Equal(t, "", msg.Attachment.Type) + require.Equal(t, int64(0), msg.Attachment.Size) + require.Equal(t, int64(0), msg.Attachment.Expires) + require.Equal(t, netip.Addr{}, msg.Sender) }) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "This is a custom message", msg.Message) - require.Equal(t, "some file.jpg", msg.Attachment.Name) - require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL) - require.Equal(t, "", msg.Attachment.Type) - require.Equal(t, int64(0), msg.Attachment.Size) - require.Equal(t, int64(0), msg.Attachment.Expires) - require.Equal(t, netip.Addr{}, msg.Sender) } func TestServer_PublishAttachmentBadURL(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?a=not+a+URL", "", nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 400, response.Code) - require.Equal(t, 400, err.HTTPCode) - require.Equal(t, 40013, err.Code) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?a=not+a+URL", "", nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 400, response.Code) + require.Equal(t, 400, err.HTTPCode) + require.Equal(t, 40013, err.Code) + }) } func TestServer_PublishAttachmentTooLargeContentLength(t *testing.T) { - content := util.RandomString(5000) // > 4096 - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", content, map[string]string{ - "Content-Length": "20000000", + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(5000) // > 4096 + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", content, map[string]string{ + "Content-Length": "20000000", + }) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 413, response.Code) + require.Equal(t, 413, err.HTTPCode) + require.Equal(t, 41301, err.Code) }) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 413, response.Code) - require.Equal(t, 413, err.HTTPCode) - require.Equal(t, 41301, err.Code) } func TestServer_PublishAttachmentTooLargeBodyAttachmentFileSizeLimit(t *testing.T) { - content := util.RandomString(5001) // > 5000, see below - c := newTestConfig(t) - c.AttachmentFileSizeLimit = 5000 - s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", content, nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 413, response.Code) - require.Equal(t, 413, err.HTTPCode) - require.Equal(t, 41301, err.Code) + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(5001) // > 5000, see below + c := newTestConfig(t) + c.AttachmentFileSizeLimit = 5000 + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", content, nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 413, response.Code) + require.Equal(t, 413, err.HTTPCode) + require.Equal(t, 41301, err.Code) + }) } func TestServer_PublishAttachmentExpiryBeforeDelivery(t *testing.T) { - c := newTestConfig(t) - c.AttachmentExpiryDuration = 10 * time.Minute - s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", util.RandomString(5000), map[string]string{ - "Delay": "11 min", // > AttachmentExpiryDuration + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.AttachmentExpiryDuration = 10 * time.Minute + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", util.RandomString(5000), map[string]string{ + "Delay": "11 min", // > AttachmentExpiryDuration + }) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 400, response.Code) + require.Equal(t, 400, err.HTTPCode) + require.Equal(t, 40015, err.Code) }) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 400, response.Code) - require.Equal(t, 400, err.HTTPCode) - require.Equal(t, 40015, err.Code) } func TestServer_PublishAttachmentTooLargeBodyVisitorAttachmentTotalSizeLimit(t *testing.T) { - c := newTestConfig(t) - c.VisitorAttachmentTotalSizeLimit = 10000 - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorAttachmentTotalSizeLimit = 10000 + s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", "text file!"+util.RandomString(4990), nil) - msg := toMessage(t, response.Body.String()) - require.Equal(t, 200, response.Code) - require.Equal(t, "You received a file: attachment.txt", msg.Message) - require.Equal(t, int64(5000), msg.Attachment.Size) + response := request(t, s, "PUT", "/mytopic", "text file!"+util.RandomString(4990), nil) + msg := toMessage(t, response.Body.String()) + require.Equal(t, 200, response.Code) + require.Equal(t, "You received a file: attachment.txt", msg.Message) + require.Equal(t, int64(5000), msg.Attachment.Size) - content := util.RandomString(5001) // 5000+5001 > , see below - response = request(t, s, "PUT", "/mytopic", content, nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 413, response.Code) - require.Equal(t, 413, err.HTTPCode) - require.Equal(t, 41301, err.Code) + content := util.RandomString(5001) // 5000+5001 > , see below + response = request(t, s, "PUT", "/mytopic", content, nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 413, response.Code) + require.Equal(t, 413, err.HTTPCode) + require.Equal(t, 41301, err.Code) + }) } func TestServer_PublishAttachmentAndExpire(t *testing.T) { - t.Parallel() - content := util.RandomString(5000) // > 4096 + forEachBackend(t, func(t *testing.T) { + t.Parallel() + content := util.RandomString(5000) // > 4096 - c := newTestConfig(t) - c.AttachmentExpiryDuration = time.Millisecond // Hack - s := newTestServer(t, c) + c := newTestConfig(t) + c.AttachmentExpiryDuration = time.Millisecond // Hack + s := newTestServer(t, c) - // Publish and make sure we can retrieve it - response := request(t, s, "PUT", "/mytopic", content, nil) - msg := toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - file := filepath.Join(s.config.AttachmentCacheDir, msg.ID) - require.FileExists(t, file) - - path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, content, response.Body.String()) - - // Prune and makes sure it's gone - waitFor(t, func() bool { - s.execManager() // May run many times - return !util.FileExists(file) - }) - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 404, response.Code) -} - -func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) { - t.Parallel() - content := util.RandomString(5000) // > 4096 - - c := newTestConfigWithAuthFile(t) - c.AttachmentExpiryDuration = time.Millisecond // Hack - s := newTestServer(t, c) - - // Create tier with certain limits - sevenDays := time.Duration(604800) * time.Second - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - MessageLimit: 10, - MessageExpiryDuration: sevenDays, - AttachmentFileSizeLimit: 50_000, - AttachmentTotalSizeLimit: 200_000, - AttachmentExpiryDuration: sevenDays, // 7 days - AttachmentBandwidthLimit: 100000, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "test")) - - // Publish and make sure we can retrieve it - response := request(t, s, "PUT", "/mytopic", content, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.True(t, msg.Attachment.Expires > time.Now().Add(sevenDays-30*time.Second).Unix()) - require.True(t, msg.Expires > time.Now().Add(sevenDays-30*time.Second).Unix()) - file := filepath.Join(s.config.AttachmentCacheDir, msg.ID) - require.FileExists(t, file) - - path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, content, response.Body.String()) - - // Prune and makes sure it's still there - time.Sleep(time.Second) // Sigh ... - s.execManager() - require.FileExists(t, file) - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) -} - -func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) { - content := util.RandomString(5000) // > 4096 - - c := newTestConfigWithAuthFile(t) - c.VisitorAttachmentDailyBandwidthLimit = 1000 // Much lower than tier bandwidth! - s := newTestServer(t, c) - - // Create tier with certain limits - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - MessageLimit: 10, - MessageExpiryDuration: time.Hour, - AttachmentFileSizeLimit: 50_000, - AttachmentTotalSizeLimit: 200_000, - AttachmentExpiryDuration: time.Hour, - AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "test")) - - // Publish and make sure we can retrieve it - rr := request(t, s, "PUT", "/mytopic", content, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, rr.Code) - msg := toMessage(t, rr.Body.String()) - - // Retrieve it (first time succeeds) - rr = request(t, s, "GET", "/file/"+msg.ID, content, nil) // File downloads do not send auth headers!! - require.Equal(t, 200, rr.Code) - require.Equal(t, content, rr.Body.String()) - - // Retrieve it AGAIN (fails, due to bandwidth limit) - rr = request(t, s, "GET", "/file/"+msg.ID, content, nil) - require.Equal(t, 429, rr.Code) -} - -func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) { - smallFile := util.RandomString(20_000) - largeFile := util.RandomString(50_000) - - c := newTestConfigWithAuthFile(t) - c.AttachmentFileSizeLimit = 20_000 - c.VisitorAttachmentTotalSizeLimit = 40_000 - s := newTestServer(t, c) - - // Create tier with certain limits - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "test", - MessageLimit: 100, - AttachmentFileSizeLimit: 50_000, - AttachmentTotalSizeLimit: 200_000, - AttachmentExpiryDuration: 30 * time.Second, - AttachmentBandwidthLimit: 1000000, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "test")) - - // Publish small file as anonymous - response := request(t, s, "PUT", "/mytopic", smallFile, nil) - msg := toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) - - // Publish large file as anonymous - response = request(t, s, "PUT", "/mytopic", largeFile, nil) - require.Equal(t, 413, response.Code) - require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) - - // Publish too large file as phil - response = request(t, s, "PUT", "/mytopic", largeFile+" a few more bytes", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 413, response.Code) - require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) - - // Publish large file as phil (4x) - for i := 0; i < 4; i++ { - response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - msg = toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) - } - response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 413, response.Code) - require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) -} - -func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) { - content := util.RandomString(5000) // > 4096 - - c := newTestConfig(t) - c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 123 // A little more than 1 upload and 3 downloads - s := newTestServer(t, c) - - // Publish attachment - response := request(t, s, "PUT", "/mytopic", content, nil) - msg := toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - - // Value it 4 times successfully - path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") - for i := 1; i <= 4; i++ { // 4 successful downloads - response = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, content, response.Body.String()) - } - - // And then fail with a 429 - response = request(t, s, "GET", path, "", nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 429, response.Code) - require.Equal(t, 42905, err.Code) -} - -func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) { - content := util.RandomString(5000) // > 4096 - - c := newTestConfig(t) - c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 500 // 5 successful uploads - s := newTestServer(t, c) - - // 5 successful uploads - for i := 1; i <= 5; i++ { + // Publish and make sure we can retrieve it response := request(t, s, "PUT", "/mytopic", content, nil) msg := toMessage(t, response.Body.String()) require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - } + file := filepath.Join(s.config.AttachmentCacheDir, msg.ID) + require.FileExists(t, file) - // And a failed one - response := request(t, s, "PUT", "/mytopic", content, nil) - err := toHTTPError(t, response.Body.String()) - require.Equal(t, 413, response.Code) - require.Equal(t, 41301, err.Code) + path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, content, response.Body.String()) + + // Prune and makes sure it's gone + waitFor(t, func() bool { + s.execManager() // May run many times + return !util.FileExists(file) + }) + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 404, response.Code) + }) +} + +func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + t.Parallel() + content := util.RandomString(5000) // > 4096 + + c := newTestConfigWithAuthFile(t) + c.AttachmentExpiryDuration = time.Millisecond // Hack + s := newTestServer(t, c) + + // Create tier with certain limits + sevenDays := time.Duration(604800) * time.Second + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + MessageLimit: 10, + MessageExpiryDuration: sevenDays, + AttachmentFileSizeLimit: 50_000, + AttachmentTotalSizeLimit: 200_000, + AttachmentExpiryDuration: sevenDays, // 7 days + AttachmentBandwidthLimit: 100000, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "test")) + + // Publish and make sure we can retrieve it + response := request(t, s, "PUT", "/mytopic", content, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + require.True(t, msg.Attachment.Expires > time.Now().Add(sevenDays-30*time.Second).Unix()) + require.True(t, msg.Expires > time.Now().Add(sevenDays-30*time.Second).Unix()) + file := filepath.Join(s.config.AttachmentCacheDir, msg.ID) + require.FileExists(t, file) + + path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, content, response.Body.String()) + + // Prune and makes sure it's still there + time.Sleep(time.Second) // Sigh ... + s.execManager() + require.FileExists(t, file) + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + }) +} + +func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(5000) // > 4096 + + c := newTestConfigWithAuthFile(t) + c.VisitorAttachmentDailyBandwidthLimit = 1000 // Much lower than tier bandwidth! + s := newTestServer(t, c) + + // Create tier with certain limits + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + MessageLimit: 10, + MessageExpiryDuration: time.Hour, + AttachmentFileSizeLimit: 50_000, + AttachmentTotalSizeLimit: 200_000, + AttachmentExpiryDuration: time.Hour, + AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "test")) + + // Publish and make sure we can retrieve it + rr := request(t, s, "PUT", "/mytopic", content, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + msg := toMessage(t, rr.Body.String()) + + // Retrieve it (first time succeeds) + rr = request(t, s, "GET", "/file/"+msg.ID, content, nil) // File downloads do not send auth headers!! + require.Equal(t, 200, rr.Code) + require.Equal(t, content, rr.Body.String()) + + // Retrieve it AGAIN (fails, due to bandwidth limit) + rr = request(t, s, "GET", "/file/"+msg.ID, content, nil) + require.Equal(t, 429, rr.Code) + }) +} + +func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + smallFile := util.RandomString(20_000) + largeFile := util.RandomString(50_000) + + c := newTestConfigWithAuthFile(t) + c.AttachmentFileSizeLimit = 20_000 + c.VisitorAttachmentTotalSizeLimit = 40_000 + s := newTestServer(t, c) + + // Create tier with certain limits + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "test", + MessageLimit: 100, + AttachmentFileSizeLimit: 50_000, + AttachmentTotalSizeLimit: 200_000, + AttachmentExpiryDuration: 30 * time.Second, + AttachmentBandwidthLimit: 1000000, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "test")) + + // Publish small file as anonymous + response := request(t, s, "PUT", "/mytopic", smallFile, nil) + msg := toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) + + // Publish large file as anonymous + response = request(t, s, "PUT", "/mytopic", largeFile, nil) + require.Equal(t, 413, response.Code) + require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) + + // Publish too large file as phil + response = request(t, s, "PUT", "/mytopic", largeFile+" a few more bytes", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 413, response.Code) + require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) + + // Publish large file as phil (4x) + for i := 0; i < 4; i++ { + response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + msg = toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + require.FileExists(t, filepath.Join(s.config.AttachmentCacheDir, msg.ID)) + } + response = request(t, s, "PUT", "/mytopic", largeFile, map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 413, response.Code) + require.Equal(t, 41301, toHTTPError(t, response.Body.String()).Code) + }) +} + +func TestServer_PublishAttachmentBandwidthLimit(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(5000) // > 4096 + + c := newTestConfig(t) + c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 123 // A little more than 1 upload and 3 downloads + s := newTestServer(t, c) + + // Publish attachment + response := request(t, s, "PUT", "/mytopic", content, nil) + msg := toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + + // Value it 4 times successfully + path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345") + for i := 1; i <= 4; i++ { // 4 successful downloads + response = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, content, response.Body.String()) + } + + // And then fail with a 429 + response = request(t, s, "GET", path, "", nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 429, response.Code) + require.Equal(t, 42905, err.Code) + }) +} + +func TestServer_PublishAttachmentBandwidthLimitUploadOnly(t *testing.T) { + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(5000) // > 4096 + + c := newTestConfig(t) + c.VisitorAttachmentDailyBandwidthLimit = 5*5000 + 500 // 5 successful uploads + s := newTestServer(t, c) + + // 5 successful uploads + for i := 1; i <= 5; i++ { + response := request(t, s, "PUT", "/mytopic", content, nil) + msg := toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + } + + // And a failed one + response := request(t, s, "PUT", "/mytopic", content, nil) + err := toHTTPError(t, response.Body.String()) + require.Equal(t, 413, response.Code) + require.Equal(t, 41301, err.Code) + }) } func TestServer_PublishAttachmentAndImmediatelyGetItWithCacheTimeout(t *testing.T) { - // This tests the awkward util.Retry in handleFile: Due to the async persisting of messages, - // the message is not immediately available when attempting to download it. + forEachBackend(t, func(t *testing.T) { + // This tests the awkward util.Retry in handleFile: Due to the async persisting of messages, + // the message is not immediately available when attempting to download it. - c := newTestConfig(t) - c.CacheBatchTimeout = 500 * time.Millisecond - c.CacheBatchSize = 10 - s := newTestServer(t, c) - content := "this is an ATTACHMENT" - rr := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, nil) - m := toMessage(t, rr.Body.String()) - require.Equal(t, "myfile.txt", m.Attachment.Name) + c := newTestConfig(t) + c.CacheBatchTimeout = 500 * time.Millisecond + c.CacheBatchSize = 10 + s := newTestServer(t, c) + content := "this is an ATTACHMENT" + rr := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, nil) + m := toMessage(t, rr.Body.String()) + require.Equal(t, "myfile.txt", m.Attachment.Name) - path := strings.TrimPrefix(m.Attachment.URL, "http://127.0.0.1:12345") - rr = request(t, s, "GET", path, "", nil) - require.Equal(t, 200, rr.Code) // Not 404! - require.Equal(t, content, rr.Body.String()) + path := strings.TrimPrefix(m.Attachment.URL, "http://127.0.0.1:12345") + rr = request(t, s, "GET", path, "", nil) + require.Equal(t, 200, rr.Code) // Not 404! + require.Equal(t, content, rr.Body.String()) + }) } func TestServer_PublishAttachmentAccountStats(t *testing.T) { - content := util.RandomString(4999) // > 4096 + forEachBackend(t, func(t *testing.T) { + content := util.RandomString(4999) // > 4096 - c := newTestConfig(t) - c.AttachmentFileSizeLimit = 5000 - c.VisitorAttachmentTotalSizeLimit = 6000 - s := newTestServer(t, c) + c := newTestConfig(t) + c.AttachmentFileSizeLimit = 5000 + c.VisitorAttachmentTotalSizeLimit = 6000 + s := newTestServer(t, c) - // Upload one attachment - response := request(t, s, "PUT", "/mytopic", content, nil) - msg := toMessage(t, response.Body.String()) - require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") + // Upload one attachment + response := request(t, s, "PUT", "/mytopic", content, nil) + msg := toMessage(t, response.Body.String()) + require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/") - // User stats - response = request(t, s, "GET", "/v1/account", "", nil) - require.Equal(t, 200, response.Code) - account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) - require.Nil(t, err) - require.Equal(t, int64(5000), account.Limits.AttachmentFileSize) - require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize) - require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize) - require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining) - require.Equal(t, int64(1), account.Stats.Messages) + // User stats + response = request(t, s, "GET", "/v1/account", "", nil) + require.Equal(t, 200, response.Code) + account, err := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(response.Body)) + require.Nil(t, err) + require.Equal(t, int64(5000), account.Limits.AttachmentFileSize) + require.Equal(t, int64(6000), account.Limits.AttachmentTotalSize) + require.Equal(t, int64(4999), account.Stats.AttachmentTotalSize) + require.Equal(t, int64(1001), account.Stats.AttachmentTotalSizeRemaining) + require.Equal(t, int64(1), account.Stats.Messages) + }) } func TestServer_Visitor_XForwardedFor_None(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11:1234" - r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty! - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "8.9.10.11", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty! + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "8.9.10.11", v.ip.String()) + }) } func TestServer_Visitor_XForwardedFor_Single(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11:1234" - r.Header.Set("X-Forwarded-For", "1.1.1.1") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "1.1.1.1", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("X-Forwarded-For", "1.1.1.1") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "1.1.1.1", v.ip.String()) + }) } func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11:1234" - r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "234.5.2.1", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "234.5.2.1", v.ip.String()) + }) } func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - c.ProxyForwardedHeader = "X-Client-IP" - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11:1234" - r.Header.Set("X-Client-IP", "1.2.3.4") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "1.2.3.4", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "X-Client-IP" + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("X-Client-IP", "1.2.3.4") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "1.2.3.4", v.ip.String()) + }) } func TestServer_Visitor_Custom_ClientIP_Header_IPv6(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - c.ProxyForwardedHeader = "X-Client-IP" - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "[2001:db8:9999::1]:1234" - r.Header.Set("X-Client-IP", "2001:db8:7777::1") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "2001:db8:7777::1", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "X-Client-IP" + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "[2001:db8:9999::1]:1234" + r.Header.Set("X-Client-IP", "2001:db8:7777::1") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "2001:db8:7777::1", v.ip.String()) + }) } func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - c.ProxyForwardedHeader = "Forwarded" - c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")} - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "8.9.10.11:1234" - r.Header.Set("Forwarded", " for=5.6.7.8, by=example.com;for=1.2.3.4") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "5.6.7.8", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "Forwarded" + c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("1.2.3.0/24")} + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "8.9.10.11:1234" + r.Header.Set("Forwarded", " for=5.6.7.8, by=example.com;for=1.2.3.4") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "5.6.7.8", v.ip.String()) + }) } func TestServer_Visitor_Custom_Forwarded_Header_IPv6(t *testing.T) { - c := newTestConfig(t) - c.BehindProxy = true - c.ProxyForwardedHeader = "Forwarded" - c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:1111::/64")} - s := newTestServer(t, c) - r, _ := http.NewRequest("GET", "/bla", nil) - r.RemoteAddr = "[2001:db8:2222::1]:1234" - r.Header.Set("Forwarded", " for=[2001:db8:1111::1], by=example.com;for=[2001:db8:3333::1]") - v, err := s.maybeAuthenticate(r) - require.Nil(t, err) - require.Equal(t, "2001:db8:3333::1", v.ip.String()) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.BehindProxy = true + c.ProxyForwardedHeader = "Forwarded" + c.ProxyTrustedPrefixes = []netip.Prefix{netip.MustParsePrefix("2001:db8:1111::/64")} + s := newTestServer(t, c) + r, _ := http.NewRequest("GET", "/bla", nil) + r.RemoteAddr = "[2001:db8:2222::1]:1234" + r.Header.Set("Forwarded", " for=[2001:db8:1111::1], by=example.com;for=[2001:db8:3333::1]") + v, err := s.maybeAuthenticate(r) + require.Nil(t, err) + require.Equal(t, "2001:db8:3333::1", v.ip.String()) + }) } func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { - t.Parallel() - count := 50000 - c := newTestConfig(t) - c.TotalTopicLimit = 50001 - c.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + count := 50000 + c := newTestConfig(t) + c.TotalTopicLimit = 50001 + c.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" + s := newTestServer(t, c) - // Add lots of messages - log.Info("Adding %d messages", count) - start := time.Now() - messages := make([]*model.Message, 0) - for i := 0; i < count; i++ { - topicID := fmt.Sprintf("topic%d", i) - _, err := s.topicsFromIDs(topicID) // Add topic to internal s.topics array - require.Nil(t, err) - messages = append(messages, newDefaultMessage(topicID, "some message")) - } - require.Nil(t, s.messageCache.AddMessages(messages)) - log.Info("Done: Adding %d messages; took %s", count, time.Since(start).Round(time.Millisecond)) - - // Update stats - statsChan := make(chan bool) - go func() { - log.Info("Updating stats") + // Add lots of messages + log.Info("Adding %d messages", count) start := time.Now() - s.execManager() - log.Info("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond)) - statsChan <- true - }() - time.Sleep(50 * time.Millisecond) // Make sure it starts first + messages := make([]*model.Message, 0) + for i := 0; i < count; i++ { + topicID := fmt.Sprintf("topic%d", i) + _, err := s.topicsFromIDs(topicID) // Add topic to internal s.topics array + require.Nil(t, err) + messages = append(messages, newDefaultMessage(topicID, "some message")) + } + require.Nil(t, s.messageCache.AddMessages(messages)) + log.Info("Done: Adding %d messages; took %s", count, time.Since(start).Round(time.Millisecond)) - // Publish message (during stats update) - log.Info("Publishing message") - start = time.Now() - response := request(t, s, "PUT", "/mytopic", "some body", nil) - m := toMessage(t, response.Body.String()) - require.Equal(t, "some body", m.Message) - require.True(t, time.Since(start) < 100*time.Millisecond) - log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond)) + // Update stats + statsChan := make(chan bool) + go func() { + log.Info("Updating stats") + start := time.Now() + s.execManager() + log.Info("Done: Updating stats; took %s", time.Since(start).Round(time.Millisecond)) + statsChan <- true + }() + time.Sleep(50 * time.Millisecond) // Make sure it starts first - // Wait for all goroutines - select { - case <-statsChan: - case <-time.After(10 * time.Second): - t.Fatal("Timed out waiting for Go routines") - } - log.Info("Done: Waiting for all locks") + // Publish message (during stats update) + log.Info("Publishing message") + start = time.Now() + response := request(t, s, "PUT", "/mytopic", "some body", nil) + m := toMessage(t, response.Body.String()) + require.Equal(t, "some body", m.Message) + require.True(t, time.Since(start) < 100*time.Millisecond) + log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond)) + + // Wait for all goroutines + select { + case <-statsChan: + case <-time.After(10 * time.Second): + t.Fatal("Timed out waiting for Go routines") + } + log.Info("Done: Waiting for all locks") + }) } func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testing.T) { - conf := newTestConfigWithAuthFile(t) - s := newTestServer(t, conf) - defer s.closeDatabases() + forEachBackend(t, func(t *testing.T) { + conf := newTestConfigWithAuthFile(t) + s := newTestServer(t, conf) + defer s.closeDatabases() - // Create user without tier - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + // Create user without tier + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - // Publish a message (anonymous user) - rr := request(t, s, "POST", "/mytopic", "hi", nil) - require.Equal(t, 200, rr.Code) + // Publish a message (anonymous user) + rr := request(t, s, "POST", "/mytopic", "hi", nil) + require.Equal(t, 200, rr.Code) - // Publish a message (non-tier user) - rr = request(t, s, "POST", "/mytopic", "hi", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), + // Publish a message (non-tier user) + rr = request(t, s, "POST", "/mytopic", "hi", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, rr.Code) + + // User stats (anonymous user) + rr = request(t, s, "GET", "/v1/account", "", nil) + account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, int64(2), account.Stats.Messages) + + // User stats (non-tier user) + rr = request(t, s, "GET", "/v1/account", "", map[string]string{ + "Authorization": util.BasicAuth("phil", "phil"), + }) + account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) + require.Equal(t, int64(2), account.Stats.Messages) }) - require.Equal(t, 200, rr.Code) - - // User stats (anonymous user) - rr = request(t, s, "GET", "/v1/account", "", nil) - account, _ := util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, int64(2), account.Stats.Messages) - - // User stats (non-tier user) - rr = request(t, s, "GET", "/v1/account", "", map[string]string{ - "Authorization": util.BasicAuth("phil", "phil"), - }) - account, _ = util.UnmarshalJSON[apiAccountResponse](io.NopCloser(rr.Body)) - require.Equal(t, int64(2), account.Stats.Messages) } func TestServer_SubscriberRateLimiting_Success(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - // "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor - subscriber1Fn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4:1234" - } - rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn) - require.Equal(t, 200, rr.Code) - require.Equal(t, "", rr.Body.String()) - require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) - - // "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name) - subscriber2Fn := func(r *http.Request) { - r.RemoteAddr = "8.7.7.1:1234" - } - rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, subscriber2Fn) - require.Equal(t, 200, rr.Code) - require.Equal(t, "", rr.Body.String()) - require.Equal(t, "8.7.7.1", s.topics["up012345678912"].rateVisitor.ip.String()) - - // Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the - // GET request before is also counted towards the request limiter. - for i := 0; i < 2; i++ { - rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil) + // "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor + subscriber1Fn := func(r *http.Request) { + r.RemoteAddr = "1.2.3.4:1234" + } + rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn) require.Equal(t, 200, rr.Code) - } - rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil) - require.Equal(t, 429, rr.Code) + require.Equal(t, "", rr.Body.String()) + require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) - // Publish another 2 messages to "up012345678912" as visitor 9.9.9.9 - for i := 0; i < 2; i++ { - rr := request(t, s, "PUT", "/up012345678912", "some message", nil) - require.Equal(t, 200, rr.Code) // If we fail here, handlePublish is using the wrong visitor! - } - rr = request(t, s, "PUT", "/up012345678912", "some message", nil) - require.Equal(t, 429, rr.Code) - - // Hurray! At this point, visitor 9.9.9.9 has published 4 messages, even though - // VisitorRequestLimitBurst is 3. That means it's working. - - // Now let's confirm that so far we haven't used up any of visitor 9.9.9.9's request limiter - // by publishing another 3 requests from it. - for i := 0; i < 3; i++ { - rr := request(t, s, "PUT", "/some-other-topic", "some message", nil) + // "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name) + subscriber2Fn := func(r *http.Request) { + r.RemoteAddr = "8.7.7.1:1234" + } + rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, subscriber2Fn) require.Equal(t, 200, rr.Code) - } - rr = request(t, s, "PUT", "/some-other-topic", "some message", nil) - require.Equal(t, 429, rr.Code) + require.Equal(t, "", rr.Body.String()) + require.Equal(t, "8.7.7.1", s.topics["up012345678912"].rateVisitor.ip.String()) + + // Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the + // GET request before is also counted towards the request limiter. + for i := 0; i < 2; i++ { + rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil) + require.Equal(t, 200, rr.Code) + } + rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil) + require.Equal(t, 429, rr.Code) + + // Publish another 2 messages to "up012345678912" as visitor 9.9.9.9 + for i := 0; i < 2; i++ { + rr := request(t, s, "PUT", "/up012345678912", "some message", nil) + require.Equal(t, 200, rr.Code) // If we fail here, handlePublish is using the wrong visitor! + } + rr = request(t, s, "PUT", "/up012345678912", "some message", nil) + require.Equal(t, 429, rr.Code) + + // Hurray! At this point, visitor 9.9.9.9 has published 4 messages, even though + // VisitorRequestLimitBurst is 3. That means it's working. + + // Now let's confirm that so far we haven't used up any of visitor 9.9.9.9's request limiter + // by publishing another 3 requests from it. + for i := 0; i < 3; i++ { + rr := request(t, s, "PUT", "/some-other-topic", "some message", nil) + require.Equal(t, 200, rr.Code) + } + rr = request(t, s, "PUT", "/some-other-topic", "some message", nil) + require.Equal(t, 429, rr.Code) + }) } func TestServer_SubscriberRateLimiting_NotWrongTopic(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - subscriberFn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4:1234" - } - rr := request(t, s, "GET", "/alerts,upAAAAAAAAAAAA,upBBBBBBBBBBBB/json?poll=1", "", nil, subscriberFn) - require.Equal(t, 200, rr.Code) - require.Equal(t, "", rr.Body.String()) - require.Nil(t, s.topics["alerts"].rateVisitor) - require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) - require.Equal(t, "1.2.3.4", s.topics["upBBBBBBBBBBBB"].rateVisitor.ip.String()) + subscriberFn := func(r *http.Request) { + r.RemoteAddr = "1.2.3.4:1234" + } + rr := request(t, s, "GET", "/alerts,upAAAAAAAAAAAA,upBBBBBBBBBBBB/json?poll=1", "", nil, subscriberFn) + require.Equal(t, 200, rr.Code) + require.Equal(t, "", rr.Body.String()) + require.Nil(t, s.topics["alerts"].rateVisitor) + require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) + require.Equal(t, "1.2.3.4", s.topics["upBBBBBBBBBBBB"].rateVisitor.ip.String()) + }) } func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorSubscriberRateLimiting = false - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorSubscriberRateLimiting = false + s := newTestServer(t, c) - // Subscriber rate limiting is disabled! + // Subscriber rate limiting is disabled! - // Registering visitor 1.2.3.4 to topic has no effect - rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "1.2.3.4:1234" - }) - require.Equal(t, 200, rr.Code) - require.Equal(t, "", rr.Body.String()) - require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor) - - // Registering visitor 8.7.7.1 to topic has no effect - rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "8.7.7.1:1234" - }) - require.Equal(t, 200, rr.Code) - require.Equal(t, "", rr.Body.String()) - require.Nil(t, s.topics["up012345678912"].rateVisitor) - - // Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9 - for i := 0; i < 3; i++ { - rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil) + // Registering visitor 1.2.3.4 to topic has no effect + rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) { + r.RemoteAddr = "1.2.3.4:1234" + }) require.Equal(t, 200, rr.Code) - } - rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil) - require.Equal(t, 429, rr.Code) - rr = request(t, s, "PUT", "/up012345678912", "some message", nil) - require.Equal(t, 429, rr.Code) + require.Equal(t, "", rr.Body.String()) + require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor) + + // Registering visitor 8.7.7.1 to topic has no effect + rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) { + r.RemoteAddr = "8.7.7.1:1234" + }) + require.Equal(t, 200, rr.Code) + require.Equal(t, "", rr.Body.String()) + require.Nil(t, s.topics["up012345678912"].rateVisitor) + + // Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9 + for i := 0; i < 3; i++ { + rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil) + require.Equal(t, 200, rr.Code) + } + rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil) + require.Equal(t, 429, rr.Code) + rr = request(t, s, "PUT", "/up012345678912", "some message", nil) + require.Equal(t, 429, rr.Code) + }) } func TestServer_SubscriberRateLimiting_UP_Only(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - // "Register" 5 different UnifiedPush visitors - for i := 0; i < 5; i++ { - subscriberFn := func(r *http.Request) { - r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) - } - rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, subscriberFn) - require.Equal(t, 200, rr.Code) - } - - // Publish 2 messages per topic - for i := 0; i < 5; i++ { - for j := 0; j < 2; j++ { - rr := request(t, s, "PUT", fmt.Sprintf("/up12345678901%d?up=1", i), "some message", nil) + // "Register" 5 different UnifiedPush visitors + for i := 0; i < 5; i++ { + subscriberFn := func(r *http.Request) { + r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) + } + rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, subscriberFn) require.Equal(t, 200, rr.Code) } - } + + // Publish 2 messages per topic + for i := 0; i < 5; i++ { + for j := 0; j < 2; j++ { + rr := request(t, s, "PUT", fmt.Sprintf("/up12345678901%d?up=1", i), "some message", nil) + require.Equal(t, 200, rr.Code) + } + } + }) } func TestServer_Matrix_SubscriberRateLimiting_UP_Only(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - // "Register" 5 different UnifiedPush visitors - for i := 0; i < 5; i++ { - rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, func(r *http.Request) { - r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) - }) - require.Equal(t, 200, rr.Code) - } - - // Publish 2 messages per topic - for i := 0; i < 5; i++ { - notification := fmt.Sprintf(`{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/up12345678901%d?up=1"}]}}`, i) - for j := 0; j < 2; j++ { - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String()) + // "Register" 5 different UnifiedPush visitors + for i := 0; i < 5; i++ { + rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, func(r *http.Request) { + r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1) + }) + require.Equal(t, 200, rr.Code) } - response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) - require.Equal(t, 429, response.Code, notification) - require.Equal(t, 42901, toHTTPError(t, response.Body.String()).Code) - } + + // Publish 2 messages per topic + for i := 0; i < 5; i++ { + notification := fmt.Sprintf(`{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/up12345678901%d?up=1"}]}}`, i) + for j := 0; j < 2; j++ { + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"rejected":[]}`+"\n", response.Body.String()) + } + response := request(t, s, "POST", "/_matrix/push/v1/notify", notification, nil) + require.Equal(t, 429, response.Code, notification) + require.Equal(t, 42901, toHTTPError(t, response.Body.String()).Code) + } + }) } func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) { - c := newTestConfig(t) - c.VisitorRequestLimitBurst = 3 - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.VisitorRequestLimitBurst = 3 + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - // "Register" rate visitor - subscriberFn := func(r *http.Request) { - r.RemoteAddr = "1.2.3.4:1234" - } - rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn) - require.Equal(t, 200, rr.Code) - require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) - require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor) + // "Register" rate visitor + subscriberFn := func(r *http.Request) { + r.RemoteAddr = "1.2.3.4:1234" + } + rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn) + require.Equal(t, 200, rr.Code) + require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String()) + require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor) - // Publish message, observe rate visitor tokens being decreased - response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value()) - require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value()) - require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor) + // Publish message, observe rate visitor tokens being decreased + response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value()) + require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value()) + require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor) - // Expire visitor - s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour) - s.pruneVisitors() + // Expire visitor + s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour) + s.pruneVisitors() - // Publish message again, observe that rateVisitor is not used anymore and is reset - response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value()) - require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor) - require.Nil(t, s.visitors["ip:1.2.3.4"]) + // Publish message again, observe that rateVisitor is not used anymore and is reset + response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value()) + require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor) + require.Nil(t, s.visitors["ip:1.2.3.4"]) + }) } func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.AuthDefault = user.PermissionReadWrite - c.VisitorSubscriberRateLimiting = true - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.AuthDefault = user.PermissionReadWrite + c.VisitorSubscriberRateLimiting = true + s := newTestServer(t, c) - // Create some ACLs - require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) + // Create some ACLs + require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead)) - // Set rate visitor as ip:1.2.3.4 on topic - // - "up123456789012": Allowed, because no ACLs and nobody owns the topic - // - "announcements": NOT allowed, because it has read-only permissions for everyone - rr := request(t, s, "GET", "/up123456789012,announcements/json?poll=1", "", nil, func(r *http.Request) { - r.RemoteAddr = "1.2.3.4:1234" + // Set rate visitor as ip:1.2.3.4 on topic + // - "up123456789012": Allowed, because no ACLs and nobody owns the topic + // - "announcements": NOT allowed, because it has read-only permissions for everyone + rr := request(t, s, "GET", "/up123456789012,announcements/json?poll=1", "", nil, func(r *http.Request) { + r.RemoteAddr = "1.2.3.4:1234" + }) + require.Equal(t, 200, rr.Code) + require.Equal(t, "1.2.3.4", s.topics["up123456789012"].rateVisitor.ip.String()) + require.Nil(t, s.topics["announcements"].rateVisitor) }) - require.Equal(t, 200, rr.Code) - require.Equal(t, "1.2.3.4", s.topics["up123456789012"].rateVisitor.ip.String()) - require.Nil(t, s.topics["announcements"].rateVisitor) } func TestServer_MessageHistoryAndStatsEndpoint(t *testing.T) { - c := newTestConfig(t) - c.ManagerInterval = 2 * time.Second - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfig(t) + c.ManagerInterval = 2 * time.Second + s := newTestServer(t, c) - // Publish some messages, and get stats - for i := 0; i < 5; i++ { - response := request(t, s, "POST", "/mytopic", "some message", nil) + // Publish some messages, and get stats + for i := 0; i < 5; i++ { + response := request(t, s, "POST", "/mytopic", "some message", nil) + require.Equal(t, 200, response.Code) + } + require.Equal(t, int64(5), s.messages) + require.Equal(t, []int64{0}, s.messagesHistory) + + response := request(t, s, "GET", "/v1/stats", "", nil) require.Equal(t, 200, response.Code) - } - require.Equal(t, int64(5), s.messages) - require.Equal(t, []int64{0}, s.messagesHistory) + require.Equal(t, `{"messages":5,"messages_rate":0}`+"\n", response.Body.String()) - response := request(t, s, "GET", "/v1/stats", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"messages":5,"messages_rate":0}`+"\n", response.Body.String()) + // Run manager and see message history update + s.execManager() + require.Equal(t, []int64{0, 5}, s.messagesHistory) - // Run manager and see message history update - s.execManager() - require.Equal(t, []int64{0, 5}, s.messagesHistory) - - response = request(t, s, "GET", "/v1/stats", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"messages":5,"messages_rate":2.5}`+"\n", response.Body.String()) // 5 messages in 2 seconds = 2.5 messages per second - - // Publish some more messages - for i := 0; i < 10; i++ { - response := request(t, s, "POST", "/mytopic", "some message", nil) + response = request(t, s, "GET", "/v1/stats", "", nil) require.Equal(t, 200, response.Code) - } - require.Equal(t, int64(15), s.messages) - require.Equal(t, []int64{0, 5}, s.messagesHistory) + require.Equal(t, `{"messages":5,"messages_rate":2.5}`+"\n", response.Body.String()) // 5 messages in 2 seconds = 2.5 messages per second - response = request(t, s, "GET", "/v1/stats", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"messages":15,"messages_rate":2.5}`+"\n", response.Body.String()) // Rate did not update yet + // Publish some more messages + for i := 0; i < 10; i++ { + response := request(t, s, "POST", "/mytopic", "some message", nil) + require.Equal(t, 200, response.Code) + } + require.Equal(t, int64(15), s.messages) + require.Equal(t, []int64{0, 5}, s.messagesHistory) - // Run manager and see message history update - s.execManager() - require.Equal(t, []int64{0, 5, 15}, s.messagesHistory) + response = request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":15,"messages_rate":2.5}`+"\n", response.Body.String()) // Rate did not update yet - response = request(t, s, "GET", "/v1/stats", "", nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"messages":15,"messages_rate":3.75}`+"\n", response.Body.String()) // 15 messages in 4 seconds = 3.75 messages per second + // Run manager and see message history update + s.execManager() + require.Equal(t, []int64{0, 5, 15}, s.messagesHistory) + + response = request(t, s, "GET", "/v1/stats", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"messages":15,"messages_rate":3.75}`+"\n", response.Body.String()) // 15 messages in 4 seconds = 3.75 messages per second + }) } func TestServer_MessageHistoryMaxSize(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - for i := 0; i < 20; i++ { - s.messages = int64(i) - s.execManager() - } - require.Equal(t, []int64{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, s.messagesHistory) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + for i := 0; i < 20; i++ { + s.messages = int64(i) + s.execManager() + } + require.Equal(t, []int64{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, s.messagesHistory) + }) } func TestServer_MessageCountPersistence(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - s := newTestServer(t, c) - s.messages = 1234 - s.execManager() - waitFor(t, func() bool { - messages, err := s.messageCache.Stats() - require.Nil(t, err) - return messages == 1234 - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + s := newTestServer(t, c) + s.messages = 1234 + s.execManager() + waitFor(t, func() bool { + messages, err := s.messageCache.Stats() + require.Nil(t, err) + return messages == 1234 + }) - s = newTestServer(t, c) - require.Equal(t, int64(1234), s.messages) + s = newTestServer(t, c) + require.Equal(t, int64(1234), s.messages) + }) } func TestServer_PublishWithUTF8MimeHeader(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{ - "X-Filename": "some =?UTF-8?q?=C3=A4?=ttachment.txt", - "X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=", - "X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=", - "X-Tags": "=?UTF-8?B?8J+HqfCfh6o=?=, =?UTF-8?B?bnRmeSDlvojmo5I=?=", - "X-Click": "=?uTf-8?b?aHR0cHM6Ly/wn5KpLmxh?=", - "X-Actions": "http, \"=?utf-8?q?Mettre =C3=A0 jour?=\", \"https://my.tld/webhook/netbird-update\"; =?utf-8?b?aHR0cCwg6L+Z5piv5LiA5Liq5qCH562+LCBodHRwczovL/CfkqkubGE=?=", + response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{ + "X-Filename": "some =?UTF-8?q?=C3=A4?=ttachment.txt", + "X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=", + "X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=", + "X-Tags": "=?UTF-8?B?8J+HqfCfh6o=?=, =?UTF-8?B?bnRmeSDlvojmo5I=?=", + "X-Click": "=?uTf-8?b?aHR0cHM6Ly/wn5KpLmxh?=", + "X-Actions": "http, \"=?utf-8?q?Mettre =C3=A0 jour?=\", \"https://my.tld/webhook/netbird-update\"; =?utf-8?b?aHR0cCwg6L+Z5piv5LiA5Liq5qCH562+LCBodHRwczovL/CfkqkubGE=?=", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "🇩🇪", m.Message) + require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title) + require.Equal(t, "some ättachment.txt", m.Attachment.Name) + require.Equal(t, "🇩🇪", m.Tags[0]) + require.Equal(t, "ntfy 很棒", m.Tags[1]) + require.Equal(t, "https://💩.la", m.Click) + require.Equal(t, "Mettre à jour", m.Actions[0].Label) + require.Equal(t, "http", m.Actions[1].Action) + require.Equal(t, "这是一个标签", m.Actions[1].Label) + require.Equal(t, "https://💩.la", m.Actions[1].URL) }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "🇩🇪", m.Message) - require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title) - require.Equal(t, "some ättachment.txt", m.Attachment.Name) - require.Equal(t, "🇩🇪", m.Tags[0]) - require.Equal(t, "ntfy 很棒", m.Tags[1]) - require.Equal(t, "https://💩.la", m.Click) - require.Equal(t, "Mettre à jour", m.Actions[0].Label) - require.Equal(t, "http", m.Actions[1].Action) - require.Equal(t, "这是一个标签", m.Actions[1].Label) - require.Equal(t, "https://💩.la", m.Actions[1].URL) } func TestServer_UpstreamBaseURL_Success(t *testing.T) { - t.Parallel() - var pollID atomic.Pointer[string] - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.Nil(t, err) - require.Equal(t, "/87c9cddf7b0105f5fe849bf084c6e600be0fde99be3223335199b4965bd7b735", r.URL.Path) - require.Equal(t, "", string(body)) - require.NotEmpty(t, r.Header.Get("X-Poll-ID")) - pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) - })) - defer upstreamServer.Close() + forEachBackend(t, func(t *testing.T) { + t.Parallel() + var pollID atomic.Pointer[string] + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/87c9cddf7b0105f5fe849bf084c6e600be0fde99be3223335199b4965bd7b735", r.URL.Path) + require.Equal(t, "", string(body)) + require.NotEmpty(t, r.Header.Get("X-Poll-ID")) + pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) + })) + defer upstreamServer.Close() - c := newTestConfigWithAuthFile(t) - c.BaseURL = "http://myserver.internal" - c.UpstreamBaseURL = upstreamServer.URL - s := newTestServer(t, c) + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + s := newTestServer(t, c) - // Send message, and wait for upstream server to receive it - response := request(t, s, "PUT", "/mytopic", `hi there`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.NotEmpty(t, m.ID) - require.Equal(t, "hi there", m.Message) - waitFor(t, func() bool { - pID := pollID.Load() - return pID != nil && *pID == m.ID + // Send message, and wait for upstream server to receive it + response := request(t, s, "PUT", "/mytopic", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) + waitFor(t, func() bool { + pID := pollID.Load() + return pID != nil && *pID == m.ID + }) }) } func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) { - t.Parallel() - var pollID atomic.Pointer[string] - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.Nil(t, err) - require.Equal(t, "/a1c72bcb4daf5af54d13ef86aea8f76c11e8b88320d55f1811d5d7b173bcc1df", r.URL.Path) - require.Equal(t, "Bearer tk_1234567890", r.Header.Get("Authorization")) - require.Equal(t, "", string(body)) - require.NotEmpty(t, r.Header.Get("X-Poll-ID")) - pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) - })) - defer upstreamServer.Close() + forEachBackend(t, func(t *testing.T) { + t.Parallel() + var pollID atomic.Pointer[string] + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/a1c72bcb4daf5af54d13ef86aea8f76c11e8b88320d55f1811d5d7b173bcc1df", r.URL.Path) + require.Equal(t, "Bearer tk_1234567890", r.Header.Get("Authorization")) + require.Equal(t, "", string(body)) + require.NotEmpty(t, r.Header.Get("X-Poll-ID")) + pollID.Store(util.String(r.Header.Get("X-Poll-ID"))) + })) + defer upstreamServer.Close() - c := newTestConfigWithAuthFile(t) - c.BaseURL = "http://myserver.internal" - c.UpstreamBaseURL = upstreamServer.URL - c.UpstreamAccessToken = "tk_1234567890" - s := newTestServer(t, c) + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + c.UpstreamAccessToken = "tk_1234567890" + s := newTestServer(t, c) - // Send message, and wait for upstream server to receive it - response := request(t, s, "PUT", "/mytopic1", `hi there`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.NotEmpty(t, m.ID) - require.Equal(t, "hi there", m.Message) - waitFor(t, func() bool { - pID := pollID.Load() - return pID != nil && *pID == m.ID + // Send message, and wait for upstream server to receive it + response := request(t, s, "PUT", "/mytopic1", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) + waitFor(t, func() bool { + pID := pollID.Load() + return pID != nil && *pID == m.ID + }) }) } func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { - t.Parallel() - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("UnifiedPush messages should not be forwarded") - })) - defer upstreamServer.Close() + forEachBackend(t, func(t *testing.T) { + t.Parallel() + upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("UnifiedPush messages should not be forwarded") + })) + defer upstreamServer.Close() - c := newTestConfigWithAuthFile(t) - c.BaseURL = "http://myserver.internal" - c.UpstreamBaseURL = upstreamServer.URL - s := newTestServer(t, c) + c := newTestConfigWithAuthFile(t) + c.BaseURL = "http://myserver.internal" + c.UpstreamBaseURL = upstreamServer.URL + s := newTestServer(t, c) - // Send UP message, this should not forward to upstream server - response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.NotEmpty(t, m.ID) - require.Equal(t, "hi there", m.Message) + // Send UP message, this should not forward to upstream server + response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.NotEmpty(t, m.ID) + require.Equal(t, "hi there", m.Message) - // Forwarding is done asynchronously, so wait a bit. - // This ensures that the t.Fatal above is actually not triggered. - time.Sleep(500 * time.Millisecond) + // Forwarding is done asynchronously, so wait a bit. + // This ensures that the t.Fatal above is actually not triggered. + time.Sleep(500 * time.Millisecond) + }) } func TestServer_MessageTemplate(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "{{.foo}}", - "X-Title": "{{.nested.title}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "bar", m.Message) - require.Equal(t, "here", m.Title) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) + }) } func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "Message": "{{.foo}} is {{.foo}}", - "Title": "{{.nested.title}} is {{.nested.title}}", - "Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "Message": "{{.foo}} is {{.foo}}", + "Title": "{{.nested.title}} is {{.nested.title}}", + "Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "bar is bar", m.Message) - require.Equal(t, "here is here", m.Title) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar is bar", m.Message) + require.Equal(t, "here is here", m.Title) + }) } func TestServer_MessageTemplate_JSONBody(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` - response := request(t, s, "PUT", "/", body, map[string]string{ - "m": "{{.foo}}", - "t": "{{.nested.title}}", - "tpl": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` + response := request(t, s, "PUT", "/", body, map[string]string{ + "m": "{{.foo}}", + "t": "{{.nested.title}}", + "tpl": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "bar", m.Message) - require.Equal(t, "here", m.Title) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) + }) } func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` - response := request(t, s, "PUT", "/", body, map[string]string{ - "X-Message": "{{.foo}}", - "X-Title": "{{.nested.title}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` + response := request(t, s, "PUT", "/", body, map[string]string{ + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", + "X-Template": "1", + }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code) + require.Equal(t, 400, response.Code) + require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "{{.food}}", - "X-Title": "{{.neste.title}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Message": "{{.food}}", + "X-Title": "{{.neste.title}}", + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "", m.Message) - require.Equal(t, "", m.Title) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.Message) + require.Equal(t, "", m.Title) + }) } func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "{{.foo}} is {{.nested.title}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Message": "{{.foo}} is {{.nested.title}}", + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "bar is here", m.Message) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar is here", m.Message) + }) } func TestServer_MessageTemplate_Range(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` - response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ - "X-Message": `Severe URLs:\n{{range .errors}}{{if eq .level "severe"}}- {{.url}}\n{{end}}{{end}}`, - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` + response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ + "X-Message": `Severe URLs:\n{{range .errors}}{{if eq .level "severe"}}- {{.url}}\n{{end}}{{end}}`, + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com", m.Message) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Severe URLs:\n- https://severe1.com\n- https://severe2.com", m.Message) + }) } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25 - s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Message": "{{.foo}}", - "X-Title": "{{.nested.title}}", - "X-Template": "yes", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25 + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ + "X-Message": "{{.foo}}", + "X-Title": "{{.nested.title}}", + "X-Template": "yes", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "bar", m.Message) - require.Equal(t, "here", m.Title) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "bar", m.Message) + require.Equal(t, "here", m.Title) + }) } func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21 - s := newTestServer(t, c) - response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{ - "X-Message": "{{.foo}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21 + s := newTestServer(t, c) + response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{ + "X-Message": "{{.foo}}", + "X-Template": "1", + }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) + require.Equal(t, 400, response.Code) + require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MessageTemplate_Grafana(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - body := `{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}` - response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+{{.title}}&message={{.message}}", body, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title) - require.Equal(t, `**Resolved** + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + body := `{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}` + response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+{{.title}}&message={{.message}}", body, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title) + require.Equal(t, `**Resolved** Value: B=18.98211314475876, C=0 Labels: @@ -3030,156 +3317,173 @@ Annotations: - summary = 15m load average too high Source: localhost:3000/alerting/grafana/NW9oDw-4z/view Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter`, m.Message) + }) } func TestServer_MessageTemplate_GitHub(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}` - response := request(t, s, "PUT", `/mytopic?tpl=yes&message=[{{.pull_request.head.repo.full_name}}]+Pull+request+{{if+eq+.action+"opened"}}OPENED{{else}}CLOSED{{end}}:+{{.pull_request.title}}`, body, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "", m.Title) - require.Equal(t, `[binwiederhier/dabble] Pull request OPENED: A sample PR from Phil`, m.Message) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}` + response := request(t, s, "PUT", `/mytopic?tpl=yes&message=[{{.pull_request.head.repo.full_name}}]+Pull+request+{{if+eq+.action+"opened"}}OPENED{{else}}CLOSED{{end}}:+{{.pull_request.title}}`, body, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "", m.Title) + require.Equal(t, `[binwiederhier/dabble] Pull request OPENED: A sample PR from Phil`, m.Message) + }) } func TestServer_MessageTemplate_GitHub2(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}` - response := request(t, s, "PUT", `/mytopic?tpl=yes&title={{if+eq+.action+"opened"}}New+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{else}}[{{.action}}]+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{end}}&message={{.pull_request.title}}+in+{{.repository.full_name}}.+View+more+at+{{.pull_request.html_url}}`, body, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, `New PR: #1 by binwiederhier`, m.Title) - require.Equal(t, `A sample PR from Phil in binwiederhier/dabble. View more at https://github.com/binwiederhier/dabble/pull/1`, m.Message) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + body := `{"action":"opened","number":1,"pull_request":{"url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1","id":1783420972,"node_id":"PR_kwDOHAbdo85qTNgs","html_url":"https://github.com/binwiederhier/dabble/pull/1","diff_url":"https://github.com/binwiederhier/dabble/pull/1.diff","patch_url":"https://github.com/binwiederhier/dabble/pull/1.patch","issue_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1","number":1,"state":"open","locked":false,"title":"A sample PR from Phil","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"body":null,"created_at":"2024-03-21T02:52:09Z","updated_at":"2024-03-21T02:52:09Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"draft":false,"commits_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits","review_comments_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments","review_comment_url":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b","head":{"label":"binwiederhier:aa","ref":"aa","sha":"5703842cc5715ed1e358d23ebb693db09747ae9b","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"base":{"label":"binwiederhier:main","ref":"main","sha":"72d931a20bb83d123ab45accaf761150c8b01211","user":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"repo":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE"}},"_links":{"self":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1"},"html":{"href":"https://github.com/binwiederhier/dabble/pull/1"},"issue":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1"},"comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/issues/1/comments"},"review_comments":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/comments"},"review_comment":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/binwiederhier/dabble/pulls/1/commits"},"statuses":{"href":"https://api.github.com/repos/binwiederhier/dabble/statuses/5703842cc5715ed1e358d23ebb693db09747ae9b"}},"author_association":"OWNER","auto_merge":null,"active_lock_reason":null,"merged":false,"mergeable":null,"rebaseable":null,"mergeable_state":"unknown","merged_by":null,"comments":0,"review_comments":0,"maintainer_can_modify":false,"commits":1,"additions":1,"deletions":1,"changed_files":1},"repository":{"id":470212003,"node_id":"R_kgDOHAbdow","name":"dabble","full_name":"binwiederhier/dabble","private":false,"owner":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false},"html_url":"https://github.com/binwiederhier/dabble","description":"A repo for dabbling","fork":false,"url":"https://api.github.com/repos/binwiederhier/dabble","forks_url":"https://api.github.com/repos/binwiederhier/dabble/forks","keys_url":"https://api.github.com/repos/binwiederhier/dabble/keys{/key_id}","collaborators_url":"https://api.github.com/repos/binwiederhier/dabble/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/binwiederhier/dabble/teams","hooks_url":"https://api.github.com/repos/binwiederhier/dabble/hooks","issue_events_url":"https://api.github.com/repos/binwiederhier/dabble/issues/events{/number}","events_url":"https://api.github.com/repos/binwiederhier/dabble/events","assignees_url":"https://api.github.com/repos/binwiederhier/dabble/assignees{/user}","branches_url":"https://api.github.com/repos/binwiederhier/dabble/branches{/branch}","tags_url":"https://api.github.com/repos/binwiederhier/dabble/tags","blobs_url":"https://api.github.com/repos/binwiederhier/dabble/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/binwiederhier/dabble/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/binwiederhier/dabble/git/refs{/sha}","trees_url":"https://api.github.com/repos/binwiederhier/dabble/git/trees{/sha}","statuses_url":"https://api.github.com/repos/binwiederhier/dabble/statuses/{sha}","languages_url":"https://api.github.com/repos/binwiederhier/dabble/languages","stargazers_url":"https://api.github.com/repos/binwiederhier/dabble/stargazers","contributors_url":"https://api.github.com/repos/binwiederhier/dabble/contributors","subscribers_url":"https://api.github.com/repos/binwiederhier/dabble/subscribers","subscription_url":"https://api.github.com/repos/binwiederhier/dabble/subscription","commits_url":"https://api.github.com/repos/binwiederhier/dabble/commits{/sha}","git_commits_url":"https://api.github.com/repos/binwiederhier/dabble/git/commits{/sha}","comments_url":"https://api.github.com/repos/binwiederhier/dabble/comments{/number}","issue_comment_url":"https://api.github.com/repos/binwiederhier/dabble/issues/comments{/number}","contents_url":"https://api.github.com/repos/binwiederhier/dabble/contents/{+path}","compare_url":"https://api.github.com/repos/binwiederhier/dabble/compare/{base}...{head}","merges_url":"https://api.github.com/repos/binwiederhier/dabble/merges","archive_url":"https://api.github.com/repos/binwiederhier/dabble/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/binwiederhier/dabble/downloads","issues_url":"https://api.github.com/repos/binwiederhier/dabble/issues{/number}","pulls_url":"https://api.github.com/repos/binwiederhier/dabble/pulls{/number}","milestones_url":"https://api.github.com/repos/binwiederhier/dabble/milestones{/number}","notifications_url":"https://api.github.com/repos/binwiederhier/dabble/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/binwiederhier/dabble/labels{/name}","releases_url":"https://api.github.com/repos/binwiederhier/dabble/releases{/id}","deployments_url":"https://api.github.com/repos/binwiederhier/dabble/deployments","created_at":"2022-03-15T15:06:17Z","updated_at":"2022-03-15T15:06:17Z","pushed_at":"2024-03-21T02:52:10Z","git_url":"git://github.com/binwiederhier/dabble.git","ssh_url":"git@github.com:binwiederhier/dabble.git","clone_url":"https://github.com/binwiederhier/dabble.git","svn_url":"https://github.com/binwiederhier/dabble","homepage":null,"size":1,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":0,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":1,"license":null,"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"topics":[],"visibility":"public","forks":0,"open_issues":1,"watchers":0,"default_branch":"main"},"sender":{"login":"binwiederhier","id":664597,"node_id":"MDQ6VXNlcjY2NDU5Nw==","avatar_url":"https://avatars.githubusercontent.com/u/664597?v=4","gravatar_id":"","url":"https://api.github.com/users/binwiederhier","html_url":"https://github.com/binwiederhier","followers_url":"https://api.github.com/users/binwiederhier/followers","following_url":"https://api.github.com/users/binwiederhier/following{/other_user}","gists_url":"https://api.github.com/users/binwiederhier/gists{/gist_id}","starred_url":"https://api.github.com/users/binwiederhier/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/binwiederhier/subscriptions","organizations_url":"https://api.github.com/users/binwiederhier/orgs","repos_url":"https://api.github.com/users/binwiederhier/repos","events_url":"https://api.github.com/users/binwiederhier/events{/privacy}","received_events_url":"https://api.github.com/users/binwiederhier/received_events","type":"User","site_admin":false}}` + response := request(t, s, "PUT", `/mytopic?tpl=yes&title={{if+eq+.action+"opened"}}New+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{else}}[{{.action}}]+PR:+%23{{.number}}+by+{{.pull_request.user.login}}{{end}}&message={{.pull_request.title}}+in+{{.repository.full_name}}.+View+more+at+{{.pull_request.html_url}}`, body, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `New PR: #1 by binwiederhier`, m.Title) + require.Equal(t, `A sample PR from Phil in binwiederhier/dabble. View more at https://github.com/binwiederhier/dabble/pull/1`, m.Message) + }) } func TestServer_MessageTemplate_DisallowedCalls(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - disallowedTemplates := []string{ - `{{template ""}}`, - `{{- template ""}}`, - `{{- + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + disallowedTemplates := []string{ + `{{template ""}}`, + `{{- template ""}}`, + `{{- template ""}}`, - `{{ call abc}}`, - `{{ define "aa"}}`, - `We cannot {{define "aa"}}`, - `We cannot {{ call "aa"}}`, - `We cannot {{- template "aa"}}`, - } - for _, disallowedTemplate := range disallowedTemplates { - messageTemplate := disallowedTemplate - t.Run(disallowedTemplate, func(t *testing.T) { - t.Parallel() - response := request(t, s, "PUT", `/mytopic`, `{}`, map[string]string{ - "Template": "yes", - "Message": messageTemplate, + `{{ call abc}}`, + `{{ define "aa"}}`, + `We cannot {{define "aa"}}`, + `We cannot {{ call "aa"}}`, + `We cannot {{- template "aa"}}`, + } + for _, disallowedTemplate := range disallowedTemplates { + messageTemplate := disallowedTemplate + t.Run(disallowedTemplate, func(t *testing.T) { + t.Parallel() + response := request(t, s, "PUT", `/mytopic`, `{}`, map[string]string{ + "Template": "yes", + "Message": messageTemplate, + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40044, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40044, toHTTPError(t, response.Body.String()).Code) - }) - } + } + }) } func TestServer_MessageTemplate_SprigFunctions(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - bodies := []string{ - `{"foo":"bar","nested":{"title":"here"}}`, - `{"topic":"ntfy-test"}`, - `{"topic":"another-topic"}`, - } - templates := []string{ - `{{.foo | upper}} is {{.nested.title | repeat 3}}`, - `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, - `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, - } - targets := []string{ - `BAR is hereherehere`, - `Topic: test`, - `Topic: another-topic`, - } - for i, body := range bodies { - template := templates[i] - target := targets[i] - t.Run(template, func(t *testing.T) { - response := request(t, s, "PUT", `/mytopic`, body, map[string]string{ - "Template": "yes", - "Message": template, + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + bodies := []string{ + `{"foo":"bar","nested":{"title":"here"}}`, + `{"topic":"ntfy-test"}`, + `{"topic":"another-topic"}`, + } + templates := []string{ + `{{.foo | upper}} is {{.nested.title | repeat 3}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + `{{if hasPrefix "ntfy-" .topic}}Topic: {{trimPrefix "ntfy-" .topic}}{{ else }}Topic: {{.topic}}{{end}}`, + } + targets := []string{ + `BAR is hereherehere`, + `Topic: test`, + `Topic: another-topic`, + } + for i, body := range bodies { + template := templates[i] + target := targets[i] + t.Run(template, func(t *testing.T) { + response := request(t, s, "PUT", `/mytopic`, body, map[string]string{ + "Template": "yes", + "Message": template, + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, target, m.Message) }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, target, m.Message) - }) - } + } + }) } func TestServer_MessageTemplate_UnsafeSprigFunctions(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ - "X-Message": `{{ env "PATH" }}`, - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ env "PATH" }}`, + "X-Template": "1", + }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) + require.Equal(t, 400, response.Code) + require.Equal(t, 40043, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MessageTemplate_InlineNewlines(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{}`, map[string]string{ - "X-Message": `{{"New\nlines"}}`, - "X-Title": `{{"New\nlines"}}`, - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{"New\nlines"}}`, + "X-Title": `{{"New\nlines"}}`, + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, `New + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `New lines`, m.Message) - require.Equal(t, `New + require.Equal(t, `New lines`, m.Title) + }) } func TestServer_MessageTemplate_InlineNewlinesOutsideOfTemplate(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"foo":"bar","food":"bag"}`, map[string]string{ - "X-Message": `{{.foo}}{{"\n"}}{{.food}}`, - "X-Title": `{{.food}}{{"\n"}}{{.foo}}`, - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"foo":"bar","food":"bag"}`, map[string]string{ + "X-Message": `{{.foo}}{{"\n"}}{{.food}}`, + "X-Title": `{{.food}}{{"\n"}}{{.foo}}`, + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, `bar + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `bar bag`, m.Message) - require.Equal(t, `bag + require.Equal(t, `bag bar`, m.Title) + }) } func TestServer_MessageTemplate_TemplateFileNewlines(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.TemplateDir = t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "newline.yml"), []byte(` + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "newline.yml"), []byte(` title: | {{.food}}{{"\n"}}{{.foo}} message: | {{.foo}}{{"\n"}}{{.food}} `), 0644)) - s := newTestServer(t, c) - response := request(t, s, "POST", "/mytopic?template=newline", `{"foo":"bar","food":"bag"}`, nil) - fmt.Println(response.Body.String()) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, `bar + s := newTestServer(t, c) + response := request(t, s, "POST", "/mytopic?template=newline", `{"foo":"bar","food":"bag"}`, nil) + fmt.Println(response.Body.String()) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, `bar bag`, m.Message) - require.Equal(t, `bag + require.Equal(t, `bag bar`, m.Title) + }) } var ( @@ -3191,13 +3495,14 @@ var ( ) func TestServer_MessageTemplate_FromNamedTemplate_GitHubCommentCreated(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "💬 New comment on issue #1389 instant alerts without Pull to refresh", m.Title) - require.Equal(t, `Commenter: https://github.com/wunter8 + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic?template=github", githubCommentCreatedJSON, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "💬 New comment on issue #1389 instant alerts without Pull to refresh", m.Title) + require.Equal(t, `Commenter: https://github.com/wunter8 Repository: https://github.com/binwiederhier/ntfy Comment link: https://github.com/binwiederhier/ntfy/issues/1389#issuecomment-3078214289 @@ -3207,16 +3512,18 @@ These are the things you need to do to get iOS push notifications to work: 2. put the URL you copied in the ntfy `+"`"+`base-url`+"`"+` config in server.yml or NTFY_BASE_URL in env variables 3. put the URL you copied in the default server URL setting in the iOS ntfy app 4. set `+"`"+`upstream-base-url`+"`"+` in server.yml or NTFY_UPSTREAM_BASE_URL in env variables to "https://ntfy.sh" (without a trailing slash)`, m.Message) + }) } func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "🐛 Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title) - require.Equal(t, `Opened by: https://github.com/TheUser-dev + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "🐛 Issue opened: #1391 http 500 error (ntfy error 50001)", m.Title) + require.Equal(t, `Opened by: https://github.com/TheUser-dev Repository: https://github.com/binwiederhier/ntfy Issue link: https://github.com/binwiederhier/ntfy/issues/1391 Labels: 🪲 bug @@ -3235,534 +3542,581 @@ closed with HTTP 500 (ntfy error 50001) (error=database table is locked, http_me :crystal_ball: **Additional context** Looks like this has already been fixed by #498, regression?`, m.Message) + }) } func TestServer_MessageTemplate_FromNamedTemplate_GitHubIssueOpened_OverrideConfigTemplate(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.TemplateDir = t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "github.yml"), []byte(` + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "github.yml"), []byte(` title: | Custom title: action={{ .action }} trunctitle={{ .issue.title | trunc 10 }} message: | Custom message {{ .issue.number }} `), 0644)) - s := newTestServer(t, c) - response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) - fmt.Println(response.Body.String()) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Custom title: action=opened trunctitle=http 500 e", m.Title) - require.Equal(t, "Custom message 1391", m.Message) + s := newTestServer(t, c) + response := request(t, s, "POST", "/mytopic?template=github", githubIssueOpenedJSON, nil) + fmt.Println(response.Body.String()) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Custom title: action=opened trunctitle=http 500 e", m.Title) + require.Equal(t, "Custom message 1391", m.Message) + }) } func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ - "X-Message": `{{ repeat 9999 "mystring" }}`, - "X-Template": "1", + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ repeat 9999 "mystring" }}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template") }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code) - require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template") } func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ - "X-Message": `{{ repeat 10001 "mystring" }}`, - "X-Template": "1", + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ repeat 10001 "mystring" }}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000") }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) - require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000") } func TestServer_MessageTemplate_Until100_000(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ - "X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`, - "X-Template": "1", + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{ + "X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 400, response.Code) + require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) + require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code) - require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations") } func TestServer_MessageTemplate_Priority(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"priority":"5"}`, map[string]string{ - "X-Message": "Test message", - "X-Priority": "{{.priority}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"priority":"5"}`, map[string]string{ + "X-Message": "Test message", + "X-Priority": "{{.priority}}", + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Test message", m.Message) - require.Equal(t, 5, m.Priority) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Test message", m.Message) + require.Equal(t, 5, m.Priority) + }) } func TestServer_MessageTemplate_Priority_Conditional(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Test with error status -> priority 5 - response := request(t, s, "PUT", "/mytopic", `{"status":"Error","message":"Something went wrong"}`, map[string]string{ - "X-Message": "Status: {{.status}} - {{.message}}", - "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, - "X-Template": "1", - }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Status: Error - Something went wrong", m.Message) - require.Equal(t, 5, m.Priority) + // Test with error status -> priority 5 + response := request(t, s, "PUT", "/mytopic", `{"status":"Error","message":"Something went wrong"}`, map[string]string{ + "X-Message": "Status: {{.status}} - {{.message}}", + "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Status: Error - Something went wrong", m.Message) + require.Equal(t, 5, m.Priority) - // Test with success status -> priority 3 - response = request(t, s, "PUT", "/mytopic", `{"status":"Success","message":"All good"}`, map[string]string{ - "X-Message": "Status: {{.status}} - {{.message}}", - "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, - "X-Template": "1", + // Test with success status -> priority 3 + response = request(t, s, "PUT", "/mytopic", `{"status":"Success","message":"All good"}`, map[string]string{ + "X-Message": "Status: {{.status}} - {{.message}}", + "X-Priority": `{{if eq .status "Error"}}5{{else}}3{{end}}`, + "X-Template": "1", + }) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, "Status: Success - All good", m.Message) + require.Equal(t, 3, m.Priority) }) - require.Equal(t, 200, response.Code) - m = toMessage(t, response.Body.String()) - require.Equal(t, "Status: Success - All good", m.Message) - require.Equal(t, 3, m.Priority) } func TestServer_MessageTemplate_Priority_NamedValue(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"severity":"high"}`, map[string]string{ - "X-Message": "Alert", - "X-Priority": "{{.severity}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"severity":"high"}`, map[string]string{ + "X-Message": "Alert", + "X-Priority": "{{.severity}}", + "X-Template": "1", + }) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, 4, m.Priority) // "high" = 4 + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, 4, m.Priority) // "high" = 4 + }) } func TestServer_MessageTemplate_Priority_Invalid(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic", `{"priority":"invalid"}`, map[string]string{ - "X-Message": "Test message", - "X-Priority": "{{.priority}}", - "X-Template": "1", - }) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic", `{"priority":"invalid"}`, map[string]string{ + "X-Message": "Test message", + "X-Priority": "{{.priority}}", + "X-Template": "1", + }) - require.Equal(t, 400, response.Code) - require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code) + require.Equal(t, 400, response.Code) + require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code) + }) } func TestServer_MessageTemplate_Priority_QueryParam(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "PUT", "/mytopic?template=1&priority={{.priority}}", `{"priority":"max"}`, nil) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "PUT", "/mytopic?template=1&priority={{.priority}}", `{"priority":"max"}`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, 5, m.Priority) // "max" = 5 + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, 5, m.Priority) // "max" = 5 + }) } func TestServer_MessageTemplate_Priority_FromTemplateFile(t *testing.T) { - t.Parallel() - c := newTestConfig(t) - c.TemplateDir = t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "priority-test.yml"), []byte(` + forEachBackend(t, func(t *testing.T) { + t.Parallel() + c := newTestConfig(t) + c.TemplateDir = t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(c.TemplateDir, "priority-test.yml"), []byte(` title: "{{.title}}" message: "{{.message}}" priority: '{{if eq .level "critical"}}5{{else if eq .level "warning"}}4{{else}}3{{end}}' `), 0644)) - s := newTestServer(t, c) + s := newTestServer(t, c) - // Test with critical level - response := request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"System down","level":"critical"}`, nil) - require.Equal(t, 200, response.Code) - m := toMessage(t, response.Body.String()) - require.Equal(t, "Alert", m.Title) - require.Equal(t, "System down", m.Message) - require.Equal(t, 5, m.Priority) + // Test with critical level + response := request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"System down","level":"critical"}`, nil) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "Alert", m.Title) + require.Equal(t, "System down", m.Message) + require.Equal(t, 5, m.Priority) - // Test with warning level - response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"High load","level":"warning"}`, nil) - require.Equal(t, 200, response.Code) - m = toMessage(t, response.Body.String()) - require.Equal(t, 4, m.Priority) + // Test with warning level + response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"High load","level":"warning"}`, nil) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, 4, m.Priority) - // Test with info level - response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"All good","level":"info"}`, nil) - require.Equal(t, 200, response.Code) - m = toMessage(t, response.Body.String()) - require.Equal(t, 3, m.Priority) + // Test with info level + response = request(t, s, "POST", "/mytopic?template=priority-test", `{"title":"Alert","message":"All good","level":"info"}`, nil) + require.Equal(t, 200, response.Code) + m = toMessage(t, response.Body.String()) + require.Equal(t, 3, m.Priority) + }) } func TestServer_DeleteMessage(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a message with a sequence ID - response := request(t, s, "PUT", "/mytopic/seq123", "original message", nil) - require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "seq123", msg.SequenceID) - require.Equal(t, "message", msg.Event) + // Publish a message with a sequence ID + response := request(t, s, "PUT", "/mytopic/seq123", "original message", nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "seq123", msg.SequenceID) + require.Equal(t, "message", msg.Event) - // Delete the message using DELETE method - response = request(t, s, "DELETE", "/mytopic/seq123", "", nil) - require.Equal(t, 200, response.Code) - deleteMsg := toMessage(t, response.Body.String()) - require.Equal(t, "seq123", deleteMsg.SequenceID) - require.Equal(t, "message_delete", deleteMsg.Event) + // Delete the message using DELETE method + response = request(t, s, "DELETE", "/mytopic/seq123", "", nil) + require.Equal(t, 200, response.Code) + deleteMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq123", deleteMsg.SequenceID) + require.Equal(t, "message_delete", deleteMsg.Event) - // Poll and verify both messages are returned - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 2, len(lines)) + // Poll and verify both messages are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) - msg1 := toMessage(t, lines[0]) - msg2 := toMessage(t, lines[1]) - require.Equal(t, "message", msg1.Event) - require.Equal(t, "message_delete", msg2.Event) - require.Equal(t, "seq123", msg1.SequenceID) - require.Equal(t, "seq123", msg2.SequenceID) + msg1 := toMessage(t, lines[0]) + msg2 := toMessage(t, lines[1]) + require.Equal(t, "message", msg1.Event) + require.Equal(t, "message_delete", msg2.Event) + require.Equal(t, "seq123", msg1.SequenceID) + require.Equal(t, "seq123", msg2.SequenceID) + }) } func TestServer_ClearMessage(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a message with a sequence ID - response := request(t, s, "PUT", "/mytopic/seq456", "original message", nil) - require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "seq456", msg.SequenceID) - require.Equal(t, "message", msg.Event) + // Publish a message with a sequence ID + response := request(t, s, "PUT", "/mytopic/seq456", "original message", nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "seq456", msg.SequenceID) + require.Equal(t, "message", msg.Event) - // Clear the message using PUT /topic/seq/clear - response = request(t, s, "PUT", "/mytopic/seq456/clear", "", nil) - require.Equal(t, 200, response.Code) - clearMsg := toMessage(t, response.Body.String()) - require.Equal(t, "seq456", clearMsg.SequenceID) - require.Equal(t, "message_clear", clearMsg.Event) + // Clear the message using PUT /topic/seq/clear + response = request(t, s, "PUT", "/mytopic/seq456/clear", "", nil) + require.Equal(t, 200, response.Code) + clearMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq456", clearMsg.SequenceID) + require.Equal(t, "message_clear", clearMsg.Event) - // Poll and verify both messages are returned - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 2, len(lines)) + // Poll and verify both messages are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) - msg1 := toMessage(t, lines[0]) - msg2 := toMessage(t, lines[1]) - require.Equal(t, "message", msg1.Event) - require.Equal(t, "message_clear", msg2.Event) - require.Equal(t, "seq456", msg1.SequenceID) - require.Equal(t, "seq456", msg2.SequenceID) + msg1 := toMessage(t, lines[0]) + msg2 := toMessage(t, lines[1]) + require.Equal(t, "message", msg1.Event) + require.Equal(t, "message_clear", msg2.Event) + require.Equal(t, "seq456", msg1.SequenceID) + require.Equal(t, "seq456", msg2.SequenceID) + }) } func TestServer_ClearMessage_ReadEndpoint(t *testing.T) { - // Test that /topic/seq/read also works - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + // Test that /topic/seq/read also works + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a message - response := request(t, s, "PUT", "/mytopic/seq789", "original message", nil) - require.Equal(t, 200, response.Code) + // Publish a message + response := request(t, s, "PUT", "/mytopic/seq789", "original message", nil) + require.Equal(t, 200, response.Code) - // Clear using /read endpoint - response = request(t, s, "PUT", "/mytopic/seq789/read", "", nil) - require.Equal(t, 200, response.Code) - clearMsg := toMessage(t, response.Body.String()) - require.Equal(t, "seq789", clearMsg.SequenceID) - require.Equal(t, "message_clear", clearMsg.Event) + // Clear using /read endpoint + response = request(t, s, "PUT", "/mytopic/seq789/read", "", nil) + require.Equal(t, 200, response.Code) + clearMsg := toMessage(t, response.Body.String()) + require.Equal(t, "seq789", clearMsg.SequenceID) + require.Equal(t, "message_clear", clearMsg.Event) + }) } func TestServer_UpdateMessage(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish original message - response := request(t, s, "PUT", "/mytopic/update-seq", "original message", nil) - require.Equal(t, 200, response.Code) - msg1 := toMessage(t, response.Body.String()) - require.Equal(t, "update-seq", msg1.SequenceID) - require.Equal(t, "original message", msg1.Message) + // Publish original message + response := request(t, s, "PUT", "/mytopic/update-seq", "original message", nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.Equal(t, "update-seq", msg1.SequenceID) + require.Equal(t, "original message", msg1.Message) - // Update the message (same sequence ID, new content) - response = request(t, s, "PUT", "/mytopic/update-seq", "updated message", nil) - require.Equal(t, 200, response.Code) - msg2 := toMessage(t, response.Body.String()) - require.Equal(t, "update-seq", msg2.SequenceID) - require.Equal(t, "updated message", msg2.Message) - require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs + // Update the message (same sequence ID, new content) + response = request(t, s, "PUT", "/mytopic/update-seq", "updated message", nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, "update-seq", msg2.SequenceID) + require.Equal(t, "updated message", msg2.Message) + require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs - // Poll and verify both versions are returned - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 2, len(lines)) + // Poll and verify both versions are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) - polledMsg1 := toMessage(t, lines[0]) - polledMsg2 := toMessage(t, lines[1]) - require.Equal(t, "original message", polledMsg1.Message) - require.Equal(t, "updated message", polledMsg2.Message) - require.Equal(t, "update-seq", polledMsg1.SequenceID) - require.Equal(t, "update-seq", polledMsg2.SequenceID) + polledMsg1 := toMessage(t, lines[0]) + polledMsg2 := toMessage(t, lines[1]) + require.Equal(t, "original message", polledMsg1.Message) + require.Equal(t, "updated message", polledMsg2.Message) + require.Equal(t, "update-seq", polledMsg1.SequenceID) + require.Equal(t, "update-seq", polledMsg2.SequenceID) + }) } func TestServer_UpdateMessage_UsingMessageID(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish original message without a sequence ID - response := request(t, s, "PUT", "/mytopic", "original message", nil) - require.Equal(t, 200, response.Code) - msg1 := toMessage(t, response.Body.String()) - require.NotEmpty(t, msg1.ID) - require.Empty(t, msg1.SequenceID) // No sequence ID provided - require.Equal(t, "original message", msg1.Message) + // Publish original message without a sequence ID + response := request(t, s, "PUT", "/mytopic", "original message", nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.NotEmpty(t, msg1.ID) + require.Empty(t, msg1.SequenceID) // No sequence ID provided + require.Equal(t, "original message", msg1.Message) - // Update the message using the message ID as the sequence ID - response = request(t, s, "PUT", "/mytopic/"+msg1.ID, "updated message", nil) - require.Equal(t, 200, response.Code) - msg2 := toMessage(t, response.Body.String()) - require.Equal(t, msg1.ID, msg2.SequenceID) // Message ID is now used as sequence ID - require.Equal(t, "updated message", msg2.Message) - require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs + // Update the message using the message ID as the sequence ID + response = request(t, s, "PUT", "/mytopic/"+msg1.ID, "updated message", nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, msg1.ID, msg2.SequenceID) // Message ID is now used as sequence ID + require.Equal(t, "updated message", msg2.Message) + require.NotEqual(t, msg1.ID, msg2.ID) // Different message IDs - // Poll and verify both versions are returned - response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) - require.Equal(t, 200, response.Code) - lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") - require.Equal(t, 2, len(lines)) + // Poll and verify both versions are returned + response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil) + require.Equal(t, 200, response.Code) + lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n") + require.Equal(t, 2, len(lines)) - polledMsg1 := toMessage(t, lines[0]) - polledMsg2 := toMessage(t, lines[1]) - require.Equal(t, "original message", polledMsg1.Message) - require.Equal(t, "updated message", polledMsg2.Message) - require.Empty(t, polledMsg1.SequenceID) // Original has no sequence ID - require.Equal(t, msg1.ID, polledMsg2.SequenceID) // Update uses original message ID as sequence ID + polledMsg1 := toMessage(t, lines[0]) + polledMsg2 := toMessage(t, lines[1]) + require.Equal(t, "original message", polledMsg1.Message) + require.Equal(t, "updated message", polledMsg2.Message) + require.Empty(t, polledMsg1.SequenceID) // Original has no sequence ID + require.Equal(t, msg1.ID, polledMsg2.SequenceID) // Update uses original message ID as sequence ID + }) } func TestServer_DeleteAndClear_InvalidSequenceID(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Test invalid sequence ID for delete (returns 404 because route doesn't match) - response := request(t, s, "DELETE", "/mytopic/invalid*seq", "", nil) - require.Equal(t, 404, response.Code) + // Test invalid sequence ID for delete (returns 404 because route doesn't match) + response := request(t, s, "DELETE", "/mytopic/invalid*seq", "", nil) + require.Equal(t, 404, response.Code) - // Test invalid sequence ID for clear (returns 404 because route doesn't match) - response = request(t, s, "PUT", "/mytopic/invalid*seq/clear", "", nil) - require.Equal(t, 404, response.Code) + // Test invalid sequence ID for clear (returns 404 because route doesn't match) + response = request(t, s, "PUT", "/mytopic/invalid*seq/clear", "", nil) + require.Equal(t, 404, response.Code) + }) } func TestServer_DeleteMessage_WithFirebase(t *testing.T) { - sender := newTestFirebaseSender(10) - s := newTestServer(t, newTestConfig(t)) - s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + forEachBackend(t, func(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) - // Publish a message - response := request(t, s, "PUT", "/mytopic/firebase-seq", "test message", nil) - require.Equal(t, 200, response.Code) + // Publish a message + response := request(t, s, "PUT", "/mytopic/firebase-seq", "test message", nil) + require.Equal(t, 200, response.Code) - time.Sleep(100 * time.Millisecond) // Firebase publishing happens - require.Equal(t, 1, len(sender.Messages())) - require.Equal(t, "message", sender.Messages()[0].Data["event"]) + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "message", sender.Messages()[0].Data["event"]) - // Delete the message - response = request(t, s, "DELETE", "/mytopic/firebase-seq", "", nil) - require.Equal(t, 200, response.Code) + // Delete the message + response = request(t, s, "DELETE", "/mytopic/firebase-seq", "", nil) + require.Equal(t, 200, response.Code) - time.Sleep(100 * time.Millisecond) // Firebase publishing happens - require.Equal(t, 2, len(sender.Messages())) - require.Equal(t, "message_delete", sender.Messages()[1].Data["event"]) - require.Equal(t, "firebase-seq", sender.Messages()[1].Data["sequence_id"]) + time.Sleep(100 * time.Millisecond) // Firebase publishing happens + require.Equal(t, 2, len(sender.Messages())) + require.Equal(t, "message_delete", sender.Messages()[1].Data["event"]) + require.Equal(t, "firebase-seq", sender.Messages()[1].Data["sequence_id"]) + }) } func TestServer_ClearMessage_WithFirebase(t *testing.T) { - sender := newTestFirebaseSender(10) - s := newTestServer(t, newTestConfig(t)) - s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) + forEachBackend(t, func(t *testing.T) { + sender := newTestFirebaseSender(10) + s := newTestServer(t, newTestConfig(t)) + s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true}) - // Publish a message - response := request(t, s, "PUT", "/mytopic/firebase-clear-seq", "test message", nil) - require.Equal(t, 200, response.Code) + // Publish a message + response := request(t, s, "PUT", "/mytopic/firebase-clear-seq", "test message", nil) + require.Equal(t, 200, response.Code) - time.Sleep(100 * time.Millisecond) - require.Equal(t, 1, len(sender.Messages())) + time.Sleep(100 * time.Millisecond) + require.Equal(t, 1, len(sender.Messages())) - // Clear the message - response = request(t, s, "PUT", "/mytopic/firebase-clear-seq/clear", "", nil) - require.Equal(t, 200, response.Code) + // Clear the message + response = request(t, s, "PUT", "/mytopic/firebase-clear-seq/clear", "", nil) + require.Equal(t, 200, response.Code) - time.Sleep(100 * time.Millisecond) - require.Equal(t, 2, len(sender.Messages())) - require.Equal(t, "message_clear", sender.Messages()[1].Data["event"]) - require.Equal(t, "firebase-clear-seq", sender.Messages()[1].Data["sequence_id"]) + time.Sleep(100 * time.Millisecond) + require.Equal(t, 2, len(sender.Messages())) + require.Equal(t, "message_clear", sender.Messages()[1].Data["event"]) + require.Equal(t, "firebase-clear-seq", sender.Messages()[1].Data["sequence_id"]) + }) } func TestServer_UpdateScheduledMessage(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a scheduled message (future delivery) - response := request(t, s, "PUT", "/mytopic/sched-seq?delay=1h", "original scheduled message", nil) - require.Equal(t, 200, response.Code) - msg1 := toMessage(t, response.Body.String()) - require.Equal(t, "sched-seq", msg1.SequenceID) - require.Equal(t, "original scheduled message", msg1.Message) + // Publish a scheduled message (future delivery) + response := request(t, s, "PUT", "/mytopic/sched-seq?delay=1h", "original scheduled message", nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.Equal(t, "sched-seq", msg1.SequenceID) + require.Equal(t, "original scheduled message", msg1.Message) - // Verify scheduled message exists - response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "original scheduled message", messages[0].Message) + // Verify scheduled message exists + response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "original scheduled message", messages[0].Message) - // Update the scheduled message (same sequence ID, new content) - response = request(t, s, "PUT", "/mytopic/sched-seq?delay=2h", "updated scheduled message", nil) - require.Equal(t, 200, response.Code) - msg2 := toMessage(t, response.Body.String()) - require.Equal(t, "sched-seq", msg2.SequenceID) - require.Equal(t, "updated scheduled message", msg2.Message) - require.NotEqual(t, msg1.ID, msg2.ID) + // Update the scheduled message (same sequence ID, new content) + response = request(t, s, "PUT", "/mytopic/sched-seq?delay=2h", "updated scheduled message", nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, "sched-seq", msg2.SequenceID) + require.Equal(t, "updated scheduled message", msg2.Message) + require.NotEqual(t, msg1.ID, msg2.ID) - // Verify only the updated message exists (old scheduled was deleted) - response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "updated scheduled message", messages[0].Message) - require.Equal(t, msg2.ID, messages[0].ID) + // Verify only the updated message exists (old scheduled was deleted) + response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "updated scheduled message", messages[0].Message) + require.Equal(t, msg2.ID, messages[0].ID) + }) } func TestServer_DeleteScheduledMessage(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a scheduled message (future delivery) - response := request(t, s, "PUT", "/mytopic/delete-sched-seq?delay=1h", "scheduled message to delete", nil) - require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "delete-sched-seq", msg.SequenceID) + // Publish a scheduled message (future delivery) + response := request(t, s, "PUT", "/mytopic/delete-sched-seq?delay=1h", "scheduled message to delete", nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "delete-sched-seq", msg.SequenceID) - // Verify scheduled message exists - response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "scheduled message to delete", messages[0].Message) + // Verify scheduled message exists + response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "scheduled message to delete", messages[0].Message) - // Delete the scheduled message - response = request(t, s, "DELETE", "/mytopic/delete-sched-seq", "", nil) - require.Equal(t, 200, response.Code) - deleteMsg := toMessage(t, response.Body.String()) - require.Equal(t, "delete-sched-seq", deleteMsg.SequenceID) - require.Equal(t, "message_delete", deleteMsg.Event) + // Delete the scheduled message + response = request(t, s, "DELETE", "/mytopic/delete-sched-seq", "", nil) + require.Equal(t, 200, response.Code) + deleteMsg := toMessage(t, response.Body.String()) + require.Equal(t, "delete-sched-seq", deleteMsg.SequenceID) + require.Equal(t, "message_delete", deleteMsg.Event) - // Verify scheduled message was deleted, only delete event remains - response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "message_delete", messages[0].Event) - require.Equal(t, "delete-sched-seq", messages[0].SequenceID) + // Verify scheduled message was deleted, only delete event remains + response = request(t, s, "GET", "/mytopic/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "message_delete", messages[0].Event) + require.Equal(t, "delete-sched-seq", messages[0].SequenceID) + }) } func TestServer_UpdateScheduledMessage_TopicScoped(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish scheduled messages with same sequence ID in different topics - response := request(t, s, "PUT", "/topic1/shared-seq?delay=1h", "topic1 scheduled", nil) - require.Equal(t, 200, response.Code) + // Publish scheduled messages with same sequence ID in different topics + response := request(t, s, "PUT", "/topic1/shared-seq?delay=1h", "topic1 scheduled", nil) + require.Equal(t, 200, response.Code) - response = request(t, s, "PUT", "/topic2/shared-seq?delay=1h", "topic2 scheduled", nil) - require.Equal(t, 200, response.Code) + response = request(t, s, "PUT", "/topic2/shared-seq?delay=1h", "topic2 scheduled", nil) + require.Equal(t, 200, response.Code) - // Update scheduled message in topic1 only - response = request(t, s, "PUT", "/topic1/shared-seq?delay=2h", "topic1 updated", nil) - require.Equal(t, 200, response.Code) + // Update scheduled message in topic1 only + response = request(t, s, "PUT", "/topic1/shared-seq?delay=2h", "topic1 updated", nil) + require.Equal(t, 200, response.Code) - // Verify topic1 has only the updated message - response = request(t, s, "GET", "/topic1/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages := toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "topic1 updated", messages[0].Message) + // Verify topic1 has only the updated message + response = request(t, s, "GET", "/topic1/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages := toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "topic1 updated", messages[0].Message) - // Verify topic2 still has its original scheduled message (not affected) - response = request(t, s, "GET", "/topic2/json?poll=1&scheduled=1", "", nil) - require.Equal(t, 200, response.Code) - messages = toMessages(t, response.Body.String()) - require.Equal(t, 1, len(messages)) - require.Equal(t, "topic2 scheduled", messages[0].Message) + // Verify topic2 still has its original scheduled message (not affected) + response = request(t, s, "GET", "/topic2/json?poll=1&scheduled=1", "", nil) + require.Equal(t, 200, response.Code) + messages = toMessages(t, response.Body.String()) + require.Equal(t, 1, len(messages)) + require.Equal(t, "topic2 scheduled", messages[0].Message) + }) } func TestServer_UpdateScheduledMessage_WithAttachment(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a scheduled message with an attachment - content := util.RandomString(5000) // > 4096 to trigger attachment - response := request(t, s, "PUT", "/mytopic/attach-seq?delay=1h", content, nil) - require.Equal(t, 200, response.Code) - msg1 := toMessage(t, response.Body.String()) - require.Equal(t, "attach-seq", msg1.SequenceID) - require.NotNil(t, msg1.Attachment) + // Publish a scheduled message with an attachment + content := util.RandomString(5000) // > 4096 to trigger attachment + response := request(t, s, "PUT", "/mytopic/attach-seq?delay=1h", content, nil) + require.Equal(t, 200, response.Code) + msg1 := toMessage(t, response.Body.String()) + require.Equal(t, "attach-seq", msg1.SequenceID) + require.NotNil(t, msg1.Attachment) - // Verify attachment file exists - attachmentFile1 := filepath.Join(s.config.AttachmentCacheDir, msg1.ID) - require.FileExists(t, attachmentFile1) + // Verify attachment file exists + attachmentFile1 := filepath.Join(s.config.AttachmentCacheDir, msg1.ID) + require.FileExists(t, attachmentFile1) - // Update the scheduled message with a new attachment - newContent := util.RandomString(5000) - response = request(t, s, "PUT", "/mytopic/attach-seq?delay=2h", newContent, nil) - require.Equal(t, 200, response.Code) - msg2 := toMessage(t, response.Body.String()) - require.Equal(t, "attach-seq", msg2.SequenceID) - require.NotEqual(t, msg1.ID, msg2.ID) + // Update the scheduled message with a new attachment + newContent := util.RandomString(5000) + response = request(t, s, "PUT", "/mytopic/attach-seq?delay=2h", newContent, nil) + require.Equal(t, 200, response.Code) + msg2 := toMessage(t, response.Body.String()) + require.Equal(t, "attach-seq", msg2.SequenceID) + require.NotEqual(t, msg1.ID, msg2.ID) - // Verify old attachment file was deleted - require.NoFileExists(t, attachmentFile1) + // Verify old attachment file was deleted + require.NoFileExists(t, attachmentFile1) - // Verify new attachment file exists - attachmentFile2 := filepath.Join(s.config.AttachmentCacheDir, msg2.ID) - require.FileExists(t, attachmentFile2) + // Verify new attachment file exists + attachmentFile2 := filepath.Join(s.config.AttachmentCacheDir, msg2.ID) + require.FileExists(t, attachmentFile2) + }) } func TestServer_DeleteScheduledMessage_WithAttachment(t *testing.T) { - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - // Publish a scheduled message with an attachment - content := util.RandomString(5000) // > 4096 to trigger attachment - response := request(t, s, "PUT", "/mytopic/delete-attach-seq?delay=1h", content, nil) - require.Equal(t, 200, response.Code) - msg := toMessage(t, response.Body.String()) - require.Equal(t, "delete-attach-seq", msg.SequenceID) - require.NotNil(t, msg.Attachment) + // Publish a scheduled message with an attachment + content := util.RandomString(5000) // > 4096 to trigger attachment + response := request(t, s, "PUT", "/mytopic/delete-attach-seq?delay=1h", content, nil) + require.Equal(t, 200, response.Code) + msg := toMessage(t, response.Body.String()) + require.Equal(t, "delete-attach-seq", msg.SequenceID) + require.NotNil(t, msg.Attachment) - // Verify attachment file exists - attachmentFile := filepath.Join(s.config.AttachmentCacheDir, msg.ID) - require.FileExists(t, attachmentFile) + // Verify attachment file exists + attachmentFile := filepath.Join(s.config.AttachmentCacheDir, msg.ID) + require.FileExists(t, attachmentFile) - // Delete the scheduled message - response = request(t, s, "DELETE", "/mytopic/delete-attach-seq", "", nil) - require.Equal(t, 200, response.Code) - deleteMsg := toMessage(t, response.Body.String()) - require.Equal(t, "message_delete", deleteMsg.Event) + // Delete the scheduled message + response = request(t, s, "DELETE", "/mytopic/delete-attach-seq", "", nil) + require.Equal(t, 200, response.Code) + deleteMsg := toMessage(t, response.Body.String()) + require.Equal(t, "message_delete", deleteMsg.Event) - // Verify attachment file was deleted - require.NoFileExists(t, attachmentFile) + // Verify attachment file was deleted + require.NoFileExists(t, attachmentFile) + }) } func newMemTestCache(t *testing.T) message.Store { @@ -3771,19 +4125,66 @@ func newMemTestCache(t *testing.T) message.Store { return c } +var testBackendDatabaseURLs sync.Map + +func forEachBackend(t *testing.T, f func(t *testing.T)) { + t.Run("sqlite", func(t *testing.T) { + f(t) + }) + t.Run("postgres", func(t *testing.T) { + dsn := os.Getenv("NTFY_TEST_DATABASE_URL") + if dsn == "" { + t.Skip("NTFY_TEST_DATABASE_URL not set") + } + schema := fmt.Sprintf("test_%s", util.RandomString(10)) + setupDB, err := sql.Open("pgx", dsn) + require.Nil(t, err) + _, err = setupDB.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema)) + require.Nil(t, err) + require.Nil(t, setupDB.Close()) + u, err := url.Parse(dsn) + require.Nil(t, err) + q := u.Query() + q.Set("search_path", schema) + u.RawQuery = q.Encode() + schemaDSN := u.String() + testBackendDatabaseURLs.Store(t, schemaDSN) + t.Cleanup(func() { + testBackendDatabaseURLs.Delete(t) + cleanDB, _ := sql.Open("pgx", dsn) + cleanDB.Exec(fmt.Sprintf("DROP SCHEMA %s CASCADE", schema)) + cleanDB.Close() + }) + f(t) + }) +} + +func testBackendDatabaseURL(t *testing.T) string { + if dbURL, ok := testBackendDatabaseURLs.Load(t); ok { + return dbURL.(string) + } + return "" +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" - conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") - conf.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" + if dbURL := testBackendDatabaseURL(t); dbURL != "" { + conf.DatabaseURL = dbURL + } else { + conf.CacheFile = filepath.Join(t.TempDir(), "cache.db") + conf.CacheStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" + } conf.AttachmentCacheDir = t.TempDir() conf.TemplateDir = t.TempDir() return conf } func configureAuth(t *testing.T, conf *Config) *Config { - conf.AuthFile = filepath.Join(t.TempDir(), "user.db") - conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" + if conf.DatabaseURL == "" { + conf.AuthFile = filepath.Join(t.TempDir(), "user.db") + conf.AuthStartupQueries = "pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory;" + } conf.AuthBcryptCost = bcrypt.MinCost // This speeds up tests a lot return conf } @@ -3959,110 +4360,114 @@ func (w *closableResponseWriter) Close() { } func TestServer_SubscribeHTTP_NoWriteAfterHandlerReturn(t *testing.T) { - // This test reproduces the panic from https://github.com/binwiederhier/ntfy/issues/338: - // - // panic: runtime error: invalid memory address or nil pointer dereference - // bufio.(*Writer).Flush(...) - // net/http.(*response).Flush(...) - // server.(*Server).handleSubscribeHTTP.func2(...) - // server.(*topic).Publish.func1.1(...) - // - // The race: topic.Publish() copies the subscriber list and calls each subscriber in its own - // goroutine. If the subscriber disconnects, the handler returns and Go's HTTP server cleans up - // the response writer. But a Publish goroutine that copied the subscriber list BEFORE - // Unsubscribe may still call sub() AFTER the handler returns. - // - // This test deterministically reproduces the scenario by: - // 1. Subscribing via handleSubscribeHTTP (which registers a sub closure on the topic) - // 2. Copying the subscriber function from the topic (simulating what topic.Publish does) - // 3. Cancelling the subscription and waiting for the handler to fully return - // 4. Calling the copied subscriber function AFTER the handler has returned - // 5. Checking that no write/flush occurred on the (now-invalid) response writer - // - // Without the wlock+closed fix, calling the subscriber after the handler returns writes to - // the closed response writer (which in production causes a nil pointer panic on Flush). - // With the fix, the subscriber sees closed=true and returns without writing. - t.Parallel() - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + // This test reproduces the panic from https://github.com/binwiederhier/ntfy/issues/338: + // + // panic: runtime error: invalid memory address or nil pointer dereference + // bufio.(*Writer).Flush(...) + // net/http.(*response).Flush(...) + // server.(*Server).handleSubscribeHTTP.func2(...) + // server.(*topic).Publish.func1.1(...) + // + // The race: topic.Publish() copies the subscriber list and calls each subscriber in its own + // goroutine. If the subscriber disconnects, the handler returns and Go's HTTP server cleans up + // the response writer. But a Publish goroutine that copied the subscriber list BEFORE + // Unsubscribe may still call sub() AFTER the handler returns. + // + // This test deterministically reproduces the scenario by: + // 1. Subscribing via handleSubscribeHTTP (which registers a sub closure on the topic) + // 2. Copying the subscriber function from the topic (simulating what topic.Publish does) + // 3. Cancelling the subscription and waiting for the handler to fully return + // 4. Calling the copied subscriber function AFTER the handler has returned + // 5. Checking that no write/flush occurred on the (now-invalid) response writer + // + // Without the wlock+closed fix, calling the subscriber after the handler returns writes to + // the closed response writer (which in production causes a nil pointer panic on Flush). + // With the fix, the subscriber sees closed=true and returns without writing. + t.Parallel() + s := newTestServer(t, newTestConfig(t)) - rw := newClosableResponseWriter() - ctx, cancel := context.WithCancel(context.Background()) - req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil) - require.Nil(t, err) - req.RemoteAddr = "9.9.9.9:1234" + rw := newClosableResponseWriter() + ctx, cancel := context.WithCancel(context.Background()) + req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil) + require.Nil(t, err) + req.RemoteAddr = "9.9.9.9:1234" - // Start the subscribe handler (blocks until context is cancelled) - handlerDone := make(chan struct{}) - go func() { - s.handle(rw, req) - close(handlerDone) - }() - time.Sleep(100 * time.Millisecond) // Wait for subscription to be registered + // Start the subscribe handler (blocks until context is cancelled) + handlerDone := make(chan struct{}) + go func() { + s.handle(rw, req) + close(handlerDone) + }() + time.Sleep(100 * time.Millisecond) // Wait for subscription to be registered - // Grab a copy of the subscriber function from the topic, exactly as topic.Publish() does - // via subscribersCopy(). This must happen BEFORE cancel/Unsubscribe removes the subscriber. - s.mu.RLock() - tp := s.topics["mytopic"] - s.mu.RUnlock() - require.NotNil(t, tp) - subscribersCopy := tp.subscribersCopy() - require.Equal(t, 1, len(subscribersCopy)) + // Grab a copy of the subscriber function from the topic, exactly as topic.Publish() does + // via subscribersCopy(). This must happen BEFORE cancel/Unsubscribe removes the subscriber. + s.mu.RLock() + tp := s.topics["mytopic"] + s.mu.RUnlock() + require.NotNil(t, tp) + subscribersCopy := tp.subscribersCopy() + require.Equal(t, 1, len(subscribersCopy)) - var copiedSub subscriber - for _, sub := range subscribersCopy { - copiedSub = sub.subscriber - } + var copiedSub subscriber + for _, sub := range subscribersCopy { + copiedSub = sub.subscriber + } - // Cancel the subscription and wait for the handler to fully return. - // At this point, the deferred cleanup in handleSubscribeHTTP runs: - // - With fix: wlock.Lock() waits for in-flight sub(), sets closed=true, wlock.Unlock() - // - Without fix: nothing prevents future sub() calls from writing - cancel() - <-handlerDone + // Cancel the subscription and wait for the handler to fully return. + // At this point, the deferred cleanup in handleSubscribeHTTP runs: + // - With fix: wlock.Lock() waits for in-flight sub(), sets closed=true, wlock.Unlock() + // - Without fix: nothing prevents future sub() calls from writing + cancel() + <-handlerDone - // Simulate Go's HTTP server cleaning up the response writer after the handler returns. - // In production, this is finishRequest() which nils out the bufio.Writer. - rw.Close() + // Simulate Go's HTTP server cleaning up the response writer after the handler returns. + // In production, this is finishRequest() which nils out the bufio.Writer. + rw.Close() - // Now call the copied subscriber function, simulating a straggler Publish goroutine - // that copied the subscriber list before Unsubscribe ran. In production, this is exactly - // how the panic occurs: the goroutine spawned by topic.Publish calls sub() after the - // handler has already returned and Go has cleaned up the response writer. - v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("9.9.9.9"), nil) - msg := newDefaultMessage("mytopic", "straggler message") - _ = copiedSub(v, msg) + // Now call the copied subscriber function, simulating a straggler Publish goroutine + // that copied the subscriber list before Unsubscribe ran. In production, this is exactly + // how the panic occurs: the goroutine spawned by topic.Publish calls sub() after the + // handler has already returned and Go has cleaned up the response writer. + v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("9.9.9.9"), nil) + msg := newDefaultMessage("mytopic", "straggler message") + _ = copiedSub(v, msg) - require.False(t, rw.wroteAfterClose.Load(), - "sub() wrote to the response writer after the handler returned; "+ - "in production this causes a nil pointer panic in bufio.(*Writer).Flush()") + require.False(t, rw.wroteAfterClose.Load(), + "sub() wrote to the response writer after the handler returned; "+ + "in production this causes a nil pointer panic in bufio.(*Writer).Flush()") + }) } func TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection(t *testing.T) { - // Test that handleError does not call WriteHeader for WebSocket errors wrapped - // with errWebSocketPostUpgrade (indicating the connection was hijacked) - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + // Test that handleError does not call WriteHeader for WebSocket errors wrapped + // with errWebSocketPostUpgrade (indicating the connection was hijacked) + s := newTestServer(t, newTestConfig(t)) - // Create a WebSocket upgrade request - r, _ := http.NewRequest("GET", "/mytopic/ws", nil) - r.Header.Set("Upgrade", "websocket") - r.Header.Set("Connection", "Upgrade") - v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("1.2.3.4"), nil) + // Create a WebSocket upgrade request + r, _ := http.NewRequest("GET", "/mytopic/ws", nil) + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Connection", "Upgrade") + v := newVisitor(s.config, s.messageCache, s.userManager, netip.MustParseAddr("1.2.3.4"), nil) - // Test post-upgrade errors wrapped with errWebSocketPostUpgrade (should NOT call WriteHeader) - postUpgradeErr := &errWebSocketPostUpgrade{errors.New("websocket: close 1000 (normal)")} - mock := newMockResponseWriter() - s.handleError(mock, r, v, postUpgradeErr) - require.False(t, mock.writeHeaderHit, "WriteHeader should not be called for post-upgrade errors") - - // Test pre-upgrade errors (should call WriteHeader) - preUpgradeErrors := []error{ - errHTTPBadRequestWebSocketsUpgradeHeaderMissing, - errHTTPTooManyRequestsLimitSubscriptions, - errHTTPInternalError, - } - for _, err := range preUpgradeErrors { + // Test post-upgrade errors wrapped with errWebSocketPostUpgrade (should NOT call WriteHeader) + postUpgradeErr := &errWebSocketPostUpgrade{errors.New("websocket: close 1000 (normal)")} mock := newMockResponseWriter() - s.handleError(mock, r, v, err) - require.True(t, mock.writeHeaderHit, "WriteHeader should be called for error: %s", err.Error()) - } + s.handleError(mock, r, v, postUpgradeErr) + require.False(t, mock.writeHeaderHit, "WriteHeader should not be called for post-upgrade errors") + + // Test pre-upgrade errors (should call WriteHeader) + preUpgradeErrors := []error{ + errHTTPBadRequestWebSocketsUpgradeHeaderMissing, + errHTTPTooManyRequestsLimitSubscriptions, + errHTTPInternalError, + } + for _, err := range preUpgradeErrors { + mock := newMockResponseWriter() + s.handleError(mock, r, v, err) + require.True(t, mock.writeHeaderHit, "WriteHeader should be called for error: %s", err.Error()) + } + }) } diff --git a/server/server_twilio_test.go b/server/server_twilio_test.go index 9b6dcff5..29768b53 100644 --- a/server/server_twilio_test.go +++ b/server/server_twilio_test.go @@ -14,217 +14,224 @@ import ( ) func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) { - var called, verified atomic.Bool - var code atomic.Pointer[string] - twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.Nil(t, err) - require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) - if r.URL.Path == "/v2/Services/VA1234567890/Verifications" { - if code.Load() != nil { + forEachBackend(t, func(t *testing.T) { + var called, verified atomic.Bool + var code atomic.Pointer[string] + twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization")) + if r.URL.Path == "/v2/Services/VA1234567890/Verifications" { + if code.Load() != nil { + t.Fatal("Should be only called once") + } + require.Equal(t, "Channel=sms&To=%2B12223334444", string(body)) + code.Store(util.String("123456")) + } else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" { + if verified.Load() { + t.Fatal("Should be only called once") + } + require.Equal(t, "Code=123456&To=%2B12223334444", string(body)) + verified.Store(true) + } else { + t.Fatal("Unexpected path:", r.URL.Path) + } + })) + defer twilioVerifyServer.Close() + twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { t.Fatal("Should be only called once") } - require.Equal(t, "Channel=sms&To=%2B12223334444", string(body)) - code.Store(util.String("123456")) - } else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" { - if verified.Load() { - t.Fatal("Should be only called once") - } - require.Equal(t, "Code=123456&To=%2B12223334444", string(body)) - verified.Store(true) - } else { - t.Fatal("Unexpected path:", r.URL.Path) - } - })) - defer twilioVerifyServer.Close() - twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if called.Load() { - t.Fatal("Should be only called once") - } - body, err := io.ReadAll(r.Body) + 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=%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)) + called.Store(true) + })) + defer twilioCallsServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioVerifyBaseURL = twilioVerifyServer.URL + c.TwilioCallsBaseURL = twilioCallsServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + c.TwilioVerifyService = "VA1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") 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=%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)) - called.Store(true) - })) - defer twilioCallsServer.Close() - c := newTestConfigWithAuthFile(t) - c.TwilioVerifyBaseURL = twilioVerifyServer.URL - c.TwilioCallsBaseURL = twilioCallsServer.URL - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - c.TwilioVerifyService = "VA1234567890" - s := newTestServer(t, c) + // Send verification code for phone number + response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + waitFor(t, func() bool { + return *code.Load() == "123456" + }) - // Add tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 10, - CallLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - u, err := s.userManager.User("phil") - require.Nil(t, err) + // Add phone number with code + response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) + waitFor(t, func() bool { + return verified.Load() + }) + phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) + require.Nil(t, err) + require.Equal(t, 1, len(phoneNumbers)) + require.Equal(t, "+12223334444", phoneNumbers[0]) - // Send verification code for phone number - response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - waitFor(t, func() bool { - return *code.Load() == "123456" - }) + // Do the thing + response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "yes", + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) - // Add phone number with code - response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - waitFor(t, func() bool { - return verified.Load() - }) - phoneNumbers, err := s.userManager.PhoneNumbers(u.ID) - require.Nil(t, err) - require.Equal(t, 1, len(phoneNumbers)) - require.Equal(t, "+12223334444", phoneNumbers[0]) + // Remove the phone number + response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + }) + require.Equal(t, 200, response.Code) - // Do the thing - response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - "x-call": "yes", + // Verify the phone number is gone from the DB + phoneNumbers, err = s.userManager.PhoneNumbers(u.ID) + require.Nil(t, err) + require.Equal(t, 0, len(phoneNumbers)) }) - require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) - waitFor(t, func() bool { - return called.Load() - }) - - // Remove the phone number - response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - }) - require.Equal(t, 200, response.Code) - - // Verify the phone number is gone from the DB - phoneNumbers, err = s.userManager.PhoneNumbers(u.ID) - require.Nil(t, err) - require.Equal(t, 0, len(phoneNumbers)) } func TestServer_Twilio_Call_Success(t *testing.T) { - var called atomic.Bool - twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if called.Load() { - t.Fatal("Should be only called once") - } - body, err := io.ReadAll(r.Body) + forEachBackend(t, func(t *testing.T) { + var called atomic.Bool + twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + 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+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)) + called.Store(true) + })) + defer twilioServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = twilioServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") 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+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)) - called.Store(true) - })) - defer twilioServer.Close() + require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = twilioServer.URL - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) - - // Add tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 10, - CallLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - u, err := s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) - - // Do the thing - response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - "x-call": "+11122233344", - }) - require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) - waitFor(t, func() bool { - return called.Load() + // Do the thing + response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "+11122233344", + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) }) } func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) { - var called atomic.Bool - twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if called.Load() { - t.Fatal("Should be only called once") - } - body, err := io.ReadAll(r.Body) + forEachBackend(t, func(t *testing.T) { + var called atomic.Bool + twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + 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+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)) + called.Store(true) + })) + defer twilioServer.Close() + + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = twilioServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) + + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") 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+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)) - called.Store(true) - })) - defer twilioServer.Close() + require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = twilioServer.URL - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) - - // Add tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 10, - CallLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - u, err := s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) - - // Do the thing - response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - "x-call": "yes", // <<<------ - }) - require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) - waitFor(t, func() bool { - return called.Load() + // Do the thing + response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "yes", // <<<------ + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) }) } func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) { - var called atomic.Bool - twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if called.Load() { - t.Fatal("Should be only called once") - } - 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+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)) - called.Store(true) - })) - defer twilioServer.Close() + forEachBackend(t, func(t *testing.T) { + var called atomic.Bool + twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if called.Load() { + t.Fatal("Should be only called once") + } + 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+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)) + called.Store(true) + })) + defer twilioServer.Close() - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = twilioServer.URL - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - c.TwilioCallFormat = template.Must(template.New("twiml").Parse(` + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = twilioServer.URL + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + c.TwilioCallFormat = template.Must(template.New("twiml").Parse(` @@ -240,88 +247,97 @@ func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) { Auf Wiederhören. `)) - s := newTestServer(t, c) + s := newTestServer(t, c) - // Add tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 10, - CallLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - u, err := s.userManager.User("phil") - require.Nil(t, err) - require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + u, err := s.userManager.User("phil") + require.Nil(t, err) + require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344")) - // Do the thing - response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - "x-call": "+11122233344", - }) - require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) - waitFor(t, func() bool { - return called.Load() + // Do the thing + response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "+11122233344", + }) + require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message) + waitFor(t, func() bool { + return called.Load() + }) }) } func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = "http://dummy.invalid" - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "http://dummy.invalid" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) - // Add tier and user - require.Nil(t, s.userManager.AddTier(&user.Tier{ - Code: "pro", - MessageLimit: 10, - CallLimit: 1, - })) - require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) - require.Nil(t, s.userManager.ChangeTier("phil", "pro")) + // Add tier and user + require.Nil(t, s.userManager.AddTier(&user.Tier{ + Code: "pro", + MessageLimit: 10, + CallLimit: 1, + })) + require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) + require.Nil(t, s.userManager.ChangeTier("phil", "pro")) - // Do the thing - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "authorization": util.BasicAuth("phil", "phil"), - "x-call": "+11122233344", + // Do the thing + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "authorization": util.BasicAuth("phil", "phil"), + "x-call": "+11122233344", + }) + require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code) } func TestServer_Twilio_Call_InvalidNumber(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = "https://127.0.0.1" - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "https://127.0.0.1" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "x-call": "+invalid", + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+invalid", + }) + require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code) } func TestServer_Twilio_Call_Anonymous(t *testing.T) { - c := newTestConfigWithAuthFile(t) - c.TwilioCallsBaseURL = "https://127.0.0.1" - c.TwilioAccount = "AC1234567890" - c.TwilioAuthToken = "AAEAA1234567890" - c.TwilioPhoneNumber = "+1234567890" - s := newTestServer(t, c) + forEachBackend(t, func(t *testing.T) { + c := newTestConfigWithAuthFile(t) + c.TwilioCallsBaseURL = "https://127.0.0.1" + c.TwilioAccount = "AC1234567890" + c.TwilioAuthToken = "AAEAA1234567890" + c.TwilioPhoneNumber = "+1234567890" + s := newTestServer(t, c) - response := request(t, s, "POST", "/mytopic", "test", map[string]string{ - "x-call": "+123123", + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+123123", + }) + require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40035, 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", + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "POST", "/mytopic", "test", map[string]string{ + "x-call": "+1234", + }) + require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code) }) - require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code) } diff --git a/server/server_webpush_test.go b/server/server_webpush_test.go index 7bea9460..a79f0369 100644 --- a/server/server_webpush_test.go +++ b/server/server_webpush_test.go @@ -26,236 +26,262 @@ const ( ) func TestServer_WebPush_Enabled(t *testing.T) { - conf := newTestConfig(t) - conf.WebRoot = "" // Disable web app - s := newTestServer(t, conf) + forEachBackend(t, func(t *testing.T) { + conf := newTestConfig(t) + conf.WebRoot = "" // Disable web app + s := newTestServer(t, conf) - rr := request(t, s, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 404, rr.Code) + rr := request(t, s, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 404, rr.Code) - conf2 := newTestConfig(t) - s2 := newTestServer(t, conf2) + conf2 := newTestConfig(t) + s2 := newTestServer(t, conf2) - rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 404, rr.Code) + rr = request(t, s2, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 404, rr.Code) - conf3 := newTestConfigWithWebPush(t) - s3 := newTestServer(t, conf3) + conf3 := newTestConfigWithWebPush(t) + s3 := newTestServer(t, conf3) - rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil) - require.Equal(t, 200, rr.Code) - require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type")) + rr = request(t, s3, "GET", "/manifest.webmanifest", "", nil) + require.Equal(t, 200, rr.Code) + require.Equal(t, "application/manifest+json", rr.Header().Get("Content-Type")) + }) } func TestServer_WebPush_Disabled(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 404, response.Code) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 404, response.Code) + }) } func TestServer_WebPush_TopicAdd(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) - require.Len(t, subs, 1) - require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) - require.Equal(t, subs[0].P256dh, "p256dh-key") - require.Equal(t, subs[0].Auth, "auth-key") - require.Equal(t, subs[0].UserID, "") + require.Len(t, subs, 1) + require.Equal(t, subs[0].Endpoint, testWebPushEndpoint) + require.Equal(t, subs[0].P256dh, "p256dh-key") + require.Equal(t, subs[0].Auth, "auth-key") + require.Equal(t, subs[0].UserID, "") + }) } func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil) - require.Equal(t, 400, response.Code) - require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String()) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil) + require.Equal(t, 400, response.Code) + require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String()) + }) } func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - topicList := make([]string, 51) - for i := range topicList { - topicList[i] = util.RandomString(5) - } + topicList := make([]string, 51) + for i := range topicList { + topicList[i] = util.RandomString(5) + } - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil) - require.Equal(t, 400, response.Code) - require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String()) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil) + require.Equal(t, 400, response.Code) + require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String()) + }) } func TestServer_WebPush_TopicUnsubscribe(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - addSubscription(t, s, testWebPushEndpoint, "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) + addSubscription(t, s, testWebPushEndpoint, "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - requireSubscriptionCount(t, s, "test-topic", 0) + requireSubscriptionCount(t, s, "test-topic", 0) + }) } func TestServer_WebPush_Delete(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - addSubscription(t, s, testWebPushEndpoint, "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) + addSubscription(t, s, testWebPushEndpoint, "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) - response := request(t, s, "DELETE", "/v1/webpush", fmt.Sprintf(`{"endpoint":"%s"}`, testWebPushEndpoint), nil) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + response := request(t, s, "DELETE", "/v1/webpush", fmt.Sprintf(`{"endpoint":"%s"}`, testWebPushEndpoint), nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - requireSubscriptionCount(t, s, "test-topic", 0) + requireSubscriptionCount(t, s, "test-topic", 0) + }) } func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - config.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, config) + forEachBackend(t, func(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + config.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + require.Len(t, subs, 1) + require.True(t, strings.HasPrefix(subs[0].UserID, "u_")) }) - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - require.Len(t, subs, 1) - require.True(t, strings.HasPrefix(subs[0].UserID, "u_")) } func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - config.AuthDefault = user.PermissionDenyAll - s := newTestServer(t, config) + forEachBackend(t, func(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + config.AuthDefault = user.PermissionDenyAll + s := newTestServer(t, config) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) - require.Equal(t, 403, response.Code) + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil) + require.Equal(t, 403, response.Code) - requireSubscriptionCount(t, s, "test-topic", 0) + requireSubscriptionCount(t, s, "test-topic", 0) + }) } func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) { - config := configureAuth(t, newTestConfigWithWebPush(t)) - s := newTestServer(t, config) + forEachBackend(t, func(t *testing.T) { + config := configureAuth(t, newTestConfigWithWebPush(t)) + s := newTestServer(t, config) - require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) - require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) + require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false)) + require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) - response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), + response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + + require.Equal(t, 200, response.Code) + require.Equal(t, `{"success":true}`+"\n", response.Body.String()) + + requireSubscriptionCount(t, s, "test-topic", 1) + + request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{ + "Authorization": util.BasicAuth("ben", "ben"), + }) + // should've been deleted with the account + requireSubscriptionCount(t, s, "test-topic", 0) }) - - require.Equal(t, 200, response.Code) - require.Equal(t, `{"success":true}`+"\n", response.Body.String()) - - requireSubscriptionCount(t, s, "test-topic", 1) - - request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{ - "Authorization": util.BasicAuth("ben", "ben"), - }) - // should've been deleted with the account - requireSubscriptionCount(t, s, "test-topic", 0) } func TestServer_WebPush_Publish(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - var received atomic.Bool - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - require.Equal(t, "/push-receive", r.URL.Path) - require.Equal(t, "high", r.Header.Get("Urgency")) - require.Equal(t, "", r.Header.Get("Topic")) - received.Store(true) - })) - defer pushService.Close() + var received atomic.Bool + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + require.Equal(t, "/push-receive", r.URL.Path) + require.Equal(t, "high", r.Header.Get("Urgency")) + require.Equal(t, "", r.Header.Get("Topic")) + received.Store(true) + })) + defer pushService.Close() - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") - request(t, s, "POST", "/test-topic", "web push test", nil) + addSubscription(t, s, pushService.URL+"/push-receive", "test-topic") + request(t, s, "POST", "/test-topic", "web push test", nil) - waitFor(t, func() bool { - return received.Load() + waitFor(t, func() bool { + return received.Load() + }) }) } func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - var received atomic.Bool - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - w.WriteHeader(http.StatusGone) - received.Store(true) - })) - defer pushService.Close() + var received atomic.Bool + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + w.WriteHeader(http.StatusGone) + received.Store(true) + })) + defer pushService.Close() - addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc") - requireSubscriptionCount(t, s, "test-topic", 1) - requireSubscriptionCount(t, s, "test-topic-abc", 1) + addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc") + requireSubscriptionCount(t, s, "test-topic", 1) + requireSubscriptionCount(t, s, "test-topic-abc", 1) - request(t, s, "POST", "/test-topic", "web push test", nil) + request(t, s, "POST", "/test-topic", "web push test", nil) - waitFor(t, func() bool { - return received.Load() + waitFor(t, func() bool { + return received.Load() + }) + + // Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint + + requireSubscriptionCount(t, s, "test-topic", 0) + requireSubscriptionCount(t, s, "test-topic-abc", 0) }) - - // Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint - - requireSubscriptionCount(t, s, "test-topic", 0) - requireSubscriptionCount(t, s, "test-topic-abc", 0) } func TestServer_WebPush_Expiry(t *testing.T) { - s := newTestServer(t, newTestConfigWithWebPush(t)) + forEachBackend(t, func(t *testing.T) { + s := newTestServer(t, newTestConfigWithWebPush(t)) - var received atomic.Bool + var received atomic.Bool - pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := io.ReadAll(r.Body) - require.Nil(t, err) - w.WriteHeader(200) - w.Write([]byte(``)) - received.Store(true) - })) - defer pushService.Close() + pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := io.ReadAll(r.Body) + require.Nil(t, err) + w.WriteHeader(200) + w.Write([]byte(``)) + received.Store(true) + })) + defer pushService.Close() - endpoint := pushService.URL + "/push-receive" - addSubscription(t, s, endpoint, "test-topic") - requireSubscriptionCount(t, s, "test-topic", 1) + endpoint := pushService.URL + "/push-receive" + addSubscription(t, s, endpoint, "test-topic") + requireSubscriptionCount(t, s, "test-topic", 1) - require.Nil(t, s.webPush.SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-55*24*time.Hour).Unix())) + require.Nil(t, s.webPush.SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-55*24*time.Hour).Unix())) - s.pruneAndNotifyWebPushSubscriptions() - requireSubscriptionCount(t, s, "test-topic", 1) + s.pruneAndNotifyWebPushSubscriptions() + requireSubscriptionCount(t, s, "test-topic", 1) - waitFor(t, func() bool { - return received.Load() - }) + waitFor(t, func() bool { + return received.Load() + }) - require.Nil(t, s.webPush.SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-60*24*time.Hour).Unix())) + require.Nil(t, s.webPush.SetSubscriptionUpdatedAt(endpoint, time.Now().Add(-60*24*time.Hour).Unix())) - s.pruneAndNotifyWebPushSubscriptions() - waitFor(t, func() bool { - subs, err := s.webPush.SubscriptionsForTopic("test-topic") - require.Nil(t, err) - return len(subs) == 0 + s.pruneAndNotifyWebPushSubscriptions() + waitFor(t, func() bool { + subs, err := s.webPush.SubscriptionsForTopic("test-topic") + require.Nil(t, err) + return len(subs) == 0 + }) }) } @@ -285,7 +311,9 @@ func newTestConfigWithWebPush(t *testing.T) *Config { conf := newTestConfig(t) privateKey, publicKey, err := webpush.GenerateVAPIDKeys() require.Nil(t, err) - conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db") + if conf.DatabaseURL == "" { + conf.WebPushFile = filepath.Join(t.TempDir(), "webpush.db") + } conf.WebPushEmailAddress = "testing@example.com" conf.WebPushPrivateKey = privateKey conf.WebPushPublicKey = publicKey