Files
team/server/public/plugin/environment_with_rpcerr_test.go
ce5644223a [MM-68400] Four plugin hooks and ChannelGuard enforcement (#36152)
* allow workflow_dispatch trigger for Server CI (for plugins CI)

* [MM-68402] MBE Phase 2: declare four generic plugin hooks (#36291)

* new hooks-only phase 2

* remove ChannelWillBeMoved

* remove RecapWillBeProcessed and MessageWillBeRewrittenByAI

Drop the AI/recap hooks from the new-hook surface; AI-LLM paths
remain uncovered in tech preview and are documented as residuals.

* [MM-68403] MBE Phase 3: ChannelGuards primitive (storage + cache + plugin API) (#36365)

* phase 3

* phase 3: register ChannelGuard mock in test setup helper

NewChannels' startup-time call to reloadGuardCache invokes
s.ChannelGuard().GetAll(); without an expectation on the mock store,
every test that sets up the server with GetMockStoreForSetupFunctions
panics during init.

* phase 3: register ChannelGuard mock in retrylayer test

retrylayer.New walks every store getter to wrap it; without the mock
expectation on ChannelGuard, TestRetry panics during layer construction.

* use rctx properly in the store methods

* phase 3: match rctx arg in testlib ChannelGuard mock

GetAll now takes request.CTX, so the testify expectation must include
mock.Anything; otherwise the call panics under the mocked store.

* phase 3: set api.ctx in TestChannelGuardLowercaseNormalization

The test constructs PluginAPI directly without a ctx, which used to
work when App.RegisterChannelGuard built its own EmptyContext. Now
that the App methods take rctx from the caller, the nil ctx panics
inside RequestContextWithMaster.

* [MM-68404] MBE Phase 4: App-layer plugin hook wiring (#36407)

* phase 4

* Fix nil rctx in TestChannelGuardLowercaseNormalization

The PluginAPI struct literal was missing ctx: rctx after a refactor
moved the rctx declaration below the struct construction, leaving
api.ctx as nil. This caused a nil pointer dereference in reloadGuardCache
when RegisterChannelGuard called store.RequestContextWithMaster(nil).

* Remove ChannelWillBeMoved hook call from MoveChannel (phase 4)

The hook and its ID were removed from mbe-phase-2 but the call site in
MoveChannel and its i18n string were not cleaned up during the rebase.

* remove channel will be moved test

* Remove RecapWillBeProcessed and MessageWillBeRewrittenByAI hook calls (phase 4)

The hooks and their IDs were removed from mbe-phase-2 but the call sites
in ProcessRecapChannel and RewriteMessage, their i18n strings, and their
tests were not cleaned up during the rebase.

* Revert channel_id plumbing on rewrite endpoint (phase 4)

The channel_id field on RewriteRequest was added in phase 4 to feed the
synthetic post passed to MessageWillBeRewrittenByAI. With that hook
removed from mbe-phase-2, channel_id has no consumer; revert the field,
the api4 validation, the app.RewriteMessage parameter, and the
corresponding webapp client + hook plumbing.

* [MM-68555] MBE Phase 5: Channel-guard enforcement + two-phase dispatch (#36473)

* phase 5

* Bake plugin counter-file paths into source instead of env vars

t.Setenv panics when an ancestor test calls t.Parallel, so the two
channel-guard tests broke under ENABLE_FULLY_PARALLEL_TESTS in CI.
Build each plugin source per-subtest with its temp file path embedded
as a Go literal — same pattern as TestPluginUploadsAPI.

* Remove guarded helpers and tests for dropped hooks (phase 5)

The runGuardedRecapWillBeProcessed and runGuardedMessageWillBeRewrittenByAI
helpers were never wired (their app-layer call sites were already removed
in the phase-4 cleanup), and the corresponding sub-tests across panic /
allow / reject / partial plugins reference hooks that no longer exist.

* [MM-68405] MBE Phase 6: fire MessagesWillBeConsumed on the edit path (#36475)

---------

---------

---------

---------

* rebase onto master

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-05-21 18:16:05 +00:00

145 lines
4.4 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package plugin
import (
"errors"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin/utils"
"github.com/mattermost/mattermost/server/public/shared/mlog"
)
func TestRunMultiPluginHookWithRPCErr(t *testing.T) {
pluginDir, err := os.MkdirTemp("", "mm-rpcerr-plugin")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(pluginDir) })
webappPluginDir, err := os.MkdirTemp("", "mm-rpcerr-webapp")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(webappPluginDir) })
pluginID1 := "test-rpc-err-plugin"
pluginID2 := "test-rpc-err-plugin-2"
require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, pluginID1), 0700))
require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, pluginID2), 0700))
backend1 := filepath.Join(pluginDir, pluginID1, "backend.exe")
backend2 := filepath.Join(pluginDir, pluginID2, "backend.exe")
utils.CompileGo(t, `
package main
import (
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) MessageHasBeenPosted(c *plugin.Context, post *model.Post) {}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`, backend1)
copyExecutable(t, backend1, backend2)
require.NoError(t, os.WriteFile(
filepath.Join(pluginDir, pluginID1, "plugin.json"),
[]byte(`{"id":"`+pluginID1+`","server":{"executable":"backend.exe"}}`),
0600,
))
require.NoError(t, os.WriteFile(
filepath.Join(pluginDir, pluginID2, "plugin.json"),
[]byte(`{"id":"`+pluginID2+`","server":{"executable":"backend.exe"}}`),
0600,
))
logger := mlog.CreateConsoleTestLogger(t)
apiImpl := func(*model.Manifest) API { return nil }
env, err := NewEnvironment(apiImpl, nil, pluginDir, webappPluginDir, logger, nil)
require.NoError(t, err)
t.Cleanup(env.Shutdown)
_, _, err = env.Activate(pluginID1)
require.NoError(t, err)
require.True(t, env.IsActive(pluginID1))
_, _, err = env.Activate(pluginID2)
require.NoError(t, err)
require.True(t, env.IsActive(pluginID2))
t.Run("both plugins healthy - closure invoked once per plugin", func(t *testing.T) {
seen := map[string]int{}
runErr := env.RunMultiPluginHookWithRPCErr(func(hooks HooksWithRPCErr, manifest *model.Manifest) (bool, error) {
seen[manifest.Id]++
require.NoError(t, hooks.MessageHasBeenPostedWithRPCErr(&Context{}, &model.Post{}))
return true, nil
}, MessageHasBeenPostedID)
require.NoError(t, runErr)
require.Equal(t, map[string]int{pluginID1: 1, pluginID2: 1}, seen)
})
t.Run("closure error propagates and stops iteration", func(t *testing.T) {
sentinel := errors.New("from closure")
var calls int
runErr := env.RunMultiPluginHookWithRPCErr(func(_ HooksWithRPCErr, _ *model.Manifest) (bool, error) {
calls++
return true, sentinel
}, MessageHasBeenPostedID)
require.ErrorIs(t, runErr, sentinel)
require.Equal(t, 1, calls)
})
t.Run("hook id not implemented - closure never invoked", func(t *testing.T) {
var calls int
runErr := env.RunMultiPluginHookWithRPCErr(func(_ HooksWithRPCErr, _ *model.Manifest) (bool, error) {
calls++
return true, nil
}, MessageHasBeenUpdatedID)
require.NoError(t, runErr)
require.Equal(t, 0, calls)
})
t.Run("rpc transport error surfaces after plugin process dies", func(t *testing.T) {
rp, ok := env.registeredPlugins.Load(pluginID1)
require.True(t, ok)
sup := rp.(registeredPlugin).supervisor
require.NotNil(t, sup)
sup.client.Kill()
// Give the rpc client a moment to notice the dead connection.
require.Eventually(t, func() bool {
var rpcErr error
_ = env.RunMultiPluginHookWithRPCErr(func(hooks HooksWithRPCErr, manifest *model.Manifest) (bool, error) {
if manifest.Id != pluginID1 {
return true, nil
}
rpcErr = hooks.MessageHasBeenPostedWithRPCErr(&Context{}, &model.Post{})
return true, nil
}, MessageHasBeenPostedID)
return rpcErr != nil
}, 2*time.Second, 50*time.Millisecond)
})
}
func copyExecutable(t *testing.T, src, dst string) {
t.Helper()
in, err := os.Open(src)
require.NoError(t, err)
defer in.Close()
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0700)
require.NoError(t, err)
defer out.Close()
_, err = io.Copy(out, in)
require.NoError(t, err)
}