mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 21:27:40 -05:00
MM-70071: Automatically select hosted push notification server based on license (#37802)
* MM-70071: Automatically select hosted push notification server based on license Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * test: fix mock-store fallout from push endpoint license listener Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * MM-70071: address review feedback on push endpoint sync Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * ci: retrigger enterprise tests against updated companion branch Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * ci: retrigger flaky artifact build Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * MM-70071: revert any hosted push endpoint to test on entitlement loss Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * MM-70071: add nil-safe License.HasMHPNS entitlement check Co-authored-by: nick.misasi <nick.misasi@mattermost.com> * MM-70071: drop preview-tree docs for auto-selected push server Monorepo MDX is still unpublished; this belongs in mattermost/docs. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-70071: stub InitEmailBatching on guest-invite email mocks License teardown now SaveConfigs the push endpoint, which fires the existing email-batching config listener. Co-authored-by: Cursor <cursoragent@cursor.com> * MM-70071: don't re-init email batching on push-server license sync License teardown SaveConfigs the push endpoint, which fired the existing email-batching listener and panicked tests that mock EmailService. Re-init batching only when EnableEmailBatching changes, and stub the remaining invite mock used during helper cleanup. Co-authored-by: Nick Misasi <nick13misasi@gmail.com> * MM-70071: re-init email batching when the interval setting changes Keep EmailBatchingInterval live at runtime; only ignore unrelated config writes such as the push-server license sync. Co-authored-by: Nick Misasi <nick13misasi@gmail.com> * MM-70071: isolate mock tests from push endpoint sync Use custom push endpoints in shared mock fixtures so unrelated tests do not need config-listener expectations. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
co-authored by
Nick Misasi
Cursor
Mattermost Build
parent
c36f979edd
commit
62056e5a7c
@@ -370,7 +370,7 @@ func SetupConfigWithStoreMock(tb testing.TB, updateConfig func(cfg *model.Config
|
||||
}
|
||||
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
th := setupTestHelper(tb, testlib.GetMockStoreForSetupFunctions(), nil, nil, false, false, nil, nil)
|
||||
th := setupTestHelper(tb, testlib.GetMockStoreForSetupFunctions(), nil, nil, false, false, useCustomPushNotificationServer, nil)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
|
||||
@@ -383,10 +383,15 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
return th
|
||||
}
|
||||
|
||||
func useCustomPushNotificationServer(config *model.Config) {
|
||||
*config.EmailSettings.PushNotificationServer = "https://push.example.com"
|
||||
}
|
||||
|
||||
func SetupEnterpriseWithStoreMock(tb testing.TB, options ...app.Option) *TestHelper {
|
||||
removeSpuriousErrors := func(config *model.Config) {
|
||||
// If not set, you will receive an unactionable error in the console
|
||||
*config.ServiceSettings.SiteURL = "http://localhost:8065"
|
||||
useCustomPushNotificationServer(config)
|
||||
}
|
||||
|
||||
th := setupTestHelper(tb, testlib.GetMockStoreForSetupFunctions(), nil, nil, true, false, removeSpuriousErrors, options)
|
||||
|
||||
@@ -6878,6 +6878,11 @@ func TestGetChannelModerations(t *testing.T) {
|
||||
scheme := th.SetupTeamScheme(t)
|
||||
scheme.DefaultChannelGuestRole = ""
|
||||
|
||||
// Restore the real store so helper cleanup (cache invalidation, license reload)
|
||||
// doesn't run against the partial mock.
|
||||
originalStore := th.App.Srv().Store()
|
||||
t.Cleanup(func() { th.App.Srv().SetStore(originalStore) })
|
||||
|
||||
mockStore := mocks.Store{}
|
||||
|
||||
// Playbooks DB job requires a plugin mock
|
||||
@@ -7034,6 +7039,11 @@ func TestPatchChannelModerations(t *testing.T) {
|
||||
scheme := th.SetupTeamScheme(t)
|
||||
scheme.DefaultChannelGuestRole = ""
|
||||
|
||||
// Restore the real store so helper cleanup (cache invalidation, license reload)
|
||||
// doesn't run against the partial mock.
|
||||
originalStore := th.App.Srv().Store()
|
||||
t.Cleanup(func() { th.App.Srv().SetStore(originalStore) })
|
||||
|
||||
mockStore := mocks.Store{}
|
||||
|
||||
// Playbooks DB job requires a plugin mock
|
||||
|
||||
@@ -261,9 +261,13 @@ func SetupWithoutPreloadMigrations(tb testing.TB) *TestHelper {
|
||||
return setupTestHelper(dbStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), false, true, nil, nil, tb)
|
||||
}
|
||||
|
||||
func useCustomPushNotificationServer(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = "https://push.example.com"
|
||||
}
|
||||
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), false, false, nil, nil, tb)
|
||||
th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), false, false, useCustomPushNotificationServer, nil, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
|
||||
@@ -284,7 +288,7 @@ func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
|
||||
func SetupEnterpriseWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), true, false, nil, nil, tb)
|
||||
th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), true, false, useCustomPushNotificationServer, nil, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
|
||||
|
||||
@@ -30,15 +30,7 @@ func (a *App) canSendPushNotifications() bool {
|
||||
}
|
||||
|
||||
pushServer := *a.Config().EmailSettings.PushNotificationServer
|
||||
// Check for MHPNS servers (both current and legacy DNS aliases)
|
||||
isMHPNSServer := pushServer == model.MHPNS ||
|
||||
pushServer == model.MHPNSLegacyUS ||
|
||||
pushServer == model.MHPNSLegacyDE ||
|
||||
pushServer == model.MHPNSGlobal ||
|
||||
pushServer == model.MHPNSUS ||
|
||||
pushServer == model.MHPNSEU ||
|
||||
pushServer == model.MHPNSAP
|
||||
if license := a.Srv().License(); isMHPNSServer && (license == nil || !*license.Features.MHPNS) {
|
||||
if model.IsMHPNSEndpoint(pushServer) && !a.Srv().License().HasMHPNS() {
|
||||
a.Log().LogM(mlog.MlvlNotificationWarn, "Push notifications are disabled - license missing",
|
||||
mlog.String("status", model.NotificationStatusNotSent),
|
||||
mlog.String("reason", "push_disabled_license"),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/public/shared/request"
|
||||
)
|
||||
|
||||
// syncPushNotificationServerWithLicense switches EmailSettings.PushNotificationServer to the
|
||||
// hosted push notification service (MHPNS) endpoint when the license grants MHPNS access, and
|
||||
// back to the test (TPNS) endpoint when it no longer does. It runs on license changes, on
|
||||
// server start, and when this node becomes the cluster leader.
|
||||
//
|
||||
// The contract: promote only from exact TPNS to the Global endpoint; on entitlement loss,
|
||||
// revert any Mattermost-hosted production endpoint (global, regional, or legacy) to TPNS, so
|
||||
// a lapsed license never leaves push pointing at an endpoint that refuses to send. Custom
|
||||
// endpoints and env-managed values are never touched. The sync stays stateless because both
|
||||
// directions derive entirely from the current config value and the license.
|
||||
func (s *Server) syncPushNotificationServerWithLicense() {
|
||||
if !s.IsLeader() {
|
||||
return
|
||||
}
|
||||
|
||||
license := s.License()
|
||||
// Cloud config is centrally managed; never rewrite it here.
|
||||
if license.IsCloud() {
|
||||
return
|
||||
}
|
||||
|
||||
if s.platform.IsConfigReadOnly() {
|
||||
return
|
||||
}
|
||||
|
||||
// Respect an environment-variable override on the setting. The config store re-applies env
|
||||
// overrides on save anyway, so without this guard a save would be futile and only produce
|
||||
// spurious audit records, logs, and cluster config traffic on every license event.
|
||||
if emailOverrides, ok := s.platform.GetEnvironmentOverrides()["EmailSettings"].(map[string]any); ok {
|
||||
if _, overridden := emailOverrides["PushNotificationServer"]; overridden {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
entitled := license.HasMHPNS()
|
||||
|
||||
// Decide and mutate on the same snapshot so a concurrent config write between the
|
||||
// decision and the save can't be stomped with a stale value. The residual race between
|
||||
// Clone and Set is inherent to every SaveConfig caller.
|
||||
cfg := s.platform.Config().Clone()
|
||||
current := *cfg.EmailSettings.PushNotificationServer
|
||||
|
||||
var target string
|
||||
switch {
|
||||
case entitled && current == model.GenericNotificationServer:
|
||||
target = model.MHPNSGlobal
|
||||
case !entitled && model.IsMHPNSEndpoint(current):
|
||||
target = model.GenericNotificationServer
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
cfg.EmailSettings.PushNotificationServer = model.NewPointer(target)
|
||||
if _, _, appErr := s.platform.SaveConfig(cfg, true); appErr != nil {
|
||||
mlog.Warn("Failed to switch push notification server for license entitlement",
|
||||
mlog.String("old", current), mlog.String("new", target), mlog.Err(appErr))
|
||||
return
|
||||
}
|
||||
mlog.Info("Automatically switched push notification server based on license entitlement",
|
||||
mlog.String("old", current), mlog.String("new", target))
|
||||
|
||||
rctx := request.EmptyContext(s.Log())
|
||||
appInstance := New(ServerConnector(s.Channels()))
|
||||
rec := appInstance.MakeAuditRecord(rctx, model.AuditEventAutoSelectPushNotificationServer, model.AuditStatusSuccess)
|
||||
model.AddEventParameterToAuditRec(rec, "old_push_notification_server", current)
|
||||
model.AddEventParameterToAuditRec(rec, "new_push_notification_server", target)
|
||||
appInstance.LogAuditRec(rctx, rec, nil)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
emailmocks "github.com/mattermost/mattermost/server/v8/channels/app/email/mocks"
|
||||
clustermocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
)
|
||||
|
||||
func TestSyncPushNotificationServerWithLicense(t *testing.T) {
|
||||
// Not parallel: subtests mutate the license, shared config, and environment.
|
||||
th := Setup(t)
|
||||
|
||||
licenseWithMHPNS := model.NewTestLicense("mhpns")
|
||||
licenseWithoutMHPNS := model.NewTestLicense()
|
||||
licenseWithoutMHPNS.Features.MHPNS = model.NewPointer(false)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
license *model.License
|
||||
initialServer string
|
||||
expectedServer string
|
||||
}{
|
||||
{
|
||||
name: "entitled license switches TPNS to Global",
|
||||
license: licenseWithMHPNS,
|
||||
initialServer: model.GenericNotificationServer,
|
||||
expectedServer: model.MHPNSGlobal,
|
||||
},
|
||||
{
|
||||
name: "entitlement removed reverts Global to TPNS",
|
||||
license: licenseWithoutMHPNS,
|
||||
initialServer: model.MHPNSGlobal,
|
||||
expectedServer: model.GenericNotificationServer,
|
||||
},
|
||||
{
|
||||
name: "license removed reverts Global to TPNS",
|
||||
license: nil,
|
||||
initialServer: model.MHPNSGlobal,
|
||||
expectedServer: model.GenericNotificationServer,
|
||||
},
|
||||
{
|
||||
name: "entitled license leaves custom endpoint untouched",
|
||||
license: licenseWithMHPNS,
|
||||
initialServer: "https://push.example.com",
|
||||
expectedServer: "https://push.example.com",
|
||||
},
|
||||
{
|
||||
name: "entitled license leaves regional endpoint untouched",
|
||||
license: licenseWithMHPNS,
|
||||
initialServer: model.MHPNSEU,
|
||||
expectedServer: model.MHPNSEU,
|
||||
},
|
||||
{
|
||||
name: "entitled license leaves Global untouched",
|
||||
license: licenseWithMHPNS,
|
||||
initialServer: model.MHPNSGlobal,
|
||||
expectedServer: model.MHPNSGlobal,
|
||||
},
|
||||
{
|
||||
name: "unentitled license reverts regional endpoint (MHPNSUS) to TPNS",
|
||||
license: licenseWithoutMHPNS,
|
||||
initialServer: model.MHPNSUS,
|
||||
expectedServer: model.GenericNotificationServer,
|
||||
},
|
||||
{
|
||||
name: "unentitled license reverts legacy endpoint (MHPNSLegacyDE) to TPNS",
|
||||
license: licenseWithoutMHPNS,
|
||||
initialServer: model.MHPNSLegacyDE,
|
||||
expectedServer: model.GenericNotificationServer,
|
||||
},
|
||||
{
|
||||
name: "unentitled license leaves TPNS untouched",
|
||||
license: licenseWithoutMHPNS,
|
||||
initialServer: model.GenericNotificationServer,
|
||||
expectedServer: model.GenericNotificationServer,
|
||||
},
|
||||
{
|
||||
name: "unentitled license leaves custom endpoint untouched",
|
||||
license: licenseWithoutMHPNS,
|
||||
initialServer: "https://push.example.com",
|
||||
expectedServer: "https://push.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(nil)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = tc.initialServer
|
||||
*cfg.EmailSettings.SendPushNotifications = true
|
||||
})
|
||||
|
||||
// Setting the license fires the listener registered in NewServer,
|
||||
// which runs syncPushNotificationServerWithLicense.
|
||||
th.App.Srv().SetLicense(tc.license)
|
||||
|
||||
cfg := th.App.Config()
|
||||
assert.Equal(t, tc.expectedServer, *cfg.EmailSettings.PushNotificationServer)
|
||||
assert.True(t, *cfg.EmailSettings.SendPushNotifications, "SendPushNotifications must never be modified")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("direct call switches TPNS to Global on the startup path", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(licenseWithMHPNS)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = model.GenericNotificationServer
|
||||
})
|
||||
|
||||
th.Server.syncPushNotificationServerWithLicense()
|
||||
|
||||
assert.Equal(t, model.MHPNSGlobal, *th.App.Config().EmailSettings.PushNotificationServer)
|
||||
})
|
||||
|
||||
t.Run("license with nil MHPNS feature is unentitled and reverts Global to TPNS", func(t *testing.T) {
|
||||
license := model.NewTestLicense()
|
||||
th.App.Srv().SetLicense(license)
|
||||
// SetLicense normalizes feature defaults, back-filling any nil pointer, so a license
|
||||
// with a nil MHPNS can only reach the sync through the direct path. Clear the field
|
||||
// on the stored license to prove the entitlement check is nil-safe and treats the
|
||||
// license as unentitled. Restore it afterwards: license logging during teardown
|
||||
// dereferences every feature pointer via Features.ToMap.
|
||||
license.Features.MHPNS = nil
|
||||
t.Cleanup(func() { license.Features.MHPNS = model.NewPointer(false) })
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = model.MHPNSGlobal
|
||||
})
|
||||
|
||||
th.Server.syncPushNotificationServerWithLicense()
|
||||
|
||||
assert.Equal(t, model.GenericNotificationServer, *th.App.Config().EmailSettings.PushNotificationServer)
|
||||
})
|
||||
|
||||
t.Run("environment override leaves setting untouched", func(t *testing.T) {
|
||||
t.Setenv("MM_EMAILSETTINGS_PUSHNOTIFICATIONSERVER", model.GenericNotificationServer)
|
||||
|
||||
// The config value alone can't prove the env-override guard fired: a save of this
|
||||
// config would be a no-op anyway, because the store re-applies env overrides and
|
||||
// skips config listeners when the effective config is unchanged. The one side
|
||||
// effect a futile save cannot avoid is cluster propagation — SaveConfig calls
|
||||
// ConfigChanged on the cluster interface unconditionally — so probe that to prove
|
||||
// the guard returned before saving.
|
||||
clusterMock := &clustermocks.ClusterInterface{}
|
||||
clusterMock.On("IsLeader").Return(true).Maybe()
|
||||
clusterMock.On("GetClusterId").Return("").Maybe()
|
||||
clusterMock.On("SendClusterMessage", mock.Anything).Return().Maybe()
|
||||
clusterMock.On("RegisterClusterMessageHandler", mock.Anything, mock.Anything).Return().Maybe()
|
||||
clusterMock.On("StopInterNodeCommunication").Return().Maybe()
|
||||
clusterMock.On("Shutdown").Return().Maybe()
|
||||
clusterMock.On("ConfigChanged", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
|
||||
// The subtest needs its own harness so the mock is installed before the platform
|
||||
// starts; swapping the cluster interface mid-test races with platform goroutines
|
||||
// that read it. Setup also isolates the env var set above.
|
||||
envTh := SetupWithClusterMock(t, clusterMock)
|
||||
|
||||
envTh.App.Srv().SetLicense(licenseWithMHPNS)
|
||||
envTh.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = model.GenericNotificationServer
|
||||
})
|
||||
|
||||
envTh.Server.syncPushNotificationServerWithLicense()
|
||||
|
||||
clusterMock.AssertNotCalled(t, "ConfigChanged", mock.Anything, mock.Anything, mock.Anything)
|
||||
assert.Equal(t, model.GenericNotificationServer, *envTh.App.Config().EmailSettings.PushNotificationServer)
|
||||
})
|
||||
|
||||
t.Run("reverting hosted endpoint does not re-init email batching", func(t *testing.T) {
|
||||
originalEmailService := th.App.Srv().EmailService
|
||||
t.Cleanup(func() {
|
||||
th.App.Srv().EmailService = originalEmailService
|
||||
})
|
||||
|
||||
emailServiceMock := emailmocks.ServiceInterface{}
|
||||
th.App.Srv().EmailService = &emailServiceMock
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.PushNotificationServer = model.MHPNSGlobal
|
||||
})
|
||||
th.App.Srv().SetLicense(nil)
|
||||
|
||||
emailServiceMock.AssertNotCalled(t, "InitEmailBatching")
|
||||
assert.Equal(t, model.GenericNotificationServer, *th.App.Config().EmailSettings.PushNotificationServer)
|
||||
})
|
||||
}
|
||||
@@ -128,6 +128,9 @@ type Server struct {
|
||||
clusterLeaderListenerId string
|
||||
loggerLicenseListenerId string
|
||||
|
||||
pushNotificationServerLicenseListenerId string
|
||||
pushNotificationServerClusterLeaderListenerId string
|
||||
|
||||
platform *platform.PlatformService
|
||||
platformOptions []platform.Option
|
||||
telemetryService *telemetry.TelemetryService
|
||||
@@ -506,9 +509,11 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Start email batching because it's not like the other jobs
|
||||
s.platform.AddConfigListener(func(_, _ *model.Config) {
|
||||
s.EmailService.InitEmailBatching()
|
||||
// Re-init email batching only when its enable flag or interval changes.
|
||||
s.platform.AddConfigListener(func(oldCfg, newCfg *model.Config) {
|
||||
if emailBatchingSettingChanged(oldCfg, newCfg) {
|
||||
s.EmailService.InitEmailBatching()
|
||||
}
|
||||
})
|
||||
|
||||
pwd, _ := os.Getwd()
|
||||
@@ -540,6 +545,15 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.platform.EnableLoggingMetrics()
|
||||
})
|
||||
|
||||
// Keep the push notification server in sync with the license's HPNS entitlement, and let a
|
||||
// newly-elected cluster leader repair any transition missed while another node was leader.
|
||||
s.pushNotificationServerLicenseListenerId = s.AddLicenseListener(func(oldLicense, newLicense *model.License) {
|
||||
s.syncPushNotificationServerWithLicense()
|
||||
})
|
||||
s.pushNotificationServerClusterLeaderListenerId = s.AddClusterLeaderChangedListener(func() {
|
||||
s.syncPushNotificationServerWithLicense()
|
||||
})
|
||||
|
||||
// if enabled - perform initial product notices fetch
|
||||
if *s.platform.Config().AnnouncementSettings.AdminNoticesEnabled || *s.platform.Config().AnnouncementSettings.UserNoticesEnabled {
|
||||
s.platform.Go(func() {
|
||||
@@ -767,6 +781,8 @@ func (s *Server) Shutdown() {
|
||||
|
||||
s.RemoveLicenseListener(s.loggerLicenseListenerId)
|
||||
s.RemoveClusterLeaderChangedListener(s.clusterLeaderListenerId)
|
||||
s.RemoveLicenseListener(s.pushNotificationServerLicenseListenerId)
|
||||
s.RemoveClusterLeaderChangedListener(s.pushNotificationServerClusterLeaderListenerId)
|
||||
|
||||
var err error
|
||||
s.serviceMux.RLock()
|
||||
@@ -1008,6 +1024,8 @@ func (s *Server) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
s.syncPushNotificationServerWithLicense()
|
||||
|
||||
s.checkPushNotificationServerURL()
|
||||
|
||||
if err = s.platform.ReloadConfig(); err != nil {
|
||||
@@ -2045,3 +2063,13 @@ func (s *Server) Platform() *platform.PlatformService {
|
||||
func (s *Server) Log() *mlog.Logger {
|
||||
return s.platform.Logger()
|
||||
}
|
||||
|
||||
func emailBatchingSettingChanged(oldCfg, newCfg *model.Config) bool {
|
||||
if oldCfg == nil || newCfg == nil {
|
||||
return true
|
||||
}
|
||||
return model.SafeDereference(oldCfg.EmailSettings.EnableEmailBatching) !=
|
||||
model.SafeDereference(newCfg.EmailSettings.EnableEmailBatching) ||
|
||||
model.SafeDereference(oldCfg.EmailSettings.EmailBatchingInterval) !=
|
||||
model.SafeDereference(newCfg.EmailSettings.EmailBatchingInterval)
|
||||
}
|
||||
|
||||
@@ -566,3 +566,50 @@ func TestOriginChecker(t *testing.T) {
|
||||
require.Equalf(t, tc.Pass, res, "Test case (%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailBatchingSettingChanged(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := func(enabled bool, interval int) *model.Config {
|
||||
c := &model.Config{}
|
||||
c.EmailSettings.EnableEmailBatching = model.NewPointer(enabled)
|
||||
c.EmailSettings.EmailBatchingInterval = model.NewPointer(interval)
|
||||
return c
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
oldCfg *model.Config
|
||||
newCfg *model.Config
|
||||
expected bool
|
||||
}{
|
||||
{name: "nil old config", oldCfg: nil, newCfg: cfg(true, 30), expected: true},
|
||||
{name: "nil new config", oldCfg: cfg(true, 30), newCfg: nil, expected: true},
|
||||
{name: "unchanged disabled", oldCfg: cfg(false, 30), newCfg: cfg(false, 30), expected: false},
|
||||
{name: "unchanged enabled", oldCfg: cfg(true, 30), newCfg: cfg(true, 30), expected: false},
|
||||
{name: "enabled", oldCfg: cfg(false, 30), newCfg: cfg(true, 30), expected: true},
|
||||
{name: "disabled", oldCfg: cfg(true, 30), newCfg: cfg(false, 30), expected: true},
|
||||
{name: "interval changed", oldCfg: cfg(true, 30), newCfg: cfg(true, 300), expected: true},
|
||||
{
|
||||
name: "push notification server change is ignored",
|
||||
oldCfg: func() *model.Config {
|
||||
c := cfg(false, 30)
|
||||
c.EmailSettings.PushNotificationServer = model.NewPointer(model.MHPNSGlobal)
|
||||
return c
|
||||
}(),
|
||||
newCfg: func() *model.Config {
|
||||
c := cfg(false, 30)
|
||||
c.EmailSettings.PushNotificationServer = model.NewPointer(model.GenericNotificationServer)
|
||||
return c
|
||||
}(),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tc.expected, emailBatchingSettingChanged(tc.oldCfg, tc.newCfg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2219,7 +2219,9 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
license := model.NewTestLicenseWithFalseDefaults("mhpns")
|
||||
license.SkuShortName = model.LicenseShortSkuEnterprise
|
||||
th.App.Srv().SetLicense(license)
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
*cfg.TeamSettings.LockProfileFieldsForEmailUsers = model.TeamSettingsLockProfileFieldsNameAndUsername
|
||||
@@ -2368,6 +2370,8 @@ func TestInviteNewUsersToTeamGracefully(t *testing.T) {
|
||||
t.Run("it returns error for deactivated user without sending email", func(t *testing.T) {
|
||||
emailServiceMock := emailmocks.ServiceInterface{}
|
||||
emailServiceMock.On("Stop").Once().Return()
|
||||
// The teardown license reset saves a config change, whose listener re-inits email batching.
|
||||
emailServiceMock.On("InitEmailBatching").Return().Maybe()
|
||||
th.App.Srv().EmailService = &emailServiceMock
|
||||
|
||||
_, appErr := th.App.UpdateActive(th.Context, th.BasicUser2, false)
|
||||
|
||||
@@ -142,15 +142,16 @@ const (
|
||||
|
||||
// Configuration
|
||||
const (
|
||||
AuditEventConfigReload = "configReload" // reload server configuration
|
||||
AuditEventGetConfig = "getConfig" // get current server configuration
|
||||
AuditEventLocalGetClientConfig = "localGetClientConfig" // get client configuration locally
|
||||
AuditEventLocalGetConfig = "localGetConfig" // get server configuration locally
|
||||
AuditEventLocalPatchConfig = "localPatchConfig" // update server configuration locally
|
||||
AuditEventLocalUpdateConfig = "localUpdateConfig" // update server configuration locally
|
||||
AuditEventMigrateConfig = "migrateConfig" // migrate configs with file values from one store to another
|
||||
AuditEventPatchConfig = "patchConfig" // update server configuration
|
||||
AuditEventUpdateConfig = "updateConfig" // update server configuration
|
||||
AuditEventAutoSelectPushNotificationServer = "autoSelectPushNotificationServer" // automatically switch push notification server based on license entitlement
|
||||
AuditEventConfigReload = "configReload" // reload server configuration
|
||||
AuditEventGetConfig = "getConfig" // get current server configuration
|
||||
AuditEventLocalGetClientConfig = "localGetClientConfig" // get client configuration locally
|
||||
AuditEventLocalGetConfig = "localGetConfig" // get server configuration locally
|
||||
AuditEventLocalPatchConfig = "localPatchConfig" // update server configuration locally
|
||||
AuditEventLocalUpdateConfig = "localUpdateConfig" // update server configuration locally
|
||||
AuditEventMigrateConfig = "migrateConfig" // migrate configs with file values from one store to another
|
||||
AuditEventPatchConfig = "patchConfig" // update server configuration
|
||||
AuditEventUpdateConfig = "updateConfig" // update server configuration
|
||||
)
|
||||
|
||||
// Custom Profile Attributes
|
||||
|
||||
@@ -430,6 +430,11 @@ func (l *License) HasSharedChannels() bool {
|
||||
MinimumProfessionalLicense(l)
|
||||
}
|
||||
|
||||
// HasMHPNS reports whether the license grants access to the Mattermost hosted push notification service.
|
||||
func (l *License) HasMHPNS() bool {
|
||||
return l != nil && l.Features != nil && l.Features.MHPNS != nil && *l.Features.MHPNS
|
||||
}
|
||||
|
||||
// NewTestLicense returns a license that expires in the future and has the given features.
|
||||
func NewTestLicense(features ...string) *License {
|
||||
ret := &License{
|
||||
|
||||
@@ -486,6 +486,56 @@ func TestLicenseHasSharedChannels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicenseHasMHPNS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
license *License
|
||||
expectedValue bool
|
||||
}{
|
||||
{
|
||||
"nil license",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil features",
|
||||
&License{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil MHPNS feature",
|
||||
&License{
|
||||
Features: &Features{},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"MHPNS feature disabled",
|
||||
&License{
|
||||
Features: &Features{
|
||||
MHPNS: new(false),
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"MHPNS feature enabled",
|
||||
&License{
|
||||
Features: &Features{
|
||||
MHPNS: new(true),
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expectedValue, testCase.license.HasMHPNS())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimumProfessionalLicense(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -43,6 +44,19 @@ const (
|
||||
PushReceived = "Received by device"
|
||||
)
|
||||
|
||||
// IsMHPNSEndpoint reports whether the given push notification server URL is one of the
|
||||
// Mattermost-hosted (HPNS) production endpoints.
|
||||
func IsMHPNSEndpoint(url string) bool {
|
||||
return slices.Contains([]string{
|
||||
MHPNSLegacyUS,
|
||||
MHPNSLegacyDE,
|
||||
MHPNSGlobal,
|
||||
MHPNSUS,
|
||||
MHPNSEU,
|
||||
MHPNSAP,
|
||||
}, url)
|
||||
}
|
||||
|
||||
// PushSubType allows for passing additional message type information
|
||||
// to mobile clients in a backwards-compatible way
|
||||
type PushSubType string
|
||||
|
||||
@@ -54,3 +54,29 @@ func TestPushNotificationDeviceId(t *testing.T) {
|
||||
msg.Platform = ""
|
||||
msg.DeviceId = ""
|
||||
}
|
||||
|
||||
func TestIsMHPNSEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
expected bool
|
||||
}{
|
||||
{name: "legacy US endpoint", url: MHPNSLegacyUS, expected: true},
|
||||
{name: "legacy DE endpoint", url: MHPNSLegacyDE, expected: true},
|
||||
{name: "global endpoint", url: MHPNSGlobal, expected: true},
|
||||
{name: "US endpoint", url: MHPNSUS, expected: true},
|
||||
{name: "EU endpoint", url: MHPNSEU, expected: true},
|
||||
{name: "AP endpoint", url: MHPNSAP, expected: true},
|
||||
{name: "legacy MHPNS alias", url: MHPNS, expected: true},
|
||||
{name: "test endpoint", url: GenericNotificationServer, expected: false},
|
||||
{name: "custom endpoint", url: "https://push.example.com", expected: false},
|
||||
{name: "empty string", url: "", expected: false},
|
||||
{name: "case variant", url: "https://GLOBAL.push.mattermost.com", expected: false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.expected, IsMHPNSEndpoint(tc.url))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user