* Add test notification tool

* Add frontend styles

* Remove option from admin view

* Refactor create post and add translations

* Fix several CI errors

* Fix API and frontend snapshots

* Refactor trailing and leading icon on buttons

* Add different button states

* i18n-extract

* Fix wrong text

* Add tests

* Fix wrong string

* Fix test

* feat: E2E send test notifications (#28371)

* Refactor send desktop notification

* Address rest of the feedback

* Fix tests

* Add correct link

* Fix test

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
Этот коммит содержится в:
Daniel Espino García
2024-11-08 13:57:06 +01:00
коммит произвёл GitHub
родитель 30a6ddc995
Коммит 118d0346ee
31 изменённых файлов: 1170 добавлений и 56 удалений

Просмотреть файл

@@ -45,6 +45,7 @@ func (api *API) InitSystem() {
api.BaseRoutes.System.Handle("/timezones", api.APISessionRequired(getSupportedTimezones)).Methods(http.MethodGet)
api.BaseRoutes.APIRoot.Handle("/audits", api.APISessionRequired(getAudits)).Methods(http.MethodGet)
api.BaseRoutes.APIRoot.Handle("/notifications/test", api.APISessionRequired(testNotifications)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/email/test", api.APISessionRequired(testEmail)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/site_url/test", api.APISessionRequired(testSiteURL)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/file/s3_test", api.APISessionRequired(testS3)).Methods(http.MethodPost)
@@ -232,6 +233,16 @@ func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func testNotifications(c *Context, w http.ResponseWriter, r *http.Request) {
_, err := c.App.SendTestMessage(c.AppContext, c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
ReturnStatusOK(w)
}
func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)

Просмотреть файл

@@ -1103,6 +1103,7 @@ type AppIface interface {
SendPasswordReset(rctx request.CTX, email string, siteURL string) (bool, *model.AppError)
SendPersistentNotifications() error
SendReportToUser(rctx request.CTX, job *model.Job, format string) *model.AppError
SendTestMessage(c request.CTX, userID string) (*model.Post, *model.AppError)
SendTestPushNotification(deviceID string) string
ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string)
SessionHasPermissionTo(session model.Session, permission *model.Permission) bool

Просмотреть файл

@@ -599,6 +599,10 @@ func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppE
}
func (a *App) ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post, isGM bool) bool {
if prop := post.GetProp(model.PostPropsForceNotification); prop != nil && prop != "" {
return true
}
if notifyPropsAllowedReason := DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned, isGM); notifyPropsAllowedReason != "" {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, notifyPropsAllowedReason, model.NotificationNoPlatform)
a.NotificationsLog().Debug("Notification not sent - notify props",

Просмотреть файл

@@ -1103,6 +1103,39 @@ func TestSendPushNotifications(t *testing.T) {
})
}
func TestShouldSendPushNotifications(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("should return true if forced", func(t *testing.T) {
user := &model.User{Id: model.NewId(), Email: "unit@test.com", NotifyProps: make(map[string]string)}
user.NotifyProps[model.PushNotifyProp] = model.UserNotifyNone
post := &model.Post{UserId: user.Id, ChannelId: model.NewId()}
post.AddProp(model.PostPropsForceNotification, model.NewId())
channelNotifyProps := map[string]string{model.PushNotifyProp: model.ChannelNotifyNone, model.MarkUnreadNotifyProp: model.ChannelMarkUnreadMention}
status := &model.Status{UserId: user.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: post.ChannelId}
result := th.App.ShouldSendPushNotification(user, channelNotifyProps, false, status, post, false)
assert.True(t, result)
})
t.Run("should return false if force undefined", func(t *testing.T) {
user := &model.User{Id: model.NewId(), Email: "unit@test.com", NotifyProps: make(map[string]string)}
user.NotifyProps[model.PushNotifyProp] = model.UserNotifyNone
post := &model.Post{UserId: user.Id, ChannelId: model.NewId()}
channelNotifyProps := map[string]string{model.PushNotifyProp: model.ChannelNotifyNone, model.MarkUnreadNotifyProp: model.ChannelMarkUnreadMention}
status := &model.Status{UserId: user.Id, Status: model.StatusOnline, Manual: false, LastActivityAt: model.GetMillis(), ActiveChannel: post.ChannelId}
result := th.App.ShouldSendPushNotification(user, channelNotifyProps, false, status, post, false)
assert.False(t, result)
})
}
// testPushNotificationHandler is an HTTP handler to record push notifications
// being sent from the client.
// It records the number of requests sent to it, and stores all the requests

