MM-27918 In-Product notices support (#15316)

Этот коммит содержится в:
Eli Yukelzon
2020-09-21 10:28:46 +03:00
коммит произвёл GitHub
родитель 43ed6ad690
Коммит 4e9ddd4686
63 изменённых файлов: 4549 добавлений и 7 удалений

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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, &noticesCache, 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 Обычный файл
Просмотреть файл

@@ -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
}