// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package web import ( "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/plugin/utils" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" "github.com/mattermost/mattermost/server/v8/config" ) var apiClient *model.Client4 var URL string type TestHelper struct { App *app.App Context request.CTX Server *app.Server Web *Web BasicUser *model.User BasicChannel *model.Channel BasicTeam *model.Team SystemAdminUser *model.User tempWorkspace string IncludeCacheLayer bool TestLogger *mlog.Logger } func SetupWithStoreMock(tb testing.TB) *TestHelper { if testing.Short() { tb.SkipNow() } th := setupTestHelper(tb, false, nil) emptyMockStore := mocks.Store{} emptyMockStore.On("Close").Return(nil) th.App.Srv().SetStore(&emptyMockStore) return th } func Setup(tb testing.TB) *TestHelper { if testing.Short() { tb.SkipNow() } store := mainHelper.GetStore() store.DropAllTables() return setupTestHelper(tb, true, nil) } func setupTestHelper(tb testing.TB, includeCacheLayer bool, options []app.Option) *TestHelper { memoryStore := config.NewTestMemoryStore() newConfig := memoryStore.Get().Clone() newConfig.SqlSettings = *mainHelper.GetSQLSettings() *newConfig.AnnouncementSettings.AdminNoticesEnabled = false *newConfig.AnnouncementSettings.UserNoticesEnabled = false *newConfig.PluginSettings.AutomaticPrepackagedPlugins = false *newConfig.LogSettings.EnableSentry = false // disable error reporting during tests *newConfig.LogSettings.ConsoleJson = false // Check for environment variable override for console log level (useful for debugging tests) consoleLevel := os.Getenv("MM_LOGSETTINGS_CONSOLELEVEL") if consoleLevel == "" { consoleLevel = mlog.LvlStdLog.Name } *newConfig.LogSettings.ConsoleLevel = consoleLevel _, _, err := memoryStore.Set(newConfig) require.NoError(tb, err) options = append(options, app.ConfigStore(memoryStore)) if includeCacheLayer { // Adds the cache layer to the test store options = append(options, app.StoreOverrideWithCache(mainHelper.Store)) } else { options = append(options, app.StoreOverride(mainHelper.Store)) } testLogger, err := mlog.NewLogger() require.NoError(tb, err) logCfg, err := config.MloggerConfigFromLoggerConfig(&newConfig.LogSettings, nil, config.GetLogFileLocation) require.NoError(tb, err) err = testLogger.ConfigureTargets(logCfg, nil) require.NoError(tb, err, "failed to configure test logger") // lock logger config so server init cannot override it during testing. testLogger.LockConfiguration() options = append(options, app.SetLogger(testLogger)) s, err := app.NewServer(options...) require.NoError(tb, err) a := app.New(app.ServerConnector(s.Channels())) prevListenAddress := *s.Config().ServiceSettings.ListenAddress a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = "localhost:0" }) err = s.Start() require.NoError(tb, err) a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress }) // Disable strict password requirements for test a.UpdateConfig(func(cfg *model.Config) { *cfg.PasswordSettings.MinimumLength = 5 *cfg.PasswordSettings.Lowercase = false *cfg.PasswordSettings.Uppercase = false *cfg.PasswordSettings.Symbol = false *cfg.PasswordSettings.Number = false }) web := New(s) URL = fmt.Sprintf("http://localhost:%v", s.ListenAddr.Port) apiClient = model.NewAPIv4Client(URL) s.Store().MarkSystemRanUnitTests() a.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.EnableOpenServer = true }) th := &TestHelper{ App: a, Context: request.EmptyContext(testLogger), Server: s, Web: web, IncludeCacheLayer: includeCacheLayer, TestLogger: testLogger, } tb.Cleanup(func() { if th.IncludeCacheLayer { // Clean all the caches appErr := th.App.Srv().InvalidateAllCaches() require.Nil(tb, appErr) } th.Server.Shutdown() }) return th } func (th *TestHelper) InitPlugins() *TestHelper { pluginDir := filepath.Join(th.tempWorkspace, "plugins") webappDir := filepath.Join(th.tempWorkspace, "webapp") th.App.InitPlugins(th.Context, pluginDir, webappDir) return th } func (th *TestHelper) NewPluginAPI(manifest *model.Manifest) plugin.API { return th.App.NewPluginAPI(th.Context, manifest) } func (th *TestHelper) InitBasic(tb testing.TB) *TestHelper { tb.Helper() var appErr *model.AppError th.SystemAdminUser, appErr = th.App.CreateUser(th.Context, &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SystemAdminRoleId}) require.Nil(tb, appErr) th.BasicUser, appErr = th.App.CreateUser(th.Context, &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", EmailVerified: true, Roles: model.SystemUserRoleId}) require.Nil(tb, appErr) th.BasicTeam, appErr = th.App.CreateTeam(th.Context, &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: th.BasicUser.Email, Type: model.TeamOpen}) require.Nil(tb, appErr) _, appErr = th.App.JoinUserToTeam(th.Context, th.BasicTeam, th.BasicUser, "") require.Nil(tb, appErr) th.BasicChannel, appErr = th.App.CreateChannel(th.Context, &model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.ChannelTypeOpen, TeamId: th.BasicTeam.Id, CreatorId: th.BasicUser.Id}, true) require.Nil(tb, appErr) return th } func TestStaticFilesRequest(t *testing.T) { th := Setup(t).InitPlugins() pluginID := "com.mattermost.sample" // Setup the directory directly in the plugin working path. pluginDir := filepath.Join(*th.App.Config().PluginSettings.Directory, pluginID) err := os.MkdirAll(pluginDir, 0777) require.NoError(t, err) pluginDir, err = filepath.Abs(pluginDir) require.NoError(t, err) // Compile the backend backend := filepath.Join(pluginDir, "backend.exe") pluginCode := ` package main import ( "github.com/mattermost/mattermost/server/public/plugin" ) type MyPlugin struct { plugin.MattermostPlugin } func main() { plugin.ClientMain(&MyPlugin{}) } ` utils.CompileGo(t, pluginCode, backend) // Write out the frontend mainJS := `var x = alert();` mainJSPath := filepath.Join(pluginDir, "main.js") require.NoError(t, err) err = os.WriteFile(mainJSPath, []byte(mainJS), 0777) require.NoError(t, err) // Write the plugin.json manifest pluginManifest := `{"id": "com.mattermost.sample", "server": {"executable": "backend.exe"}, "webapp": {"bundle_path":"main.js"}, "settings_schema": {"settings": []}}` err = os.WriteFile(filepath.Join(pluginDir, "plugin.json"), []byte(pluginManifest), 0600) require.NoError(t, err) // Activate the plugin manifest, activated, reterr := th.App.GetPluginsEnvironment().Activate(pluginID) require.NoError(t, reterr) require.NotNil(t, manifest) require.True(t, activated) // Verify access to the bundle with requisite headers req, err := http.NewRequest("GET", "/static/plugins/com.mattermost.sample/com.mattermost.sample_724ed0e2ebb2b841_bundle.js", nil) require.NoError(t, err) res := httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) assert.Equal(t, mainJS, res.Body.String()) assert.Equal(t, []string{"max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) // Verify cached access to the bundle with an If-Modified-Since timestamp in the future future := time.Now().Add(24 * time.Hour) req, err = http.NewRequest("GET", "/static/plugins/com.mattermost.sample/com.mattermost.sample_724ed0e2ebb2b841_bundle.js", nil) require.NoError(t, err) req.Header.Add("If-Modified-Since", future.Format(time.RFC850)) res = httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, http.StatusNotModified, res.Code) assert.Empty(t, res.Body.String()) assert.Equal(t, []string{"max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) // Verify access to the bundle with an If-Modified-Since timestamp in the past past := time.Now().Add(-24 * time.Hour) req, err = http.NewRequest("GET", "/static/plugins/com.mattermost.sample/com.mattermost.sample_724ed0e2ebb2b841_bundle.js", nil) require.NoError(t, err) req.Header.Add("If-Modified-Since", past.Format(time.RFC850)) res = httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) assert.Equal(t, mainJS, res.Body.String()) assert.Equal(t, []string{"max-age=31556926, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) // Verify handling of 404. req, err = http.NewRequest("GET", "/static/plugins/com.mattermost.sample/404.js", nil) require.NoError(t, err) res = httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, http.StatusNotFound, res.Code) assert.Equal(t, "404 page not found\n", res.Body.String()) assert.Equal(t, []string{"no-cache, public"}, res.Result().Header[http.CanonicalHeaderKey("Cache-Control")]) } func TestPublicFilesRequest(t *testing.T) { th := Setup(t).InitPlugins() pluginDir, err := os.MkdirTemp("", "") require.NoError(t, err) webappPluginDir, err := os.MkdirTemp("", "") require.NoError(t, err) defer os.RemoveAll(pluginDir) defer os.RemoveAll(webappPluginDir) env, err := plugin.NewEnvironment(th.NewPluginAPI, app.NewDriverImpl(th.Server), pluginDir, webappPluginDir, th.App.Log(), nil) require.NoError(t, err) pluginID := "com.mattermost.sample" pluginCode := ` package main import ( "github.com/mattermost/mattermost/server/public/plugin" ) type MyPlugin struct { plugin.MattermostPlugin } func main() { plugin.ClientMain(&MyPlugin{}) } ` // Compile and write the plugin backend := filepath.Join(pluginDir, pluginID, "backend.exe") utils.CompileGo(t, pluginCode, backend) // Write the plugin.json manifest pluginManifest := `{"id": "com.mattermost.sample", "server": {"executable": "backend.exe"}, "settings_schema": {"settings": []}}` err = os.WriteFile(filepath.Join(pluginDir, pluginID, "plugin.json"), []byte(pluginManifest), 0600) require.NoError(t, err) // Write the test public file helloHTML := `Hello from the static files public folder for the com.mattermost.sample plugin!` htmlFolderPath := filepath.Join(pluginDir, pluginID, "public") err = os.MkdirAll(htmlFolderPath, os.ModePerm) require.NoError(t, err) htmlFilePath := filepath.Join(htmlFolderPath, "hello.html") htmlFileErr := os.WriteFile(htmlFilePath, []byte(helloHTML), 0600) assert.NoError(t, htmlFileErr) nefariousHTML := `You shouldn't be able to get here!` htmlFileErr = os.WriteFile(filepath.Join(pluginDir, pluginID, "nefarious-file-access.html"), []byte(nefariousHTML), 0600) assert.NoError(t, htmlFileErr) manifest, activated, reterr := env.Activate(pluginID) require.NoError(t, reterr) require.NotNil(t, manifest) require.True(t, activated) th.App.Channels().SetPluginsEnvironment(env) req, err := http.NewRequest("GET", "/plugins/com.mattermost.sample/public/hello.html", nil) require.NoError(t, err) res := httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, helloHTML, res.Body.String()) req, err = http.NewRequest("GET", "/plugins/com.mattermost.sample/nefarious-file-access.html", nil) require.NoError(t, err) res = httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, 404, res.Code) req, err = http.NewRequest("GET", "/plugins/com.mattermost.sample/public/../nefarious-file-access.html", nil) require.NoError(t, err) res = httptest.NewRecorder() th.Web.MainRouter.ServeHTTP(res, req) assert.Equal(t, 301, res.Code) } /* Test disabled for now so we don't require the client to build. Maybe re-enable after client gets moved out. func TestStatic(t *testing.T) { Setup() // add a short delay to make sure the server is ready to receive requests time.Sleep(1 * time.Second) resp, err := http.Get(URL + "/static/root.html") assert.NoErrorf(t, err, "got error while trying to get static files %v", err) assert.Equalf(t, resp.StatusCode, http.StatusOK, "couldn't get static files %v", resp.StatusCode) } */ func TestStaticFilesCaching(t *testing.T) { th := Setup(t).InitPlugins() fakeMainBundleName := "main.1234ab.js" fakeRootHTML := `