// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package utils_test import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/v8/channels/utils" ) func TestUpdateAssetsSubpathFromConfig(t *testing.T) { t.Run("dev build", func(t *testing.T) { var oldBuildNumber = model.BuildNumber model.BuildNumber = "dev" defer func() { model.BuildNumber = oldBuildNumber }() err := utils.UpdateAssetsSubpathFromConfig(nil) require.NoError(t, err) }) t.Run("IS_CI=true", func(t *testing.T) { err := os.Setenv("IS_CI", "true") require.NoError(t, err) defer func() { err = os.Unsetenv("IS_CI") require.NoError(t, err) }() err = utils.UpdateAssetsSubpathFromConfig(nil) require.NoError(t, err) }) t.Run("no config", func(t *testing.T) { tempDir, err := os.MkdirTemp("", "test_update_assets_subpath") require.NoError(t, err) defer func() { err = os.RemoveAll(tempDir) require.NoError(t, err) }() err = os.Chdir(tempDir) require.NoError(t, err) err = utils.UpdateAssetsSubpathFromConfig(nil) require.Error(t, err) }) } func TestUpdateAssetsSubpath(t *testing.T) { t.Run("no client dir", func(t *testing.T) { tempDir, err := os.MkdirTemp("", "test_update_assets_subpath") require.NoError(t, err) defer func() { err = os.RemoveAll(tempDir) require.NoError(t, err) }() err = os.Chdir(tempDir) require.NoError(t, err) err = utils.UpdateAssetsSubpath("/") require.Error(t, err) }) t.Run("valid", func(t *testing.T) { tempDir, err := os.MkdirTemp("", "test_update_assets_subpath") require.NoError(t, err) defer func() { err = os.RemoveAll(tempDir) require.NoError(t, err) }() err = os.Chdir(tempDir) require.NoError(t, err) err = os.Mkdir(model.ClientDir, 0700) require.NoError(t, err) testCases := []struct { Description string RootHTML string MainCSS string ManifestJSON string Subpath string ExpectedError error ExpectedRootHTML string ExpectedMainCSS string ExpectedManifestJSON string }{ { "no changes required, empty subpath provided", baseRootHTML, baseCSS, baseManifestJSON, "", nil, baseRootHTML, baseCSS, baseManifestJSON, }, { "no changes required", baseRootHTML, baseCSS, baseManifestJSON, "/", nil, baseRootHTML, baseCSS, baseManifestJSON, }, { "content security policy not found (missing quotes)", contentSecurityPolicyNotFoundHTML, baseCSS, baseManifestJSON, "/subpath", fmt.Errorf("failed to update root.html: %w", fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")), contentSecurityPolicyNotFoundHTML, baseCSS, baseManifestJSON, }, { "content security policy not found (missing unsafe-eval)", contentSecurityPolicyNotFound2HTML, baseCSS, baseManifestJSON, "/subpath", fmt.Errorf("failed to update root.html: %w", fmt.Errorf("failed to find 'Content-Security-Policy' meta tag to rewrite")), contentSecurityPolicyNotFound2HTML, baseCSS, baseManifestJSON, }, { "subpath", baseRootHTML, baseCSS, baseManifestJSON, "/subpath", nil, subpathRootHTML, subpathCSS, subpathManifestJSON, }, { "new subpath from old", subpathRootHTML, subpathCSS, subpathManifestJSON, "/nested/subpath", nil, newSubpathRootHTML, newSubpathCSS, newSubpathManifestJSON, }, { "resetting to /", subpathRootHTML, subpathCSS, baseManifestJSON, "/", nil, baseRootHTML, baseCSS, baseManifestJSON, }, } for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "root.html"), []byte(testCase.RootHTML), 0700)) require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "main.css"), []byte(testCase.MainCSS), 0700)) require.NoError(t, os.WriteFile(filepath.Join(tempDir, model.ClientDir, "manifest.json"), []byte(testCase.ManifestJSON), 0700)) err := utils.UpdateAssetsSubpath(testCase.Subpath) if testCase.ExpectedError != nil { require.Equal(t, testCase.ExpectedError, err) } else { require.NoError(t, err) } contents, err := os.ReadFile(filepath.Join(tempDir, model.ClientDir, "root.html")) require.NoError(t, err) // Rewrite the expected and contents for simpler diffs when failed. expectedRootHTML := strings.Replace(testCase.ExpectedRootHTML, ">", ">\n", -1) contentsStr := strings.Replace(string(contents), ">", ">\n", -1) require.Equal(t, expectedRootHTML, contentsStr) contents, err = os.ReadFile(filepath.Join(tempDir, model.ClientDir, "main.css")) require.NoError(t, err) require.Equal(t, testCase.ExpectedMainCSS, string(contents)) contents, err = os.ReadFile(filepath.Join(tempDir, model.ClientDir, "manifest.json")) require.NoError(t, err) require.Equal(t, testCase.ExpectedManifestJSON, string(contents)) }) } }) } func TestGetSubpathFromConfig(t *testing.T) { testCases := []struct { Description string SiteURL *string ExpectedError bool ExpectedSubpath string }{ { "empty SiteURL", model.NewPointer(""), false, "/", }, { "invalid SiteURL", model.NewPointer("cache_object:foo/bar"), true, "", }, { "nil SiteURL", nil, false, "/", }, { "no trailing slash", model.NewPointer("http://localhost:8065"), false, "/", }, { "trailing slash", model.NewPointer("http://localhost:8065/"), false, "/", }, { "subpath, no trailing slash", model.NewPointer("http://localhost:8065/subpath"), false, "/subpath", }, { "trailing slash", model.NewPointer("http://localhost:8065/subpath/"), false, "/subpath", }, } for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { config := &model.Config{ ServiceSettings: model.ServiceSettings{ SiteURL: testCase.SiteURL, }, } subpath, err := utils.GetSubpathFromConfig(config) if testCase.ExpectedError { require.Error(t, err) } else { require.NoError(t, err) } require.Equal(t, testCase.ExpectedSubpath, subpath) }) } } const contentSecurityPolicyNotFoundHTML = ` Mattermost <

