Files
team/server/channels/api4/file_test.go
9ed85b6850 Cherry-pick Go 1.25.8 upgrade and FIPS password fixes to release-11.4 (#36123)
* ci: shard server Postgres tests into 4 parallel runners (#35739)

Cherry-pick of 00296f9b49 from master.

* updated go to version 1.25.8 (#35817)

Cherry-pick of 32a10bac62 from master.

* Move password hashers from server/v8 to server/public (#35805)

Cherry-pick of cca3f08574 from master.

* Generate instead of hard-coding test passwords, enforce new minimum for FIPS, shard CI, fix FIPS builds (#35905)

Cherry-pick of 1bdd985b41 from master.

* upgrade golangci-lint (#35845)

Cherry-pick of 157e0400b2 from master.

On release-11.4 (unlike 11.5), UpdatePostsChannelTypeByChannelId and
BackfillPostsChannelType exist in the elasticsearch/opensearch files,
so the imports they require (fmt, updatebyquery, scriptlanguage) must
be added — they were not in the cherry-pick diff since they already
existed on master.

Also updates html2text/tablewriter/olekukonko deps to fix a workspace
conflict where enterprise's Go 1.25.8 bump pulled in a newer html2text
that requires tablewriter ≥ 1.1 (was pinned to 0.0.5 via a replace
directive). Matches the subset of 866f759e4b (Mm 66662 bump deps)
relevant to this incompatibility.

* Fix FIPS-incompatible passwords and config in e2e test suites (#36001)

Cherry-pick of b51b4a8937 from master.

release-11.4 differences vs release-11.5:
- demo_plugin_installation.spec.ts also deleted (modify/delete conflict, git rm)
- authentication_2_spec.ts conflict: HEAD missing check() call; took
  incoming (check + FIPS-length passwords)
- member_invitation_ui_spec.ts: HEAD has 'Create Account' (capital A)
  matching release-11.4 button text; kept HEAD's button text with
  incoming's newTestPassword()

* fix(cypress): demo plugin (#36056)

Cherry-pick of 39f1979aa5 from master.

demo_plugin_installation.spec.ts was deleted in step 10 (modify/delete);
re-created here with both the FIPS-password change and the URL fix.

* test: clean up channel store data after TestChannelStore (#36066)

TestChannelStore sub-tests create channels, members, and team members
using fake TeamIds and UserIds (model.NewId() for non-existent rows).
These records are left in the database and cause integrity tests
(TestCheck*) running in the same binary to fail their full-table scans.

Register a t.Cleanup on TestChannelStore that purges the affected
tables entirely. A blanket purge is safe: the schema enforces no FK
constraints, and every test suite creates its own data independently.

* Fix command injection in server-test-template workflow (#36080)

Cherry-pick of 9223935f16 from master.

* fix(ci): restore testname format in sharded gotestsum runs (#36078)

run-shard-tests.sh called gotestsum directly without --format, so it
fell back to gotestsum's default (pkgname) instead of the testname
format set by the Makefile. Pass --format "${GOTESTSUM_FORMAT:-testname}"
to match the Makefile default.

Co-authored-by: Mattermost Build <[email protected]>

* fixup: drop demo_plugin playwright specs — master-only APIs

demo_plugin/helpers.ts and demo_plugin_installation.spec.ts use
installAndEnablePlugin and isPluginActive which don't exist on
release-11.4's PlaywrightExtended type. These files were re-created
by the step-11 cherry-pick of 39f1979aa5, but they don't compile on
this branch (same class of problem as actions.spec.ts in step 10).

* fixup: add missing await in PluginLinkTooltip test

The test added in 39f1979aa5 is missing an await on renderWithContext,
causing the pointer-events assertion to race. Fix from 0c7d3f27c6
(on a feature branch, not yet on master).

* fixup: drop pointer-events test incompatible with jsdom@20

The 'should not block interaction with elements outside the tooltip' test
checks getComputedStyle for pointer-events: none, which is set only in SCSS.
release-11.4 uses jest-environment-jsdom@29 (jsdom@20), which does not apply
stylesheets, so getComputedStyle always returns "". The test passes on
release-11.5+ only because they use jest-environment-jsdom@30 (jsdom@25+).

Drop the test rather than backport the jsdom upgrade.

* fixup: add missing i18n keys for elasticsearch/opensearch functions

BackfillPostsChannelType and UpdatePostsChannelTypeByChannelId exist on
release-11.4 (unlike release-11.5+). The golangci-lint cherry-pick added
new error strings to those functions referencing i18n keys that weren't
in en.json.

* fixup: run make i18n-extract to reorder keys correctly

* fixup: drop terms-and-privacy checkbox from MM-T1774 on release-11.4

The #signup-body-card-form-check-terms-and-privacy element did not exist
on release-11.4's signup page. It was added to master after this branch
was cut, so the check() call from the incoming cherry-pick causes the test
to fail with "element never found".

* fixup: remove UpdatePostsChannelTypeByChannelId/BackfillPostsChannelType

These functions were introduced by MM-67540 (allow searching public channel
messages without channel membership), which never landed on release-11.4.
The golangci-lint cherry-pick only modernizes existing code; the rerere
resolution incorrectly added the full function bodies from master.

Remove both functions and their now-unused imports (fmt, updatebyquery,
scriptlanguage) from elasticsearch.go and opensearch.go. Also remove the
two i18n keys added to support them.

Same treatment as release-11.5, where these functions were also absent.

---------

Co-authored-by: Mattermost Build <[email protected]>
2026-04-16 12:10:03 -03:00

1617 lines
60 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/app"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
)
var testDir = ""
func init() {
testDir, _ = fileutils.FindDir("tests")
}
// File Section
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
func randomBytes(t *testing.T, n int) []byte {
bb := make([]byte, n)
_, err := rand.Read(bb)
require.NoError(t, err)
return bb
}
func fileBytes(t *testing.T, path string) []byte {
path = filepath.Join(testDir, path)
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
bb, err := io.ReadAll(f)
require.NoError(t, err)
return bb
}
func testDoUploadFileRequest(tb testing.TB, c *model.Client4, url string, blob []byte, contentType string,
contentLength int64,
) (*model.FileUploadResponse, *model.Response, error) {
req, err := http.NewRequest("POST", c.APIURL+"/files"+url, bytes.NewReader(blob))
require.NoError(tb, err)
if contentLength != 0 {
req.ContentLength = contentLength
}
req.Header.Set("Content-Type", contentType)
if c.AuthToken != "" {
req.Header.Set(model.HeaderAuth, c.AuthType+" "+c.AuthToken)
}
resp, err := c.HTTPClient.Do(req)
require.NoError(tb, err)
require.NotNil(tb, resp)
defer closeBody(resp)
if resp.StatusCode >= 300 {
return nil, model.BuildResponse(resp), model.AppErrorFromJSON(resp.Body)
}
var res model.FileUploadResponse
if jsonErr := json.NewDecoder(resp.Body).Decode(&res); jsonErr != nil {
return nil, nil, model.NewAppError("doUploadFile", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
}
return &res, model.BuildResponse(resp), nil
}
func testUploadFilesPost(
tb testing.TB,
c *model.Client4,
channelId string,
names []string,
blobs [][]byte,
clientIds []string,
useChunked bool,
isBookmark bool,
) (*model.FileUploadResponse, *model.Response, error) {
// Do not check len(clientIds), leave it entirely to the user to
// provide. The server will error out if it does not match the number
// of files, but it's not critical here.
require.NotEmpty(tb, names)
require.NotEmpty(tb, blobs)
require.Equal(tb, len(names), len(blobs))
fileUploadResponse := &model.FileUploadResponse{}
for i, blob := range blobs {
var cl int64
if useChunked {
cl = -1
} else {
cl = int64(len(blob))
}
ct := http.DetectContentType(blob)
postURL := fmt.Sprintf("?channel_id=%v", url.QueryEscape(channelId)) +
fmt.Sprintf("&filename=%v", url.QueryEscape(names[i]))
if len(clientIds) > i {
postURL += fmt.Sprintf("&client_id=%v", url.QueryEscape(clientIds[i]))
}
if isBookmark {
postURL += "&bookmark=true"
}
fur, resp, err := testDoUploadFileRequest(tb, c, postURL, blob, ct, cl)
if err != nil {
return nil, resp, err
}
fileUploadResponse.FileInfos = append(fileUploadResponse.FileInfos, fur.FileInfos[0])
if len(clientIds) > 0 {
if len(fur.ClientIds) > 0 {
fileUploadResponse.ClientIds = append(fileUploadResponse.ClientIds, fur.ClientIds[0])
} else {
fileUploadResponse.ClientIds = append(fileUploadResponse.ClientIds, "")
}
}
}
return fileUploadResponse, nil, nil
}
func testUploadFilesMultipart(
tb testing.TB,
c *model.Client4,
channelId string,
names []string,
blobs [][]byte,
clientIds []string,
isBookmark bool,
) (
*model.FileUploadResponse,
*model.Response,
error,
) {
// Do not check len(clientIds), leave it entirely to the user to
// provide. The server will error out if it does not match the number
// of files, but it's not critical here.
require.NotEmpty(tb, names)
require.NotEmpty(tb, blobs)
require.Equal(tb, len(names), len(blobs))
mwBody := &bytes.Buffer{}
mw := multipart.NewWriter(mwBody)
err := mw.WriteField("channel_id", channelId)
require.NoError(tb, err)
for i, blob := range blobs {
ct := http.DetectContentType(blob)
if len(clientIds) > i {
err = mw.WriteField("client_ids", clientIds[i])
require.NoError(tb, err)
}
h := textproto.MIMEHeader{}
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="files"; filename="%s"`, escapeQuotes(names[i])))
h.Set("Content-Type", ct)
// If we error here, writing to mw, the deferred handler
var part io.Writer
part, err = mw.CreatePart(h)
require.NoError(tb, err)
_, err = io.Copy(part, bytes.NewReader(blob))
require.NoError(tb, err)
}
require.NoError(tb, mw.Close())
url := ""
if isBookmark {
url += "?bookmark=true"
}
fur, resp, err := testDoUploadFileRequest(tb, c, url, mwBody.Bytes(), mw.FormDataContentType(), -1)
if err != nil {
return nil, resp, err
}
return fur, resp, nil
}
func TestUploadFiles(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
channel := th.BasicChannel
date := time.Now().Format("20060102")
// Get better error messages
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableDeveloper = true })
tests := []struct {
title string
client *model.Client4
blobs [][]byte
names []string
clientIds []string
skipSuccessValidation bool
skipPayloadValidation bool
skipSimplePost bool
skipMultipart bool
channelId string
useChunkedInSimplePost bool
expectedCreatorId string
expectedPayloadNames []string
expectImage bool
expectedImageWidths []int
expectedImageHeights []int
expectedImageThumbnailNames []string
expectedImagePreviewNames []string
expectedImageHasPreview []bool
expectedImageMiniPreview []bool
setupConfig func(a *app.App) func(a *app.App)
checkResponse func(tb testing.TB, resp *model.Response)
uploadAsBookmark bool
}{
// Upload a bunch of files, mixed images and non-images
{
title: "Happy",
names: []string{"test.png", "testgif.gif", "testplugin.tar.gz", "test-search.md", "test.tiff"},
expectedCreatorId: th.BasicUser.Id,
expectedImageMiniPreview: []bool{true, true, false, false, true},
},
// Upload a bunch of files, with clientIds
{
title: "Happy client_ids",
names: []string{"test.png", "testgif.gif", "testplugin.tar.gz", "test-search.md", "test.tiff"},
clientIds: []string{"1", "2", "3", "4", "5"},
expectedImageMiniPreview: []bool{true, true, false, false, true},
expectedCreatorId: th.BasicUser.Id,
},
// Upload a bunch of images. testgif.gif is an animated GIF,
// so it does not have HasPreviewImage set.
{
title: "Happy images",
names: []string{"test.png", "testgif.gif"},
expectImage: true,
expectedCreatorId: th.BasicUser.Id,
expectedImageWidths: []int{408, 118},
expectedImageHeights: []int{336, 118},
expectedImageHasPreview: []bool{true, false},
expectedImageMiniPreview: []bool{true, true},
},
{
title: "Happy invalid image",
names: []string{"testgif.gif"},
blobs: [][]byte{fileBytes(t, "test-search.md")},
skipPayloadValidation: true,
expectedCreatorId: th.BasicUser.Id,
expectedImageMiniPreview: []bool{false},
},
// Simple POST, chunked encoding
{
title: "Happy image chunked post",
skipMultipart: true,
useChunkedInSimplePost: true,
names: []string{"test.png"},
expectImage: true,
expectedImageWidths: []int{408},
expectedImageHeights: []int{336},
expectedImageHasPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
expectedImageMiniPreview: []bool{true},
},
// Image thumbnail and preview: size and orientation. Note that
// the expected image dimensions remain the same regardless of the
// orientation - what we save in FileInfo is used by the
// clients to size UI elements, so the dimensions are "actual".
{
title: "Happy image thumbnail/preview 1",
names: []string{"orientation_test_1.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_1_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_1_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
expectedImageMiniPreview: []bool{true},
},
{
title: "Happy image thumbnail/preview 2",
names: []string{"orientation_test_2.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_2_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_2_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 3",
names: []string{"orientation_test_3.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_3_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_3_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 4",
names: []string{"orientation_test_4.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_4_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_4_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 5",
names: []string{"orientation_test_5.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_5_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_5_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 6",
names: []string{"orientation_test_6.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_6_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_6_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 7",
names: []string{"orientation_test_7.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_7_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_7_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy image thumbnail/preview 8",
names: []string{"orientation_test_8.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_8_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_8_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// 5MB+ JPEG
{
title: "Happy image thumbnail/preview 5MB+",
names: []string{"orientation_test_9.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_9_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_9_expected_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{4000},
expectedImageHeights: []int{2667},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// TIFF preview test
{
title: "Happy image thumbnail/preview 9",
names: []string{"test.tiff"},
expectedImageThumbnailNames: []string{"test_expected_tiff_thumb.jpeg"},
expectedImagePreviewNames: []string{"test_expected_tiff_preview.jpeg"},
expectImage: true,
expectedImageWidths: []int{701},
expectedImageHeights: []int{701},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// Extremely wide image test
{
title: "Happy image thumbnail/preview 10",
names: []string{"10000x1.png"},
expectedImageThumbnailNames: []string{"10000x1_expected_thumb.png"},
expectedImagePreviewNames: []string{"10000x1_expected_preview.png"},
expectImage: true,
expectedImageWidths: []int{10000},
expectedImageHeights: []int{1},
expectedImageHasPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// Extremely high image test
{
title: "Happy image thumbnail/preview 11",
names: []string{"1x10000.png"},
expectedImageThumbnailNames: []string{"1x10000_expected_thumb.png"},
expectedImagePreviewNames: []string{"1x10000_expected_preview.png"},
expectImage: true,
expectedImageWidths: []int{1},
expectedImageHeights: []int{10000},
expectedImageHasPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
},
// animated GIF
{
title: "Happy image thumbnail/preview 12",
names: []string{"testgif.gif"},
expectedImageThumbnailNames: []string{"testgif_expected_thumbnail.jpg"},
expectedImagePreviewNames: []string{"testgif_expected_preview.jpg"},
expectImage: true,
expectedImageWidths: []int{118},
expectedImageHeights: []int{118},
expectedImageHasPreview: []bool{false},
expectedCreatorId: th.BasicUser.Id,
},
{
title: "Happy admin",
client: th.SystemAdminClient,
names: []string{"test.png"},
expectedImageMiniPreview: []bool{true},
expectedCreatorId: th.SystemAdminUser.Id,
},
{
title: "Happy stream",
useChunkedInSimplePost: true,
skipPayloadValidation: true,
names: []string{"1Mb-stream"},
blobs: [][]byte{randomBytes(t, 1024*1024)},
setupConfig: func(a *app.App) func(a *app.App) {
maxFileSize := *a.Config().FileSettings.MaxFileSize
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 1024 * 1024 })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
}
},
expectedImageMiniPreview: []bool{false},
expectedCreatorId: th.BasicUser.Id,
},
// Webp image test
{
title: "Webp image",
names: []string{"testwebp.webp"},
expectImage: true,
expectedImageWidths: []int{128},
expectedImageHeights: []int{256},
expectedImageHasPreview: []bool{true},
expectedCreatorId: th.BasicUser.Id,
expectedImageMiniPreview: []bool{true},
},
// Error cases
{
title: "Error channel_id does not exist",
channelId: model.NewId(),
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckForbiddenStatus,
},
{
// on simple post this uploads the last file
// successfully, without a ClientId
title: "Error too few client_ids",
skipSimplePost: true,
names: []string{"test.png", "testplugin.tar.gz", "test-search.md"},
clientIds: []string{"1", "4"},
expectedImageMiniPreview: []bool{true, false, false},
skipSuccessValidation: true,
checkResponse: CheckBadRequestStatus,
},
{
title: "Error invalid channel_id",
channelId: "../../junk",
names: []string{"test.png"},
expectedImageMiniPreview: []bool{true},
skipSuccessValidation: true,
checkResponse: CheckBadRequestStatus,
},
{
title: "Error admin channel_id does not exist",
client: th.SystemAdminClient,
channelId: model.NewId(),
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckForbiddenStatus,
},
{
title: "Error admin invalid channel_id",
client: th.SystemAdminClient,
channelId: "../../junk",
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckBadRequestStatus,
},
{
title: "Error admin disabled uploads",
client: th.SystemAdminClient,
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckForbiddenStatus,
setupConfig: func(a *app.App) func(a *app.App) {
enableFileAttachments := *a.Config().FileSettings.EnableFileAttachments
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = false })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnableFileAttachments = enableFileAttachments })
}
},
},
{
title: "Error file too large",
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckRequestEntityTooLargeStatus,
setupConfig: func(a *app.App) func(a *app.App) {
maxFileSize := *a.Config().FileSettings.MaxFileSize
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 279590 })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
}
},
},
// File too large (chunked, simple POST only, multipart would've been redundant with above)
{
title: "File too large chunked",
useChunkedInSimplePost: true,
skipMultipart: true,
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckRequestEntityTooLargeStatus,
expectedImageMiniPreview: []bool{false},
setupConfig: func(a *app.App) func(a *app.App) {
maxFileSize := *a.Config().FileSettings.MaxFileSize
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 279590 })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
}
},
},
{
title: "Error stream too large",
skipPayloadValidation: true,
names: []string{"1Mb-stream"},
blobs: [][]byte{randomBytes(t, 1024*1024)},
skipSuccessValidation: true,
checkResponse: CheckRequestEntityTooLargeStatus,
setupConfig: func(a *app.App) func(a *app.App) {
maxFileSize := *a.Config().FileSettings.MaxFileSize
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = 10 * 1024 })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxFileSize = maxFileSize })
}
},
},
{
title: "Error image too large",
names: []string{"test.png"},
skipSuccessValidation: true,
checkResponse: CheckBadRequestStatus,
setupConfig: func(a *app.App) func(a *app.App) {
maxResSize := *a.Config().FileSettings.MaxImageResolution
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxImageResolution = 90000 })
return func(a *app.App) {
a.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.MaxImageResolution = maxResSize })
}
},
},
{
title: "Bookmark images",
names: []string{"orientation_test_5.jpeg"},
expectedImageThumbnailNames: []string{"orientation_test_5_expected_thumb.jpeg"},
expectedImagePreviewNames: []string{"orientation_test_5_expected_preview.jpeg"},
channelId: channel.Id,
expectImage: true,
expectedCreatorId: model.BookmarkFileOwner,
expectedImageWidths: []int{2860},
expectedImageHeights: []int{1578},
expectedImageHasPreview: []bool{true},
expectedImageMiniPreview: []bool{true},
uploadAsBookmark: true,
},
}
for _, useMultipart := range []bool{true, false} {
for _, tc := range tests {
if tc.skipMultipart && useMultipart || tc.skipSimplePost && !useMultipart {
continue
}
title := ""
if useMultipart {
title = "multipart "
} else {
title = "simple "
}
if tc.title != "" {
title += tc.title + " "
}
title += fmt.Sprintf("%v", tc.names)
t.Run(title, func(t *testing.T) {
// Apply any necessary config changes
if tc.setupConfig != nil {
restoreConfig := tc.setupConfig(th.App)
if restoreConfig != nil {
defer restoreConfig(th.App)
}
}
// Set the default values
client := th.Client
if tc.client != nil {
client = tc.client
}
channelId := channel.Id
if tc.channelId != "" {
channelId = tc.channelId
}
blobs := tc.blobs
if len(blobs) == 0 {
for _, name := range tc.names {
blobs = append(blobs, fileBytes(t, name))
}
}
var fileResp *model.FileUploadResponse
var resp *model.Response
var err error
if useMultipart {
fileResp, resp, err = testUploadFilesMultipart(t, client, channelId, tc.names, blobs, tc.clientIds, tc.uploadAsBookmark)
} else {
fileResp, resp, err = testUploadFilesPost(t, client, channelId, tc.names, blobs, tc.clientIds, tc.useChunkedInSimplePost, tc.uploadAsBookmark)
}
if tc.checkResponse != nil {
tc.checkResponse(t, resp)
} else {
require.NoError(t, err)
}
if tc.skipSuccessValidation {
return
}
require.NotNil(t, fileResp, "Nil fileResp")
require.NotEqual(t, 0, len(fileResp.FileInfos), "Empty FileInfos")
require.Equal(t, len(tc.names), len(fileResp.FileInfos), "Mismatched actual or expected FileInfos")
for i, ri := range fileResp.FileInfos {
// The returned file info from the upload call will be missing some fields that will be stored in the database
assert.Equal(t, ri.CreatorId, tc.expectedCreatorId, "File should be assigned to user")
assert.Equal(t, ri.PostId, "", "File shouldn't have a post Id")
assert.Equal(t, ri.Path, "", "File path should not be set on returned info")
assert.Equal(t, ri.ThumbnailPath, "", "File thumbnail path should not be set on returned info")
assert.Equal(t, ri.PreviewPath, "", "File preview path should not be set on returned info")
if len(tc.expectedImageMiniPreview) == len(fileResp.FileInfos) {
assert.Equal(t, ri.MiniPreview != nil, tc.expectedImageMiniPreview[i], "File: %s mini preview state unexpected", tc.names[i])
}
if len(tc.clientIds) > i {
assert.True(t, len(fileResp.ClientIds) == len(tc.clientIds),
fmt.Sprintf("Wrong number of clientIds returned, expected %v, got %v", len(tc.clientIds), len(fileResp.ClientIds)))
assert.Equal(t, fileResp.ClientIds[i], tc.clientIds[i],
fmt.Sprintf("Wrong clientId returned, expected %v, got %v", tc.clientIds[i], fileResp.ClientIds[i]))
}
dbInfo, err := th.App.Srv().Store().FileInfo().Get(ri.Id)
require.NoError(t, err)
assert.Equal(t, dbInfo.Id, ri.Id, "File id from response should match one stored in database")
assert.Equal(t, dbInfo.CreatorId, tc.expectedCreatorId, "F ile should be assigned to user")
assert.Equal(t, dbInfo.PostId, "", "File shouldn't have a post")
assert.NotEqual(t, dbInfo.Path, "", "File path should be set in database")
_, fname := filepath.Split(dbInfo.Path)
ext := filepath.Ext(fname)
name := fname[:len(fname)-len(ext)]
expectedDir := fmt.Sprintf("%v/teams/%v/channels/%v/users/%s/%s", date, FileTeamId, channel.Id, ri.CreatorId, ri.Id)
if tc.uploadAsBookmark {
expectedDir = fmt.Sprintf("%v/teams/%v/channels/%v/%s", model.BookmarkFileOwner, FileTeamId, channel.Id, ri.Id)
}
expectedPath := fmt.Sprintf("%s/%s", expectedDir, fname)
assert.Equal(t, dbInfo.Path, expectedPath,
fmt.Sprintf("File %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.Path, expectedPath))
if tc.expectImage {
// We convert all other image types to jpeg, except pngs.
if ext != ".png" {
ext = ".jpg"
}
expectedThumbnailPath := fmt.Sprintf("%s/%s_thumb%s", expectedDir, name, ext)
expectedPreviewPath := fmt.Sprintf("%s/%s_preview%s", expectedDir, name, ext)
assert.Equal(t, dbInfo.ThumbnailPath, expectedThumbnailPath,
fmt.Sprintf("Thumbnail for %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.ThumbnailPath, expectedThumbnailPath))
assert.Equal(t, dbInfo.PreviewPath, expectedPreviewPath,
fmt.Sprintf("Preview for %v saved to:%q, expected:%q", dbInfo.Name, dbInfo.PreviewPath, expectedPreviewPath))
assert.True(t,
dbInfo.HasPreviewImage == tc.expectedImageHasPreview[i],
fmt.Sprintf("Image: HasPreviewImage should be set for %s", dbInfo.Name))
assert.True(t,
dbInfo.Width == tc.expectedImageWidths[i] && dbInfo.Height == tc.expectedImageHeights[i],
fmt.Sprintf("Image dimensions: expected %dwx%dh, got %vwx%dh",
tc.expectedImageWidths[i], tc.expectedImageHeights[i],
dbInfo.Width, dbInfo.Height))
}
if !tc.skipPayloadValidation {
compare := func(get func(context.Context, string) ([]byte, *model.Response, error), name string) {
var data []byte
data, _, err = get(context.Background(), ri.Id)
require.NoError(t, err)
var expected []byte
expected, err = os.ReadFile(filepath.Join(testDir, name))
require.NoError(t, err)
if !bytes.Equal(data, expected) {
var tf *os.File
tf, err = os.CreateTemp("", fmt.Sprintf("test_%v_*_%s", i, name))
require.NoError(t, err)
defer tf.Close()
_, err = io.Copy(tf, bytes.NewReader(data))
require.NoError(t, err)
t.Errorf("Actual data mismatched %s, written to %q - expected %d bytes, got %d.", name, tf.Name(), len(expected), len(data))
}
}
if len(tc.expectedPayloadNames) == 0 {
tc.expectedPayloadNames = tc.names
}
compare(client.GetFile, tc.expectedPayloadNames[i])
if len(tc.expectedImageThumbnailNames) > i {
compare(client.GetFileThumbnail, tc.expectedImageThumbnailNames[i])
}
if len(tc.expectedImageThumbnailNames) > i {
compare(client.GetFilePreview, tc.expectedImagePreviewNames[i])
}
}
err = th.cleanupTestFile(dbInfo)
require.NoError(t, err)
}
})
}
}
}
func TestGetFile(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
t.Run("base case", func(t *testing.T) {
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
data, _, err := client.GetFile(context.Background(), fileId)
require.NoError(t, err)
require.NotEqual(t, 0, len(data), "should not be empty")
for i := range data {
require.Equal(t, sent[i], data[i], "received file didn't match sent one")
}
_, resp, err := client.GetFile(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFile(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFile(context.Background(), fileId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
})
t.Run("content reviewer should be able to get file of channel and team they are not a member of", func(t *testing.T) {
th.LoginBasic(t)
ok := th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
require.True(t, ok, "failed to set license")
defer func() {
appErr := th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
newChannel := th.CreatePrivateChannel(t)
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
require.NoError(t, err)
post := th.CreatePostWithFilesWithClient(t, client, newChannel, fileResp.FileInfos[0])
reviewer := th.CreateUser(t)
response, err := th.SystemAdminClient.SaveContentFlaggingSettings(context.Background(), &model.ContentFlaggingSettingsRequest{
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
EnableContentFlagging: model.NewPointer(true),
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: model.NewPointer(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{reviewer.Id},
},
},
})
require.NoError(t, err)
CheckOKStatus(t, response)
response, err = client.FlagPostForContentReview(context.Background(), post.Id, &model.FlagContentRequest{
Reason: "Sensitive data",
Comment: "This is sensitive content",
})
require.NoError(t, err)
CheckOKStatus(t, response)
reviewerClient := th.CreateClient()
_, response, err = reviewerClient.Login(context.Background(), reviewer.Email, reviewer.Password)
require.NoError(t, err)
CheckOKStatus(t, response)
_, response, err = reviewerClient.GetFileAsContentReviewer(context.Background(), fileResp.FileInfos[0].Id, post.Id)
require.NoError(t, err)
CheckOKStatus(t, response)
// Try again after removing the user from content reviewers
response, err = th.SystemAdminClient.SaveContentFlaggingSettings(context.Background(), &model.ContentFlaggingSettingsRequest{
ContentFlaggingSettingsBase: model.ContentFlaggingSettingsBase{
EnableContentFlagging: model.NewPointer(true),
},
ReviewerSettings: &model.ReviewSettingsRequest{
ReviewerSettings: model.ReviewerSettings{
CommonReviewers: model.NewPointer(true),
},
ReviewerIDsSettings: model.ReviewerIDsSettings{
CommonReviewerIds: []string{th.BasicUser.Id},
},
},
})
require.NoError(t, err)
CheckOKStatus(t, response)
_, response, err = reviewerClient.GetFileAsContentReviewer(context.Background(), fileResp.FileInfos[0].Id, post.Id)
require.Error(t, err)
CheckForbiddenStatus(t, response)
})
}
func TestGetFileAsSystemAdmin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
t.Run("public channel without membership", func(t *testing.T) {
publicChannel := th.CreateChannelWithClient(t, th.Client, model.ChannelTypeOpen)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, publicChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("public channel with membership", func(t *testing.T) {
publicChannel := th.CreatePublicChannel(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, publicChannel)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, publicChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("private channel without membership", func(t *testing.T) {
privateChannel := th.CreatePrivateChannel(t)
privateFileResp, _, err := th.Client.UploadFile(context.Background(), sent, privateChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), privateFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("private channel with membership", func(t *testing.T) {
privateChannel := th.CreatePrivateChannel(t)
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
th.AddUserToChannel(t, th.SystemAdminUser, privateChannel)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, privateChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("direct message without membership", func(t *testing.T) {
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
dmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, dmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), dmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("direct message with membership", func(t *testing.T) {
dmChannel, _, err := th.SystemAdminClient.CreateDirectChannel(context.Background(), th.SystemAdminUser.Id, th.BasicUser.Id)
require.NoError(t, err)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, dmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("group message without membership", func(t *testing.T) {
user3 := th.CreateUser(t)
gmChannel, _, err := th.Client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
gmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, gmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), gmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("group message with membership", func(t *testing.T) {
user3 := th.CreateUser(t)
gmChannel, _, err := th.SystemAdminClient.CreateGroupChannel(context.Background(), []string{th.SystemAdminUser.Id, th.BasicUser.Id, user3.Id})
require.NoError(t, err)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, gmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFile(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
}
func TestGetFileHeaders(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
CheckStartsWith := func(tb testing.TB, value, prefix, message string) {
tb.Helper()
require.True(tb, strings.HasPrefix(value, prefix), fmt.Sprintf("%s: %s", message, value))
}
testHeaders := func(data []byte, filename string, expectedContentType string, getInline bool, loadFile bool) func(*testing.T) {
return func(t *testing.T) {
if loadFile {
var err error
data, err = testutils.ReadTestFile(filename)
require.NoError(t, err)
}
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, filename)
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
_, resp, err := client.GetFile(context.Background(), fileId)
require.NoError(t, err)
CheckStartsWith(t, resp.Header.Get("Content-Type"), expectedContentType, "returned incorrect Content-Type")
if getInline {
CheckStartsWith(t, resp.Header.Get("Content-Disposition"), "inline", "returned incorrect Content-Disposition")
} else {
CheckStartsWith(t, resp.Header.Get("Content-Disposition"), "attachment", "returned incorrect Content-Disposition")
}
_, resp, err = client.DownloadFile(context.Background(), fileId, true)
require.NoError(t, err)
CheckStartsWith(t, resp.Header.Get("Content-Type"), expectedContentType, "returned incorrect Content-Type")
CheckStartsWith(t, resp.Header.Get("Content-Disposition"), "attachment", "returned incorrect Content-Disposition")
}
}
data := []byte("ABC")
t.Run("png", testHeaders(data, "test.png", "image/png", true, true))
t.Run("gif", testHeaders(data, "testgif.gif", "image/gif", true, true))
t.Run("mp4", testHeaders(data, "test.mp4", "video/mp4", true, false))
t.Run("mp3", testHeaders(data, "test.mp3", "audio/mpeg", true, false))
t.Run("pdf", testHeaders(data, "test.pdf", "application/pdf", false, false))
t.Run("txt", testHeaders(data, "test.txt", "text/plain", false, false))
t.Run("html", testHeaders(data, "test.html", "text/plain", false, false))
t.Run("js", testHeaders(data, "test.js", "text/plain", false, false))
// *.go are categorized differently by different platforms
// t.Run("go", testHeaders(data, "test.go", "text/x-go; charset=utf-8", false, false))
t.Run("zip", testHeaders(data, "test.zip", "application/zip", false, false))
// Not every platform can recognize these
// t.Run("exe", testHeaders(data, "test.exe", "application/x-ms", false))
t.Run("no extension", testHeaders(data, "test", "application/octet-stream", false, false))
t.Run("no extension 2", testHeaders([]byte("<html></html>"), "test", "application/octet-stream", false, false))
}
func TestGetFileThumbnail(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
data, _, err := client.GetFileThumbnail(context.Background(), fileId)
require.NoError(t, err)
require.NotEqual(t, 0, len(data), "should not be empty")
_, resp, err := client.GetFileThumbnail(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFileThumbnail(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFileThumbnail(context.Background(), fileId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
otherUser := th.CreateUser(t)
_, _, err = client.Login(context.Background(), otherUser.Email, otherUser.Password)
require.NoError(t, err)
_, resp, err = client.GetFileThumbnail(context.Background(), fileId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), fileId)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}
func TestGetFileThumbnailAsSystemAdmin(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
t.Run("public channel without membership", func(t *testing.T) {
th.LinkUserToTeam(t, th.BasicUser, th.BasicTeam)
publicChannel := th.CreateChannelWithClient(t, th.Client, model.ChannelTypeOpen)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, publicChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("public channel with membership", func(t *testing.T) {
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
publicChannel := th.CreateChannelWithClient(t, th.Client, model.ChannelTypeOpen)
th.AddUserToChannel(t, th.SystemAdminUser, publicChannel)
fileResp, _, err := th.Client.UploadFile(context.Background(), sent, publicChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), fileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("private channel without membership", func(t *testing.T) {
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
privateChannel := th.CreatePrivateChannel(t)
privateFileResp, _, err := th.Client.UploadFile(context.Background(), sent, privateChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), privateFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("private channel with membership", func(t *testing.T) {
th.LinkUserToTeam(t, th.SystemAdminUser, th.BasicTeam)
privateChannel := th.CreatePrivateChannel(t)
th.AddUserToChannel(t, th.SystemAdminUser, privateChannel)
privateFileResp, _, err := th.Client.UploadFile(context.Background(), sent, privateChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), privateFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("direct message without membership", func(t *testing.T) {
dmChannel := th.CreateDmChannel(t, th.BasicUser2)
dmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, dmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), dmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("direct message with membership", func(t *testing.T) {
dmChannel, _, err := th.SystemAdminClient.CreateDirectChannel(context.Background(), th.SystemAdminUser.Id, th.BasicUser.Id)
require.NoError(t, err)
dmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, dmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), dmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("group message without membership", func(t *testing.T) {
user3 := th.CreateUser(t)
gmChannel, _, err := th.Client.CreateGroupChannel(context.Background(), []string{th.BasicUser.Id, th.BasicUser2.Id, user3.Id})
require.NoError(t, err)
gmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, gmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), gmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
t.Run("group message with membership", func(t *testing.T) {
user3 := th.CreateUser(t)
gmChannel, _, err := th.SystemAdminClient.CreateGroupChannel(context.Background(), []string{th.SystemAdminUser.Id, th.BasicUser.Id, user3.Id})
require.NoError(t, err)
gmFileResp, _, err := th.Client.UploadFile(context.Background(), sent, gmChannel.Id, "test.png")
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileThumbnail(context.Background(), gmFileResp.FileInfos[0].Id)
require.NoError(t, err)
})
}
func TestGetFileLink(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) })
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
_, resp, err := client.GetFileLink(context.Background(), fileId)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
// Hacky way to assign file to a post (usually would be done by CreatePost call)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileId, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = false })
_, resp, err = client.GetFileLink(context.Background(), fileId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true })
link, _, err := client.GetFileLink(context.Background(), fileId)
require.NoError(t, err)
require.NotEqual(t, "", link, "should've received public link")
_, resp, err = client.GetFileLink(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFileLink(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFileLink(context.Background(), fileId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
otherUser := th.CreateUser(t)
_, _, err = client.Login(context.Background(), otherUser.Email, otherUser.Password)
require.NoError(t, err)
_, resp, err = client.GetFileLink(context.Background(), fileId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileLink(context.Background(), fileId)
require.NoError(t, err)
fileInfo, err := th.App.Srv().Store().FileInfo().Get(fileId)
require.NoError(t, err)
err = th.cleanupTestFile(fileInfo)
require.NoError(t, err)
}
func TestGetFilePreview(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
data, _, err := client.GetFilePreview(context.Background(), fileId)
require.NoError(t, err)
require.NotEqual(t, 0, len(data), "should not be empty")
_, resp, err := client.GetFilePreview(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFilePreview(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFilePreview(context.Background(), fileId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
otherUser := th.CreateUser(t)
_, _, err = client.Login(context.Background(), otherUser.Email, otherUser.Password)
require.NoError(t, err)
_, resp, err = client.GetFilePreview(context.Background(), fileId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFilePreview(context.Background(), fileId)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}
func TestGetFileInfo(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
user := th.BasicUser
channel := th.BasicChannel
if *th.App.Config().FileSettings.DriverName == "" {
t.Skip("skipping because no file driver is enabled")
}
sent, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), sent, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
info, _, err := client.GetFileInfo(context.Background(), fileId)
require.NoError(t, err)
require.Equal(t, fileId, info.Id, "got incorrect file")
require.Equal(t, user.Id, info.CreatorId, "file should be assigned to user")
require.Equal(t, "", info.PostId, "file shouldn't have a post")
require.Equal(t, "", info.Path, "file path shouldn't have been returned to client")
require.Equal(t, "", info.ThumbnailPath, "file thumbnail path shouldn't have been returned to client")
require.Equal(t, "", info.PreviewPath, "file preview path shouldn't have been returned to client")
require.Equal(t, "image/png", info.MimeType, "mime type should've been image/png")
_, resp, err := client.GetFileInfo(context.Background(), "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.GetFileInfo(context.Background(), model.NewId())
require.Error(t, err)
CheckNotFoundStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.GetFileInfo(context.Background(), fileId)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
otherUser := th.CreateUser(t)
_, _, err = client.Login(context.Background(), otherUser.Email, otherUser.Password)
require.NoError(t, err)
_, resp, err = client.GetFileInfo(context.Background(), fileId)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, _, err = th.SystemAdminClient.GetFileInfo(context.Background(), fileId)
require.NoError(t, err)
CheckForbiddenStatus(t, resp)
}
func TestGetPublicFile(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
client := th.Client
channel := th.BasicChannel
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) })
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
fileResp, _, err := client.UploadFile(context.Background(), data, channel.Id, "test.png")
require.NoError(t, err)
fileId := fileResp.FileInfos[0].Id
// Hacky way to assign file to a post (usually would be done by CreatePost call)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileId, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
info, err := th.App.Srv().Store().FileInfo().Get(fileId)
require.NoError(t, err)
link := th.App.GeneratePublicLink(client.URL, info)
resp, err := http.Get(link)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "failed to get image with public link")
resp, err = http.Head(link)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode, "failed to respond to HEAD request")
resp, err = http.Get(link[:strings.LastIndex(link, "?")])
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should've failed to get image with public link without hash", resp.Status)
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = false })
resp, err = http.Get(link)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, resp.StatusCode, "should've failed to get image with disabled public link")
// test after the salt has changed
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.EnablePublicLink = true })
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.FileSettings.PublicLinkSalt = model.NewRandomString(32) })
resp, err = http.Get(link)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should've failed to get image with public link after salt changed")
resp, err = http.Get(link)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode, "should've failed to get image with public link after salt changed")
err = th.cleanupTestFile(info)
require.NoError(t, err)
link = th.App.GeneratePublicLink(client.URL, info)
resp, err = http.Get(link)
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, resp.StatusCode, "should've failed to get file after it is deleted")
}
func TestSearchFilesInTeam(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
th.LoginBasic(t)
client := th.Client
filename := "search for fileInfo1"
fileInfo1, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
require.Nil(t, appErr)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileInfo1.Id, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
filename = "search for fileInfo2"
fileInfo2, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
require.Nil(t, appErr)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileInfo2.Id, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
filename = "tagged search for fileInfo3"
fileInfo3, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
require.Nil(t, appErr)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileInfo3.Id, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
filename = "tagged for fileInfo4"
fileInfo4, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
require.Nil(t, appErr)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileInfo4.Id, th.BasicPost.Id, th.BasicPost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
archivedChannel := th.CreatePublicChannel(t)
fileInfo5, appErr := th.App.UploadFile(th.Context, data, archivedChannel.Id, "tagged for fileInfo3")
require.Nil(t, appErr)
post := &model.Post{ChannelId: archivedChannel.Id, Message: model.NewId() + "a"}
rpost, _, err := client.CreatePost(context.Background(), post)
require.NoError(t, err)
err = th.App.Srv().Store().FileInfo().AttachToPost(th.Context, fileInfo5.Id, rpost.Id, rpost.ChannelId, th.BasicUser.Id)
require.NoError(t, err)
_, err = th.Client.DeleteChannel(context.Background(), archivedChannel.Id)
require.NoError(t, err)
terms := "search"
isOrSearch := false
timezoneOffset := 5
searchParams := model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
}
fileInfos, _, err := client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 3, "wrong search")
terms = "search"
page := 0
perPage := 2
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
Page: &page,
PerPage: &perPage,
}
fileInfos2, _, err := client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
// We don't support paging for DB search yet, modify this when we do.
require.Len(t, fileInfos2.Order, 3, "Wrong number of fileInfos")
assert.Equal(t, fileInfos.Order[0], fileInfos2.Order[0])
assert.Equal(t, fileInfos.Order[1], fileInfos2.Order[1])
page = 1
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
Page: &page,
PerPage: &perPage,
}
fileInfos2, _, err = client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
// We don't support paging for DB search yet, modify this when we do.
require.Empty(t, fileInfos2.Order, "Wrong number of fileInfos")
fileInfos, _, err = client.SearchFiles(context.Background(), th.BasicTeam.Id, "search", false)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 3, "wrong search")
fileInfos, _, err = client.SearchFiles(context.Background(), th.BasicTeam.Id, "fileInfo2", false)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 1, "wrong number of fileInfos")
require.Equal(t, fileInfo2.Id, fileInfos.Order[0], "wrong search")
terms = "tagged"
includeDeletedChannels := true
searchParams = model.SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
TimeZoneOffset: &timezoneOffset,
IncludeDeletedChannels: &includeDeletedChannels,
}
fileInfos, _, err = client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 3, "wrong search")
// Archived channels are always included now, so this should return the same result
fileInfos, _, err = client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 3, "wrong search")
fileInfos, _, _ = client.SearchFiles(context.Background(), th.BasicTeam.Id, "*", false)
require.Empty(t, fileInfos.Order, "searching for just * shouldn't return any results")
fileInfos, _, err = client.SearchFiles(context.Background(), th.BasicTeam.Id, "fileInfo1 fileInfo2", true)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 2, "wrong search results")
_, resp, err := client.SearchFiles(context.Background(), "junk", "#sgtitlereview", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.SearchFiles(context.Background(), model.NewId(), "#sgtitlereview", false)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.SearchFiles(context.Background(), th.BasicTeam.Id, "", false)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, err = client.Logout(context.Background())
require.NoError(t, err)
_, resp, err = client.SearchFiles(context.Background(), th.BasicTeam.Id, "#sgtitlereview", false)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}
func TestSearchFilesAcrossTeams(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
data, err := testutils.ReadTestFile("test.png")
require.NoError(t, err)
th.LoginBasic(t)
client := th.Client
var teams [2]*model.Team
var channels [2]*model.Channel
for i := range 2 {
teams[i] = th.CreateTeam(t)
channels[i] = th.CreateChannelWithClientAndTeam(t, th.Client, model.ChannelTypeOpen, teams[i].Id)
th.LinkUserToTeam(t, th.BasicUser, teams[i])
th.AddUserToChannel(t, th.BasicUser, channels[i])
filename := "search for fileInfo"
fileInfo, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, filename)
require.Nil(t, appErr)
th.CreatePostInChannelWithFiles(t, channels[i], fileInfo)
}
terms := "search"
// BasicUser should have access to all the files
fileInfos, _, err := client.SearchFilesAcrossTeams(context.Background(), terms, false)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 2, "wrong search")
// a new user that only belongs to the first team should only get one result
newUser := th.CreateUser(t)
th.LinkUserToTeam(t, newUser, teams[0])
th.AddUserToChannel(t, newUser, channels[0])
th.UnlinkUserFromTeam(t, th.BasicUser, teams[1])
_, err = th.Client.Logout(context.Background())
require.NoError(t, err)
_, _, err = th.Client.Login(context.Background(), newUser.Email, newUser.Password)
require.NoError(t, err)
fileInfos, _, err = client.SearchFilesAcrossTeams(context.Background(), terms, false)
require.NoError(t, err)
require.Len(t, fileInfos.Order, 1, "wrong search")
require.Equal(t, fileInfos.FileInfos[fileInfos.Order[0]].ChannelId, channels[0].Id, "wrong search")
}