[MM-58745] export/import: enable exporting and importing bots canonically (#28214)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
3049191e2f
Коммит
f41d2d7774
@@ -153,6 +153,13 @@ func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting bots")
|
||||
botPPs, err := a.exportAllBots(ctx, job, writer, opts.IncludeProfilePictures)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profilePictures = append(profilePictures, botPPs...)
|
||||
|
||||
ctx.Logger().Info("Bulk export: exporting posts")
|
||||
attachments, err := a.exportAllPosts(ctx, job, writer, opts.IncludeAttachments, opts.IncludeArchivedChannels)
|
||||
if err != nil {
|
||||
@@ -455,6 +462,11 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer,
|
||||
for _, user := range users {
|
||||
afterId = user.Id
|
||||
|
||||
// Skip bots as they are exported separately.
|
||||
if user.IsBot {
|
||||
continue
|
||||
}
|
||||
|
||||
// Gathering here the exportable preferences to pass them on to ImportLineFromUser
|
||||
exportedPrefs := make(map[string]*string)
|
||||
allPrefs, err := a.GetPreferencesForUser(ctx, user.Id)
|
||||
@@ -531,6 +543,64 @@ func (a *App) exportAllUsers(ctx request.CTX, job *model.Job, writer io.Writer,
|
||||
return profilePictures, nil
|
||||
}
|
||||
|
||||
func (a *App) exportAllBots(ctx request.CTX, job *model.Job, writer io.Writer, includeProfilePictures bool) ([]string, *model.AppError) {
|
||||
afterId := ""
|
||||
cnt := 0
|
||||
profilePictures := []string{}
|
||||
|
||||
const pageSize = 1000
|
||||
|
||||
for {
|
||||
bots, err := a.Srv().Store().Bot().GetAllAfter(pageSize, afterId)
|
||||
if err != nil {
|
||||
return profilePictures, model.NewAppError("exportAllBots", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
cnt += len(bots)
|
||||
updateJobProgress(ctx.Logger(), a.Srv().Store(), job, "bots_exported", cnt)
|
||||
|
||||
for _, bot := range bots {
|
||||
afterId = bot.UserId
|
||||
|
||||
var ownerUsername string
|
||||
owner, err := a.Srv().Store().User().Get(ctx.Context(), bot.OwnerId)
|
||||
if err != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
if errors.As(err, &nfErr) {
|
||||
ownerUsername = bot.OwnerId
|
||||
} else {
|
||||
return profilePictures, model.NewAppError("exportAllBots", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
} else {
|
||||
ownerUsername = owner.Username
|
||||
}
|
||||
|
||||
botLine := ImportLineFromBot(bot, ownerUsername)
|
||||
|
||||
if includeProfilePictures {
|
||||
pp, err := a.GetProfileImagePath(model.UserFromBot(bot))
|
||||
if err != nil {
|
||||
return profilePictures, err
|
||||
}
|
||||
if pp != "" {
|
||||
botLine.Bot.ProfileImage = &pp
|
||||
profilePictures = append(profilePictures, pp)
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.exportWriteLine(writer, botLine); err != nil {
|
||||
return profilePictures, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(bots) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return profilePictures, nil
|
||||
}
|
||||
|
||||
func (a *App) buildUserTeamAndChannelMemberships(c request.CTX, userID string, includeArchivedChannels bool) (*[]imports.UserTeamImportData, *model.AppError) {
|
||||
var memberships []imports.UserTeamImportData
|
||||
|
||||
|
||||
@@ -177,6 +177,19 @@ func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imp
|
||||
}
|
||||
}
|
||||
|
||||
func ImportLineFromBot(bot *model.Bot, ownerUsername string) *imports.LineImportData {
|
||||
return &imports.LineImportData{
|
||||
Type: "bot",
|
||||
Bot: &imports.BotImportData{
|
||||
Username: &bot.Username,
|
||||
Owner: &ownerUsername,
|
||||
DisplayName: &bot.DisplayName,
|
||||
Description: &bot.Description,
|
||||
DeleteAt: &bot.DeleteAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ImportUserTeamDataFromTeamMember(member *model.TeamMemberForExport) *imports.UserTeamImportData {
|
||||
rolesList := strings.Fields(member.Roles)
|
||||
if member.SchemeAdmin {
|
||||
|
||||
@@ -232,6 +232,41 @@ func TestExportAllUsers(t *testing.T) {
|
||||
assert.ElementsMatch(t, deletedUsers1, deletedUsers2)
|
||||
}
|
||||
|
||||
func TestExportAllBots(t *testing.T) {
|
||||
th1 := Setup(t)
|
||||
defer th1.TearDown()
|
||||
|
||||
u := th1.CreateUser()
|
||||
bot, err := th1.App.CreateBot(th1.Context, &model.Bot{
|
||||
Username: "bot_1",
|
||||
DisplayName: model.NewId(),
|
||||
OwnerId: u.Id,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
|
||||
var b bytes.Buffer
|
||||
err = th1.App.BulkExport(th1.Context, &b, "somePath", nil, model.BulkExportOpts{})
|
||||
require.Nil(t, err)
|
||||
|
||||
th2 := Setup(t)
|
||||
defer th2.TearDown()
|
||||
err, i := th2.App.BulkImport(th2.Context, &b, nil, false, 5)
|
||||
require.Nil(t, err)
|
||||
assert.EqualValues(t, 0, i)
|
||||
|
||||
u, err = th2.App.GetUserByUsername(u.Username)
|
||||
require.Nil(t, err)
|
||||
|
||||
bots, err := th2.App.GetBots(th2.Context, &model.BotGetOptions{
|
||||
OwnerId: u.Id,
|
||||
Page: 0,
|
||||
PerPage: 10,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
require.Len(t, bots, 1)
|
||||
assert.Equal(t, bot.Username, bots[0].Username)
|
||||
}
|
||||
|
||||
func TestExportDMChannel(t *testing.T) {
|
||||
t.Run("Export a DM channel to another server", func(t *testing.T) {
|
||||
th1 := Setup(t).InitBasic()
|
||||
|
||||
@@ -96,6 +96,16 @@ func processAttachments(c request.CTX, line *imports.LineImportData, basePath st
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bot":
|
||||
if line.Bot.ProfileImage != nil {
|
||||
path := filepath.Join(basePath, *line.Bot.ProfileImage)
|
||||
*line.Bot.ProfileImage = path
|
||||
if len(filesMap) > 0 {
|
||||
if line.Bot.ProfileImageData, ok = filesMap[path]; !ok {
|
||||
return fmt.Errorf("attachment %q not found in map", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "emoji":
|
||||
if line.Emoji.Image != nil {
|
||||
path := filepath.Join(basePath, *line.Emoji.Image)
|
||||
@@ -339,6 +349,11 @@ func (a *App) importLine(c request.CTX, line imports.LineImportData, dryRun bool
|
||||
return model.NewAppError("BulkImport", "app.import.import_line.null_user.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
return a.importUser(c, line.User, dryRun)
|
||||
case line.Type == "bot":
|
||||
if line.Bot == nil {
|
||||
return model.NewAppError("BulkImport", "app.import.import_line.null_bot.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
return a.importBot(c, line.Bot, dryRun)
|
||||
case line.Type == "direct_channel":
|
||||
if line.DirectChannel == nil {
|
||||
return model.NewAppError("BulkImport", "app.import.import_line.null_direct_channel.error", nil, "", http.StatusBadRequest)
|
||||
|
||||
@@ -629,41 +629,10 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
|
||||
savedUser = user
|
||||
}
|
||||
|
||||
if data.ProfileImage != nil {
|
||||
var file io.ReadSeeker
|
||||
var err error
|
||||
if data.ProfileImageData != nil {
|
||||
// *zip.File does not support Seek, and we need a seeker to reset the cursor position after checking the picture dimension
|
||||
var f io.ReadCloser
|
||||
f, err = data.ProfileImageData.Open()
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to open the profile image data.", mlog.Err(err))
|
||||
} else {
|
||||
limitedReader := io.LimitReader(f, *a.Config().FileSettings.MaxFileSize)
|
||||
var b []byte
|
||||
b, err = io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to read all bytes from profile picture.", mlog.Err(err))
|
||||
} else {
|
||||
file = bytes.NewReader(b)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
file, err = os.Open(*data.ProfileImage)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to open the profile image.", mlog.Err(err))
|
||||
} else {
|
||||
defer file.(*os.File).Close()
|
||||
}
|
||||
}
|
||||
|
||||
if file != nil {
|
||||
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
|
||||
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if err := a.SetProfileImageFromFile(rctx, savedUser.Id, file); err != nil {
|
||||
rctx.Logger().Warn("Unable to set the profile image from a file.", mlog.Err(err))
|
||||
}
|
||||
if data.Avatar.ProfileImage != nil {
|
||||
appErr := a.importProfileImage(rctx, savedUser.Id, &data.Avatar)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,6 +834,143 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
|
||||
return a.importUserTeams(rctx, savedUser, data.Teams)
|
||||
}
|
||||
|
||||
func (a *App) importBot(rctx request.CTX, data *imports.BotImportData, dryRun bool) *model.AppError {
|
||||
var fields []mlog.Field
|
||||
if data != nil && data.Username != nil {
|
||||
fields = append(fields, mlog.String("user_name", *data.Username))
|
||||
}
|
||||
rctx.Logger().Info("Validating bot", fields...)
|
||||
|
||||
if err := imports.ValidateBotImportData(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this is a Dry Run, do not continue any further.
|
||||
if dryRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
rctx.Logger().Info("Importing bot", fields...)
|
||||
|
||||
// We want to avoid database writes if nothing has changed.
|
||||
hasBotChanged := false
|
||||
|
||||
var bot *model.Bot
|
||||
var nErr error
|
||||
bot, nErr = a.Srv().Store().Bot().GetByUsername(*data.Username)
|
||||
if nErr != nil {
|
||||
bot = &model.Bot{}
|
||||
hasBotChanged = true
|
||||
}
|
||||
|
||||
bot.Username = *data.Username
|
||||
|
||||
if data.Description != nil && bot.Description != *data.Description {
|
||||
bot.Description = *data.Description
|
||||
hasBotChanged = true
|
||||
}
|
||||
|
||||
if data.DisplayName != nil && bot.DisplayName != *data.DisplayName {
|
||||
bot.DisplayName = *data.DisplayName
|
||||
hasBotChanged = true
|
||||
}
|
||||
|
||||
var owner *model.User
|
||||
if data.Owner != nil {
|
||||
owner, nErr = a.Srv().Store().User().GetByUsername(*data.Owner)
|
||||
if nErr != nil {
|
||||
var nfErr *store.ErrNotFound
|
||||
switch {
|
||||
case errors.As(nErr, &nfErr):
|
||||
// If the owner does not exist, we assume the owner is a plugin hence keeping the owner username as is.
|
||||
bot.OwnerId = *data.Owner
|
||||
default:
|
||||
return model.NewAppError("importBot", "app.import.import_bot.owner_could_not_found.error", map[string]any{"Owner": *data.Owner}, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
} else {
|
||||
bot.OwnerId = owner.Id
|
||||
}
|
||||
}
|
||||
|
||||
var savedBot *model.Bot
|
||||
if bot.UserId == "" {
|
||||
var appErr *model.AppError
|
||||
if savedBot, appErr = a.CreateBot(rctx, bot); appErr != nil {
|
||||
var appErr *model.AppError
|
||||
var invErr *store.ErrInvalidInput
|
||||
switch {
|
||||
case errors.As(appErr, &invErr):
|
||||
switch invErr.Field {
|
||||
case "username":
|
||||
return model.NewAppError("importUser", "app.user.save.username_exists.app_error", nil, "", http.StatusBadRequest).Wrap(appErr)
|
||||
default:
|
||||
return model.NewAppError("importUser", "app.user.save.existing.app_error", nil, "", http.StatusBadRequest).Wrap(appErr)
|
||||
}
|
||||
default:
|
||||
return appErr
|
||||
}
|
||||
}
|
||||
} else if hasBotChanged {
|
||||
var err error
|
||||
if savedBot, err = a.Srv().Store().Bot().Update(bot); err != nil {
|
||||
return model.NewAppError("importBot", "app.bot.update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if savedBot == nil {
|
||||
savedBot = bot
|
||||
}
|
||||
|
||||
if data.Avatar.ProfileImage != nil {
|
||||
appErr := a.importProfileImage(rctx, savedBot.UserId, &data.Avatar)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) importProfileImage(rctx request.CTX, userID string, data *imports.Avatar) *model.AppError {
|
||||
var file io.ReadSeeker
|
||||
var err error
|
||||
if data.ProfileImageData != nil {
|
||||
// *zip.File does not support Seek, and we need a seeker to reset the cursor position after checking the picture dimension
|
||||
var f io.ReadCloser
|
||||
f, err = data.ProfileImageData.Open()
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to open the profile image data.", mlog.Err(err))
|
||||
} else {
|
||||
limitedReader := io.LimitReader(f, *a.Config().FileSettings.MaxFileSize)
|
||||
var b []byte
|
||||
b, err = io.ReadAll(limitedReader)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to read all bytes from profile picture.", mlog.Err(err))
|
||||
} else {
|
||||
file = bytes.NewReader(b)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
file, err = os.Open(*data.ProfileImage)
|
||||
if err != nil {
|
||||
rctx.Logger().Warn("Unable to open the profile image.", mlog.Err(err))
|
||||
} else {
|
||||
defer file.(*os.File).Close()
|
||||
}
|
||||
}
|
||||
|
||||
if file != nil {
|
||||
if limitErr := checkImageLimits(file, *a.Config().FileSettings.MaxImageResolution); limitErr != nil {
|
||||
return model.NewAppError("SetProfileImage", "api.user.upload_profile_user.check_image_limits.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
if err := a.SetProfileImageFromFile(rctx, userID, file); err != nil {
|
||||
rctx.Logger().Warn("Unable to set the profile image from a file.", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) importUserTeams(rctx request.CTX, user *model.User, data *[]imports.UserTeamImportData) *model.AppError {
|
||||
if data == nil {
|
||||
return nil
|
||||
|
||||
@@ -795,13 +795,15 @@ func TestImportImportUser(t *testing.T) {
|
||||
username := model.NewUsername()
|
||||
testsDir, _ := fileutils.FindDir("tests")
|
||||
data = imports.UserImportData{
|
||||
ProfileImage: model.NewPointer(filepath.Join(testsDir, "test.png")),
|
||||
Username: &username,
|
||||
Email: model.NewPointer(model.NewId() + "@example.com"),
|
||||
Nickname: model.NewPointer(model.NewId()),
|
||||
FirstName: model.NewPointer(model.NewId()),
|
||||
LastName: model.NewPointer(model.NewId()),
|
||||
Position: model.NewPointer(model.NewId()),
|
||||
Avatar: imports.Avatar{
|
||||
ProfileImage: model.NewPointer(filepath.Join(testsDir, "test.png")),
|
||||
},
|
||||
Username: &username,
|
||||
Email: model.NewPointer(model.NewId() + "@example.com"),
|
||||
Nickname: model.NewPointer(model.NewId()),
|
||||
FirstName: model.NewPointer(model.NewId()),
|
||||
LastName: model.NewPointer(model.NewId()),
|
||||
Position: model.NewPointer(model.NewId()),
|
||||
}
|
||||
appErr = th.App.importUser(th.Context, &data, false)
|
||||
require.Nil(t, appErr, "Should have succeeded to import valid user.")
|
||||
|
||||
@@ -304,7 +304,9 @@ func TestProcessAttachments(t *testing.T) {
|
||||
userLine := imports.LineImportData{
|
||||
Type: "user",
|
||||
User: &imports.UserImportData{
|
||||
ProfileImage: model.NewPointer("profile.jpg"),
|
||||
Avatar: imports.Avatar{
|
||||
ProfileImage: model.NewPointer("profile.jpg"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ type LineImportData struct {
|
||||
Team *TeamImportData `json:"team,omitempty"`
|
||||
Channel *ChannelImportData `json:"channel,omitempty"`
|
||||
User *UserImportData `json:"user,omitempty"`
|
||||
Bot *BotImportData `json:"bot,omitempty"`
|
||||
Post *PostImportData `json:"post,omitempty"`
|
||||
DirectChannel *DirectChannelImportData `json:"direct_channel,omitempty"`
|
||||
DirectPost *DirectPostImportData `json:"direct_post,omitempty"`
|
||||
@@ -54,24 +55,28 @@ type ChannelImportData struct {
|
||||
DeletedAt *int64 `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type Avatar struct {
|
||||
ProfileImage *string `json:"profile_image,omitempty"`
|
||||
ProfileImageData *zip.File `json:"-"`
|
||||
}
|
||||
|
||||
type UserImportData struct {
|
||||
ProfileImage *string `json:"profile_image,omitempty"`
|
||||
ProfileImageData *zip.File `json:"-"`
|
||||
Username *string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
AuthService *string `json:"auth_service"`
|
||||
AuthData *string `json:"auth_data,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
Nickname *string `json:"nickname"`
|
||||
FirstName *string `json:"first_name"`
|
||||
LastName *string `json:"last_name"`
|
||||
Position *string `json:"position"`
|
||||
Roles *string `json:"roles"`
|
||||
Locale *string `json:"locale"`
|
||||
UseMarkdownPreview *string `json:"feature_enabled_markdown_preview,omitempty"`
|
||||
UseFormatting *string `json:"formatting,omitempty"`
|
||||
ShowUnreadSection *string `json:"show_unread_section,omitempty"`
|
||||
DeleteAt *int64 `json:"delete_at,omitempty"`
|
||||
Avatar
|
||||
Username *string `json:"username"`
|
||||
Email *string `json:"email"`
|
||||
AuthService *string `json:"auth_service"`
|
||||
AuthData *string `json:"auth_data,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
Nickname *string `json:"nickname"`
|
||||
FirstName *string `json:"first_name"`
|
||||
LastName *string `json:"last_name"`
|
||||
Position *string `json:"position"`
|
||||
Roles *string `json:"roles"`
|
||||
Locale *string `json:"locale"`
|
||||
UseMarkdownPreview *string `json:"feature_enabled_markdown_preview,omitempty"`
|
||||
UseFormatting *string `json:"formatting,omitempty"`
|
||||
ShowUnreadSection *string `json:"show_unread_section,omitempty"`
|
||||
DeleteAt *int64 `json:"delete_at,omitempty"`
|
||||
|
||||
SendOnCtrlEnter *string `json:"send_on_ctrl_enter,omitempty"`
|
||||
CodeBlockCtrlEnter *string `json:"code_block_ctrl_enter,omitempty"`
|
||||
@@ -97,6 +102,15 @@ type UserImportData struct {
|
||||
CustomStatus *model.CustomStatus `json:"custom_status,omitempty"`
|
||||
}
|
||||
|
||||
type BotImportData struct {
|
||||
Avatar
|
||||
Username *string `json:"username"`
|
||||
Owner *string `json:"owner"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
DeleteAt *int64 `json:"delete_at,omitempty"`
|
||||
}
|
||||
|
||||
type UserNotifyPropsImportData struct {
|
||||
Desktop *string `json:"desktop"`
|
||||
DesktopSound *string `json:"desktop_sound"`
|
||||
|
||||
@@ -192,6 +192,8 @@ func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
|
||||
func ValidateUserImportData(data *UserImportData) *model.AppError {
|
||||
if data.ProfileImage != nil && data.ProfileImageData == nil {
|
||||
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
} else if err != nil {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
}
|
||||
@@ -312,6 +314,34 @@ func ValidateUserImportData(data *UserImportData) *model.AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateBotImportData(data *BotImportData) *model.AppError {
|
||||
if data.ProfileImage != nil && data.ProfileImageData == nil {
|
||||
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
|
||||
} else if err != nil {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if data.Username == nil {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest)
|
||||
} else if !model.IsValidUsername(*data.Username) {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if data.DisplayName != nil && utf8.RuneCountInString(*data.DisplayName) > model.UserFirstNameMaxRunes {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if data.Owner == nil {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_bot_import_data.owner_missing.error", nil, "", http.StatusBadRequest)
|
||||
} else if !model.IsValidUsername(*data.Owner) {
|
||||
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var validAuthServices = []string{
|
||||
"",
|
||||
model.UserAuthServiceEmail,
|
||||
|
||||
@@ -532,17 +532,19 @@ func TestImportValidateUserImportData(t *testing.T) {
|
||||
// Test a valid User with all fields populated.
|
||||
testsDir, _ := fileutils.FindDir("tests")
|
||||
data = UserImportData{
|
||||
ProfileImage: model.NewPointer(filepath.Join(testsDir, "test.png")),
|
||||
Username: model.NewPointer("bob"),
|
||||
Email: model.NewPointer("bob@example.com"),
|
||||
AuthService: model.NewPointer("ldap"),
|
||||
AuthData: model.NewPointer("bob"),
|
||||
Nickname: model.NewPointer("BobNick"),
|
||||
FirstName: model.NewPointer("Bob"),
|
||||
LastName: model.NewPointer("Blob"),
|
||||
Position: model.NewPointer("The Boss"),
|
||||
Roles: model.NewPointer("system_user"),
|
||||
Locale: model.NewPointer("en"),
|
||||
Avatar: Avatar{
|
||||
ProfileImage: model.NewPointer(filepath.Join(testsDir, "test.png")),
|
||||
},
|
||||
Username: model.NewPointer("bob"),
|
||||
Email: model.NewPointer("bob@example.com"),
|
||||
AuthService: model.NewPointer("ldap"),
|
||||
AuthData: model.NewPointer("bob"),
|
||||
Nickname: model.NewPointer("BobNick"),
|
||||
FirstName: model.NewPointer("Bob"),
|
||||
LastName: model.NewPointer("Blob"),
|
||||
Position: model.NewPointer("The Boss"),
|
||||
Roles: model.NewPointer("system_user"),
|
||||
Locale: model.NewPointer("en"),
|
||||
}
|
||||
err = ValidateUserImportData(&data)
|
||||
require.Nil(t, err, "Validation failed but should have been valid.")
|
||||
@@ -671,6 +673,56 @@ func TestImportValidateUserAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportValidateBotImportData(t *testing.T) {
|
||||
// Test with minimum required valid properties.
|
||||
data := BotImportData{
|
||||
Username: model.NewPointer("bob"),
|
||||
DisplayName: model.NewPointer("Display Name"),
|
||||
Owner: model.NewPointer("owner"),
|
||||
}
|
||||
err := ValidateBotImportData(&data)
|
||||
require.Nil(t, err, "Validation failed but should have been valid.")
|
||||
|
||||
// Test with various invalid names.
|
||||
data.Username = nil
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to nil Username.")
|
||||
|
||||
data.Username = model.NewPointer("")
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to 0 length Username.")
|
||||
|
||||
data.Username = model.NewPointer(strings.Repeat("abcdefghij", 7))
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to too long Username.")
|
||||
|
||||
data.Username = model.NewPointer("i am a username with spaces and !!!")
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to invalid characters in Username.")
|
||||
|
||||
data.Username = model.NewPointer("bob")
|
||||
|
||||
// Invalid Display Name.
|
||||
data.DisplayName = model.NewPointer(strings.Repeat("abcdefghij", 7))
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to too long DisplayName.")
|
||||
|
||||
data.DisplayName = model.NewPointer("Display Name")
|
||||
|
||||
// Invalid Owner Name.
|
||||
data.Owner = nil
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to too long DisplayName.")
|
||||
|
||||
data.Owner = model.NewPointer("")
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to too long DisplayName.")
|
||||
|
||||
data.Owner = model.NewPointer(strings.Repeat("abcdefghij", 7))
|
||||
err = ValidateBotImportData(&data)
|
||||
require.NotNil(t, err, "Should have failed due to too long OwnerID.")
|
||||
}
|
||||
|
||||
func TestImportValidateUserTeamsImportData(t *testing.T) {
|
||||
// Invalid Name.
|
||||
data := []UserTeamImportData{
|
||||
|
||||
Ссылка в новой задаче
Block a user