// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" ) func TestNoticeValidation(t *testing.T) { mainHelper.Parallel(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) mockStore.On("GetDBSchemaVersion").Return(1, nil) 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) mockPreferenceStore.On("Get", "test", "Stuff", "Data3").Return(nil, errors.New("Error!")) 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 sku string postCount, userCount int64 cloud bool teamAdmin bool systemAdmin bool serverVersion string notice *model.ProductNotice dbmsName string dbmsVer string searchEngineName string searchEngineVer string } 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.NoticeClientTypeMobile), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with config check", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ ServerConfig: map[string]any{"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]any{"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]any{"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]any{"Stuff.Data": "test"}, }, }, }, wantErr: false, wantOk: false, }, { name: "notice with working user check", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ UserConfig: map[string]any{"Stuff.Data2": "test"}, }, }, }, wantErr: false, wantOk: true, }, { name: "notice with user check for property not in database", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ UserConfig: map[string]any{"Stuff.Data3": "stuff"}, }, }, }, wantErr: false, wantOk: false, }, { 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 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.NewPointer(int64(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.NewPointer(int64(400)), NumberOfPosts: model.NewPointer(int64(3000)), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with date check", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ DisplayDate: model.NewPointer("> 2000-03-01T00:00:00Z <= 2999-04-01T00:00:00Z"), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with specific date check", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ DisplayDate: model.NewPointer(fmt.Sprintf("= %sT00:00:00Z", time.Now().UTC().Format("2006-01-02"))), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with date check that doesn't match", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ DisplayDate: model.NewPointer("> 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.NewPointer("> 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.NoticeAudienceSysadmin), }, }, }, 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.NoticeAudienceSysadmin), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with audience check (team)", args: args{ teamAdmin: true, notice: &model.ProductNotice{ Conditions: model.Conditions{ Audience: model.NewNoticeAudience(model.NoticeAudienceTeamAdmin), }, }, }, 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.NoticeAudienceTeamAdmin), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with audience check (member)", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ Audience: model.NewNoticeAudience(model.NoticeAudienceMember), }, }, }, 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.NoticeAudienceMember), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with correct sku", args: args{ sku: "e20", notice: &model.ProductNotice{ Conditions: model.Conditions{ Sku: model.NewNoticeSKU(model.NoticeSKUE20), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with incorrect sku", args: args{ sku: "e20", notice: &model.ProductNotice{ Conditions: model.Conditions{ Sku: model.NewNoticeSKU(model.NoticeSKUE10), }, }, }, wantErr: false, wantOk: false, }, { name: "notice with team sku", args: args{ sku: "", notice: &model.ProductNotice{ Conditions: model.Conditions{ Sku: model.NewNoticeSKU(model.NoticeSKUTeam), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with sku check for all", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ Sku: model.NewNoticeSKU(model.NoticeSKUAll), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with instance check cloud", args: args{ cloud: true, notice: &model.ProductNotice{ Conditions: model.Conditions{ InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceTypeCloud), }, }, }, wantErr: false, wantOk: true, }, { name: "notice with instance check both", args: args{ notice: &model.ProductNotice{ Conditions: model.Conditions{ InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceTypeBoth), }, }, }, wantErr: false, wantOk: true, }, { name: "notice on a deprecating dependency, server is all good", args: args{ dbmsName: "postgres", dbmsVer: "10", notice: &model.ProductNotice{ Conditions: model.Conditions{ DeprecatingDependency: &model.ExternalDependency{ Name: "postgres", MinimumVersion: "10", }, }, }, }, wantErr: false, wantOk: false, }, { name: "notice on deprecating elasticsearch, server has unsupported search engine", args: args{ searchEngineName: "elasticsearch", searchEngineVer: "6.4.1", notice: &model.ProductNotice{ Conditions: model.Conditions{ DeprecatingDependency: &model.ExternalDependency{ Name: "elasticsearch", MinimumVersion: "7", }, }, }, }, 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" } serverVersion := tt.args.serverVersion if serverVersion == "" { serverVersion = "5.26.1" } if ok, err := noticeMatchesConditions( th.App.Config(), th.App.Srv().Store().Preference(), "test", tt.args.client, serverVersion, clientVersion, tt.args.postCount, tt.args.userCount, tt.args.systemAdmin, tt.args.teamAdmin, tt.args.cloud, tt.args.sku, tt.args.dbmsName, tt.args.dbmsVer, tt.args.searchEngineName, tt.args.searchEngineVer, 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) { mainHelper.Parallel(t) th := Setup(t).InitBasic() defer th.TearDown() notices := model.ProductNotices{model.ProductNotice{ Conditions: model.Conditions{}, ID: "123", LocalizedMessages: map[string]model.NoticeMessageInternal{ "en": { Description: "description", Title: "title", }, }, Repeatable: nil, }} noticesBytes, err := notices.Marshal() require.NoError(t, err) notices2 := model.ProductNotices{model.ProductNotice{ Conditions: model.Conditions{ NumberOfPosts: model.NewPointer(int64(99999)), }, ID: "333", LocalizedMessages: map[string]model.NoticeMessageInternal{ "en": { Description: "description", Title: "title", }, }, Repeatable: nil, }} noticesBytes2, err := notices2.Marshal() require.NoError(t, err) server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, "notices.json") { _, err = w.Write(noticesBytes) require.NoError(t, err) } else { _, err = w.Write(noticesBytes2) require.NoError(t, err) } })) defer server1.Close() th.App.UpdateConfig(func(cfg *model.Config) { *cfg.AnnouncementSettings.AdminNoticesEnabled = true *cfg.AnnouncementSettings.UserNoticesEnabled = true *cfg.AnnouncementSettings.NoticesURL = 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.Context, th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientTypeAll, "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.Context, th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientTypeAll, "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.NoError(t, err) require.Len(t, views, 1) // fetch another set th.App.UpdateConfig(func(cfg *model.Config) { *cfg.AnnouncementSettings.NoticesURL = 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.Context, th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientTypeAll, "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.NoError(t, err) require.Len(t, views, 0) }