From 37642a4f1e99b2cb89fa6969ad34a892fabab5be Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Wed, 28 Jun 2017 15:26:38 +0100 Subject: PLT-6937: Bulk Importing of Direct/Group channels and posts. (#6761) * PLT-6937: Bulk Importing of Direct/Group channels and posts. * Show group/direct channels in sidebar. --- app/channel.go | 71 +++-- app/import.go | 240 ++++++++++++++++- app/import_test.go | 779 ++++++++++++++++++++++++++++++++++++++++++++++++++++- i18n/en.json | 88 ++++++ 4 files changed, 1146 insertions(+), 32 deletions(-) diff --git a/app/channel.go b/app/channel.go index 794379369..c9f89eb1a 100644 --- a/app/channel.go +++ b/app/channel.go @@ -160,6 +160,27 @@ func CreateChannel(channel *model.Channel, addMember bool) (*model.Channel, *mod } func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) { + if channel, err := createDirectChannel(userId, otherUserId); err != nil { + if err.Id == store.CHANNEL_EXISTS_ERROR { + return channel, nil + } else { + return nil, err + } + } else { + WaitForChannelMembership(channel.Id, userId) + + InvalidateCacheForUser(userId) + InvalidateCacheForUser(otherUserId) + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) + message.Add("teammate_id", otherUserId) + Publish(message) + + return channel, nil + } +} + +func createDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) { uc1 := Srv.Store.User().Get(userId) uc2 := Srv.Store.User().Get(otherUserId) @@ -173,22 +194,12 @@ func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *mo if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil { if result.Err.Id == store.CHANNEL_EXISTS_ERROR { - return result.Data.(*model.Channel), nil + return result.Data.(*model.Channel), result.Err } else { return nil, result.Err } } else { channel := result.Data.(*model.Channel) - - WaitForChannelMembership(channel.Id, userId) - - InvalidateCacheForUser(userId) - InvalidateCacheForUser(otherUserId) - - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil) - message.Add("teammate_id", otherUserId) - Publish(message) - return channel, nil } } @@ -219,6 +230,30 @@ func WaitForChannelMembership(channelId string, userId string) { } func CreateGroupChannel(userIds []string, creatorId string) (*model.Channel, *model.AppError) { + if channel, err := createGroupChannel(userIds, creatorId); err != nil { + if err.Id == store.CHANNEL_EXISTS_ERROR { + return channel, nil + } else { + return nil, err + } + } else { + for _, userId := range userIds { + if userId == creatorId { + WaitForChannelMembership(channel.Id, creatorId) + } + + InvalidateCacheForUser(userId) + } + + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_GROUP_ADDED, "", channel.Id, "", nil) + message.Add("teammate_ids", model.ArrayToJson(userIds)) + Publish(message) + + return channel, nil + } +} + +func createGroupChannel(userIds []string, creatorId string) (*model.Channel, *model.AppError) { if len(userIds) > model.CHANNEL_GROUP_MAX_USERS || len(userIds) < model.CHANNEL_GROUP_MIN_USERS { return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_size.app_error", nil, "", http.StatusBadRequest) } @@ -231,7 +266,7 @@ func CreateGroupChannel(userIds []string, creatorId string) (*model.Channel, *mo } if len(users) != len(userIds) { - return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids="+model.ArrayToJson(userIds), http.StatusBadRequest) + return nil, model.NewAppError("CreateGroupChannel", "api.channel.create_group.bad_user.app_error", nil, "user_ids=" + model.ArrayToJson(userIds), http.StatusBadRequest) } group := &model.Channel{ @@ -242,7 +277,7 @@ func CreateGroupChannel(userIds []string, creatorId string) (*model.Channel, *mo if result := <-Srv.Store.Channel().Save(group); result.Err != nil { if result.Err.Id == store.CHANNEL_EXISTS_ERROR { - return result.Data.(*model.Channel), nil + return result.Data.(*model.Channel), result.Err } else { return nil, result.Err } @@ -260,18 +295,8 @@ func CreateGroupChannel(userIds []string, creatorId string) (*model.Channel, *mo if result := <-Srv.Store.Channel().SaveMember(cm); result.Err != nil { return nil, result.Err } - - if user.Id == creatorId { - WaitForChannelMembership(group.Id, creatorId) - } - - InvalidateCacheForUser(user.Id) } - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_GROUP_ADDED, "", group.Id, "", nil) - message.Add("teammate_ids", model.ArrayToJson(userIds)) - Publish(message) - return channel, nil } } diff --git a/app/import.go b/app/import.go index 156591c0b..0a607e68e 100644 --- a/app/import.go +++ b/app/import.go @@ -17,17 +17,20 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + "github.com/mattermost/platform/store" ) // Import Data Models type LineImportData struct { - Type string `json:"type"` - Team *TeamImportData `json:"team"` - Channel *ChannelImportData `json:"channel"` - User *UserImportData `json:"user"` - Post *PostImportData `json:"post"` - Version *int `json:"version"` + Type string `json:"type"` + Team *TeamImportData `json:"team"` + Channel *ChannelImportData `json:"channel"` + User *UserImportData `json:"user"` + Post *PostImportData `json:"post"` + DirectChannel *DirectChannelImportData `json:"direct_channel"` + DirectPost *DirectPostImportData `json:"direct_post"` + Version *int `json:"version"` } type TeamImportData struct { @@ -97,6 +100,20 @@ type PostImportData struct { CreateAt *int64 `json:"create_at"` } +type DirectChannelImportData struct { + Members *[]string `json:"members"` + + Header *string `json:"header"` +} + +type DirectPostImportData struct { + ChannelMembers *[]string `json:"channel_members"` + User *string `json:"user"` + + Message *string `json:"message"` + CreateAt *int64 `json:"create_at"` +} + type LineImportWorkerData struct { LineImportData LineNumber int @@ -233,6 +250,18 @@ func ImportLine(line LineImportData, dryRun bool) *model.AppError { } else { return ImportPost(line.Post, 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) + } else { + return ImportDirectChannel(line.DirectChannel, dryRun) + } + case line.Type == "direct_post": + if line.DirectPost == nil { + return model.NewAppError("BulkImport", "app.import.import_line.null_direct_post.error", nil, "", http.StatusBadRequest) + } else { + return ImportDirectPost(line.DirectPost, dryRun) + } default: return model.NewLocAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "") } @@ -945,6 +974,205 @@ func validatePostImportData(data *PostImportData) *model.AppError { return nil } +func ImportDirectChannel(data *DirectChannelImportData, dryRun bool) *model.AppError { + if err := validateDirectChannelImportData(data); err != nil { + return err + } + + // If this is a Dry Run, do not continue any further. + if dryRun { + return nil + } + + var userIds []string + for _, username := range *data.Members { + if result := <-Srv.Store.User().GetByUsername(username); result.Err == nil { + user := result.Data.(*model.User) + userIds = append(userIds, user.Id) + } else { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.member_not_found.error", nil, "", http.StatusBadRequest) + } + } + + var channel *model.Channel + + if len(userIds) == 2 { + ch, err := createDirectChannel(userIds[0], userIds[1]) + if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, "", http.StatusBadRequest) + } else { + channel = ch + } + } else { + ch, err := createGroupChannel(userIds, userIds[0]) + if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest) + } else { + channel = ch + } + } + + for _, userId := range userIds { + preferences := model.Preferences{ + model.Preference{ + UserId: userId, + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Name: channel.Id, + Value: "true", + }, + } + if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil { + result.Err.StatusCode = http.StatusBadRequest + return result.Err + } + } + + if data.Header != nil { + channel.Header = *data.Header + if result := <-Srv.Store.Channel().Update(channel); result.Err != nil { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.update_header_failed.error", nil, "", http.StatusBadRequest) + } + } + + return nil +} + +func validateDirectChannelImportData(data *DirectChannelImportData) *model.AppError { + if data.Members == nil { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_required.error", nil, "") + } + + if len(*data.Members) != 2 { + if len(*data.Members) < model.CHANNEL_GROUP_MIN_USERS { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_few.error", nil, "") + } else if len(*data.Members) > model.CHANNEL_GROUP_MAX_USERS { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_many.error", nil, "") + } + } + + if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.CHANNEL_HEADER_MAX_RUNES { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_channel_import_data.header_length.error", nil, "") + } + + return nil +} + +func ImportDirectPost(data *DirectPostImportData, dryRun bool) *model.AppError { + if err := validateDirectPostImportData(data); err != nil { + return err + } + + // If this is a Dry Run, do not continue any further. + if dryRun { + return nil + } + + var userIds []string + for _, username := range *data.ChannelMembers { + if result := <-Srv.Store.User().GetByUsername(username); result.Err == nil { + user := result.Data.(*model.User) + userIds = append(userIds, user.Id) + } else { + return model.NewAppError("BulkImport", "app.import.import_direct_post.channel_member_not_found.error", nil, "", http.StatusBadRequest) + } + } + + var channel *model.Channel + if len(userIds) == 2 { + ch, err := createDirectChannel(userIds[0], userIds[1]) + if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_direct_channel.error", nil, "", http.StatusBadRequest) + } else { + channel = ch + } + } else { + ch, err := createGroupChannel(userIds, userIds[0]) + if err != nil && err.Id != store.CHANNEL_EXISTS_ERROR { + return model.NewAppError("BulkImport", "app.import.import_direct_channel.create_group_channel.error", nil, "", http.StatusBadRequest) + } else { + channel = ch + } + } + + var user *model.User + if result := <-Srv.Store.User().GetByUsername(*data.User); result.Err != nil { + return model.NewAppError("BulkImport", "app.import.import_direct_post.user_not_found.error", map[string]interface{}{"Username": *data.User}, "", http.StatusBadRequest) + } else { + user = result.Data.(*model.User) + } + + // Check if this post already exists. + var posts []*model.Post + if result := <-Srv.Store.Post().GetPostsCreatedAt(channel.Id, *data.CreateAt); result.Err != nil { + return result.Err + } else { + posts = result.Data.([]*model.Post) + } + + var post *model.Post + for _, p := range posts { + if p.Message == *data.Message { + post = p + break + } + } + + if post == nil { + post = &model.Post{} + } + + post.ChannelId = channel.Id + post.Message = *data.Message + post.UserId = user.Id + post.CreateAt = *data.CreateAt + + post.Hashtags, _ = model.ParseHashtags(post.Message) + + if post.Id == "" { + if result := <-Srv.Store.Post().Save(post); result.Err != nil { + return result.Err + } + } else { + if result := <-Srv.Store.Post().Overwrite(post); result.Err != nil { + return result.Err + } + } + + return nil +} + +func validateDirectPostImportData(data *DirectPostImportData) *model.AppError { + if data.ChannelMembers == nil { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_required.error", nil, "") + } + + if len(*data.ChannelMembers) != 2 { + if len(*data.ChannelMembers) < model.CHANNEL_GROUP_MIN_USERS { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_few.error", nil, "") + } else if len(*data.ChannelMembers) > model.CHANNEL_GROUP_MAX_USERS { + return model.NewLocAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_many.error", nil, "") + } + } + + if data.User == nil { + return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.user_missing.error", nil, "", http.StatusBadRequest) + } + + if data.Message == nil { + return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_missing.error", nil, "", http.StatusBadRequest) + } else if utf8.RuneCountInString(*data.Message) > model.POST_MESSAGE_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_length.error", nil, "", http.StatusBadRequest) + } + + if data.CreateAt == nil { + return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest) + } else if *data.CreateAt == 0 { + return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest) + } + + return nil +} + // // -- Old SlackImport Functions -- // Import functions are sutible for entering posts and users into the database without diff --git a/app/import_test.go b/app/import_test.go index 27cd9f551..365383bad 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -4,11 +4,13 @@ package app import ( - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" "runtime/debug" "strings" "testing" + + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" ) func ptrStr(s string) *string { @@ -571,6 +573,234 @@ func TestImportValidatePostImportData(t *testing.T) { } } +func TestImportValidateDirectChannelImportData(t *testing.T) { + + // Test with valid number of members for direct message. + data := DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + model.NewId(), + }, + } + if err := validateDirectChannelImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with valid number of members for group message. + data = DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + model.NewId(), + model.NewId(), + }, + } + if err := validateDirectChannelImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with all the combinations of optional parameters. + data = DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + model.NewId(), + }, + Header: ptrStr("Channel Header Here"), + } + if err := validateDirectChannelImportData(&data); err != nil { + t.Fatal("Should have succeeded with valid optional properties.") + } + + // Test with invalid Header. + data.Header = ptrStr(strings.Repeat("abcdefghij ", 103)) + if err := validateDirectChannelImportData(&data); err == nil { + t.Fatal("Should have failed due to too long header.") + } + + // Test with different combinations of invalid member counts. + data = DirectChannelImportData{ + Members: &[]string{}, + } + if err := validateDirectChannelImportData(&data); err == nil { + t.Fatal("Validation should have failed due to invalid number of members.") + } + + data = DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + }, + } + if err := validateDirectChannelImportData(&data); err == nil { + t.Fatal("Validation should have failed due to invalid number of members.") + } + + data = DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + }, + } + if err := validateDirectChannelImportData(&data); err == nil { + t.Fatal("Validation should have failed due to invalid number of members.") + } +} + +func TestImportValidateDirectPostImportData(t *testing.T) { + + // Test with minimum required valid properties. + data := DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with missing required properties. + data = DirectPostImportData{ + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to missing required property.") + } + + // Test with invalid numbers of channel members. + data = DirectPostImportData{ + ChannelMembers: &[]string{}, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to unsuitable number of members.") + } + + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to unsuitable number of members.") + } + + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to unsuitable number of members.") + } + + // Test with group message number of members. + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test with invalid message. + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr(strings.Repeat("1234567890", 500)), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to too long message.") + } + + // Test with invalid CreateAt + data = DirectPostImportData{ + ChannelMembers: &[]string{ + model.NewId(), + model.NewId(), + }, + User: ptrStr("username"), + Message: ptrStr("message"), + CreateAt: ptrInt64(0), + } + if err := validateDirectPostImportData(&data); err == nil { + t.Fatal("Should have failed due to 0 create-at value.") + } +} + func TestImportImportTeam(t *testing.T) { _ = Setup() @@ -1719,6 +1949,529 @@ func TestImportImportPost(t *testing.T) { } } +func TestImportImportDirectChannel(t *testing.T) { + th := Setup().InitBasic() + + // Check how many channels are in the database. + var directChannelCount int64 + if r := <-Srv.Store.Channel().AnalyticsTypeCount("", model.CHANNEL_DIRECT); r.Err == nil { + directChannelCount = r.Data.(int64) + } else { + t.Fatalf("Failed to get direct channel count.") + } + + var groupChannelCount int64 + if r := <-Srv.Store.Channel().AnalyticsTypeCount("", model.CHANNEL_GROUP); r.Err == nil { + groupChannelCount = r.Data.(int64) + } else { + t.Fatalf("Failed to get group channel count.") + } + + // Do an invalid channel in dry-run mode. + data := DirectChannelImportData{ + Members: &[]string{ + model.NewId(), + }, + Header: ptrStr("Channel Header"), + } + if err := ImportDirectChannel(&data, true); err == nil { + t.Fatalf("Expected error due to invalid name.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do a valid DIRECT channel with a nonexistent member in dry-run mode. + data.Members = &[]string{ + model.NewId(), + model.NewId(), + } + if err := ImportDirectChannel(&data, true); err != nil { + t.Fatalf("Expected success as cannot validate existance of channel members in dry run mode.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do a valid GROUP channel with a nonexistent member in dry-run mode. + data.Members = &[]string{ + model.NewId(), + model.NewId(), + model.NewId(), + } + if err := ImportDirectChannel(&data, true); err != nil { + t.Fatalf("Expected success as cannot validate existance of channel members in dry run mode.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do an invalid channel in apply mode. + data.Members = &[]string{ + model.NewId(), + } + if err := ImportDirectChannel(&data, false); err == nil { + t.Fatalf("Expected error due to invalid member (apply mode).") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do a valid DIRECT channel. + data.Members = &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + } + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success: %v", err.Error()) + } + + // Check that one more DIRECT channel is in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do the same DIRECT channel again. + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Update the channel's HEADER + data.Header = ptrStr("New Channel Header 2") + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Get the channel to check that the header was updated. + if channel, err := createDirectChannel(th.BasicUser.Id, th.BasicUser2.Id); err == nil || err.Id != store.CHANNEL_EXISTS_ERROR { + t.Fatal("Should have got store.CHANNEL_EXISTS_ERROR") + } else { + if channel.Header != *data.Header { + t.Fatal("Channel header has not been updated successfully.") + } + } + + // Do a GROUP channel with an extra invalid member. + user3 := th.CreateUser() + data.Members = &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + model.NewId(), + } + if err := ImportDirectChannel(&data, false); err == nil { + t.Fatalf("Should have failed due to invalid member in list.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount) + + // Do a valid GROUP channel. + data.Members = &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + } + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success.") + } + + // Check that one more GROUP channel is in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount+1) + + // Do the same DIRECT channel again. + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount+1) + + // Update the channel's HEADER + data.Header = ptrStr("New Channel Header 3") + if err := ImportDirectChannel(&data, false); err != nil { + t.Fatalf("Expected success.") + } + + // Check that no more channels are in the DB. + AssertChannelCount(t, model.CHANNEL_DIRECT, directChannelCount+1) + AssertChannelCount(t, model.CHANNEL_GROUP, groupChannelCount+1) + + // Get the channel to check that the header was updated. + userIds := []string{ + th.BasicUser.Id, + th.BasicUser2.Id, + user3.Id, + } + if channel, err := createGroupChannel(userIds, th.BasicUser.Id); err.Id != store.CHANNEL_EXISTS_ERROR { + t.Fatal("Should have got store.CHANNEL_EXISTS_ERROR") + } else { + if channel.Header != *data.Header { + t.Fatal("Channel header has not been updated successfully.") + } + } +} + +func AssertChannelCount(t *testing.T, channelType string, expectedCount int64) { + if r := <-Srv.Store.Channel().AnalyticsTypeCount("", channelType); r.Err == nil { + count := r.Data.(int64) + if count != expectedCount { + debug.PrintStack() + t.Fatalf("Channel count of type: %v. Expected: %v, Got: %v", channelType, expectedCount, count) + } + } else { + debug.PrintStack() + t.Fatalf("Failed to get channel count.") + } +} + +func TestImportImportDirectPost(t *testing.T) { + th := Setup().InitBasic() + + // Create the DIRECT channel. + channelData := DirectChannelImportData{ + Members: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + }, + } + if err := ImportDirectChannel(&channelData, false); err != nil { + t.Fatalf("Expected success: %v", err.Error()) + } + + // Get the channel. + var directChannel *model.Channel + if channel, err := createDirectChannel(th.BasicUser.Id, th.BasicUser2.Id); err.Id != store.CHANNEL_EXISTS_ERROR { + t.Fatal("Should have got store.CHANNEL_EXISTS_ERROR") + } else { + directChannel = channel + } + + // Get the number of posts in the system. + var initialPostCount int64 + if result := <-Srv.Store.Post().AnalyticsPostCount("", false, false); result.Err != nil { + t.Fatal(result.Err) + } else { + initialPostCount = result.Data.(int64) + } + + // Try adding an invalid post in dry run mode. + data := &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + }, + User: ptrStr(th.BasicUser.Username), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, true); err == nil { + t.Fatalf("Expected error.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding a valid post in dry run mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, true); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding an invalid post in apply mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + model.NewId(), + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, false); err == nil { + t.Fatalf("Expected error.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding a valid post in apply mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success: %v", err.Error()) + } + AssertAllPostsCount(t, initialPostCount, 1, "") + + // Check the post values. + if result := <-Srv.Store.Post().GetPostsCreatedAt(directChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + } + + // Import the post again. + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 1, "") + + // Check the post values. + if result := <-Srv.Store.Post().GetPostsCreatedAt(directChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + } + + // Save the post with a different time. + data.CreateAt = ptrInt64(*data.CreateAt + 1) + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 2, "") + + // Save the post with a different message. + data.Message = ptrStr("Message 2") + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 3, "") + + // Test with hashtags + data.Message = ptrStr("Message 2 #hashtagmashupcity") + data.CreateAt = ptrInt64(*data.CreateAt + 1) + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 4, "") + + if result := <-Srv.Store.Post().GetPostsCreatedAt(directChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + if post.Hashtags != "#hashtagmashupcity" { + t.Fatalf("Hashtags not as expected: %s", post.Hashtags) + } + } + + // ------------------ Group Channel ------------------------- + + // Create the GROUP channel. + user3 := th.CreateUser() + channelData = DirectChannelImportData{ + Members: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + }, + } + if err := ImportDirectChannel(&channelData, false); err != nil { + t.Fatalf("Expected success: %v", err.Error()) + } + + // Get the channel. + var groupChannel *model.Channel + userIds := []string{ + th.BasicUser.Id, + th.BasicUser2.Id, + user3.Id, + } + if channel, err := createGroupChannel(userIds, th.BasicUser.Id); err.Id != store.CHANNEL_EXISTS_ERROR { + t.Fatal("Should have got store.CHANNEL_EXISTS_ERROR") + } else { + groupChannel = channel + } + + // Get the number of posts in the system. + if result := <-Srv.Store.Post().AnalyticsPostCount("", false, false); result.Err != nil { + t.Fatal(result.Err) + } else { + initialPostCount = result.Data.(int64) + } + + // Try adding an invalid post in dry run mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + }, + User: ptrStr(th.BasicUser.Username), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, true); err == nil { + t.Fatalf("Expected error.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding a valid post in dry run mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, true); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding an invalid post in apply mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + model.NewId(), + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, false); err == nil { + t.Fatalf("Expected error.") + } + AssertAllPostsCount(t, initialPostCount, 0, "") + + // Try adding a valid post in apply mode. + data = &DirectPostImportData{ + ChannelMembers: &[]string{ + th.BasicUser.Username, + th.BasicUser2.Username, + user3.Username, + }, + User: ptrStr(th.BasicUser.Username), + Message: ptrStr("Message"), + CreateAt: ptrInt64(model.GetMillis()), + } + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success: %v", err.Error()) + } + AssertAllPostsCount(t, initialPostCount, 1, "") + + // Check the post values. + if result := <-Srv.Store.Post().GetPostsCreatedAt(groupChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + } + + // Import the post again. + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 1, "") + + // Check the post values. + if result := <-Srv.Store.Post().GetPostsCreatedAt(groupChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + } + + // Save the post with a different time. + data.CreateAt = ptrInt64(*data.CreateAt + 1) + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 2, "") + + // Save the post with a different message. + data.Message = ptrStr("Message 2") + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 3, "") + + // Test with hashtags + data.Message = ptrStr("Message 2 #hashtagmashupcity") + data.CreateAt = ptrInt64(*data.CreateAt + 1) + if err := ImportDirectPost(data, false); err != nil { + t.Fatalf("Expected success.") + } + AssertAllPostsCount(t, initialPostCount, 4, "") + + if result := <-Srv.Store.Post().GetPostsCreatedAt(groupChannel.Id, *data.CreateAt); result.Err != nil { + t.Fatal(result.Err.Error()) + } else { + posts := result.Data.([]*model.Post) + if len(posts) != 1 { + t.Fatal("Unexpected number of posts found.") + } + post := posts[0] + if post.Message != *data.Message || post.CreateAt != *data.CreateAt || post.UserId != th.BasicUser.Id { + t.Fatal("Post properties not as expected") + } + if post.Hashtags != "#hashtagmashupcity" { + t.Fatalf("Hashtags not as expected: %s", post.Hashtags) + } + } +} + func TestImportImportLine(t *testing.T) { _ = Setup() @@ -1754,6 +2507,18 @@ func TestImportImportLine(t *testing.T) { if err := ImportLine(line, false); err == nil { t.Fatalf("Expected an error when importing a line with type post with a nil post.") } + + // Try import line with direct_channel type but nil direct_channel. + line.Type = "direct_channel" + if err := ImportLine(line, false); err == nil { + t.Fatalf("Expected an error when importing a line with type direct_channel with a nil direct_channel.") + } + + // Try import line with direct_post type but nil direct_post. + line.Type = "direct_post" + if err := ImportLine(line, false); err == nil { + t.Fatalf("Expected an error when importing a line with type direct_post with a nil direct_post.") + } } func TestImportBulkImport(t *testing.T) { @@ -1762,13 +2527,21 @@ func TestImportBulkImport(t *testing.T) { teamName := model.NewId() channelName := model.NewId() username := model.NewId() + username2 := model.NewId() + username3 := model.NewId() // Run bulk import with a valid 1 of everything. data1 := `{"type": "version", "version": 1} {"type": "team", "team": {"type": "O", "display_name": "lskmw2d7a5ao7ppwqh5ljchvr4", "name": "` + teamName + `"}} {"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}} {"type": "user", "user": {"username": "` + username + `", "email": "` + username + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}} -{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}}` +{"type": "user", "user": {"username": "` + username2 + `", "email": "` + username2 + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}} +{"type": "user", "user": {"username": "` + username3 + `", "email": "` + username3 + `@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}} +{"type": "post", "post": {"team": "` + teamName + `", "channel": "` + channelName + `", "user": "` + username + `", "message": "Hello World", "create_at": 123456789012}} +{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `"]}} +{"type": "direct_channel", "direct_channel": {"members": ["` + username + `", "` + username2 + `", "` + username3 + `"]}} +{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `"], "user": "` + username + `", "message": "Hello Direct Channel", "create_at": 123456789013}} +{"type": "direct_post", "direct_post": {"channel_members": ["` + username + `", "` + username2 + `", "` + username3 + `"], "user": "` + username + `", "message": "Hello Group Channel", "create_at": 123456789014}}` if err, line := BulkImport(strings.NewReader(data1), false, 2); err != nil || line != 0 { t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line) diff --git a/i18n/en.json b/i18n/en.json index 1bd46f97a..9db609422 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2967,10 +2967,50 @@ "id": "app.import.import_channel.team_not_found.error", "translation": "Error importing channel. Team with name \"{{.TeamName}}\" could not be found." }, + { + "id": "app.import.import_direct_channel.member_not_found.error", + "translation": "Could not find channel member when importing direct channel" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Failed to create direct channel" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Failed to create group channel" + }, + { + "id": "app.import.import_direct_channel.update_header_failed.error", + "translation": "Failed to update direct channel header" + }, + { + "id": "app.import.import_direct_post.channel_member_not_found.error", + "translation": "Could not find channel member when importing direct channel post" + }, + { + "id": "app.import.import_direct_channel.create_direct_channel.error", + "translation": "Failed to get direct channel" + }, + { + "id": "app.import.import_direct_channel.create_group_channel.error", + "translation": "Failed to get group channel" + }, + { + "id": "app.import.import_direct_post.user_not_found.error", + "translation": "Post user does not exist" + }, { "id": "app.import.import_line.null_channel.error", "translation": "Import data line has type \"channel\" but the channel object is null." }, + { + "id": "app.import.import_line.null_direct_channel.error", + "translation": "Import data line has type \"direct_channel\" but the direct_channel object is null." + }, + { + "id": "app.import.import_line.null_direct_post.error", + "translation": "Import data line has type \"direct_post\" but the direct_post object is null." + }, { "id": "app.import.import_line.null_post.error", "translation": "Import data line has type \"post\" but the post object is null." @@ -3043,6 +3083,54 @@ "id": "app.import.validate_channel_import_data.type_missing.error", "translation": "Missing required channel property: type." }, + { + "id": "app.import.validate_direct_channel_import_data.members_required.error", + "translation": "Missing required direct channel property: members" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_few.error", + "translation": "Direct channel members list contains too few items" + }, + { + "id": "app.import.validate_direct_channel_import_data.members_too_many.error", + "translation": "Direct channel members list contains too many items" + }, + { + "id": "app.import.validate_direct_channel_import_data.header_length.error", + "translation": "Direct channel header is too long" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_required.error", + "translation": "Missing required direct post property: channel_members" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_few.error", + "translation": "Direct post channel members list contains too few items" + }, + { + "id": "app.import.validate_direct_post_import_data.channel_members_too_many.error", + "translation": "Direct post channel members list contains too many items" + }, + { + "id": "app.import.validate_direct_post_import_data.user_missing.error", + "translation": "Missing required direct post property: user" + }, + { + "id": "app.import.validate_direct_post_import_data.message_missing.error", + "translation": "Missing required direct post property: message" + }, + { + "id": "app.import.validate_direct_post_import_data.message_length.error", + "translation": "Message is too long" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_missing.error", + "translation": "Missing required direct post property: create_at" + }, + { + "id": "app.import.validate_direct_post_import_data.create_at_zero.error", + "translation": "CreateAt must be greater than 0" + }, { "id": "app.import.validate_post_import_data.channel_missing.error", "translation": "Missing required Post property: Channel." -- cgit v1.2.3-1-g7c22