This commit is contained in:
Pavel Zeman 2026-05-25 10:10:25 +00:00 committed by GitHub
commit 53d80cf5d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 2886 additions and 0 deletions

View 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")
})
}

View 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)
}
}
}
}
})
}

View 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)
})
}

View 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
}

View 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)
}

View 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)
}