Cannot connect to Mattermost


We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.


` const contentSecurityPolicyNotFound2HTML = ` Mattermost

Cannot connect to Mattermost


We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.


` const baseRootHTML = ` Mattermost

Cannot connect to Mattermost


We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.


` const baseCSS = `@font-face{font-family:FontAwesome;src:url(/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}` const subpathRootHTML = ` Mattermost

Cannot connect to Mattermost


We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.


` const subpathCSS = `@font-face{font-family:FontAwesome;src:url(/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/subpath/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/subpath/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/subpath/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/subpath/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}` const newSubpathRootHTML = ` Mattermost

Cannot connect to Mattermost


We're having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.


` const newSubpathCSS = `@font-face{font-family:FontAwesome;src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot);src:url(/nested/subpath/static/files/674f50d287a8c48dc19ba404d20fe713.eot?#iefix&v=4.7.0) format("embedded-opentype"),url(/nested/subpath/static/files/af7ae505a9eed503f8b8e6982036873e.woff2) format("woff2"),url(/nested/subpath/static/files/fee66e712a8a08eef5805a46892932ad.woff) format("woff"),url(/nested/subpath/static/files/b06871f281fee6b241d60582ae9369b9.ttf) format("truetype"),url(/nested/subpath/static/files/677433a0892aaed7b7d2628c313c9775.svg#fontawesomeregular) format("svg");font-weight:400;font-style:normal}` const baseManifestJSON = `{ "icons": [ { "src": "/static/icon_96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/static/icon_32x32.png", "sizes": "32x32", "type": "image/png" }, { "src": "/static/icon_16x16.png", "sizes": "16x16", "type": "image/png" }, { "src": "/static/icon_76x76.png", "sizes": "76x76", "type": "image/png" }, { "src": "/static/icon_72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/static/icon_60x60.png", "sizes": "60x60", "type": "image/png" }, { "src": "/static/icon_57x57.png", "sizes": "57x57", "type": "image/png" }, { "src": "/static/icon_152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/static/icon_144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "/static/icon_120x120.png", "sizes": "120x120", "type": "image/png" }, { "src": "/static/icon_192x192.png", "sizes": "192x192", "type": "image/png" } ], "name": "Mattermost", "short_name": "Mattermost", "orientation": "any", "display": "standalone", "start_url": ".", "description": "Mattermost is an open source, self-hosted Slack-alternative", "background_color": "#ffffff" } ` const subpathManifestJSON = `{ "icons": [ { "src": "/subpath/static/icon_96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/subpath/static/icon_32x32.png", "sizes": "32x32", "type": "image/png" }, { "src": "/subpath/static/icon_16x16.png", "sizes": "16x16", "type": "image/png" }, { "src": "/subpath/static/icon_76x76.png", "sizes": "76x76", "type": "image/png" }, { "src": "/subpath/static/icon_72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/subpath/static/icon_60x60.png", "sizes": "60x60", "type": "image/png" }, { "src": "/subpath/static/icon_57x57.png", "sizes": "57x57", "type": "image/png" }, { "src": "/subpath/static/icon_152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/subpath/static/icon_144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "/subpath/static/icon_120x120.png", "sizes": "120x120", "type": "image/png" }, { "src": "/subpath/static/icon_192x192.png", "sizes": "192x192", "type": "image/png" } ], "name": "Mattermost", "short_name": "Mattermost", "orientation": "any", "display": "standalone", "start_url": ".", "description": "Mattermost is an open source, self-hosted Slack-alternative", "background_color": "#ffffff" } ` const newSubpathManifestJSON = `{ "icons": [ { "src": "/nested/subpath/static/icon_96x96.png", "sizes": "96x96", "type": "image/png" }, { "src": "/nested/subpath/static/icon_32x32.png", "sizes": "32x32", "type": "image/png" }, { "src": "/nested/subpath/static/icon_16x16.png", "sizes": "16x16", "type": "image/png" }, { "src": "/nested/subpath/static/icon_76x76.png", "sizes": "76x76", "type": "image/png" }, { "src": "/nested/subpath/static/icon_72x72.png", "sizes": "72x72", "type": "image/png" }, { "src": "/nested/subpath/static/icon_60x60.png", "sizes": "60x60", "type": "image/png" }, { "src": "/nested/subpath/static/icon_57x57.png", "sizes": "57x57", "type": "image/png" }, { "src": "/nested/subpath/static/icon_152x152.png", "sizes": "152x152", "type": "image/png" }, { "src": "/nested/subpath/static/icon_144x144.png", "sizes": "144x144", "type": "image/png" }, { "src": "/nested/subpath/static/icon_120x120.png", "sizes": "120x120", "type": "image/png" }, { "src": "/nested/subpath/static/icon_192x192.png", "sizes": "192x192", "type": "image/png" } ], "name": "Mattermost", "short_name": "Mattermost", "orientation": "any", "display": "standalone", "start_url": ".", "description": "Mattermost is an open source, self-hosted Slack-alternative", "background_color": "#ffffff" } `