Просмотреть файл

@@ -16398,6 +16398,28 @@ func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*mode
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendTestMessage(c request.CTX, userID string) (*model.Post, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestMessage")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SendTestMessage(c, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SendTestPushNotification(deviceID string) string {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendTestPushNotification")

Просмотреть файл

@@ -242,6 +242,10 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
post.AddProp(model.PostPropsFromBot, "true")
}
if flags.ForceNotification {
post.AddProp(model.PostPropsForceNotification, model.NewId())
}
if c.Session().IsOAuth {
post.AddProp(model.PostPropsFromOAuthApp, "true")
}
@@ -2722,3 +2726,34 @@ func (a *App) CleanUpAfterPostDeletion(c request.CTX, post *model.Post, deleteBy
return nil
}
func (a *App) SendTestMessage(c request.CTX, userID string) (*model.Post, *model.AppError) {
bot, err := a.GetSystemBot(c)
if err != nil {
return nil, model.NewAppError("SendTestMessage", "app.notifications.send_test_message.errors.no_bot", nil, "", http.StatusInternalServerError).Wrap(err)
}
channel, err := a.GetOrCreateDirectChannel(c, userID, bot.UserId)
if err != nil {
return nil, model.NewAppError("SendTestMessage", "app.notifications.send_test_message.errors.no_channel", nil, "", http.StatusInternalServerError).Wrap(err)
}
user, err := a.GetUser(userID)
if err != nil {
return nil, model.NewAppError("SendTestMessage", "app.notifications.send_test_message.errors.no_user", nil, "", http.StatusInternalServerError).Wrap(err)
}
T := i18n.GetUserTranslations(user.Locale)
post := &model.Post{
ChannelId: channel.Id,
Message: T("app.notifications.send_test_message.message_body"),
Type: model.PostTypeDefault,
UserId: bot.UserId,
}
post, err = a.CreatePost(c, post, channel, model.CreatePostFlags{ForceNotification: true})
if err != nil {
return nil, model.NewAppError("SendTestMessage", "app.notifications.send_test_message.errors.create_post", nil, "", http.StatusInternalServerError).Wrap(err)
}
return post, nil
}

Просмотреть файл

@@ -1178,6 +1178,37 @@ func TestCreatePost(t *testing.T) {
wg.Wait()
})
t.Run("should sanitize the force notifications prop if the flag is not set", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
postToCreate := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "hello world",
UserId: th.BasicUser.Id,
}
postToCreate.AddProp(model.PostPropsForceNotification, model.NewId())
createdPost, err := th.App.CreatePost(th.Context, postToCreate, th.BasicChannel, model.CreatePostFlags{})
require.Nil(t, err)
require.Empty(t, createdPost.GetProp(model.PostPropsForceNotification))
})
t.Run("should add the force notifications prop if the flag is set", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.AddUserToChannel(th.BasicUser, th.BasicChannel)
postToCreate := &model.Post{
ChannelId: th.BasicChannel.Id,
Message: "hello world",
UserId: th.BasicUser.Id,
}
createdPost, err := th.App.CreatePost(th.Context, postToCreate, th.BasicChannel, model.CreatePostFlags{ForceNotification: true})
require.Nil(t, err)
require.NotEmpty(t, createdPost.GetProp(model.PostPropsForceNotification))
})
}
func TestPatchPost(t *testing.T) {
@@ -3739,3 +3770,13 @@ func TestPermanentDeletePost(t *testing.T) {
assert.Len(t, infos, 0)
})
}
func TestSendTestMessage(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("Should create the post with the correct prop", func(t *testing.T) {
post, result := th.App.SendTestMessage(th.Context, th.BasicUser.Id)
assert.Nil(t, result)
assert.NotEmpty(t, post.GetProp(model.PostPropsForceNotification))
})
}

