[MM-45564] - Notify Admin v2 (#20777)
* [MM-45564] - Notify Admin v2 * add dummy data * update dummy data * add store methods * experiment with recurring task * complete saving of the notification * make improvements * make improvements * add store layer tests * fix lint * update store layer tests * add app layer unit tests * add store layers * add app layer tests * fix lint * fix lint * fix tests * fix tests lint * fix lint * fix lint * fix retry layer test * add notifications manual trigger * filter notifications based on current plan * add test case * temp change * feedback impl * fix translations * change job scheduler * refactor job * fix store layer tests * extract i18n * fix lint * fix translations * fix translations * add license statement for new file * feedback impl-2 * fix lint * update make file * add intl ids * improve * fix lint * feedback impl * move code and rename files * fix lint * feedback impl * add config for trigger notifications api * fix tests * tmp change * undo temp changes Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c11ad8995f
Коммит
bd42f0cd8c
@@ -50,25 +50,6 @@ func (api *API) InitCloud() {
|
||||
|
||||
// POST /api/v4/cloud/webhook
|
||||
api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Cloud.Handle("/notify-admin-to-upgrade", api.APISessionRequired(handleNotifyAdminToUpgrade)).Methods("POST")
|
||||
}
|
||||
|
||||
func handleNotifyAdminToUpgrade(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
||||
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.NotifySystemAdminsToUpgrade(c.AppContext, notifyAdminRequest.CurrentTeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getSubscription(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -5,11 +5,8 @@ package api4
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -296,109 +293,6 @@ func Test_requestTrial(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNotifyAdminToUpgrade(t *testing.T) {
|
||||
t.Run("user can only notify admin once in cool off period", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
CurrentTeamId: th.BasicTeam.Id,
|
||||
})
|
||||
|
||||
bot, appErr := th.App.GetSystemBot()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// message sending is async, wait time for it
|
||||
var channel *model.Channel
|
||||
var err error
|
||||
var timeout = 5 * time.Second
|
||||
begin := time.Now()
|
||||
for {
|
||||
if time.Since(begin) > timeout {
|
||||
break
|
||||
}
|
||||
channel, err = th.App.Srv().Store.Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
|
||||
if err == nil && channel != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
require.NoError(t, err, "Expected message to have been sent within %d seconds", timeout)
|
||||
|
||||
postList, err := th.App.Srv().Store.Post().GetPosts(model.GetPostsOptions{ChannelId: channel.Id, Page: 0, PerPage: 1}, false, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(postList.Order), 1)
|
||||
|
||||
post := postList.Posts[postList.Order[0]]
|
||||
|
||||
require.Equal(t, fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), post.Type)
|
||||
require.Equal(t, bot.UserId, post.UserId)
|
||||
require.Equal(t, fmt.Sprintf("A member of %s has notified you to upgrade this workspace.", th.BasicTeam.Name), post.Message)
|
||||
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
// second time trying to call notify endpoint by same user is forbidden
|
||||
statusCode = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
CurrentTeamId: th.BasicTeam.Id,
|
||||
})
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
})
|
||||
|
||||
t.Run("user can only notify admin after cool off period", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
|
||||
defer os.Unsetenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
||||
|
||||
statusCode := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
CurrentTeamId: th.BasicTeam.Id,
|
||||
})
|
||||
|
||||
bot, appErr := th.App.GetSystemBot()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
channel, err := th.App.Srv().Store.Channel().GetByName("", model.GetDMNameFromIds(bot.UserId, th.SystemAdminUser.Id), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
postList, err := th.App.Srv().Store.Post().GetPosts(model.GetPostsOptions{ChannelId: channel.Id, Page: 0, PerPage: 1}, false, map[string]bool{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, len(postList.Order), 1)
|
||||
|
||||
post := postList.Posts[postList.Order[0]]
|
||||
|
||||
require.Equal(t, fmt.Sprintf("%sup_notification", model.PostCustomTypePrefix), post.Type)
|
||||
require.Equal(t, bot.UserId, post.UserId)
|
||||
require.Equal(t, fmt.Sprintf("A member of %s has notified you to upgrade this workspace.", th.BasicTeam.Name), post.Message)
|
||||
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// second time trying to call notify endpoint by same user is NOT forbidden because it is after cool off period set to 3 seconds
|
||||
statusCode = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
CurrentTeamId: th.BasicTeam.Id,
|
||||
})
|
||||
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
})
|
||||
|
||||
t.Run("can cloud/model.Notify", func(t *testing.T) {
|
||||
|
||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "10") // set to 10 days
|
||||
canNotify := model.CanNotify(model.GetMillis())
|
||||
require.Equal(t, false, canNotify)
|
||||
|
||||
os.Setenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS", "0.00003472222222") // set to 3 seconds
|
||||
canNotify = model.CanNotify(model.GetMillis())
|
||||
time.Sleep(5 * time.Second)
|
||||
require.Equal(t, false, canNotify)
|
||||
os.Unsetenv("MM_CLOUD_NOTIFY_ADMIN_COOL_OFF_DAYS")
|
||||
})
|
||||
}
|
||||
func Test_validateBusinessEmail(t *testing.T) {
|
||||
t.Run("Returns forbidden for non admin executors", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
57
api4/notify_admin.go
Обычный файл
57
api4/notify_admin.go
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func handleNotifyAdmin(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
||||
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
appErr := c.App.SaveAdminNotification(userId, notifyAdminRequest)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func handleTriggerNotifyAdminPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*c.App.Config().ServiceSettings.EnableAPITriggerAdminNotifications {
|
||||
c.Err = model.NewAppError("Api4.handleTriggerNotifyAdminPosts", "api.cloud.app_error", nil, "Manual triggering of notifications not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var notifyAdminRequest *model.NotifyAdminToUpgradeRequest
|
||||
err := json.NewDecoder(r.Body).Decode(¬ifyAdminRequest)
|
||||
if err != nil {
|
||||
c.SetInvalidParamWithErr("notifyAdminRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
// only system admins can manually trigger these notifications
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
appErr := c.App.SendNotifyAdminPosts(c.AppContext, "", "", notifyAdminRequest.TrialNotification)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
155
api4/notify_admin_test.go
Обычный файл
155
api4/notify_admin_test.go
Обычный файл
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
package api4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNotifyAdmin(t *testing.T) {
|
||||
t.Run("error when plan is unknown when notifying on upgrade", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: "Unknown plan",
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when plan is unknown when notifying to trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: "Unknown plan",
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
TrialNotification: true,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when feature is unknown when notifying on upgrade", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: "Unknown feature",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
})
|
||||
|
||||
t.Run("error when feature is unknown when notifying to trial", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: "Unknown feature",
|
||||
TrialNotification: true,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Unable to save notify data.")
|
||||
require.Equal(t, http.StatusInternalServerError, statusCode)
|
||||
})
|
||||
|
||||
t.Run("error when user tries to notify again on same feature within the cool off period", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
// second attempt to notify for all professional features
|
||||
statusCode, err = th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.Error(t, err)
|
||||
|
||||
require.Equal(t, err.Error(), ": Already notified admin")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
})
|
||||
|
||||
t.Run("successfully save upgrade notification", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTriggerNotifyAdmin(t *testing.T) {
|
||||
t.Run("error when EnableAPITriggerAdminNotifications is not true", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = false })
|
||||
|
||||
statusCode, err := th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": Internal error during cloud api request.")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
|
||||
})
|
||||
|
||||
t.Run("error when non admins try to trigger notifications", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic().InitLogin()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||
|
||||
statusCode, err := th.Client.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
|
||||
require.Error(t, err)
|
||||
require.Equal(t, err.Error(), ": You do not have the appropriate permissions.")
|
||||
require.Equal(t, http.StatusForbidden, statusCode)
|
||||
})
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableAPITriggerAdminNotifications = true })
|
||||
|
||||
statusCode, err := th.Client.NotifyAdmin(&model.NotifyAdminToUpgradeRequest{
|
||||
RequiredPlan: model.LicenseShortSkuProfessional,
|
||||
RequiredFeature: model.PaidFeatureAllProfessionalfeatures,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
|
||||
statusCode, err = th.SystemAdminClient.TriggerNotifyAdmin(&model.NotifyAdminToUpgradeRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, statusCode)
|
||||
})
|
||||
}
|
||||
@@ -103,6 +103,9 @@ func (api *API) InitUser() {
|
||||
api.BaseRoutes.UserThread.Handle("/following", api.APISessionRequired(unfollowThreadByUser)).Methods("DELETE")
|
||||
api.BaseRoutes.UserThread.Handle("/read/{timestamp:[0-9]+}", api.APISessionRequired(updateReadStateThreadByUser)).Methods("PUT")
|
||||
api.BaseRoutes.UserThread.Handle("/set_unread/{post_id:[A-Za-z0-9]+}", api.APISessionRequired(setUnreadThreadByPostId)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.Users.Handle("/notify-admin", api.APISessionRequired(handleNotifyAdmin)).Methods("POST")
|
||||
api.BaseRoutes.Users.Handle("/trigger-notify-admin-posts", api.APISessionRequired(handleTriggerNotifyAdminPosts)).Methods("POST")
|
||||
}
|
||||
|
||||
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Ссылка в новой задаче
Block a user