Files
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

1260 lines
42 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"image"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
storemocks "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks"
"github.com/mattermost/mattermost/server/v8/channels/utils/fileutils"
eMocks "github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine/mocks"
)
func TestGeneratePublicLinkHash(t *testing.T) {
mainHelper.Parallel(t)
filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
salt1 := model.NewRandomString(32)
salt2 := model.NewRandomString(32)
hash1 := GeneratePublicLinkHash(filename1, salt1)
hash2 := GeneratePublicLinkHash(filename2, salt1)
hash3 := GeneratePublicLinkHash(filename1, salt2)
hash := GeneratePublicLinkHash(filename1, salt1)
assert.Equal(t, hash, hash1, "hash should be equal for the same file name and salt")
assert.NotEqual(t, hash1, hash2, "hashes for different files should not be equal")
assert.NotEqual(t, hash1, hash3, "hashes for the same file with different salts should not be equal")
}
func TestDoUploadFile(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
teamID := model.NewId()
channelID := model.NewId()
userID := model.NewId()
filename := "test"
data := []byte("abcd")
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info1.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info1.Path)
require.Nil(t, appErr)
}()
value := fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info1.Id, filename)
assert.Equal(t, value, info1.Path, "stored file at incorrect path")
info2, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info2.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info2.Path)
require.Nil(t, appErr)
}()
value = fmt.Sprintf("20070204/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info2.Id, filename)
assert.Equal(t, value, info2.Path, "stored file at incorrect path")
info3, err := th.App.DoUploadFile(th.Context, time.Date(2008, 3, 5, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info3.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info3.Path)
require.Nil(t, appErr)
}()
value = fmt.Sprintf("20080305/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info3.Id, filename)
assert.Equal(t, value, info3.Path, "stored file at incorrect path")
info4, err := th.App.DoUploadFile(th.Context, time.Date(2009, 3, 5, 1, 2, 3, 4, time.Local), "../../"+teamID, "../../"+channelID, "../../"+userID, "../../"+filename, data, true)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info4.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info4.Path)
require.Nil(t, appErr)
}()
value = fmt.Sprintf("20090305/teams/%v/channels/%v/users/%v/%v/%v", teamID, channelID, userID, info4.Id, filename)
assert.Equal(t, value, info4.Path, "stored file at incorrect path")
info5, err := th.App.DoUploadFile(th.Context, time.Date(2008, 3, 5, 1, 2, 3, 4, time.Local), teamID, channelID, model.BookmarkFileOwner, filename, data, true)
require.Nil(t, err, "DoUploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info5.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info5.Path)
require.Nil(t, appErr)
}()
value = fmt.Sprintf("%v/teams/%v/channels/%v/%v/%v", model.BookmarkFileOwner, teamID, channelID, info5.Id, filename)
assert.Equal(t, value, info5.Path, "stored file at incorrect path")
}
func TestUploadFile(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
channelID := th.BasicChannel.Id
filename := "test"
data := []byte("abcd")
info1, err := th.App.UploadFile(th.Context, data, "wrong", filename)
require.NotNil(t, err, "Wrong Channel ID.")
require.Nil(t, info1, "Channel ID does not exist.")
info1, err = th.App.UploadFile(th.Context, data, "", filename)
require.Nil(t, err, "empty channel IDs should be valid")
require.NotNil(t, info1)
info1, err = th.App.UploadFile(th.Context, data, channelID, filename)
require.Nil(t, err, "UploadFile should succeed with valid data")
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info1.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info1.Path)
require.Nil(t, appErr)
}()
value := fmt.Sprintf("%v/teams/noteam/channels/%v/users/nouser/%v/%v",
time.Now().Format("20060102"), channelID, info1.Id, filename)
assert.Equal(t, value, info1.Path, "Stored file at incorrect path")
}
func TestParseOldFilenames(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
fileID := model.NewId()
tests := []struct {
description string
filenames []string
channelID string
userID string
expected [][]string
}{
{
description: "Empty input should result in empty output",
filenames: []string{},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{},
},
{
description: "Filename with invalid format should not parse",
filenames: []string{"/path/to/some/file.png"},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{},
},
{
description: "ChannelId in Filename should not match",
filenames: []string{
fmt.Sprintf("/%v/%v/%v/file.png", model.NewId(), th.BasicUser.Id, fileID),
},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{},
},
{
description: "UserId in Filename should not match",
filenames: []string{
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, model.NewId(), fileID),
},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{},
},
{
description: "../ in filename should not parse",
filenames: []string{
fmt.Sprintf("/%v/%v/%v/../../../file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{},
},
{
description: "Should only parse valid filenames",
filenames: []string{
fmt.Sprintf("/%v/%v/%v/../otherfile.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{
{
th.BasicChannel.Id,
th.BasicUser.Id,
fileID,
"file.png",
},
},
},
{
description: "Valid Filename should parse",
filenames: []string{
fmt.Sprintf("/%v/%v/%v/file.png", th.BasicChannel.Id, th.BasicUser.Id, fileID),
},
channelID: th.BasicChannel.Id,
userID: th.BasicUser.Id,
expected: [][]string{
{
th.BasicChannel.Id,
th.BasicUser.Id,
fileID,
"file.png",
},
},
},
}
for _, test := range tests {
t.Run(test.description, func(tt *testing.T) {
result := parseOldFilenames(th.Context, test.filenames, test.channelID, test.userID)
require.Equal(tt, result, test.expected)
})
}
}
func TestGetInfoForFilename(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
post := th.BasicPost
teamID := th.BasicTeam.Id
info := th.App.getInfoForFilename(th.Context, post, teamID, post.ChannelId, post.UserId, "someid", "somefile.png")
assert.Nil(t, info, "Test non-existent file")
}
func TestFindTeamIdForFilename(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
teamID := th.App.findTeamIdForFilename(th.Context, th.BasicPost, "someid", "somefile.png")
assert.Equal(t, th.BasicTeam.Id, teamID)
_, err := th.App.CreateTeamWithUser(th.Context, &model.Team{Email: th.BasicUser.Email, Name: "zz" + model.NewId(), DisplayName: "Joram's Test Team", Type: model.TeamOpen}, th.BasicUser.Id)
require.Nil(t, err)
teamID = th.App.findTeamIdForFilename(th.Context, th.BasicPost, "someid", "somefile.png")
assert.Equal(t, "", teamID)
}
func TestMigrateFilenamesToFileInfos(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
post := th.BasicPost
infos := th.App.MigrateFilenamesToFileInfos(th.Context, post)
assert.Equal(t, 0, len(infos))
post.Filenames = []string{fmt.Sprintf("/%v/%v/%v/blargh.png", th.BasicChannel.Id, th.BasicUser.Id, "someid")}
infos = th.App.MigrateFilenamesToFileInfos(th.Context, post)
assert.Equal(t, 0, len(infos))
path, _ := fileutils.FindDir("tests")
file, fileErr := os.Open(filepath.Join(path, "test.png"))
require.NoError(t, fileErr)
defer file.Close()
fileID := model.NewId()
fpath := fmt.Sprintf("/teams/%v/channels/%v/users/%v/%v/test.png", th.BasicTeam.Id, th.BasicChannel.Id, th.BasicUser.Id, fileID)
_, err := th.App.WriteFile(file, fpath)
require.Nil(t, err)
rpost, _, err := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Filenames: []string{fmt.Sprintf("/%v/%v/%v/test.png", th.BasicChannel.Id, th.BasicUser.Id, fileID)}}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
infos = th.App.MigrateFilenamesToFileInfos(th.Context, rpost)
assert.Equal(t, 1, len(infos))
rpost, _, err = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, ChannelId: th.BasicChannel.Id, Filenames: []string{fmt.Sprintf("/%v/%v/%v/../../test.png", th.BasicChannel.Id, th.BasicUser.Id, fileID)}}, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
require.Nil(t, err)
infos = th.App.MigrateFilenamesToFileInfos(th.Context, rpost)
assert.Equal(t, 0, len(infos))
}
func TestWriteZipFile(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
t.Run("write no file", func(t *testing.T) {
buf := new(bytes.Buffer)
err := th.App.WriteZipFile(buf, []model.FileData{})
require.NoError(t, err)
// Verify it's a valid zip file
reader := bytes.NewReader(buf.Bytes())
z, err := zip.NewReader(reader, int64(buf.Len()))
require.NoError(t, err)
require.Len(t, z.File, 0)
})
t.Run("write one file", func(t *testing.T) {
buf := new(bytes.Buffer)
err := th.App.WriteZipFile(buf, []model.FileData{
{
Filename: "file1.txt",
Body: []byte("content1"),
},
})
require.NoError(t, err)
// Verify the zip contents
reader := bytes.NewReader(buf.Bytes())
z, err := zip.NewReader(reader, int64(buf.Len()))
require.NoError(t, err)
require.Len(t, z.File, 1)
file := z.File[0]
assert.Equal(t, "file1.txt", file.Name)
now := time.Now().Truncate(time.Second) // Files are stored with a second precision
// Confirm that the file was created in the last 10 seconds
assert.GreaterOrEqual(t, file.Modified, now.Add(-10*time.Second))
assert.GreaterOrEqual(t, now, file.Modified)
// Check file content
fr, err := file.Open()
require.NoError(t, err)
t.Cleanup(func() {
err = fr.Close()
require.NoError(t, err)
})
content, err := io.ReadAll(fr)
require.NoError(t, err)
assert.Equal(t, []byte("content1"), content)
})
t.Run("write multiple files", func(t *testing.T) {
buf := new(bytes.Buffer)
fileDatas := []model.FileData{
{
Filename: "file1.txt",
Body: []byte("content1"),
},
{
Filename: "file2.txt",
Body: []byte("content2"),
},
{
Filename: "dir/file3.txt",
Body: []byte("content3"),
},
}
err := th.App.WriteZipFile(buf, fileDatas)
require.NoError(t, err)
// Verify the zip contents
reader := bytes.NewReader(buf.Bytes())
z, err := zip.NewReader(reader, int64(buf.Len()))
require.NoError(t, err)
require.Len(t, z.File, 3)
// Check each file
for i, zf := range z.File {
assert.Equal(t, fileDatas[i].Filename, zf.Name)
fr, err := zf.Open()
require.NoError(t, err)
content, err := io.ReadAll(fr)
require.NoError(t, err)
assert.Equal(t, fileDatas[i].Body, content)
err = fr.Close()
require.NoError(t, err)
}
})
}
func TestCopyFileInfos(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t)
teamID := model.NewId()
channelID := model.NewId()
userID := model.NewId()
filename := "test"
data := []byte("abcd")
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
require.Nil(t, err)
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info1.Id)
require.NoError(t, err)
}()
infoIds, err := th.App.CopyFileInfos(th.Context, userID, []string{info1.Id})
require.Nil(t, err)
info2, err := th.App.GetFileInfo(th.Context, infoIds[0])
require.Nil(t, err)
defer func() {
err := th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info2.Id)
require.NoError(t, err)
appErr := th.App.RemoveFile(info2.Path)
require.Nil(t, appErr)
}()
assert.NotEqual(t, info1.Id, info2.Id, "should not be equal")
assert.Equal(t, info2.PostId, "", "should be empty string")
}
func TestGenerateThumbnailImage(t *testing.T) {
mainHelper.Parallel(t)
t.Run("test generating thumbnail image", func(t *testing.T) {
// given
th := Setup(t)
img := createDummyImage()
dataPath := *th.App.Config().FileSettings.Directory
thumbnailName := "thumb.jpg"
thumbnailPath := filepath.Join(dataPath, thumbnailName)
// when
th.App.generateThumbnailImage(th.Context, img, "jpg", thumbnailName)
defer os.Remove(thumbnailPath)
// then
outputImage, err := os.Stat(thumbnailPath)
assert.NoError(t, err)
assert.Equal(t, int64(721), outputImage.Size())
})
}
func createDummyImage() *image.RGBA {
width := 200
height := 100
upperLeftCorner := image.Point{0, 0}
lowerRightCorner := image.Point{width, height}
return image.NewRGBA(image.Rectangle{upperLeftCorner, lowerRightCorner})
}
func TestSearchFilesInTeamForUser(t *testing.T) {
mainHelper.Parallel(t)
perPage := 5
searchTerm := "searchTerm"
setup := func(t *testing.T, enableElasticsearch bool) (*TestHelper, []*model.FileInfo) {
th := Setup(t).InitBasic(t)
fileInfos := make([]*model.FileInfo, 7)
for i := range cap(fileInfos) {
fileInfo, err := th.App.Srv().Store().FileInfo().Save(th.Context,
&model.FileInfo{
CreatorId: th.BasicUser.Id,
PostId: th.BasicPost.Id,
ChannelId: th.BasicPost.ChannelId,
Name: searchTerm,
Path: searchTerm,
Extension: "jpg",
MimeType: "image/jpeg",
})
time.Sleep(1 * time.Millisecond)
require.NoError(t, err)
fileInfos[i] = fileInfo
}
if enableElasticsearch {
th.App.Srv().SetLicense(model.NewTestLicense("elastic_search"))
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ElasticsearchSettings.EnableIndexing = true
*cfg.ElasticsearchSettings.EnableSearching = true
})
} else {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ElasticsearchSettings.EnableSearching = false
})
}
return th, fileInfos
}
t.Run("should return everything as first page of fileInfos from database", func(t *testing.T) {
th, fileInfos := setup(t, false)
page := 0
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
require.NotNil(t, results)
assert.Equal(t, []string{
fileInfos[6].Id,
fileInfos[5].Id,
fileInfos[4].Id,
fileInfos[3].Id,
fileInfos[2].Id,
fileInfos[1].Id,
fileInfos[0].Id,
}, results.Order)
require.True(t, allFilesHaveMembership)
})
t.Run("should not return later pages of fileInfos from database", func(t *testing.T) {
th, _ := setup(t, false)
page := 1
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
require.NotNil(t, results)
assert.Equal(t, []string{}, results.Order)
require.True(t, allFilesHaveMembership)
})
t.Run("should return first page of fileInfos from ElasticSearch", func(t *testing.T) {
th, fileInfos := setup(t, true)
page := 0
resultsPage := []string{
fileInfos[6].Id,
fileInfos[5].Id,
fileInfos[4].Id,
fileInfos[3].Id,
fileInfos[2].Id,
}
es := &mocks.SearchEngineInterface{}
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil)
es.On("Start").Return(nil).Maybe()
es.On("IsActive").Return(true)
es.On("IsHealthy").Return(true)
es.On("IsSearchEnabled").Return(true)
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
defer func() {
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
}()
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
require.NotNil(t, results)
assert.Equal(t, resultsPage, results.Order)
require.True(t, allFilesHaveMembership)
es.AssertExpectations(t)
})
t.Run("should return later pages of fileInfos from ElasticSearch", func(t *testing.T) {
th, fileInfos := setup(t, true)
page := 1
resultsPage := []string{
fileInfos[1].Id,
fileInfos[0].Id,
}
es := &mocks.SearchEngineInterface{}
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(resultsPage, nil)
es.On("Start").Return(nil).Maybe()
es.On("IsActive").Return(true)
es.On("IsHealthy").Return(true)
es.On("IsSearchEnabled").Return(true)
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
defer func() {
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
}()
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
require.NotNil(t, results)
assert.Equal(t, resultsPage, results.Order)
require.True(t, allFilesHaveMembership)
es.AssertExpectations(t)
})
t.Run("should fall back to database if ElasticSearch fails on first page", func(t *testing.T) {
th, fileInfos := setup(t, true)
page := 0
es := &mocks.SearchEngineInterface{}
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(nil, &model.AppError{})
es.On("GetName").Return("mock")
es.On("Start").Return(nil).Maybe()
es.On("IsActive").Return(true)
es.On("IsHealthy").Return(true)
es.On("IsSearchEnabled").Return(true)
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
defer func() {
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
}()
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
require.NotNil(t, results)
assert.Equal(t, []string{
fileInfos[6].Id,
fileInfos[5].Id,
fileInfos[4].Id,
fileInfos[3].Id,
fileInfos[2].Id,
fileInfos[1].Id,
fileInfos[0].Id,
}, results.Order)
require.True(t, allFilesHaveMembership)
es.AssertExpectations(t)
})
t.Run("should return nothing if ElasticSearch fails on later pages", func(t *testing.T) {
th, _ := setup(t, true)
page := 1
es := &mocks.SearchEngineInterface{}
es.On("SearchFiles", mock.Anything, mock.Anything, page, perPage).Return(nil, &model.AppError{})
es.On("GetName").Return("mock")
es.On("Start").Return(nil).Maybe()
es.On("IsActive").Return(true)
es.On("IsHealthy").Return(true)
es.On("IsSearchEnabled").Return(true)
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = es
defer func() {
th.App.Srv().Platform().SearchEngine.ElasticsearchEngine = nil
}()
results, allFilesHaveMembership, err := th.App.SearchFilesInTeamForUser(th.Context, searchTerm, th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, page, perPage)
require.Nil(t, err)
assert.Equal(t, []string{}, results.Order)
require.True(t, allFilesHaveMembership)
es.AssertExpectations(t)
})
}
func TestExtractContentFromFileInfo(t *testing.T) {
mainHelper.Parallel(t)
app := &App{}
fi := &model.FileInfo{
MimeType: "image/jpeg",
}
// Test that we don't process images.
require.NoError(t, app.ExtractContentFromFileInfo(request.TestContext(t), fi))
}
func TestGetLastAccessibleFileTime(t *testing.T) {
mainHelper.Parallel(t)
th := SetupWithStoreMock(t)
r, err := th.App.GetLastAccessibleFileTime()
require.Nil(t, err)
assert.Equal(t, int64(0), r)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockSystemStore := storemocks.SystemStore{}
mockStore.On("System").Return(&mockSystemStore)
mockSystemStore.On("GetByName", mock.Anything).Return(nil, store.NewErrNotFound("", ""))
r, err = th.App.GetLastAccessibleFileTime()
require.Nil(t, err)
assert.Equal(t, int64(0), r)
mockSystemStore = storemocks.SystemStore{}
mockStore.On("System").Return(&mockSystemStore)
mockSystemStore.On("GetByName", mock.Anything).Return(nil, errors.New("test"))
_, err = th.App.GetLastAccessibleFileTime()
require.NotNil(t, err)
mockSystemStore = storemocks.SystemStore{}
mockStore.On("System").Return(&mockSystemStore)
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessibleFileTime, Value: "10"}, nil)
r, err = th.App.GetLastAccessibleFileTime()
require.Nil(t, err)
assert.Equal(t, int64(10), r)
}
func TestComputeLastAccessibleFileTime(t *testing.T) {
mainHelper.Parallel(t)
t.Run("Updates the time, if cloud limit is applicable", func(t *testing.T) {
th := SetupWithStoreMock(t)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &eMocks.CloudInterface{}
th.App.Srv().Cloud = cloud
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(&model.ProductLimits{
Files: &model.FilesLimits{
TotalStorage: new(int64(1)),
},
}, nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockFileStore := storemocks.FileInfoStore{}
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
mockSystemStore := storemocks.SystemStore{}
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
mockStore.On("FileInfo").Return(&mockFileStore)
mockStore.On("System").Return(&mockSystemStore)
err := th.App.ComputeLastAccessibleFileTime()
require.NoError(t, err)
mockSystemStore.AssertCalled(t, "SaveOrUpdate", mock.Anything)
})
t.Run("Removes the time, if cloud limit is not applicable", func(t *testing.T) {
th := SetupWithStoreMock(t)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := &eMocks.CloudInterface{}
th.App.Srv().Cloud = cloud
cloud.Mock.On("GetCloudLimits", mock.Anything).Return(nil, nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockFileStore := storemocks.FileInfoStore{}
mockFileStore.On("GetUptoNSizeFileTime", mock.Anything).Return(int64(1), nil)
mockSystemStore := storemocks.SystemStore{}
mockSystemStore.On("GetByName", mock.Anything).Return(&model.System{Name: model.SystemLastAccessibleFileTime, Value: "10"}, nil)
mockSystemStore.On("PermanentDeleteByName", mock.Anything).Return(nil, nil)
mockSystemStore.On("SaveOrUpdate", mock.Anything).Return(nil)
mockStore.On("FileInfo").Return(&mockFileStore)
mockStore.On("System").Return(&mockSystemStore)
err := th.App.ComputeLastAccessibleFileTime()
require.NoError(t, err)
mockSystemStore.AssertNotCalled(t, "SaveOrUpdate", mock.Anything)
mockSystemStore.AssertCalled(t, "PermanentDeleteByName", mock.Anything)
})
}
func TestSetFileSearchableContent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
fileInfo, err := th.App.Srv().Store().FileInfo().Save(th.Context,
&model.FileInfo{
CreatorId: th.BasicUser.Id,
PostId: th.BasicPost.Id,
ChannelId: th.BasicPost.ChannelId,
Name: "test",
Path: "test",
Extension: "jpg",
MimeType: "image/jpeg",
})
require.NoError(t, err)
result, allFilesHaveMembership, appErr := th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60)
require.Nil(t, appErr)
assert.Equal(t, 0, len(result.Order))
require.True(t, allFilesHaveMembership)
appErr = th.App.SetFileSearchableContent(th.Context, fileInfo.Id, "searchable")
require.Nil(t, appErr)
result, allFilesHaveMembership, appErr = th.App.SearchFilesInTeamForUser(th.Context, "searchable", th.BasicUser.Id, th.BasicTeam.Id, false, false, 0, 0, 60)
require.Nil(t, appErr)
assert.Equal(t, 1, len(result.Order))
require.True(t, allFilesHaveMembership)
}
func TestPermanentDeleteFilesByPost(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
t.Run("should delete files for post", func(t *testing.T) {
// Create a post with a file attachment.
teamID := th.BasicTeam.Id
channelID := th.BasicChannel.Id
userID := th.BasicUser.Id
filename := "test"
data := []byte("abcd")
info1, err := th.App.DoUploadFile(th.Context, time.Date(2007, 2, 4, 1, 2, 3, 4, time.Local), teamID, channelID, userID, filename, data, true)
require.Nil(t, err)
post := &model.Post{
Message: "asd",
ChannelId: channelID,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: userID,
CreateAt: 0,
FileIds: []string{info1.Id},
}
post, _, err = th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
assert.Nil(t, err)
err = th.App.PermanentDeleteFilesByPost(th.Context, post.Id, nil)
require.Nil(t, err)
_, err = th.App.GetFileInfo(th.Context, info1.Id)
require.NotNil(t, err)
})
t.Run("should not delete files for post that doesn't exist", func(t *testing.T) {
err := th.App.PermanentDeleteFilesByPost(th.Context, "postId1", nil)
assert.Nil(t, err)
})
t.Run("should handle empty file list", func(t *testing.T) {
post := &model.Post{
Message: "asd",
ChannelId: th.BasicChannel.Id,
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
UserId: th.BasicUser.Id,
CreateAt: 0,
}
post, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
assert.Nil(t, err)
err = th.App.PermanentDeleteFilesByPost(th.Context, post.Id, nil)
assert.Nil(t, err)
})
t.Run("should mark both report steps as failed on GetForPost store error", func(t *testing.T) {
mockTh := SetupWithStoreMock(t)
mockStore := mockTh.App.Srv().Store().(*storemocks.Store)
mockFileStore := storemocks.FileInfoStore{}
mockFileStore.On("GetForPost", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("db connection lost"))
mockStore.On("FileInfo").Return(&mockFileStore)
report := &model.PostDeletionReport{
PostID: "test-post-id",
Timestamp: time.Now(),
}
appErr := mockTh.App.PermanentDeleteFilesByPost(request.TestContext(t), "test-post-id", report)
require.NotNil(t, appErr)
// Both file_attachments and fileinfo_rows steps should be marked as failed
require.Len(t, report.Steps, 2)
require.Equal(t, model.StepFailed, report.Steps[0].Status)
require.Equal(t, model.StepFailed, report.Steps[1].Status)
require.Contains(t, report.Steps[0].Errors[0], "db connection lost")
require.Contains(t, report.Steps[1].Errors[0], "db connection lost")
})
t.Run("should mark fileinfo_rows as failed when PermanentDeleteForPost fails", func(t *testing.T) {
mockTh := SetupWithStoreMock(t)
postID := model.NewId()
mockStore := mockTh.App.Srv().Store().(*storemocks.Store)
mockFileStore := storemocks.FileInfoStore{}
// Return file infos with non-existent paths so file store removal
// returns NotFound (which is skipped), resulting in no errors.
mockFileStore.On("GetForPost", postID, false, true, true).Return([]*model.FileInfo{
{Id: model.NewId(), Name: "file1.txt", Path: "/nonexistent/file1.txt"},
{Id: model.NewId(), Name: "file2.txt", Path: "/nonexistent/file2.txt"},
}, nil)
mockFileStore.On("PermanentDeleteForPost", mock.Anything, postID).Return(errors.New("foreign key constraint"))
mockStore.On("FileInfo").Return(&mockFileStore)
report := &model.PostDeletionReport{
PostID: postID,
Timestamp: time.Now(),
}
appErr := mockTh.App.PermanentDeleteFilesByPost(request.TestContext(t), postID, report)
require.NotNil(t, appErr)
// file_attachments step should succeed (NotFound files are skipped),
// but fileinfo_rows step should fail due to the DB error.
require.Len(t, report.Steps, 2)
require.Equal(t, model.StepSuccess, report.Steps[0].Status, "file_attachments step should succeed")
require.Equal(t, model.StepFailed, report.Steps[1].Status, "fileinfo_rows step should fail")
require.Contains(t, report.Steps[1].Errors[0], "foreign key constraint")
})
}
func TestFilterFilesByChannelPermissions(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.GuestAccountsSettings.Enable = true
})
guestUser := th.CreateGuest(t)
_, _, appErr := th.App.AddUserToTeam(th.Context, th.BasicTeam.Id, guestUser.Id, "")
require.Nil(t, appErr)
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
_, appErr = th.App.AddUserToChannel(th.Context, guestUser, privateChannel, false)
require.Nil(t, appErr)
_, appErr = th.App.AddUserToChannel(th.Context, guestUser, th.BasicChannel, false)
require.Nil(t, appErr)
post1 := th.CreatePost(t, th.BasicChannel)
post2 := th.CreatePost(t, privateChannel)
post3 := th.CreatePost(t, th.BasicChannel)
fileInfo1 := th.CreateFileInfo(t, th.BasicUser.Id, post1.Id, th.BasicChannel.Id)
fileInfo2 := th.CreateFileInfo(t, th.BasicUser.Id, post2.Id, privateChannel.Id)
fileInfo3 := th.CreateFileInfo(t, th.BasicUser.Id, post3.Id, th.BasicChannel.Id)
t.Run("should filter files when user has read_channel_content permission", func(t *testing.T) {
fileList := model.NewFileInfoList()
fileList.FileInfos[fileInfo1.Id] = fileInfo1
fileList.FileInfos[fileInfo2.Id] = fileInfo2
fileList.FileInfos[fileInfo3.Id] = fileInfo3
fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
// BasicUser should have access to all files
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 3)
require.Len(t, fileList.Order, 3)
require.True(t, allFilesHaveMembership)
})
t.Run("should filter files when guest has read_channel_content permission", func(t *testing.T) {
fileList := model.NewFileInfoList()
fileList.FileInfos[fileInfo1.Id] = fileInfo1
fileList.FileInfos[fileInfo2.Id] = fileInfo2
fileList.FileInfos[fileInfo3.Id] = fileInfo3
fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 3)
require.Len(t, fileList.Order, 3)
require.True(t, allFilesHaveMembership)
})
t.Run("should filter files when guest does not have read_channel_content permission", func(t *testing.T) {
channelGuestRole, appErr := th.App.GetRoleByName(th.Context, model.ChannelGuestRoleId)
require.Nil(t, appErr)
originalPermissions := make([]string, len(channelGuestRole.Permissions))
copy(originalPermissions, channelGuestRole.Permissions)
newPermissions := []string{}
for _, perm := range channelGuestRole.Permissions {
if perm != model.PermissionReadChannelContent.Id && perm != model.PermissionReadChannel.Id {
newPermissions = append(newPermissions, perm)
}
}
_, appErr = th.App.PatchRole(channelGuestRole, &model.RolePatch{
Permissions: &newPermissions,
})
require.Nil(t, appErr)
defer func() {
_, err := th.App.PatchRole(channelGuestRole, &model.RolePatch{
Permissions: &originalPermissions,
})
require.Nil(t, err)
}()
fileList := model.NewFileInfoList()
fileList.FileInfos[fileInfo1.Id] = fileInfo1
fileList.FileInfos[fileInfo2.Id] = fileInfo2
fileList.FileInfos[fileInfo3.Id] = fileInfo3
fileList.Order = []string{fileInfo1.Id, fileInfo2.Id, fileInfo3.Id}
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, guestUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 0)
require.Len(t, fileList.Order, 0)
require.True(t, allFilesHaveMembership)
})
t.Run("should handle empty file list", func(t *testing.T) {
fileList := model.NewFileInfoList()
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 0)
require.Len(t, fileList.Order, 0)
require.True(t, allFilesHaveMembership)
})
t.Run("should handle nil file list", func(t *testing.T) {
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, nil, th.BasicUser.Id)
require.True(t, allFilesHaveMembership)
require.Nil(t, appErr)
})
t.Run("should handle files with empty channel IDs", func(t *testing.T) {
fileList := model.NewFileInfoList()
fileWithoutChannel := &model.FileInfo{
Id: model.NewId(),
ChannelId: "",
Name: "test.txt",
}
fileList.FileInfos[fileWithoutChannel.Id] = fileWithoutChannel
fileList.Order = []string{fileWithoutChannel.Id}
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 0)
require.Len(t, fileList.Order, 0)
require.True(t, allFilesHaveMembership)
})
t.Run("should handle files from non-existent channels", func(t *testing.T) {
fileList := model.NewFileInfoList()
fileWithInvalidChannel := &model.FileInfo{
Id: model.NewId(),
ChannelId: model.NewId(),
Name: "test.txt",
}
fileList.FileInfos[fileWithInvalidChannel.Id] = fileWithInvalidChannel
fileList.Order = []string{fileWithInvalidChannel.Id}
allFilesHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.FileInfos, 0)
require.Len(t, fileList.Order, 0)
require.True(t, allFilesHaveMembership)
})
}
func TestFilterFilesByChannelPermissions_ABAC(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.PermissionPolicies = true
}).InitBasic(t)
post := th.CreatePost(t, th.BasicChannel)
t.Run("no filtering when ABAC is not enabled", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = false
})
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = nil
defer func() { th.App.Srv().ch.AccessControl = original }()
fi := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fileList := model.NewFileInfoList()
fileList.FileInfos[fi.Id] = fi
fileList.Order = []string{fi.Id}
allMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.True(t, allMembership)
require.Len(t, fileList.Order, 1)
})
t.Run("keeps files when ABAC allows download", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
mockACS := &eMocks.AccessControlServiceInterface{}
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockACS
defer func() { th.App.Srv().ch.AccessControl = original }()
mockACS.On("AccessEvaluation", mock.Anything, mock.MatchedBy(func(req model.AccessRequest) bool {
return req.Resource.ID == th.BasicChannel.Id && req.Action == model.AccessControlPolicyActionDownloadFileAttachment
})).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil))
fi1 := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fi2 := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fileList := model.NewFileInfoList()
fileList.FileInfos[fi1.Id] = fi1
fileList.FileInfos[fi2.Id] = fi2
fileList.Order = []string{fi1.Id, fi2.Id}
allMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.True(t, allMembership)
require.Len(t, fileList.Order, 2)
})
t.Run("removes files when ABAC denies download", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
mockACS := &eMocks.AccessControlServiceInterface{}
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockACS
defer func() { th.App.Srv().ch.AccessControl = original }()
mockACS.On("AccessEvaluation", mock.Anything, mock.MatchedBy(func(req model.AccessRequest) bool {
return req.Resource.ID == th.BasicChannel.Id && req.Action == model.AccessControlPolicyActionDownloadFileAttachment
})).Return(model.AccessDecision{Decision: false}, (*model.AppError)(nil))
fi := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fileList := model.NewFileInfoList()
fileList.FileInfos[fi.Id] = fi
fileList.Order = []string{fi.Id}
_, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Empty(t, fileList.Order)
require.Empty(t, fileList.FileInfos)
})
t.Run("ABAC evaluation error denies download (fail-secure)", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
mockACS := &eMocks.AccessControlServiceInterface{}
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockACS
defer func() { th.App.Srv().ch.AccessControl = original }()
mockACS.On("AccessEvaluation", mock.Anything, mock.Anything).
Return(model.AccessDecision{}, model.NewAppError("test", "test.error", nil, "", 500))
fi := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fileList := model.NewFileInfoList()
fileList.FileInfos[fi.Id] = fi
fileList.Order = []string{fi.Id}
_, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Empty(t, fileList.Order)
require.Empty(t, fileList.FileInfos)
})
t.Run("ABAC evaluates once per channel not per file", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
mockACS := &eMocks.AccessControlServiceInterface{}
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockACS
defer func() { th.App.Srv().ch.AccessControl = original }()
mockACS.On("AccessEvaluation", mock.Anything, mock.MatchedBy(func(req model.AccessRequest) bool {
return req.Resource.ID == th.BasicChannel.Id
})).Return(model.AccessDecision{Decision: true}, (*model.AppError)(nil)).Once()
fi1 := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fi2 := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fi3 := th.CreateFileInfo(t, th.BasicUser.Id, post.Id, th.BasicChannel.Id)
fileList := model.NewFileInfoList()
fileList.FileInfos[fi1.Id] = fi1
fileList.FileInfos[fi2.Id] = fi2
fileList.FileInfos[fi3.Id] = fi3
fileList.Order = []string{fi1.Id, fi2.Id, fi3.Id}
_, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Len(t, fileList.Order, 3)
mockACS.AssertNumberOfCalls(t, "AccessEvaluation", 1)
})
t.Run("skips ABAC check for channels already denied by RBAC", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.AccessControlSettings.EnableAttributeBasedAccessControl = true
})
mockACS := &eMocks.AccessControlServiceInterface{}
original := th.App.Srv().ch.AccessControl
th.App.Srv().ch.AccessControl = mockACS
defer func() { th.App.Srv().ch.AccessControl = original }()
fileList := model.NewFileInfoList()
fi := &model.FileInfo{
Id: model.NewId(),
ChannelId: model.NewId(),
Name: "test.txt",
}
fileList.FileInfos[fi.Id] = fi
fileList.Order = []string{fi.Id}
_, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
require.Nil(t, appErr)
require.Empty(t, fileList.Order)
mockACS.AssertNotCalled(t, "AccessEvaluation")
})
}
func TestSendFileUploadRejectedEvent(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
info := &model.FileInfo{
Id: model.NewId(),
Name: "secret.tdf",
ChannelId: th.BasicChannel.Id,
CreatorId: th.BasicUser.Id,
}
t.Run("publishes the event with populated fields", func(t *testing.T) {
wsMessages, closeWS := connectFakeWebSocket(t, th, th.BasicUser.Id, "", []model.WebsocketEventType{model.WebsocketEventFileUploadRejected})
defer closeWS()
th.App.sendFileUploadRejectedEvent(info, th.BasicUser.Id, "", "blocked by policy")
var received *model.WebSocketEvent
require.Eventually(t, func() bool {
select {
case received = <-wsMessages:
return true
default:
return false
}
}, 5*time.Second, 100*time.Millisecond, "did not receive file_upload_rejected event in time")
data := received.GetData()
assert.Equal(t, info.Name, data["file_name"])
assert.Equal(t, "blocked by policy", data["rejection_reason"])
assert.Equal(t, info.ChannelId, data["channel_id"])
})
t.Run("does not publish when userID is empty", func(t *testing.T) {
wsMessages, closeWS := connectFakeWebSocket(t, th, th.BasicUser.Id, "", []model.WebsocketEventType{model.WebsocketEventFileUploadRejected})
defer closeWS()
th.App.sendFileUploadRejectedEvent(info, "", "", "blocked by policy")
require.Never(t, func() bool {
select {
case <-wsMessages:
return true
default:
return false
}
}, 1*time.Second, 100*time.Millisecond, "should not publish file_upload_rejected event when userID is empty")
})
}