Просмотреть файл

@@ -5938,6 +5938,26 @@
"id": "app.notification.subject.notification.full",
"translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}"
},
{
"id": "app.notifications.send_test_message.errors.create_post",
"translation": "The post cannot be created"
},
{
"id": "app.notifications.send_test_message.errors.no_bot",
"translation": "Cannot get the system bot"
},
{
"id": "app.notifications.send_test_message.errors.no_channel",
"translation": "Cannot get the system bot direct message"
},
{
"id": "app.notifications.send_test_message.errors.no_user",
"translation": "Cannot get the user"
},
{
"id": "app.notifications.send_test_message.message_body",
"translation": "If you received this test notification, it worked!"
},
{
"id": "app.notify_admin.save.app_error",
"translation": "Unable to save notify data."

Просмотреть файл

@@ -344,6 +344,10 @@ func (c *Client4) testEmailRoute() string {
return "/email/test"
}
func (c *Client4) testNotificationRoute() string {
return "/notifications/test"
}
func (c *Client4) usageRoute() string {
return "/usage"
}
@@ -4820,6 +4824,15 @@ func (c *Client4) TestEmail(ctx context.Context, config *Config) (*Response, err
return BuildResponse(r), nil
}
func (c *Client4) TestNotifications(ctx context.Context) (*Response, error) {
r, err := c.DoAPIPost(ctx, c.testNotificationRoute(), "")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// TestSiteURL will test the validity of a site URL.
func (c *Client4) TestSiteURL(ctx context.Context, siteURL string) (*Response, error) {
requestBody := make(map[string]string)

Просмотреть файл

@@ -75,6 +75,7 @@ const (
PostPropsMentionHighlightDisabled = "mentionHighlightDisabled"
PostPropsGroupHighlightDisabled = "disable_group_highlight"
PostPropsPreviewedPost = "previewed_post"
PostPropsForceNotification = "force_notification"
PostPriorityUrgent = "urgent"
PostPropsRequestedAck = "requested_ack"
@@ -339,8 +340,9 @@ func (o *Post) EncodeJSON(w io.Writer) error {
}
type CreatePostFlags struct {
TriggerWebhooks bool
SetOnline bool
TriggerWebhooks bool
SetOnline bool
ForceNotification bool
}
type GetPostsSinceOptions struct {
@@ -500,6 +502,7 @@ func (o *Post) SanitizeProps() {
}
membersToSanitize := []string{
PropsAddChannelMember,
PostPropsForceNotification,
}
for _, member := range membersToSanitize {

Просмотреть файл

@@ -115,29 +115,34 @@ func TestPostSanitizeProps(t *testing.T) {
post1.SanitizeProps()
require.Nil(t, post1.GetProp(PropsAddChannelMember))
require.Nil(t, post1.GetProp(PostPropsForceNotification))
post2 := &Post{
Message: "test",
Props: StringInterface{
PropsAddChannelMember: "test",
PropsAddChannelMember: "test",
PostPropsForceNotification: "test",
},
}
post2.SanitizeProps()
require.Nil(t, post2.GetProp(PropsAddChannelMember))
require.Nil(t, post2.GetProp(PostPropsForceNotification))
post3 := &Post{
Message: "test",
Props: StringInterface{
PropsAddChannelMember: "no good",
"attachments": "good",
PropsAddChannelMember: "no good",
PostPropsForceNotification: "no good",
"attachments": "good",
},
}
post3.SanitizeProps()
require.Nil(t, post3.GetProp(PropsAddChannelMember))
require.Nil(t, post3.GetProp(PostPropsForceNotification))
require.NotNil(t, post3.GetProp("attachments"))
}