mattermost/server/channels/app/plugin_file_download_hook_test.go
Felipe Martin 1be8a68dd7
feat: pluginapi: filewillbedownloaded / sendtoastmessage (#34596)
* feat: filewillbedonwloaded hook

* feat: error popup

* chore: make generated pluginapi

* tests

* feat: different errors for different download types

* feat: allow toast positions

* fix: avoid using deprecated i18n function

* feat: add plugin API to show toasts

* feat: downloadType parameter

* tests: updated tests

* chore: make check-style

* chore: i18n

* chore: missing fields in tests

* chore: sorted i18n for webapp

* chore: run mmjstool

* test: fixed webapp tests with new changes

* test: missing mocks

* fix: ensure one-file attachments (previews) are handler properly as thumbnails

* chore: lint

* test: added new logic to tests

* chore: lint

* Add SendToastMessage API and FileWillBeDownloaded hook

- Introduced SendToastMessage method for sending toast notifications to users with customizable options.
- Added FileWillBeDownloaded hook to handle file download requests, allowing plugins to control access to files.
- Updated related types and constants for file download handling.
- Enhanced PluginSettings to include HookTimeoutSeconds for better timeout management.

* Update webapp/channels/src/components/single_image_view/single_image_view.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: copilot reviews

* test: head requests

* chore: linted the webapp

* tests: fixed path

* test: fixed mocked args

* allow sending message to a connection directly

* fix: hook thread safety

* chore: formatting

* chore: remove configuration from system console

* chore: release version

* chore: update signature

* chore: update release version

* chore: addressed comments

* fix: update file rejection handling to use 403 Forbidden status and include rejection reason header

* Fix nil pointer panic in runFileWillBeDownloadedHook

The atomic.Value in runFileWillBeDownloadedHook can be nil if no
plugins implement the FileWillBeDownloaded hook. This causes a panic
when trying to assert the nil interface to string.

This fix adds a nil check before the type assertion, defaulting to
an empty string (which allows the download) when no hooks have run.

Fixes:
- TestUploadDataMultipart/success panic
- TestUploadDataMultipart/resume_success panic

* test: move the logout test last

* chore: restored accidential deletion

* chore: lint

* chore: make generated

* refactor: move websocket events to new package

* chore: go vet

* chore: missing mock

* chore: revert incorrect fmt

* chore: import ordering

* chore: npm i18n-extract

* chore: update constants.tsx from master

* chore: make i18n-extract

* revert: conflict merge

* fix: add missing isFileRejected prop to SingleImageView tests

* fix: mock fetch in SingleImageView tests for async thumbnail check

The component now performs an async fetch to check thumbnail availability
before rendering. Tests need to mock fetch and use waitFor to handle
the async state updates.

* refactor: move hook logic to app layer

* chore: update version to 11.5

* Scope file download rejection toast to the requesting connection

Thread the Connection-Id header through RunFileWillBeDownloadedHook and
sendFileDownloadRejectedEvent so the WebSocket event is sent only to the
connection that initiated the download, instead of all connections for
the user.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-16 17:10:39 +01:00

529 lines
19 KiB
Go

// 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/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/plugin/plugintest"
)
func TestHookFileWillBeDownloaded(t *testing.T) {
mainHelper.Parallel(t)
t.Run("rejected", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
// Allow any logging calls (not verified in this test)
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Rejecting file download", "file_id", info.Id, "user_id", userID)
return "Download blocked by security policy"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file first
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info to pass to the hook
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call the hook through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// Verify the file download was rejected
assert.NotEmpty(t, rejectionReason)
assert.Contains(t, rejectionReason, "blocked by security policy")
mockAPI.AssertExpectations(t)
})
t.Run("allowed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
// Allow any logging calls (not verified in this test)
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Allowing file download", "file_id", info.Id, "user_id", userID)
// Return empty string to allow download
return ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call the hook through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// Verify the file download was allowed
assert.Empty(t, rejectionReason)
mockAPI.AssertExpectations(t)
})
t.Run("multiple plugins - first rejects", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI1 plugintest.API
mockAPI1.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
// Allow any logging calls (not verified in this test)
mockAPI1.On("LogInfo", mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogWarn", mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogWarn", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI1.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
var mockAPI2 plugintest.API
mockAPI2.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
// This plugin should NOT be called because first one rejects
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
// First plugin - rejects
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogWarn("First plugin rejecting", "file_id", info.Id)
return "Rejected by first plugin"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
// Second plugin - should not be called
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Second plugin should not be called")
return ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API {
// Alternate between the two mock APIs
return &mockAPI1
})
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call hooks - first one should reject, second should not be called
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
assert.NotEmpty(t, rejectionReason)
assert.Contains(t, rejectionReason, "Rejected by first plugin")
// Only first mock API should have been called
mockAPI1.AssertExpectations(t)
// mockAPI2 should not have been called (no expectations set)
})
t.Run("no plugins installed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
// No plugins - hook should return empty
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// No plugins means no rejection
assert.Empty(t, rejectionReason)
})
}
// TestHookFileWillBeDownloadedHeadRequests tests that HEAD requests trigger the FileWillBeDownloaded hook
func TestHookFileWillBeDownloadedHeadRequests(t *testing.T) {
mainHelper.Parallel(t)
t.Run("HEAD request to file endpoint triggers hook - rejection", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Blocking file download", "file_id", info.Id, "download_type", string(downloadType))
return "File download blocked by security policy"
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file first
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info to pass to the hook
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call the hook through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// Verify the file download was rejected
assert.NotEmpty(t, rejectionReason)
assert.Contains(t, rejectionReason, "blocked by security policy")
mockAPI.AssertExpectations(t)
})
t.Run("HEAD request to thumbnail endpoint triggers hook - rejection", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
// Only block thumbnail requests
if downloadType == model.FileDownloadTypeThumbnail {
p.API.LogInfo("Blocking thumbnail download", "file_id", info.Id)
return "Thumbnail download blocked"
}
return ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call the hook for thumbnail download type through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeThumbnail)
// Verify the thumbnail download was rejected
assert.NotEmpty(t, rejectionReason)
assert.Contains(t, rejectionReason, "Thumbnail download blocked")
mockAPI.AssertExpectations(t)
})
t.Run("HEAD request to preview endpoint triggers hook - rejection", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogWarn", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
// Only block preview requests
if downloadType == model.FileDownloadTypePreview {
p.API.LogInfo("Blocking preview download", "file_id", info.Id)
return "Preview download blocked"
}
return ""
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Create plugin context
// Call the hook for preview download type through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypePreview)
// Verify the preview download was rejected
assert.NotEmpty(t, rejectionReason)
assert.Contains(t, rejectionReason, "Preview download blocked")
mockAPI.AssertExpectations(t)
})
t.Run("HEAD request to file endpoint triggers hook - allowed", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Allowing file download", "file_id", info.Id, "download_type", string(downloadType))
return "" // Allow download
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
require.NotNil(t, fileInfo)
// Get the file info
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Call the hook through the app method
rejectionReason := th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// Verify the file download was allowed
assert.Empty(t, rejectionReason)
mockAPI.AssertExpectations(t)
})
t.Run("HEAD and GET with different download types", func(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
downloadTypesReceived := []model.FileDownloadType{}
var mockAPI plugintest.API
mockAPI.On("LoadPluginConfiguration", mock.Anything).Return(nil).Maybe()
mockAPI.On("LogInfo", mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
if args.String(0) == "Hook called" {
downloadType := args.String(2)
downloadTypesReceived = append(downloadTypesReceived, model.FileDownloadType(downloadType))
}
}).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
mockAPI.On("LogInfo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe().Return(nil)
tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{
`
package main
import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/model"
)
type MyPlugin struct {
plugin.MattermostPlugin
}
func (p *MyPlugin) FileWillBeDownloaded(c *plugin.Context, info *model.FileInfo, userID string, downloadType model.FileDownloadType) string {
p.API.LogInfo("Hook called", "download_type", string(downloadType))
return "" // Allow download
}
func main() {
plugin.ClientMain(&MyPlugin{})
}
`,
}, th.App, func(*model.Manifest) plugin.API { return &mockAPI })
defer tearDown()
// Upload a file
fileInfo, appErr := th.App.UploadFile(th.Context, []byte("test content"), th.BasicChannel.Id, "test.txt")
require.Nil(t, appErr)
info, appErr := th.App.GetFileInfo(th.Context, fileInfo.Id)
require.Nil(t, appErr)
// Test File download type
th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeFile)
// Test Thumbnail download type
th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypeThumbnail)
// Test Preview download type
th.App.RunFileWillBeDownloadedHook(th.Context, info, th.BasicUser.Id, "", model.FileDownloadTypePreview)
// Verify all three download types were received
assert.Len(t, downloadTypesReceived, 3)
assert.Contains(t, downloadTypesReceived, model.FileDownloadTypeFile)
assert.Contains(t, downloadTypesReceived, model.FileDownloadTypeThumbnail)
assert.Contains(t, downloadTypesReceived, model.FileDownloadTypePreview)
mockAPI.AssertExpectations(t)
})
}