MM-27918 In-Product notices support (#15316)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
43ed6ad690
Коммит
4e9ddd4686
@@ -111,6 +111,10 @@ func (a *App) initJobs() {
|
||||
if jobsExpiryNotifyInterface != nil {
|
||||
a.srv.Jobs.ExpiryNotify = jobsExpiryNotifyInterface(a)
|
||||
}
|
||||
if productNoticesJobInterface != nil {
|
||||
a.srv.Jobs.ProductNotices = productNoticesJobInterface(a)
|
||||
}
|
||||
|
||||
if jobsActiveUsersInterface != nil {
|
||||
a.srv.Jobs.ActiveUsers = jobsActiveUsersInterface(a)
|
||||
}
|
||||
|
||||
@@ -627,6 +627,7 @@ type AppIface interface {
|
||||
GetPreferencesForUser(userId string) (model.Preferences, *model.AppError)
|
||||
GetPrevPostIdFromPostList(postList *model.PostList) string
|
||||
GetPrivateChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError)
|
||||
GetProductNotices(userId, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError)
|
||||
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
|
||||
GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) (*model.ChannelList, *model.AppError)
|
||||
GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError)
|
||||
@@ -969,6 +970,7 @@ type AppIface interface {
|
||||
UpdatePasswordSendEmail(user *model.User, newPassword, method string) *model.AppError
|
||||
UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
|
||||
UpdatePreferences(userId string, preferences model.Preferences) *model.AppError
|
||||
UpdateProductNotices() *model.AppError
|
||||
UpdateRole(role *model.Role) (*model.Role, *model.AppError)
|
||||
UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError)
|
||||
UpdateSessionsIsGuest(userId string, isGuest bool)
|
||||
@@ -985,6 +987,7 @@ type AppIface interface {
|
||||
UpdateUserAuth(userId string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError)
|
||||
UpdateUserNotifyProps(userId string, props map[string]string) (*model.User, *model.AppError)
|
||||
UpdateUserRoles(userId string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError)
|
||||
UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError
|
||||
UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError)
|
||||
UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError
|
||||
UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError)
|
||||
|
||||
@@ -102,6 +102,12 @@ func RegisterJobsExpiryNotifyJobInterface(f func(*App) tjobs.ExpiryNotifyJobInte
|
||||
jobsExpiryNotifyInterface = f
|
||||
}
|
||||
|
||||
var productNoticesJobInterface func(*App) tjobs.ProductNoticesJobInterface
|
||||
|
||||
func RegisterProductNoticesJobInterface(f func(*App) tjobs.ProductNoticesJobInterface) {
|
||||
productNoticesJobInterface = f
|
||||
}
|
||||
|
||||
var ldapInterface func(*App) einterfaces.LdapInterface
|
||||
|
||||
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
|
||||
|
||||
@@ -60,6 +60,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
memoryStore.Set(config)
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
@@ -7161,6 +7161,28 @@ func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(teamId string, offset in
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetProductNotices(userId string, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProductNotices")
|
||||
|
||||
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.GetProductNotices(userId, teamId, client, clientVersion, locale)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProfileImage")
|
||||
@@ -14747,6 +14769,28 @@ func (a *OpenTracingAppLayer) UpdatePreferences(userId string, preferences model
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateProductNotices() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateProductNotices")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.UpdateProductNotices()
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRole")
|
||||
@@ -15092,6 +15136,28 @@ func (a *OpenTracingAppLayer) UpdateUserRoles(userId string, newRoles string, se
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNotices")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.UpdateViewedProductNotices(userId, noticeIds)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateWebConnUserActivity")
|
||||
|
||||
300
app/product_notices.go
Обычный файл
300
app/product_notices.go
Обычный файл
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/mattermost/mattermost-server/v5/config"
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/reflog/dateconstraints"
|
||||
)
|
||||
|
||||
const MAX_REPEAT_VIEWINGS = 3
|
||||
const MIN_SECONDS_BETWEEN_REPEAT_VIEWINGS = 60 * 60
|
||||
|
||||
// where to fetch notices from. setting as var to allow overriding during build/test
|
||||
var NOTICES_JSON_URL = "https://notices.mattermost.com/"
|
||||
|
||||
// notice.json fetch frequency in seconds. setting as var to allow overriding during build/test
|
||||
var NOTICES_JSON_FETCH_FREQUENCY_SECONDS = "3600" // one hour by default
|
||||
|
||||
// this variable can be set during build time for QA to skip caching JSON responses (to avoid CDN delay)
|
||||
var NOTICES_SKIP_CACHE = "false"
|
||||
|
||||
// http request cache
|
||||
var noticesCache = utils.RequestCache{}
|
||||
|
||||
// cached counts that are used during notice condition validation
|
||||
var cachedPostCount int64
|
||||
var cachedUserCount int64
|
||||
|
||||
// previously fetched notices
|
||||
var cachedNotices model.ProductNotices
|
||||
|
||||
func noticeMatchesConditions(config *model.Config, preferences store.PreferenceStore, userId string, client model.NoticeClientType, clientVersion, locale string, postCount, userCount int64, isSystemAdmin, isTeamAdmin bool, isCloud bool, sku string, notice *model.ProductNotice) (bool, error) {
|
||||
cnd := notice.Conditions
|
||||
|
||||
// check client type
|
||||
if cnd.ClientType != nil {
|
||||
if !cnd.ClientType.Matches(client) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if client version is in notice range
|
||||
clientVersions := cnd.DesktopVersion
|
||||
if client == model.NoticeClientType_MobileAndroid || client == model.NoticeClientType_MobileIos {
|
||||
clientVersions = cnd.MobileVersion
|
||||
}
|
||||
|
||||
clientVersionParsed, err := semver.NewVersion(clientVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", clientVersion)
|
||||
}
|
||||
|
||||
for _, v := range clientVersions {
|
||||
c, err := semver.NewConstraint(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
|
||||
}
|
||||
if !c.Check(clientVersionParsed) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if notice date range matches current
|
||||
if cnd.DisplayDate != nil {
|
||||
now := time.Now().UTC()
|
||||
c, err := date_constraints.NewConstraint(*cnd.DisplayDate)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse date range %s", *cnd.DisplayDate)
|
||||
}
|
||||
if !c.Check(&now) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if current server version is notice range
|
||||
serverVersion, _ := semver.NewVersion(model.BuildNumber)
|
||||
for _, v := range cnd.ServerVersion {
|
||||
c, err := semver.NewConstraint(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
|
||||
}
|
||||
if !c.Check(serverVersion) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if sku matches our license
|
||||
if cnd.Sku != nil {
|
||||
if !cnd.Sku.Matches(sku) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check the target audience
|
||||
if cnd.Audience != nil {
|
||||
if !cnd.Audience.Matches(isSystemAdmin, isTeamAdmin) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check user count condition against previously calculated total user count
|
||||
if cnd.NumberOfUsers != nil && userCount > 0 {
|
||||
if userCount < *cnd.NumberOfUsers {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check post count condition against previously calculated total post count
|
||||
if cnd.NumberOfPosts != nil && postCount > 0 {
|
||||
if postCount < *cnd.NumberOfPosts {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if our server config matches the notice
|
||||
for k, v := range cnd.ServerConfig {
|
||||
if !validateConfigEntry(config, k, v) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if user's config matches the notice
|
||||
for k, v := range cnd.UserConfig {
|
||||
res, err := validateUserConfigEntry(preferences, userId, k, v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !res {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check the type of installation
|
||||
if cnd.InstanceType != nil {
|
||||
if !cnd.InstanceType.Matches(isCloud) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func validateUserConfigEntry(preferences store.PreferenceStore, userId string, key string, expectedValue interface{}) (bool, error) {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 2 {
|
||||
return false, errors.New("Invalid format of user config. Must be in form of Category.SettingName")
|
||||
}
|
||||
if _, ok := expectedValue.(string); !ok {
|
||||
return false, errors.New("Invalid format of user config. Value should be string")
|
||||
}
|
||||
pref, err := preferences.Get(userId, parts[0], parts[1])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return pref.Value == expectedValue, nil
|
||||
}
|
||||
|
||||
func validateConfigEntry(conf *model.Config, path string, expectedValue interface{}) bool {
|
||||
value, found := config.GetValueByPath(strings.Split(path, "."), *conf)
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
vt := reflect.ValueOf(value)
|
||||
if vt.IsNil() {
|
||||
return expectedValue == nil
|
||||
}
|
||||
if vt.Kind() == reflect.Ptr {
|
||||
vt = vt.Elem()
|
||||
}
|
||||
val := vt.Interface()
|
||||
return val == expectedValue
|
||||
}
|
||||
|
||||
func (a *App) GetProductNotices(userId, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
|
||||
isSystemAdmin := a.SessionHasPermissionTo(*a.Session(), model.PERMISSION_MANAGE_SYSTEM)
|
||||
isTeamAdmin := a.SessionHasPermissionToTeam(*a.Session(), teamId, model.PERMISSION_MANAGE_TEAM)
|
||||
|
||||
// check if notices for regular users are disabled
|
||||
if !*a.Srv().Config().AnnouncementSettings.UserNoticesEnabled && !isTeamAdmin && !isSystemAdmin {
|
||||
return []model.NoticeMessage{}, nil
|
||||
}
|
||||
|
||||
// check if notices for admins are disabled
|
||||
if !*a.Srv().Config().AnnouncementSettings.AdminNoticesEnabled && (isTeamAdmin || isSystemAdmin) {
|
||||
return []model.NoticeMessage{}, nil
|
||||
}
|
||||
|
||||
views, err := a.Srv().Store.ProductNotices().GetViews(userId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetProductNotices", "api.system.update_viewed_notices.failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
sku := a.Srv().ClientLicense()["SkuShortName"]
|
||||
isCloud := a.Srv().ClientLicense()["Cloud"] != ""
|
||||
|
||||
filteredNotices := make([]model.NoticeMessage, 0)
|
||||
|
||||
for noticeIndex, notice := range cachedNotices {
|
||||
// check if the notice has been viewed already
|
||||
var view *model.ProductNoticeViewState
|
||||
for viewIndex, v := range views {
|
||||
if v.NoticeId == notice.ID {
|
||||
view = &views[viewIndex]
|
||||
break
|
||||
}
|
||||
}
|
||||
if view != nil {
|
||||
repeatable := notice.Repeatable != nil && *notice.Repeatable
|
||||
if repeatable {
|
||||
if view.Viewed > MAX_REPEAT_VIEWINGS {
|
||||
continue
|
||||
}
|
||||
if (time.Now().UTC().Unix() - view.Timestamp) < MIN_SECONDS_BETWEEN_REPEAT_VIEWINGS {
|
||||
continue
|
||||
}
|
||||
} else if view.Viewed > 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
result, err := noticeMatchesConditions(a.Config(), a.Srv().Store.Preference(),
|
||||
userId,
|
||||
client,
|
||||
clientVersion,
|
||||
locale,
|
||||
cachedPostCount,
|
||||
cachedUserCount,
|
||||
isSystemAdmin,
|
||||
isTeamAdmin,
|
||||
isCloud,
|
||||
sku,
|
||||
&cachedNotices[noticeIndex])
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetProductNotices", "api.system.update_notices.validating_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
if result {
|
||||
selectedLocale := "en"
|
||||
filteredNotices = append(filteredNotices, model.NoticeMessage{
|
||||
NoticeMessageInternal: notice.LocalizedMessages[selectedLocale],
|
||||
ID: notice.ID,
|
||||
TeamAdminOnly: notice.TeamAdminOnly(),
|
||||
SysAdminOnly: notice.SysAdminOnly(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filteredNotices, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError {
|
||||
if err := a.Srv().Store.ProductNotices().View(userId, noticeIds); err != nil {
|
||||
return model.NewAppError("UpdateViewedProductNotices", "api.system.update_viewed_notices.failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateProductNotices() *model.AppError {
|
||||
skip, err := strconv.ParseBool(NOTICES_SKIP_CACHE)
|
||||
if err != nil {
|
||||
skip = false
|
||||
}
|
||||
mlog.Debug("Will fetch notices from", mlog.String("url", NOTICES_JSON_URL), mlog.Bool("skip_cache", skip))
|
||||
var appErr *model.AppError
|
||||
cachedPostCount, appErr = a.Srv().Store.Post().AnalyticsPostCount("", false, false)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to fetch post count", mlog.String("error", appErr.Error()))
|
||||
}
|
||||
|
||||
cachedUserCount, appErr = a.Srv().Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to fetch user count", mlog.String("error", appErr.Error()))
|
||||
}
|
||||
|
||||
data, err := utils.GetUrlWithCache(NOTICES_JSON_URL, ¬icesCache, skip)
|
||||
if err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.fetch_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
cachedNotices, err = model.UnmarshalProductNotices(data)
|
||||
if err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.parse_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.Srv().Store.ProductNotices().ClearOldNotices(&cachedNotices); err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.clear_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
536
app/product_notices_test.go
Обычный файл
536
app/product_notices_test.go
Обычный файл
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/store/storetest/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoticeValidation(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
mockStore := th.App.Srv().Store.(*mocks.Store)
|
||||
mockRoleStore := mocks.RoleStore{}
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPreferenceStore := mocks.PreferenceStore{}
|
||||
mockStore.On("Role").Return(&mockRoleStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("Preference").Return(&mockPreferenceStore)
|
||||
mockSystemStore.On("SaveOrUpdate", &model.System{Name: "ActiveLicenseId", Value: ""}).Return(nil)
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
||||
mockSystemStore.On("Get").Return(make(model.StringMap), nil)
|
||||
|
||||
mockUserStore.On("Count", model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true, ExcludeRegularUsers: false, TeamId: "", ChannelId: "", ViewRestrictions: (*model.ViewUsersRestrictions)(nil), Roles: []string(nil), ChannelRoles: []string(nil), TeamRoles: []string(nil)}).Return(int64(1), nil)
|
||||
mockPreferenceStore.On("Get", "test", "Stuff", "Data").Return(&model.Preference{Value: "test2"}, nil)
|
||||
mockPreferenceStore.On("Get", "test", "Stuff", "Data2").Return(&model.Preference{Value: "test"}, nil)
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.AnnouncementSettings.AdminNoticesEnabled = true
|
||||
*cfg.AnnouncementSettings.UserNoticesEnabled = true
|
||||
})
|
||||
|
||||
defer th.TearDown()
|
||||
|
||||
type args struct {
|
||||
client model.NoticeClientType
|
||||
clientVersion string
|
||||
locale string
|
||||
sku string
|
||||
postCount, userCount int64
|
||||
cloud bool
|
||||
teamAdmin bool
|
||||
systemAdmin bool
|
||||
serverVersion string
|
||||
notice *model.ProductNotice
|
||||
}
|
||||
messages := map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "descr",
|
||||
Title: "title",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "general notice",
|
||||
args: args{
|
||||
client: "mobile",
|
||||
clientVersion: "1.2.3",
|
||||
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{},
|
||||
ID: "123",
|
||||
LocalizedMessages: messages,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "mobile notice",
|
||||
args: args{
|
||||
client: "desktop",
|
||||
clientVersion: "1.2.3",
|
||||
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ClientType: model.NewNoticeClientType(model.NoticeClientType_Mobile),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with config check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerConfig: map[string]interface{}{"ServiceSettings.LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing config check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerConfig: map[string]interface{}{"ServiceSettings.ZZ": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with failing user check due to bad format",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with failing user check due to mismatch",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff.Data": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with working user check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff.Data2": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 4.0.0 < 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that doesn't match",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that matches a const",
|
||||
args: args{
|
||||
serverVersion: "99.1.1",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that is invalid",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"99.0.0 + 1.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with user count",
|
||||
args: args{
|
||||
userCount: 300,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfUsers: model.NewInt64(400),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with good user count and bad post count",
|
||||
args: args{
|
||||
userCount: 500,
|
||||
postCount: 2000,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfUsers: model.NewInt64(400),
|
||||
NumberOfPosts: model.NewInt64(3000),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with date check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2000-03-01T00:00:00Z <= 2999-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "notice with date check that doesn't match",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2999-03-01T00:00:00Z <= 3000-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with bad date check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2000 -03-01T00:00:00Z <= 2999-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (admin)",
|
||||
args: args{
|
||||
systemAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Sysadmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (admin)",
|
||||
args: args{
|
||||
systemAdmin: false,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Sysadmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (team)",
|
||||
args: args{
|
||||
teamAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_TeamAdmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (team)",
|
||||
args: args{
|
||||
teamAdmin: false,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_TeamAdmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (member)",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Member),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (member)",
|
||||
args: args{
|
||||
systemAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Member),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with correct sku",
|
||||
args: args{
|
||||
sku: "e20",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_E20),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with incorrect sku",
|
||||
args: args{
|
||||
sku: "e20",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_E10),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with team sku",
|
||||
args: args{
|
||||
sku: "",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_Team),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with sku check for all",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_All),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with instance check cloud",
|
||||
args: args{
|
||||
cloud: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceType_Cloud),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with instance check both",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceType_Both),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
clientVersion := tt.args.clientVersion
|
||||
if clientVersion == "" {
|
||||
clientVersion = "1.2.3"
|
||||
}
|
||||
model.BuildNumber = tt.args.serverVersion
|
||||
if model.BuildNumber == "" {
|
||||
model.BuildNumber = "5.26.1"
|
||||
}
|
||||
if ok, err := noticeMatchesConditions(th.App.Config(), th.App.Srv().Store.Preference(), "test", tt.args.client, clientVersion, tt.args.locale, tt.args.postCount, tt.args.userCount, tt.args.systemAdmin, tt.args.teamAdmin, tt.args.cloud, tt.args.sku, tt.args.notice); (err != nil) != tt.wantErr {
|
||||
t.Errorf("noticeMatchesConditions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if ok != tt.wantOk {
|
||||
t.Errorf("noticeMatchesConditions() result = %v, wantOk %v", ok, tt.wantOk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoticeFetch(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.AnnouncementSettings.AdminNoticesEnabled = true
|
||||
*cfg.AnnouncementSettings.UserNoticesEnabled = true
|
||||
})
|
||||
|
||||
notices := model.ProductNotices{model.ProductNotice{
|
||||
Conditions: model.Conditions{},
|
||||
ID: "123",
|
||||
LocalizedMessages: map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "description",
|
||||
Title: "title",
|
||||
},
|
||||
},
|
||||
Repeatable: nil,
|
||||
}}
|
||||
noticesBytes, appErr := notices.Marshal()
|
||||
require.NoError(t, appErr)
|
||||
|
||||
notices2 := model.ProductNotices{model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfPosts: model.NewInt64(99999),
|
||||
},
|
||||
ID: "333",
|
||||
LocalizedMessages: map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "description",
|
||||
Title: "title",
|
||||
},
|
||||
},
|
||||
Repeatable: nil,
|
||||
}}
|
||||
noticesBytes2, appErr := notices2.Marshal()
|
||||
require.NoError(t, appErr)
|
||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "notices.json") {
|
||||
w.Write(noticesBytes)
|
||||
} else {
|
||||
w.Write(noticesBytes2)
|
||||
}
|
||||
}))
|
||||
defer server1.Close()
|
||||
|
||||
NOTICES_JSON_URL = fmt.Sprintf("http://%s/notices.json", server1.Listener.Addr().String())
|
||||
|
||||
// fetch fake notices
|
||||
appErr = th.App.UpdateProductNotices()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them for specified user
|
||||
messages, appErr := th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 1)
|
||||
|
||||
// mark notices as viewed
|
||||
appErr = th.App.UpdateViewedProductNotices(th.BasicUser.Id, []string{messages[0].ID})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them again, see that none are returned
|
||||
messages, appErr = th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 0)
|
||||
|
||||
// validate views table
|
||||
views, err := th.App.Srv().Store.ProductNotices().GetViews(th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, views, 1)
|
||||
|
||||
// fetch another set
|
||||
NOTICES_JSON_URL = fmt.Sprintf("http://%s/notices2.json", server1.Listener.Addr().String())
|
||||
|
||||
// fetch fake notices
|
||||
appErr = th.App.UpdateProductNotices()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them again, since conditions don't match we should be zero
|
||||
messages, appErr = th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 0)
|
||||
|
||||
// even though UpdateViewedProductNotices was called previously, the table should be empty, since there's cleanup done during UpdateProductNotices
|
||||
views, err = th.App.Srv().Store.ProductNotices().GetViews(th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, views, 0)
|
||||
}
|
||||
@@ -513,6 +513,11 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.searchConfigListenerId = searchConfigListenerId
|
||||
s.searchLicenseListenerId = searchLicenseListenerId
|
||||
|
||||
// if enabled - perform initial product notices fetch
|
||||
if *s.Config().AnnouncementSettings.AdminNoticesEnabled || *s.Config().AnnouncementSettings.UserNoticesEnabled {
|
||||
go fakeApp.UpdateProductNotices()
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user