mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-27 12:13:29 -04:00
Merge 63a575762c into cfafefe58c
This commit is contained in:
commit
53d80cf5d5
6 changed files with 2886 additions and 0 deletions
635
server/channels/app/file_test_coverage.go
Normal file
635
server/channels/app/file_test_coverage.go
Normal file
|
|
@ -0,0 +1,635 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUploadFileX_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("driver not configured", func(t *testing.T) {
|
||||
oldDriverName := th.App.Config().FileSettings.DriverName
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.DriverName = model.NewPointer("")
|
||||
})
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.DriverName = oldDriverName
|
||||
})
|
||||
|
||||
_, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"test.txt",
|
||||
strings.NewReader("test content"),
|
||||
)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.upload_file.storage.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("file too large via ContentLength", func(t *testing.T) {
|
||||
oldMaxSize := th.App.Config().FileSettings.MaxFileSize
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxFileSize = model.NewPointer(int64(10))
|
||||
})
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxFileSize = oldMaxSize
|
||||
})
|
||||
|
||||
_, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"test.txt",
|
||||
strings.NewReader("test content that exceeds size limit"),
|
||||
UploadFileSetContentLength(100),
|
||||
)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.upload_file.too_large_detailed.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("file too large detected after write", func(t *testing.T) {
|
||||
oldMaxSize := th.App.Config().FileSettings.MaxFileSize
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxFileSize = model.NewPointer(int64(10))
|
||||
})
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxFileSize = oldMaxSize
|
||||
})
|
||||
|
||||
largeContent := strings.Repeat("a", 20)
|
||||
_, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"test.txt",
|
||||
strings.NewReader(largeContent),
|
||||
)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.upload_file.too_large_detailed.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("image resolution limit exceeded", func(t *testing.T) {
|
||||
oldMaxRes := th.App.Config().FileSettings.MaxImageResolution
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxImageResolution = model.NewPointer(int64(10))
|
||||
})
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.FileSettings.MaxImageResolution = oldMaxRes
|
||||
})
|
||||
|
||||
// Create a simple 1x1 PNG
|
||||
pngData := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
|
||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x64, // 100x100 dimensions
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x9A, 0x20, 0x6C, 0x5C,
|
||||
}
|
||||
|
||||
_, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"large.png",
|
||||
bytes.NewReader(pngData),
|
||||
)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.upload_file.large_image_detailed.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFileInfo_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("file not found", func(t *testing.T) {
|
||||
nonExistentId := model.NewId()
|
||||
_, appErr := th.App.GetFileInfo(th.Context, nonExistentId)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("invalid file id", func(t *testing.T) {
|
||||
_, appErr := th.App.GetFileInfo(th.Context, "invalid-id")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFileInfos_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("invalid page parameters", func(t *testing.T) {
|
||||
_, appErr := th.App.GetFileInfos(th.Context, -1, 10, nil)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get_with_options.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("per page limit exceeded", func(t *testing.T) {
|
||||
_, appErr := th.App.GetFileInfos(th.Context, 0, 1001, nil)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get_with_options.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("file info not found", func(t *testing.T) {
|
||||
_, appErr := th.App.GetFile(th.Context, model.NewId())
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("file exists in db but not in storage", func(t *testing.T) {
|
||||
// Upload a file
|
||||
data := []byte("test content")
|
||||
info, appErr := th.App.UploadFile(
|
||||
th.Context,
|
||||
data,
|
||||
th.BasicChannel.Id,
|
||||
"test.txt",
|
||||
)
|
||||
require.Nil(t, appErr)
|
||||
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
||||
|
||||
// Remove from storage but keep in db
|
||||
appErr = th.App.RemoveFile(info.Path)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Try to get the file
|
||||
_, appErr = th.App.GetFile(th.Context, info.Id)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.file_reader.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileReader_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("file not exists", func(t *testing.T) {
|
||||
_, appErr := th.App.FileReader("nonexistent/path/file.txt")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.file_reader.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("invalid path", func(t *testing.T) {
|
||||
_, appErr := th.App.FileReader("")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.file_reader.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("write to invalid path", func(t *testing.T) {
|
||||
reader := strings.NewReader("test content")
|
||||
written, appErr := th.App.WriteFile(reader, "")
|
||||
assert.Equal(t, int64(0), written)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.write_file.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCopyFileInfos_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("copy non-existent file", func(t *testing.T) {
|
||||
nonExistentId := model.NewId()
|
||||
_, appErr := th.App.CopyFileInfos(th.Context, th.BasicUser.Id, []string{nonExistentId})
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("copy with invalid file id", func(t *testing.T) {
|
||||
_, appErr := th.App.CopyFileInfos(th.Context, th.BasicUser.Id, []string{"invalid-id"})
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.get.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileExists_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("check empty path", func(t *testing.T) {
|
||||
exists, appErr := th.App.FileExists("")
|
||||
assert.False(t, exists)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.file_exists.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileSize_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("get size of non-existent file", func(t *testing.T) {
|
||||
size, appErr := th.App.FileSize("nonexistent/file.txt")
|
||||
assert.Equal(t, int64(0), size)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.file_size.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("remove non-existent file", func(t *testing.T) {
|
||||
appErr := th.App.RemoveFile("nonexistent/file.txt")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.remove_file.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("remove empty path", func(t *testing.T) {
|
||||
appErr := th.App.RemoveFile("")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.remove_file.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMoveFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("move non-existent file", func(t *testing.T) {
|
||||
appErr := th.App.MoveFile("old/path.txt", "new/path.txt")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.move_file.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("move to invalid destination", func(t *testing.T) {
|
||||
// First create a file
|
||||
data := []byte("test content")
|
||||
info, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, "test.txt")
|
||||
require.Nil(t, appErr)
|
||||
defer th.App.RemoveFile(info.Path)
|
||||
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
||||
|
||||
// Try to move to empty destination
|
||||
appErr = th.App.MoveFile(info.Path, "")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.move_file.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("append to non-existent file", func(t *testing.T) {
|
||||
reader := strings.NewReader("append content")
|
||||
written, appErr := th.App.AppendFile(reader, "nonexistent/file.txt")
|
||||
assert.Equal(t, int64(0), written)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.append_file.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestListDirectory_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("list non-existent directory", func(t *testing.T) {
|
||||
paths, appErr := th.App.ListDirectory("nonexistent/directory")
|
||||
assert.Empty(t, paths)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.list_directory.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveDirectory_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("remove non-existent directory", func(t *testing.T) {
|
||||
appErr := th.App.RemoveDirectory("nonexistent/directory")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.remove_directory.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetFileSearchableContent_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("set content for non-existent file", func(t *testing.T) {
|
||||
appErr := th.App.SetFileSearchableContent(th.Context, model.NewId(), "searchable content")
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.file_info.set_searchable_content.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusNotFound, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckMandatoryS3Fields_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("missing S3 bucket", func(t *testing.T) {
|
||||
settings := &model.FileSettings{
|
||||
DriverName: model.NewPointer(model.ImageDriverS3),
|
||||
AmazonS3AccessKeyId: model.NewPointer("test-key"),
|
||||
AmazonS3SecretAccessKey: model.NewPointer("test-secret"),
|
||||
AmazonS3Bucket: model.NewPointer(""),
|
||||
AmazonS3PathPrefix: model.NewPointer(""),
|
||||
AmazonS3Region: model.NewPointer("us-east-1"),
|
||||
AmazonS3Endpoint: model.NewPointer(""),
|
||||
AmazonS3SSL: model.NewPointer(true),
|
||||
AmazonS3SignV2: model.NewPointer(false),
|
||||
AmazonS3SSE: model.NewPointer(false),
|
||||
AmazonS3Trace: model.NewPointer(false),
|
||||
AmazonS3RequestTimeoutMilliseconds: model.NewPointer(int64(5000)),
|
||||
}
|
||||
|
||||
appErr := th.App.CheckMandatoryS3Fields(settings)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.admin.test_s3.missing_s3_bucket", appErr.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTestFileStoreConnectionWithConfig_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("invalid S3 credentials", func(t *testing.T) {
|
||||
settings := &model.FileSettings{
|
||||
DriverName: model.NewPointer(model.ImageDriverS3),
|
||||
AmazonS3AccessKeyId: model.NewPointer("invalid-key"),
|
||||
AmazonS3SecretAccessKey: model.NewPointer("invalid-secret"),
|
||||
AmazonS3Bucket: model.NewPointer("test-bucket"),
|
||||
AmazonS3PathPrefix: model.NewPointer(""),
|
||||
AmazonS3Region: model.NewPointer("us-east-1"),
|
||||
AmazonS3Endpoint: model.NewPointer("s3.amazonaws.com"),
|
||||
AmazonS3SSL: model.NewPointer(true),
|
||||
AmazonS3SignV2: model.NewPointer(false),
|
||||
AmazonS3SSE: model.NewPointer(false),
|
||||
AmazonS3Trace: model.NewPointer(false),
|
||||
AmazonS3RequestTimeoutMilliseconds: model.NewPointer(int64(5000)),
|
||||
}
|
||||
|
||||
appErr := th.App.TestFileStoreConnectionWithConfig(settings)
|
||||
require.NotNil(t, appErr)
|
||||
// Could be auth error or connection error depending on network
|
||||
assert.Contains(t, []string{"api.file.test_connection_s3_auth.app_error", "api.file.test_connection.app_error"}, appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermanentDeleteFilesByPost_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("delete files for non-existent post", func(t *testing.T) {
|
||||
// This should not error, just return gracefully
|
||||
appErr := th.App.PermanentDeleteFilesByPost(th.Context, model.NewId())
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
|
||||
t.Run("delete files when file in storage missing", func(t *testing.T) {
|
||||
// Create a post with a file
|
||||
data := []byte("test content")
|
||||
info, appErr := th.App.UploadFile(th.Context, data, th.BasicChannel.Id, "test.txt")
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Create a post
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "test post",
|
||||
FileIds: []string{info.Id},
|
||||
}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Remove file from storage but keep in db
|
||||
appErr = th.App.RemoveFile(info.Path)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Delete files by post - should handle missing file gracefully
|
||||
appErr = th.App.PermanentDeleteFilesByPost(th.Context, post.Id)
|
||||
require.Nil(t, appErr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadFileX_ImageProcessingErrors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("corrupted image data", func(t *testing.T) {
|
||||
// Create corrupted PNG data
|
||||
corruptedPNG := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
||||
0x00, 0x00, 0x00, 0x0D, 0xFF, 0xFF, 0xFF, 0xFF, // Corrupted IHDR
|
||||
}
|
||||
|
||||
info, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"corrupted.png",
|
||||
bytes.NewReader(corruptedPNG),
|
||||
)
|
||||
// Should still upload but without preview generation
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, info)
|
||||
defer th.App.RemoveFile(info.Path)
|
||||
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
||||
})
|
||||
|
||||
t.Run("SVG with invalid content", func(t *testing.T) {
|
||||
invalidSVG := []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="invalid" height="invalid"></svg>`)
|
||||
|
||||
info, appErr := th.App.UploadFileX(
|
||||
th.Context,
|
||||
th.BasicChannel.Id,
|
||||
"invalid.svg",
|
||||
bytes.NewReader(invalidSVG),
|
||||
UploadFileSetRaw(),
|
||||
)
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, info)
|
||||
assert.False(t, info.HasPreviewImage)
|
||||
defer th.App.RemoveFile(info.Path)
|
||||
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteZipFile_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("write to failing writer", func(t *testing.T) {
|
||||
fileDatas := []model.FileData{
|
||||
{
|
||||
Filename: "test.txt",
|
||||
Body: []byte("test content"),
|
||||
},
|
||||
}
|
||||
|
||||
// Use a writer that fails
|
||||
failWriter := &failingWriter{failAfter: 10}
|
||||
err := th.App.WriteZipFile(failWriter, fileDatas)
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "write failed")
|
||||
})
|
||||
}
|
||||
|
||||
// failingWriter is a writer that fails after writing a certain number of bytes
|
||||
type failingWriter struct {
|
||||
written int
|
||||
failAfter int
|
||||
}
|
||||
|
||||
func (w *failingWriter) Write(p []byte) (n int, err error) {
|
||||
if w.written+len(p) > w.failAfter {
|
||||
return 0, fmt.Errorf("write failed")
|
||||
}
|
||||
w.written += len(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestFilterFilesByChannelPermissions_EdgeCases(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("empty file list", func(t *testing.T) {
|
||||
fileList := &model.FileInfoList{
|
||||
FileInfos: map[string]*model.FileInfo{},
|
||||
Order: []string{},
|
||||
}
|
||||
allHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
assert.True(t, allHaveMembership)
|
||||
})
|
||||
|
||||
t.Run("nil file list", func(t *testing.T) {
|
||||
allHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, nil, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
assert.True(t, allHaveMembership)
|
||||
})
|
||||
|
||||
t.Run("files from deleted channel", func(t *testing.T) {
|
||||
// Create a channel
|
||||
channel, appErr := th.App.CreateChannel(th.Context, &model.Channel{
|
||||
TeamId: th.BasicTeam.Id,
|
||||
Type: model.ChannelTypeOpen,
|
||||
Name: "test-channel",
|
||||
DisplayName: "Test Channel",
|
||||
}, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Upload a file
|
||||
data := []byte("test content")
|
||||
info, appErr := th.App.UploadFile(th.Context, data, channel.Id, "test.txt")
|
||||
require.Nil(t, appErr)
|
||||
defer th.App.RemoveFile(info.Path)
|
||||
defer th.App.Srv().Store().FileInfo().PermanentDelete(th.Context, info.Id)
|
||||
|
||||
// Delete the channel
|
||||
appErr = th.App.DeleteChannel(th.Context, channel, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Try to filter files
|
||||
fileList := &model.FileInfoList{
|
||||
FileInfos: map[string]*model.FileInfo{
|
||||
info.Id: info,
|
||||
},
|
||||
Order: []string{info.Id},
|
||||
}
|
||||
allHaveMembership, appErr := th.App.FilterFilesByChannelPermissions(th.Context, fileList, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
assert.False(t, allHaveMembership)
|
||||
assert.Empty(t, fileList.FileInfos)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileModTime_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("get mod time of non-existent file", func(t *testing.T) {
|
||||
modTime, appErr := th.App.FileModTime("nonexistent/file.txt")
|
||||
require.NotNil(t, appErr)
|
||||
assert.True(t, modTime.IsZero())
|
||||
assert.Equal(t, "api.file.file_mod_time.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestZipReader_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("zip reader for non-existent file", func(t *testing.T) {
|
||||
_, appErr := th.App.ZipReader("nonexistent/file.zip", true)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.file.zip_file_reader.app_error", appErr.Id)
|
||||
assert.Equal(t, http.StatusInternalServerError, appErr.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtractContentFromFileInfo_EdgeCases(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("extract from image file", func(t *testing.T) {
|
||||
// Create a simple PNG file info
|
||||
info := &model.FileInfo{
|
||||
Id: model.NewId(),
|
||||
Name: "test.png",
|
||||
MimeType: "image/png",
|
||||
Path: "test/path.png",
|
||||
}
|
||||
|
||||
// Should return nil for images
|
||||
err := th.App.ExtractContentFromFileInfo(th.Context, info)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("extract from file not in storage", func(t *testing.T) {
|
||||
info := &model.FileInfo{
|
||||
Id: model.NewId(),
|
||||
Name: "test.txt",
|
||||
MimeType: "text/plain",
|
||||
Path: "nonexistent/path.txt",
|
||||
}
|
||||
|
||||
err := th.App.ExtractContentFromFileInfo(th.Context, info)
|
||||
require.NotNil(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to open file")
|
||||
})
|
||||
}
|
||||
465
server/channels/app/notification_test_coverage.go
Normal file
465
server/channels/app/notification_test_coverage.go
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestCanSendPushNotifications_ErrorPaths(t *testing.T) {
|
||||
t.Run("disabled by config", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.SendPushNotifications = false
|
||||
})
|
||||
|
||||
result := th.App.canSendPushNotifications()
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("MHPNS without license", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.Srv().SetLicense(nil)
|
||||
|
||||
servers := []string{
|
||||
model.MHPNS,
|
||||
model.MHPNSLegacyUS,
|
||||
model.MHPNSLegacyDE,
|
||||
model.MHPNSGlobal,
|
||||
model.MHPNSUS,
|
||||
model.MHPNSEU,
|
||||
model.MHPNSAP,
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.SendPushNotifications = true
|
||||
*cfg.EmailSettings.PushNotificationServer = server
|
||||
})
|
||||
|
||||
result := th.App.canSendPushNotifications()
|
||||
assert.False(t, result, "Should be false for server: %s", server)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MHPNS with license but feature disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
mhpnsFeature := false
|
||||
license := &model.License{
|
||||
Features: &model.Features{
|
||||
MHPNS: &mhpnsFeature,
|
||||
},
|
||||
}
|
||||
th.App.Srv().SetLicense(license)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.SendPushNotifications = true
|
||||
*cfg.EmailSettings.PushNotificationServer = model.MHPNS
|
||||
})
|
||||
|
||||
result := th.App.canSendPushNotifications()
|
||||
assert.False(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserAllowsEmail_EdgeCases(t *testing.T) {
|
||||
t.Run("system message with comments notify", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
NotifyProps: model.StringMap{
|
||||
model.EmailNotifyProp: model.UserNotifyNone,
|
||||
model.CommentsNotifyProp: model.CommentsNotifyRoot,
|
||||
model.PushStatusNotifyProp: model.StatusAway,
|
||||
model.DesktopSoundNotifyProp: "true",
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
|
||||
systemPost := &model.Post{
|
||||
Type: model.PostTypeSystemGeneric,
|
||||
Message: "system message",
|
||||
}
|
||||
|
||||
channelProps := model.StringMap{
|
||||
model.EmailNotifyProp: model.ChannelNotifyDefault,
|
||||
}
|
||||
|
||||
result := th.App.userAllowsEmail(th.Context, user, channelProps, systemPost)
|
||||
assert.False(t, result, "System messages should not trigger email notifications")
|
||||
})
|
||||
|
||||
t.Run("urgent post overrides notify props", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
NotifyProps: model.StringMap{
|
||||
model.EmailNotifyProp: model.UserNotifyNone,
|
||||
},
|
||||
}
|
||||
|
||||
urgentPost := &model.Post{
|
||||
Message: "urgent message",
|
||||
Metadata: &model.PostMetadata{
|
||||
Priority: &model.PostPriority{
|
||||
Priority: model.NewPointer(model.PostPriorityUrgent),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
channelProps := model.StringMap{
|
||||
model.EmailNotifyProp: model.ChannelNotifyDefault,
|
||||
}
|
||||
|
||||
result := th.App.userAllowsEmail(th.Context, user, channelProps, urgentPost)
|
||||
assert.True(t, result, "Urgent posts should override notification preferences")
|
||||
})
|
||||
|
||||
t.Run("channel notify all overrides user setting", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
NotifyProps: model.StringMap{
|
||||
model.EmailNotifyProp: model.UserNotifyNone,
|
||||
},
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
Message: "test message",
|
||||
}
|
||||
|
||||
channelProps := model.StringMap{
|
||||
model.EmailNotifyProp: model.ChannelNotifyAll,
|
||||
}
|
||||
|
||||
result := th.App.userAllowsEmail(th.Context, user, channelProps, post)
|
||||
assert.True(t, result, "Channel notify all should override user none setting")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendNoUsersNotifiedByGroupInChannel(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
groupName := "testgroup"
|
||||
group := &model.Group{
|
||||
Id: model.NewId(),
|
||||
Name: &groupName,
|
||||
DisplayName: "Test Group",
|
||||
}
|
||||
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "@testgroup",
|
||||
}
|
||||
|
||||
// This should not panic and complete successfully
|
||||
th.App.sendNoUsersNotifiedByGroupInChannel(th.Context, th.BasicUser, post, th.BasicChannel, group)
|
||||
}
|
||||
|
||||
func TestFilterUsersByVisible_ErrorPaths(t *testing.T) {
|
||||
t.Run("nil viewer", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
otherUsers := []*model.User{th.BasicUser2}
|
||||
|
||||
filtered, err := th.App.FilterUsersByVisible(th.Context, nil, otherUsers)
|
||||
assert.NotNil(t, err)
|
||||
assert.Nil(t, filtered)
|
||||
})
|
||||
|
||||
t.Run("empty user list", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
filtered, err := th.App.FilterUsersByVisible(th.Context, th.BasicUser, []*model.User{})
|
||||
assert.Nil(t, err)
|
||||
assert.Empty(t, filtered)
|
||||
})
|
||||
|
||||
t.Run("filters deactivated users", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
deactivatedUser := th.CreateUser(t)
|
||||
_, err := th.App.UpdateActive(th.Context, deactivatedUser, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
otherUsers := []*model.User{th.BasicUser2, deactivatedUser}
|
||||
|
||||
filtered, err := th.App.FilterUsersByVisible(th.Context, th.BasicUser, otherUsers)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, filtered, 1)
|
||||
assert.Equal(t, th.BasicUser2.Id, filtered[0].Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetGroupsAllowedForReferenceInChannel_ErrorCases(t *testing.T) {
|
||||
t.Run("direct channel returns empty", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
dm, appErr := th.App.GetOrCreateDirectChannel(th.Context, th.BasicUser.Id, th.BasicUser2.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
groups, err := th.App.getGroupsAllowedForReferenceInChannel(dm, th.BasicTeam)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, groups)
|
||||
})
|
||||
|
||||
t.Run("group message channel returns empty", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
gm := &model.Channel{
|
||||
Type: model.ChannelTypeGroup,
|
||||
}
|
||||
|
||||
groups, err := th.App.getGroupsAllowedForReferenceInChannel(gm, th.BasicTeam)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, groups)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInsertGroupMentions_ErrorPaths(t *testing.T) {
|
||||
t.Run("group with no members", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
|
||||
|
||||
emptyGroupName := "emptygroup"
|
||||
group := &model.Group{
|
||||
Id: model.NewId(),
|
||||
Name: &emptyGroupName,
|
||||
DisplayName: "Empty Group",
|
||||
}
|
||||
|
||||
mentions := &MentionResults{
|
||||
Mentions: make(map[string]MentionType),
|
||||
}
|
||||
profileMap := make(map[string]*model.User)
|
||||
|
||||
anyMentioned, err := th.App.insertGroupMentions(
|
||||
th.BasicUser.Id,
|
||||
group,
|
||||
th.BasicChannel,
|
||||
profileMap,
|
||||
mentions,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, anyMentioned)
|
||||
assert.Empty(t, mentions.Mentions)
|
||||
})
|
||||
|
||||
t.Run("group with inactive members only", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
|
||||
|
||||
group := th.CreateGroup(t)
|
||||
|
||||
inactiveUser := th.CreateUser(t)
|
||||
_, err := th.App.UpdateActive(th.Context, inactiveUser, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, err = th.App.UpsertGroupMember(group.Id, inactiveUser.Id)
|
||||
require.Nil(t, err)
|
||||
|
||||
mentions := &MentionResults{
|
||||
Mentions: make(map[string]MentionType),
|
||||
}
|
||||
profileMap := map[string]*model.User{
|
||||
inactiveUser.Id: inactiveUser,
|
||||
}
|
||||
|
||||
anyMentioned, err := th.App.insertGroupMentions(
|
||||
th.BasicUser.Id,
|
||||
group,
|
||||
th.BasicChannel,
|
||||
profileMap,
|
||||
mentions,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, anyMentioned)
|
||||
assert.Empty(t, mentions.Mentions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMentionKeywordsInChannel_EdgeCases(t *testing.T) {
|
||||
t.Run("with channel mentions disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
profiles := map[string]*model.User{
|
||||
th.BasicUser.Id: {
|
||||
Id: th.BasicUser.Id,
|
||||
Username: th.BasicUser.Username,
|
||||
NotifyProps: model.StringMap{
|
||||
model.ChannelMentionsNotifyProp: "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
channelMemberProps := map[string]model.StringMap{
|
||||
th.BasicUser.Id: {
|
||||
model.MarkUnreadNotifyProp: model.UserNotifyAll,
|
||||
},
|
||||
}
|
||||
|
||||
keywords := th.App.getMentionKeywordsInChannel(
|
||||
profiles,
|
||||
true, // allowChannelMentions
|
||||
channelMemberProps,
|
||||
nil, // groups
|
||||
)
|
||||
|
||||
// Should not include @all, @here, @channel when disabled for user
|
||||
_, hasAll := keywords["@all"]
|
||||
_, hasHere := keywords["@here"]
|
||||
_, hasChannel := keywords["@channel"]
|
||||
assert.False(t, hasAll)
|
||||
assert.False(t, hasHere)
|
||||
assert.False(t, hasChannel)
|
||||
})
|
||||
|
||||
t.Run("with custom mention keywords", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
profiles := map[string]*model.User{
|
||||
th.BasicUser.Id: {
|
||||
Id: th.BasicUser.Id,
|
||||
Username: th.BasicUser.Username,
|
||||
NotifyProps: model.StringMap{
|
||||
model.MentionKeysNotifyProp: "custom1,custom2",
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
channelMemberProps := map[string]model.StringMap{
|
||||
th.BasicUser.Id: {},
|
||||
}
|
||||
|
||||
keywords := th.App.getMentionKeywordsInChannel(
|
||||
profiles,
|
||||
true,
|
||||
channelMemberProps,
|
||||
nil,
|
||||
)
|
||||
|
||||
_, hasCustom1 := keywords["custom1"]
|
||||
_, hasCustom2 := keywords["custom2"]
|
||||
_, hasUsername := keywords["@"+th.BasicUser.Username]
|
||||
assert.True(t, hasCustom1)
|
||||
assert.True(t, hasCustom2)
|
||||
assert.True(t, hasUsername)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemoveNotifications_ErrorPaths(t *testing.T) {
|
||||
t.Run("remove from archived channel", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
post := &model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "test",
|
||||
}
|
||||
|
||||
archivedChannel := &model.Channel{
|
||||
Id: th.BasicChannel.Id,
|
||||
DeleteAt: model.GetMillis(),
|
||||
}
|
||||
|
||||
err := th.App.RemoveNotifications(th.Context, post, archivedChannel)
|
||||
assert.NoError(t, err) // Should succeed without doing anything
|
||||
})
|
||||
}
|
||||
|
||||
func TestCountNotificationReason_EdgeCases(t *testing.T) {
|
||||
t.Run("with metrics disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MetricsSettings.Enable = false
|
||||
})
|
||||
|
||||
// Should not panic when metrics are disabled
|
||||
th.App.CountNotificationReason(
|
||||
model.NotificationStatusError,
|
||||
model.NotificationTypePush,
|
||||
model.NotificationReasonFetchError,
|
||||
model.NotificationNoPlatform,
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("with all notification types and reasons", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.MetricsSettings.Enable = true
|
||||
})
|
||||
|
||||
statuses := []model.NotificationStatus{
|
||||
model.NotificationStatusSuccess,
|
||||
model.NotificationStatusNotSent,
|
||||
model.NotificationStatusError,
|
||||
}
|
||||
|
||||
types := []model.NotificationType{
|
||||
model.NotificationTypePush,
|
||||
model.NotificationTypeEmail,
|
||||
model.NotificationTypeWebsocket,
|
||||
}
|
||||
|
||||
reasons := []model.NotificationReason{
|
||||
model.NotificationReasonFetchError,
|
||||
model.NotificationReasonMissingProfile,
|
||||
model.NotificationReasonChannelMuted,
|
||||
}
|
||||
|
||||
// Only use the one platform constant that exists
|
||||
platforms := []string{
|
||||
model.NotificationNoPlatform,
|
||||
}
|
||||
|
||||
// Test all combinations - should not panic
|
||||
for _, status := range statuses {
|
||||
for _, notifType := range types {
|
||||
for _, reason := range reasons {
|
||||
for _, platform := range platforms {
|
||||
th.App.CountNotificationReason(status, notifType, reason, platform)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
467
server/channels/app/oauth_test_coverage.go
Normal file
467
server/channels/app/oauth_test_coverage.go
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestCreateOAuthApp_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
|
||||
_, err := th.App.CreateOAuthApp(app)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.register_oauth_app.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Duplicate app name for same creator", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "duplicate-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
|
||||
// Create first app
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, createdApp)
|
||||
|
||||
// Try to create duplicate
|
||||
app2 := &model.OAuthApp{
|
||||
Name: "duplicate-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://different.com/callback"},
|
||||
}
|
||||
|
||||
_, err = th.App.CreateOAuthApp(app2)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.oauth.save_app.existing.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateOAuthApp_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
oldApp := &model.OAuthApp{Id: model.NewId()}
|
||||
updatedApp := &model.OAuthApp{Name: "updated"}
|
||||
|
||||
_, err := th.App.UpdateOAuthApp(oldApp, updatedApp)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Update non-existent app", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
oldApp := &model.OAuthApp{
|
||||
Id: model.NewId(),
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
updatedApp := &model.OAuthApp{
|
||||
Name: "updated",
|
||||
CallbackUrls: []string{"https://example.com"},
|
||||
}
|
||||
|
||||
_, err := th.App.UpdateOAuthApp(oldApp, updatedApp)
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteOAuthApp_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
err := th.App.DeleteOAuthApp(th.Context, model.NewId())
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Delete non-existent app", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
err := th.App.DeleteOAuthApp(th.Context, model.NewId())
|
||||
// Store returns nil for non-existent deletes, so this should succeed
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOAuthApp_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
_, err := th.App.GetOAuthApp(model.NewId())
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Non-existent app", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
_, err := th.App.GetOAuthApp(model.NewId())
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.oauth.get_app.find.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOAuthApps_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
_, err := th.App.GetOAuthApps(0, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAllowOAuthAppAccessToUser_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: model.NewId(),
|
||||
RedirectURI: "https://example.com/callback",
|
||||
}
|
||||
|
||||
_, err := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Non-existent app", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: model.NewId(),
|
||||
RedirectURI: "https://example.com/callback",
|
||||
ResponseType: model.AuthCodeResponseType,
|
||||
State: "test-state",
|
||||
}
|
||||
|
||||
_, err := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.oauth.get_app.find.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Invalid redirect URI", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
// Create OAuth app
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: createdApp.Id,
|
||||
RedirectURI: "https://evil.com/callback", // Different from registered
|
||||
ResponseType: model.AuthCodeResponseType,
|
||||
State: "test-state",
|
||||
}
|
||||
|
||||
_, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest)
|
||||
assert.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.redirect_callback.app_error", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("Public client without PKCE", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
// Create public OAuth app (no client secret)
|
||||
app := &model.OAuthApp{
|
||||
Name: "public-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, _ := th.App.CreateOAuthAppInternal(app, false) // Don't generate secret
|
||||
require.Empty(t, createdApp.ClientSecret)
|
||||
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: createdApp.Id,
|
||||
RedirectURI: "https://example.com/callback",
|
||||
ResponseType: model.AuthCodeResponseType,
|
||||
State: "test-state",
|
||||
CodeChallenge: "", // Missing PKCE challenge
|
||||
}
|
||||
|
||||
_, err := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.pkce_required_public.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Unsupported response type", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: createdApp.Id,
|
||||
RedirectURI: "https://example.com/callback",
|
||||
ResponseType: "unsupported_type",
|
||||
State: "test-state",
|
||||
}
|
||||
|
||||
redirectURI, appErr := th.App.AllowOAuthAppAccessToUser(th.Context, th.BasicUser.Id, authRequest)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Contains(t, redirectURI, "error=unsupported_response_type")
|
||||
assert.Contains(t, redirectURI, "state=test-state")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeauthorizeOAuthAppForUser_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
err := th.App.DeauthorizeOAuthAppForUser(th.Context, th.BasicUser.Id, model.NewId())
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Non-existent authorization", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
// This should succeed even if preference doesn't exist
|
||||
err := th.App.DeauthorizeOAuthAppForUser(th.Context, th.BasicUser.Id, model.NewId())
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOAuthAccessTokenForCodeFlow_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
_, err := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, "clientId", model.AccessTokenGrantType, "https://example.com", "code", "secret", "", "", "")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.get_access_token.disabled.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Invalid grant type", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, appErr := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, createdApp.Id, "invalid_grant", "https://example.com", "code", createdApp.ClientSecret, "", "", "")
|
||||
assert.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.oauth.get_access_token.bad_grant.app_error", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("Non-existent client", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
_, err := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, model.NewId(), model.AccessTokenGrantType, "https://example.com", "code", "secret", "", "", "")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.get_access_token.credentials.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("Invalid auth code", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
_, appErr := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, createdApp.Id, model.AccessTokenGrantType, "https://example.com/callback", "invalid_code", createdApp.ClientSecret, "", "", "")
|
||||
assert.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.oauth.get_access_token.expired_code.app_error", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("Wrong client secret", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Create auth code
|
||||
authData := &model.AuthData{
|
||||
UserId: th.BasicUser.Id,
|
||||
ClientId: createdApp.Id,
|
||||
CreateAt: model.GetMillis(),
|
||||
RedirectUri: "https://example.com/callback",
|
||||
State: "test",
|
||||
Scope: model.DefaultScope,
|
||||
Code: model.NewId() + model.NewId(),
|
||||
}
|
||||
_, nErr := th.App.Srv().Store().OAuth().SaveAuthData(authData)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
_, appErr := th.App.GetOAuthAccessTokenForCodeFlow(th.Context, createdApp.Id, model.AccessTokenGrantType, authData.RedirectUri, authData.Code, "wrong_secret", "", "", "")
|
||||
assert.NotNil(t, appErr)
|
||||
assert.Equal(t, "api.oauth.get_access_token.credentials.app_error", appErr.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegenerateOAuthAppSecret_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
app := &model.OAuthApp{Id: model.NewId()}
|
||||
_, err := th.App.RegenerateOAuthAppSecret(app)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevokeAccessToken_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("Invalid token", func(t *testing.T) {
|
||||
err := th.App.RevokeAccessToken(th.Context, "invalid_token")
|
||||
// Returns nil even if token not found
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOAuthCodeRedirect_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("Invalid redirect URI", func(t *testing.T) {
|
||||
authRequest := &model.AuthorizeRequest{
|
||||
ClientId: model.NewId(),
|
||||
RedirectURI: "://invalid-uri", // Invalid URI format
|
||||
State: "test-state",
|
||||
}
|
||||
|
||||
redirectURI, err := th.App.GetOAuthCodeRedirect(th.BasicUser.Id, authRequest)
|
||||
assert.Nil(t, err)
|
||||
assert.Contains(t, redirectURI, "error=redirect_uri_parse_error")
|
||||
assert.Contains(t, redirectURI, "state=test-state")
|
||||
})
|
||||
|
||||
t.Run("Store save failure", func(t *testing.T) {
|
||||
// This test would require mocking the store to simulate a failure
|
||||
// Since we're using real stores, we can't easily simulate this error path
|
||||
// The code handles it properly by returning an error query parameter
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAuthorizedAppsForUser_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("Invalid user", func(t *testing.T) {
|
||||
apps, err := th.App.GetAuthorizedAppsForUser(model.NewId(), 0, 10)
|
||||
// Should return empty list, not error
|
||||
assert.Nil(t, err)
|
||||
assert.Empty(t, apps)
|
||||
})
|
||||
|
||||
t.Run("Store error handling", func(t *testing.T) {
|
||||
// Create an OAuth app and authorize it
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true })
|
||||
|
||||
app := &model.OAuthApp{
|
||||
Name: "test-app",
|
||||
CreatorId: th.BasicUser.Id,
|
||||
CallbackUrls: []string{"https://example.com/callback"},
|
||||
}
|
||||
createdApp, err := th.App.CreateOAuthApp(app)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Authorize the app
|
||||
pref := model.Preference{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PreferenceCategoryAuthorizedOAuthApp,
|
||||
Name: createdApp.Id,
|
||||
Value: model.DefaultScope,
|
||||
}
|
||||
nErr := th.App.Srv().Store().Preference().Save(model.Preferences{pref})
|
||||
require.NoError(t, nErr)
|
||||
|
||||
// Get authorized apps
|
||||
apps, appErr := th.App.GetAuthorizedAppsForUser(th.BasicUser.Id, 0, 10)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Len(t, apps, 1)
|
||||
|
||||
// Delete the app to simulate orphaned preference
|
||||
appErr = th.App.DeleteOAuthApp(th.Context, createdApp.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Should handle missing app gracefully
|
||||
apps, appErr = th.App.GetAuthorizedAppsForUser(th.BasicUser.Id, 0, 10)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Empty(t, apps)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetOAuthAppsByCreator_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
t.Run("OAuth disabled", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = false })
|
||||
|
||||
_, err := th.App.GetOAuthAppsByCreator(th.BasicUser.Id, 0, 10)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "api.oauth.allow_oauth.turn_off.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
419
server/channels/app/plugin_test_coverage.go
Normal file
419
server/channels/app/plugin_test_coverage.go
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
)
|
||||
|
||||
// TestEnablePlugin_ErrorPaths tests error handling for EnablePlugin
|
||||
func TestEnablePlugin_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
err := th.App.EnablePlugin("test-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("plugin not found", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
err := th.App.EnablePlugin("non-existent-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("already enabled plugin", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
cfg.PluginSettings.PluginStates["test-plugin"] = &model.PluginState{Enable: true}
|
||||
})
|
||||
|
||||
// Try to enable an already enabled plugin - should not error but be idempotent
|
||||
err := th.App.EnablePlugin("non-existent-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
// TestDisablePlugin_ErrorPaths tests error handling for DisablePlugin
|
||||
func TestDisablePlugin_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
err := th.App.DisablePlugin("test-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("plugin not found", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
err := th.App.DisablePlugin("non-existent-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("already disabled plugin", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
cfg.PluginSettings.PluginStates["test-plugin"] = &model.PluginState{Enable: false}
|
||||
})
|
||||
|
||||
// Try to disable an already disabled plugin - should not error but be idempotent
|
||||
err := th.App.DisablePlugin("non-existent-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
// TestInstallPlugin_ErrorPaths tests error handling for InstallPlugin
|
||||
func TestInstallPlugin_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
manifest, err := th.App.InstallPlugin(bytes.NewReader([]byte("test")), false)
|
||||
assert.Nil(t, manifest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("invalid plugin file", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
manifest, err := th.App.InstallPlugin(bytes.NewReader([]byte("not a valid tar.gz")), false)
|
||||
assert.Nil(t, manifest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.extract.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("nil reader", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
manifest, err := th.App.InstallPlugin(&nilPluginReader{}, false)
|
||||
assert.Nil(t, manifest)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.extract.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRemovePlugin_ErrorPaths tests error handling for RemovePlugin
|
||||
func TestRemovePlugin_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
err := th.App.ch.RemovePlugin("test-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("plugin not found", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
err := th.App.ch.RemovePlugin("non-existent-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("cannot remove prepackaged plugin", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
// Set up a mock prepackaged plugin in the environment
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
if env != nil {
|
||||
prepackagedPlugins := []*plugin.PrepackagedPlugin{
|
||||
{
|
||||
Manifest: &model.Manifest{
|
||||
Id: "prepackaged-plugin",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
env.SetPrepackagedPlugins(prepackagedPlugins, nil)
|
||||
}
|
||||
|
||||
err := th.App.ch.RemovePlugin("prepackaged-plugin")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.prepackaged.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusBadRequest, err.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetPlugins_ErrorPaths tests error handling for GetPlugins
|
||||
func TestGetPlugins_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
resp, err := th.App.GetPlugins()
|
||||
assert.Nil(t, resp)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("empty plugin list", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
resp, err := th.App.GetPlugins()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Empty(t, resp.Active)
|
||||
assert.Empty(t, resp.Inactive)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetPluginStatuses_ErrorPaths tests error handling for GetPluginStatuses
|
||||
func TestGetPluginStatuses_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
statuses, err := th.App.GetPluginStatuses()
|
||||
assert.Nil(t, statuses)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("empty status list", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
statuses, err := th.App.GetPluginStatuses()
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, statuses)
|
||||
assert.Empty(t, statuses)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetPluginStatus_SinglePlugin tests GetPluginStatus for edge cases
|
||||
func TestGetPluginStatus_SinglePlugin(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
status, err := th.App.GetPluginStatus("test-plugin")
|
||||
assert.Nil(t, status)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("plugin not found", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
status, err := th.App.GetPluginStatus("non-existent-plugin")
|
||||
assert.Nil(t, status)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPluginStateManagement_EdgeCases tests plugin state management edge cases
|
||||
func TestPluginStateManagement_EdgeCases(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("enable then disable quickly", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.RequirePluginSignature = false
|
||||
})
|
||||
|
||||
// First we need a plugin to work with
|
||||
// This is a basic test to ensure state changes work properly
|
||||
// In a real scenario, we'd have an actual plugin installed
|
||||
|
||||
err := th.App.EnablePlugin("non-existent")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
|
||||
err = th.App.DisablePlugin("non-existent")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("config save failure", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
// Test that even with a non-existent plugin, we handle the error correctly
|
||||
err := th.App.EnablePlugin("test-plugin-not-found")
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.not_installed.app_error", err.Id)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSyncPlugins_ErrorPaths tests error cases in plugin synchronization
|
||||
func TestSyncPlugins_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("plugins disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
err := th.App.SyncPlugins()
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "app.plugin.disabled.app_error", err.Id)
|
||||
assert.Equal(t, http.StatusNotImplemented, err.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("no plugins to sync", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
})
|
||||
|
||||
// With no plugins in the file store, sync should succeed but do nothing
|
||||
err := th.App.SyncPlugins()
|
||||
assert.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPluginEnvironmentNil tests handling when plugin environment is nil
|
||||
func TestPluginEnvironmentNil(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
t.Run("get plugins environment when disabled", func(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = false
|
||||
})
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
assert.Nil(t, env)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper type for simulating nil/empty plugin readers
|
||||
type nilPluginReader struct{}
|
||||
|
||||
func (r *nilPluginReader) Read(p []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (r *nilPluginReader) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
440
server/channels/app/user_test_coverage.go
Normal file
440
server/channels/app/user_test_coverage.go
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestCreateUser_DuplicateEmail(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create a user with unique email
|
||||
user1 := &model.User{
|
||||
Email: model.NewId() + "@example.com",
|
||||
Username: model.NewId() + "_user1",
|
||||
Password: "Password1",
|
||||
}
|
||||
createdUser1, err := th.App.CreateUser(th.Context, user1)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, createdUser1)
|
||||
|
||||
// Try to create another user with the same email
|
||||
user2 := &model.User{
|
||||
Email: user1.Email, // Same email
|
||||
Username: model.NewId() + "_user2",
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err = th.App.CreateUser(th.Context, user2)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.user.save.email_exists.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestCreateUser_DuplicateUsername(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create a user with unique username
|
||||
user1 := &model.User{
|
||||
Email: model.NewId() + "@example.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
createdUser1, err := th.App.CreateUser(th.Context, user1)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, createdUser1)
|
||||
|
||||
// Try to create another user with the same username
|
||||
user2 := &model.User{
|
||||
Email: model.NewId() + "@example.com",
|
||||
Username: user1.Username, // Same username
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err = th.App.CreateUser(th.Context, user2)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.user.save.username_exists.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestCreateUser_RestrictedDomain(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Enable email domain restrictions
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.RestrictCreationToDomains = "allowed.com"
|
||||
})
|
||||
|
||||
// Try to create user with disallowed domain
|
||||
user := &model.User{
|
||||
Email: "user@notallowed.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err := th.App.CreateUser(th.Context, user)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.create_user.accepted_domain.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
|
||||
// Create user with allowed domain should work
|
||||
allowedUser := &model.User{
|
||||
Email: "user@allowed.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
createdUser, err := th.App.CreateUser(th.Context, allowedUser)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, createdUser)
|
||||
}
|
||||
|
||||
func TestCreateUser_AtUserLimit(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Set a very low user limit
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.MaxUsersPerTeam = 1
|
||||
})
|
||||
|
||||
// Try to create a user when at limit
|
||||
user := &model.User{
|
||||
Email: model.NewId() + "@example.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err := th.App.CreateUser(th.Context, user)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.create_user.user_limits.exceeded", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetUser_NotFound(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to get non-existent user
|
||||
_, err := th.App.GetUser(model.NewId())
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetUserByUsername_NotFound(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to get user by non-existent username
|
||||
_, err := th.App.GetUserByUsername("nonexistentusername")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.user.get_by_username.app_error", err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetUserByEmail_NotFound(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to get user by non-existent email
|
||||
_, err := th.App.GetUserByEmail("nonexistent@example.com")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdatePassword_InvalidUser(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to update password for non-existent user
|
||||
err := th.App.UpdatePasswordAsUser(th.Context, model.NewId(), "currentPassword", "newPassword123")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdatePassword_OAuthUser(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create OAuth user
|
||||
user := th.CreateUser(t)
|
||||
authData := model.NewId()
|
||||
th.App.Srv().Store().User().UpdateAuthData(user.Id, model.ServiceGitlab, &authData, "", false)
|
||||
|
||||
// Get updated user
|
||||
oauthUser, _ := th.App.GetUser(user.Id)
|
||||
|
||||
// Try to update password for OAuth user
|
||||
err := th.App.UpdatePasswordAsUser(th.Context, oauthUser.Id, "currentPassword", "newPassword123")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.update_password.oauth.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdatePassword_IncorrectCurrentPassword(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to update with wrong current password
|
||||
err := th.App.UpdatePasswordAsUser(th.Context, th.BasicUser.Id, "wrongpassword", "newPassword123")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.update_password.incorrect.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdateUser_EmailChangeRestriction(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Get the basic user
|
||||
user, _ := th.App.GetUser(th.BasicUser.Id)
|
||||
originalEmail := user.Email
|
||||
|
||||
// Try to change email when it's restricted
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.RequireEmailVerification = true
|
||||
})
|
||||
|
||||
user.Email = "newemail@example.com"
|
||||
_, err := th.App.UpdateUserAsUser(th.Context, user, false)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.update_user.email_change.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
|
||||
// Verify email wasn't changed
|
||||
updatedUser, _ := th.App.GetUser(user.Id)
|
||||
assert.Equal(t, originalEmail, updatedUser.Email)
|
||||
}
|
||||
|
||||
func TestPatchUser_InvalidPatch(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to patch non-existent user
|
||||
patch := &model.UserPatch{
|
||||
Username: model.NewPointer("newusername"),
|
||||
}
|
||||
_, err := th.App.PatchUser(th.Context, model.NewId(), patch, false)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
|
||||
// Try to patch with invalid username
|
||||
invalidPatch := &model.UserPatch{
|
||||
Username: model.NewPointer("a"), // Too short
|
||||
}
|
||||
_, err = th.App.PatchUser(th.Context, th.BasicUser.Id, invalidPatch, false)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.user.update.find.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestDeactivateUser_NotFound(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to deactivate non-existent user
|
||||
err := th.App.UpdateUserActive(th.Context, model.NewId(), false)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
assert.Equal(t, 404, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdateActive_AtUserLimitReactivation(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// First deactivate a user
|
||||
user := th.CreateUser(t)
|
||||
err := th.App.UpdateUserActive(th.Context, user.Id, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Set user limit that we're at
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.MaxUsersPerTeam = 3 // We have BasicUser, BasicUser2, and SystemAdminUser
|
||||
})
|
||||
|
||||
// Try to reactivate when at limit
|
||||
err = th.App.UpdateUserActive(th.Context, user.Id, true)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.user.update_active.user_limit.exceeded", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestResetPasswordFromCode_InvalidCode(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to reset password with invalid token
|
||||
err := th.App.ResetPasswordFromToken(th.Context, "invalidtoken", "newPassword123")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.reset_password.invalid_link.app_error", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestCreateUserWithInviteId_RestrictedDomain(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Set allowed domains on team
|
||||
restoreTeam := saveTeamState(th)
|
||||
defer restoreTeam()
|
||||
|
||||
th.BasicTeam.AllowedDomains = "allowed.com"
|
||||
_, err := th.App.UpdateTeam(th.BasicTeam)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Try to create user with disallowed domain
|
||||
user := &model.User{
|
||||
Email: "user@notallowed.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err = th.App.CreateUserWithInviteId(th.Context, user, th.BasicTeam.InviteId, "")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.team.invite_members.invalid_email.app_error", err.Id)
|
||||
assert.Equal(t, 403, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdateUser_UsernameConflictWithGroup(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create a group
|
||||
groupName := strings.ToLower(model.NewId())
|
||||
group := &model.Group{
|
||||
Name: &groupName,
|
||||
DisplayName: "Test Group",
|
||||
RemoteId: nil,
|
||||
Source: model.GroupSourceCustom,
|
||||
}
|
||||
createdGroup, err := th.App.CreateGroup(group)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Try to update user with username matching group name
|
||||
user, _ := th.App.GetUser(th.BasicUser.Id)
|
||||
user.Username = *createdGroup.Name
|
||||
_, appErr := th.App.UpdateUser(th.Context, user, false)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "app.user.username_taken_by_group.app_error", appErr.Id)
|
||||
assert.Equal(t, 400, appErr.StatusCode)
|
||||
}
|
||||
|
||||
func TestPermanentDeleteUser_FailStoreDelete(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create a user to delete
|
||||
user := th.CreateUser(t)
|
||||
|
||||
// Add the user to channels and create some posts to make deletion more complex
|
||||
th.LinkUserToTeam(t, user, th.BasicTeam)
|
||||
th.AddUserToChannel(t, user, th.BasicChannel)
|
||||
post := &model.Post{
|
||||
UserId: user.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "Test message",
|
||||
}
|
||||
_, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, err)
|
||||
|
||||
// Deactivate first (required before permanent delete)
|
||||
err = th.App.UpdateUserActive(th.Context, user.Id, false)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Try permanent delete - it should succeed but let's verify it handles errors gracefully
|
||||
err = th.App.PermanentDeleteUser(th.Context, user)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Verify user is gone
|
||||
_, err = th.App.GetUser(user.Id)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAccountError, err.Id)
|
||||
}
|
||||
|
||||
func TestCreateUserFromSignup_NotOpenServer(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Disable open server
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.TeamSettings.EnableOpenServer = false
|
||||
})
|
||||
|
||||
// Make sure it's not first account
|
||||
users, _ := th.App.Srv().Store().User().GetAll()
|
||||
require.Greater(t, len(users), 0)
|
||||
|
||||
// Try to create user via signup
|
||||
user := &model.User{
|
||||
Email: model.NewId() + "@example.com",
|
||||
Username: "testuser_" + model.NewId(),
|
||||
Password: "Password1",
|
||||
}
|
||||
_, err := th.App.CreateUserFromSignup(th.Context, user, "")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.user.create_user.no_open_server", err.Id)
|
||||
assert.Equal(t, 403, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetUserByAuth_MissingAuthData(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Try to get user by auth data that doesn't exist
|
||||
_, err := th.App.GetUserByAuth(model.NewPointer("nonexistentauth"), model.ServiceGitlab)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, MissingAuthAccountError, err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestUpdatePasswordSendEmail_WeakPassword(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Enable strict password requirements
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PasswordSettings.MinimumLength = 12
|
||||
*cfg.PasswordSettings.Lowercase = true
|
||||
*cfg.PasswordSettings.Uppercase = true
|
||||
*cfg.PasswordSettings.Symbol = true
|
||||
*cfg.PasswordSettings.Number = true
|
||||
})
|
||||
|
||||
// Try to set weak password
|
||||
user, _ := th.App.GetUser(th.BasicUser.Id)
|
||||
err := th.App.UpdatePassword(th.Context, user, "weak")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "model.user.is_valid.pwd", err.Id)
|
||||
assert.Equal(t, 400, err.StatusCode)
|
||||
}
|
||||
460
server/channels/app/webhook_test_coverage.go
Normal file
460
server/channels/app/webhook_test_coverage.go
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
func TestDeleteIncomingWebhook_Error(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
||||
|
||||
err := th.App.DeleteIncomingWebhook("nonexistent")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.incoming_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test deletion of non-existent webhook
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
||||
|
||||
err = th.App.DeleteIncomingWebhook(model.NewId())
|
||||
require.Nil(t, err) // Delete is idempotent, no error for non-existent
|
||||
}
|
||||
|
||||
func TestGetIncomingWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
||||
|
||||
_, err := th.App.GetIncomingWebhook("test")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.incoming_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test getting non-existent webhook
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
||||
|
||||
_, err = th.App.GetIncomingWebhook(model.NewId())
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetIncomingWebhooksPageByUser_DisabledError(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
||||
|
||||
_, err := th.App.GetIncomingWebhooksPageByUser(th.BasicUser.Id, 0, 10)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.incoming_webhook.disabled.app_error", err.Id)
|
||||
}
|
||||
|
||||
func TestGetIncomingWebhooksCount_DisabledError(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
||||
|
||||
count, err := th.App.GetIncomingWebhooksCount(th.BasicTeam.Id, th.BasicUser.Id)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.incoming_webhook.disabled.app_error", err.Id)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestCreateOutgoingWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
hook := &model.OutgoingWebhook{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
CallbackURLs: []string{"http://example.com"},
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
_, err := th.App.CreateOutgoingWebhook(hook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test with non-existent channel
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
|
||||
|
||||
hook.ChannelId = model.NewId()
|
||||
_, err = th.App.CreateOutgoingWebhook(hook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
|
||||
// Test with private channel
|
||||
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
||||
hook.ChannelId = privateChannel.Id
|
||||
_, err = th.App.CreateOutgoingWebhook(hook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, err.StatusCode)
|
||||
|
||||
// Test with no channel and no trigger words
|
||||
hook.ChannelId = ""
|
||||
hook.TriggerWords = []string{}
|
||||
_, err = th.App.CreateOutgoingWebhook(hook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.webhook.create_outgoing.triggers.app_error", err.Id)
|
||||
}
|
||||
|
||||
func TestUpdateOutgoingWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Create a webhook to update
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
|
||||
|
||||
oldHook := &model.OutgoingWebhook{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
CallbackURLs: []string{"http://example.com"},
|
||||
CreatorId: th.BasicUser.Id,
|
||||
TriggerWords: []string{"trigger"},
|
||||
}
|
||||
createdHook, err := th.App.CreateOutgoingWebhook(oldHook)
|
||||
require.Nil(t, err)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
updatedHook := *createdHook
|
||||
updatedHook.DisplayName = "Updated"
|
||||
_, err = th.App.UpdateOutgoingWebhook(th.Context, createdHook, &updatedHook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test updating to private channel
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
|
||||
|
||||
privateChannel := th.CreatePrivateChannel(t, th.BasicTeam)
|
||||
updatedHook.ChannelId = privateChannel.Id
|
||||
_, err = th.App.UpdateOutgoingWebhook(th.Context, createdHook, &updatedHook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.webhook.create_outgoing.not_open.app_error", err.Id)
|
||||
|
||||
// Test updating to different team's channel
|
||||
otherTeam := th.CreateTeam(t)
|
||||
otherChannel := th.CreateChannel(t, otherTeam)
|
||||
updatedHook.ChannelId = otherChannel.Id
|
||||
_, err = th.App.UpdateOutgoingWebhook(th.Context, createdHook, &updatedHook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.webhook.create_outgoing.permissions.app_error", err.Id)
|
||||
|
||||
// Test removing channel ID without trigger words
|
||||
updatedHook.ChannelId = ""
|
||||
updatedHook.TriggerWords = []string{}
|
||||
_, err = th.App.UpdateOutgoingWebhook(th.Context, createdHook, &updatedHook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.webhook.create_outgoing.triggers.app_error", err.Id)
|
||||
}
|
||||
|
||||
func TestDeleteOutgoingWebhook_Error(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
err := th.App.DeleteOutgoingWebhook("test")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test deletion of non-existent webhook
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
|
||||
|
||||
err = th.App.DeleteOutgoingWebhook(model.NewId())
|
||||
require.Nil(t, err) // Delete is idempotent
|
||||
}
|
||||
|
||||
func TestGetOutgoingWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
_, err := th.App.GetOutgoingWebhook("test")
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test getting non-existent webhook
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true })
|
||||
|
||||
_, err = th.App.GetOutgoingWebhook(model.NewId())
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestGetOutgoingWebhooksPageByUser_DisabledError(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
_, err := th.App.GetOutgoingWebhooksPageByUser(th.BasicUser.Id, 0, 10)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
}
|
||||
|
||||
func TestRegenOutgoingWebhookToken_Error(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = false })
|
||||
|
||||
hook := &model.OutgoingWebhook{Id: model.NewId()}
|
||||
_, err := th.App.RegenOutgoingWebhookToken(hook)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "api.outgoing_webhook.disabled.app_error", err.Id)
|
||||
}
|
||||
|
||||
func TestHandleIncomingWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test when webhooks are disabled
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = false })
|
||||
|
||||
err := th.App.HandleIncomingWebhook(th.Context, "test", &model.IncomingWebhookRequest{Text: "test"})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.disabled.app_error", err.Id)
|
||||
|
||||
// Test with nil request
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
||||
|
||||
err = th.App.HandleIncomingWebhook(th.Context, "test", nil)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.parse.app_error", err.Id)
|
||||
|
||||
// Test with empty text and no attachments
|
||||
err = th.App.HandleIncomingWebhook(th.Context, "test", &model.IncomingWebhookRequest{Text: ""})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.text.app_error", err.Id)
|
||||
|
||||
// Test with non-existent webhook
|
||||
err = th.App.HandleIncomingWebhook(th.Context, model.NewId(), &model.IncomingWebhookRequest{Text: "test"})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.invalid.app_error", err.Id)
|
||||
|
||||
// Test channel locked error
|
||||
hook := &model.IncomingWebhook{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
DisplayName: "test",
|
||||
Description: "test",
|
||||
ChannelLocked: true,
|
||||
}
|
||||
webhook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, hook)
|
||||
require.Nil(t, err)
|
||||
|
||||
otherChannel := th.CreateChannel(t, th.BasicTeam)
|
||||
err = th.App.HandleIncomingWebhook(th.Context, webhook.Id, &model.IncomingWebhookRequest{
|
||||
Text: "test",
|
||||
ChannelName: otherChannel.Name,
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.channel_locked.app_error", err.Id)
|
||||
|
||||
// Test user not found error (@mention to non-existent user)
|
||||
err = th.App.HandleIncomingWebhook(th.Context, webhook.Id, &model.IncomingWebhookRequest{
|
||||
Text: "test",
|
||||
ChannelName: "@nonexistentuser",
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "web.incoming_webhook.user.app_error", err.Id)
|
||||
|
||||
// Test channel not found error
|
||||
err = th.App.HandleIncomingWebhook(th.Context, webhook.Id, &model.IncomingWebhookRequest{
|
||||
Text: "test",
|
||||
ChannelName: "#nonexistentchannel",
|
||||
})
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
}
|
||||
|
||||
func TestCreateCommandWebhook_Error(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
args := &model.CommandArgs{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
RootId: "",
|
||||
}
|
||||
|
||||
// Create a command webhook
|
||||
hook, err := th.App.CreateCommandWebhook(model.NewId(), args)
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, hook)
|
||||
|
||||
// Test creating duplicate webhook (should work - no unique constraint)
|
||||
hook2, err := th.App.CreateCommandWebhook(hook.CommandId, args)
|
||||
require.Nil(t, err)
|
||||
require.NotEqual(t, hook.Id, hook2.Id)
|
||||
}
|
||||
|
||||
func TestHandleCommandWebhook_Errors(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Test with nil response
|
||||
err := th.App.HandleCommandWebhook(th.Context, "test", nil)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.command_webhook.handle_command_webhook.parse", err.Id)
|
||||
|
||||
// Test with non-existent webhook
|
||||
response := &model.CommandResponse{Text: "test"}
|
||||
err = th.App.HandleCommandWebhook(th.Context, model.NewId(), response)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
|
||||
// Create a command and webhook to test with
|
||||
cmd := &model.Command{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
URL: "http://nowhere.com",
|
||||
Method: model.CommandMethodPost,
|
||||
Trigger: "trigger",
|
||||
AutoComplete: true,
|
||||
AutoCompleteHint: "hint",
|
||||
DisplayName: "display",
|
||||
Description: "description",
|
||||
}
|
||||
cmd, appErr := th.App.CreateCommand(cmd)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
args := &model.CommandArgs{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
RootId: "",
|
||||
}
|
||||
|
||||
hook, appErr := th.App.CreateCommandWebhook(cmd.Id, args)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Test exceeding use limit
|
||||
for i := 0; i < 5; i++ {
|
||||
err = th.App.HandleCommandWebhook(th.Context, hook.Id, response)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
// 6th call should fail
|
||||
err = th.App.HandleCommandWebhook(th.Context, hook.Id, response)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, "app.command_webhook.try_use.invalid", err.Id)
|
||||
}
|
||||
|
||||
func TestTriggerWebhook_ErrorPaths(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableOutgoingWebhooks = true
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "127.0.0.1"
|
||||
// Set a very short timeout to test timeout errors
|
||||
*cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = 1
|
||||
})
|
||||
|
||||
hook := &model.OutgoingWebhook{
|
||||
Id: model.NewId(),
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
TeamId: th.BasicTeam.Id,
|
||||
CallbackURLs: []string{"http://127.0.0.1:1"}, // Port 1 should fail to connect
|
||||
CreatorId: th.BasicUser.Id,
|
||||
TriggerWords: []string{"trigger"},
|
||||
ContentType: "application/json",
|
||||
Username: "webhook-username",
|
||||
IconURL: "http://example.com/icon.png",
|
||||
}
|
||||
|
||||
payload := &model.OutgoingWebhookPayload{
|
||||
Token: hook.Token,
|
||||
TeamId: hook.TeamId,
|
||||
TeamDomain: th.BasicTeam.Name,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
ChannelName: th.BasicChannel.Name,
|
||||
Timestamp: th.BasicPost.CreateAt,
|
||||
UserId: th.BasicPost.UserId,
|
||||
UserName: th.BasicUser.Username,
|
||||
PostId: th.BasicPost.Id,
|
||||
Text: th.BasicPost.Message,
|
||||
TriggerWord: "trigger",
|
||||
}
|
||||
|
||||
// This should complete without panic even though the webhook fails
|
||||
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, th.BasicChannel)
|
||||
|
||||
// Wait a bit to ensure the goroutine has time to fail
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Test with slow server (timeout)
|
||||
slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
}))
|
||||
defer slowServer.Close()
|
||||
|
||||
hook.CallbackURLs = []string{slowServer.URL}
|
||||
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, th.BasicChannel)
|
||||
|
||||
// Wait for timeout
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
// Test with invalid JSON response
|
||||
invalidJSONServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{invalid json`))
|
||||
}))
|
||||
defer invalidJSONServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = 30
|
||||
})
|
||||
|
||||
hook.CallbackURLs = []string{invalidJSONServer.URL}
|
||||
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, th.BasicChannel)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Test JSON encoding error with bad content type
|
||||
hook.ContentType = "unknown"
|
||||
th.App.TriggerWebhook(th.Context, payload, hook, th.BasicPost, th.BasicChannel)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
Loading…
Reference in a new issue