diff options
47 files changed, 1221 insertions, 509 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af8fcfc8f..fb640da3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Code Contribution Guidelines -Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](http://docs.mattermost.com/developer/contribution-guide.html) which describes the process for making code contributions across Mattermost projects and [join our "Contributors" community channel](https://pre-release.mattermost.com/core/channels/tickets) to ask questions from community members and the Mattermost core team. +Thank you for your interest in contributing! Please see the [Mattermost Contribution Guide](https://developers.mattermost.com/contribute/getting-started/) which describes the process for making code contributions across Mattermost projects and [join our "Contributors" community channel](https://pre-release.mattermost.com/core/channels/tickets) to ask questions from community members and the Mattermost core team. ### Review Process for this Repo @@ -275,15 +275,15 @@ gofmt: ## Runs gofmt against all packages. @echo "gofmt success"; \ store-mocks: ## Creates mock files. - go get github.com/vektra/mockery/... + go get -u github.com/vektra/mockery/... $(GOPATH)/bin/mockery -dir store -all -output store/storetest/mocks -note 'Regenerate this file using `make store-mocks`.' ldap-mocks: ## Creates mock files for ldap. - go get github.com/vektra/mockery/... + go get -u github.com/vektra/mockery/... $(GOPATH)/bin/mockery -dir enterprise/ldap -all -output enterprise/ldap/mocks -note 'Regenerate this file using `make ldap-mocks`.' plugin-mocks: ## Creates mock files for plugins. - go get github.com/vektra/mockery/... + go get -u github.com/vektra/mockery/... $(GOPATH)/bin/mockery -dir plugin -name API -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.' $(GOPATH)/bin/mockery -dir plugin -name Hooks -output plugin/plugintest -outpkg plugintest -case underscore -note 'Regenerate this file using `make plugin-mocks`.' @@ -5,6 +5,7 @@ Mattermost is an open source, private cloud, Slack-alternative from [https://mat It's written in Golang and React and runs as a single Linux binary with MySQL or PostgreSQL. Every month on the 16th [a new compiled version is released under an MIT license](https://www.mattermost.org/download/). - [Review product documentation](http://docs.mattermost.com/). +- [Review developer documentation](http://developers.mattermost.com/). - [Download compiled version](https://mattermost.org/download). <img width="1006" alt="screenshot at nov 29 14-11-32" src="https://user-images.githubusercontent.com/29708087/33394101-404e23e4-d50f-11e7-8fe5-99d4802a9768.png"> diff --git a/api4/channel.go b/api4/channel.go index f21b45d56..1599b6e70 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1069,6 +1069,11 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } + if !(channel.Type == model.CHANNEL_OPEN || channel.Type == model.CHANNEL_PRIVATE) { + c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest) + return + } + if c.Params.UserId != c.Session.UserId { if channel.Type == model.CHANNEL_OPEN && !c.App.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) { c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) diff --git a/api4/channel_test.go b/api4/channel_test.go index 5ca4dee6e..8593ea831 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -2038,6 +2038,30 @@ func TestRemoveChannelMember(t *testing.T) { _, resp = Client.RemoveUserFromChannel(privateChannel.Id, user2.Id) CheckNoError(t, resp) + + // Test on preventing removal of user from a direct channel + directChannel, resp := Client.CreateDirectChannel(user1.Id, user2.Id) + CheckNoError(t, resp) + + _, resp = Client.RemoveUserFromChannel(directChannel.Id, user1.Id) + CheckBadRequestStatus(t, resp) + + _, resp = Client.RemoveUserFromChannel(directChannel.Id, user2.Id) + CheckBadRequestStatus(t, resp) + + _, resp = th.SystemAdminClient.RemoveUserFromChannel(directChannel.Id, user1.Id) + CheckBadRequestStatus(t, resp) + + // Test on preventing removal of user from a group channel + user3 := th.CreateUser() + groupChannel, resp := Client.CreateGroupChannel([]string{user1.Id, user2.Id, user3.Id}) + CheckNoError(t, resp) + + _, resp = Client.RemoveUserFromChannel(groupChannel.Id, user1.Id) + CheckBadRequestStatus(t, resp) + + _, resp = th.SystemAdminClient.RemoveUserFromChannel(groupChannel.Id, user1.Id) + CheckBadRequestStatus(t, resp) } func TestAutocompleteChannels(t *testing.T) { diff --git a/api4/system_test.go b/api4/system_test.go index 099c193d0..205572cf6 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -10,6 +10,7 @@ import ( "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetPing(t *testing.T) { @@ -47,9 +48,7 @@ func TestGetConfig(t *testing.T) { cfg, resp := th.SystemAdminClient.GetConfig() CheckNoError(t, resp) - if len(cfg.TeamSettings.SiteName) == 0 { - t.Fatal() - } + require.NotEqual(t, "", cfg.TeamSettings.SiteName) if *cfg.LdapSettings.BindPassword != model.FAKE_SETTING && len(*cfg.LdapSettings.BindPassword) != 0 { t.Fatal("did not sanitize properly") @@ -121,28 +120,14 @@ func TestUpdateConfig(t *testing.T) { cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) CheckNoError(t, resp) - if len(cfg.TeamSettings.SiteName) == 0 { - t.Fatal() - } else { - if cfg.TeamSettings.SiteName != "MyFancyName" { - t.Log("It should update the SiteName") - t.Fatal() - } - } + require.Equal(t, "MyFancyName", cfg.TeamSettings.SiteName, "It should update the SiteName") //Revert the change cfg.TeamSettings.SiteName = SiteName cfg, resp = th.SystemAdminClient.UpdateConfig(cfg) CheckNoError(t, resp) - if len(cfg.TeamSettings.SiteName) == 0 { - t.Fatal() - } else { - if cfg.TeamSettings.SiteName != SiteName { - t.Log("It should update the SiteName") - t.Fatal() - } - } + require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName") t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) { oldEnableUploads := *th.App.GetConfig().PluginSettings.EnableUploads diff --git a/app/diagnostics.go b/app/diagnostics.go index 7034fc1a3..63bc506c3 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -6,46 +6,51 @@ package app import ( "path/filepath" "runtime" + "strings" + + "github.com/segmentio/analytics-go" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" - "github.com/segmentio/analytics-go" ) const ( SEGMENT_KEY = "fwb7VPbFeQ7SKp3wHm1RzFUuXZudqVok" - TRACK_CONFIG_SERVICE = "config_service" - TRACK_CONFIG_TEAM = "config_team" - TRACK_CONFIG_CLIENT_REQ = "config_client_requirements" - TRACK_CONFIG_SQL = "config_sql" - TRACK_CONFIG_LOG = "config_log" - TRACK_CONFIG_FILE = "config_file" - TRACK_CONFIG_RATE = "config_rate" - TRACK_CONFIG_EXTENSION = "config_extension" - TRACK_CONFIG_EMAIL = "config_email" - TRACK_CONFIG_PRIVACY = "config_privacy" - TRACK_CONFIG_THEME = "config_theme" - TRACK_CONFIG_OAUTH = "config_oauth" - TRACK_CONFIG_LDAP = "config_ldap" - TRACK_CONFIG_COMPLIANCE = "config_compliance" - TRACK_CONFIG_LOCALIZATION = "config_localization" - TRACK_CONFIG_SAML = "config_saml" - TRACK_CONFIG_PASSWORD = "config_password" - TRACK_CONFIG_CLUSTER = "config_cluster" - TRACK_CONFIG_METRICS = "config_metrics" - TRACK_CONFIG_WEBRTC = "config_webrtc" - TRACK_CONFIG_SUPPORT = "config_support" - TRACK_CONFIG_NATIVEAPP = "config_nativeapp" - TRACK_CONFIG_EXPERIMENTAL = "config_experimental" - TRACK_CONFIG_ANALYTICS = "config_analytics" - TRACK_CONFIG_ANNOUNCEMENT = "config_announcement" - TRACK_CONFIG_ELASTICSEARCH = "config_elasticsearch" - TRACK_CONFIG_PLUGIN = "config_plugin" - TRACK_CONFIG_DATA_RETENTION = "config_data_retention" - TRACK_CONFIG_MESSAGE_EXPORT = "config_message_export" - TRACK_CONFIG_DISPLAY = "config_display" - TRACK_CONFIG_TIMEZONE = "config_timezone" + TRACK_CONFIG_SERVICE = "config_service" + TRACK_CONFIG_TEAM = "config_team" + TRACK_CONFIG_CLIENT_REQ = "config_client_requirements" + TRACK_CONFIG_SQL = "config_sql" + TRACK_CONFIG_LOG = "config_log" + TRACK_CONFIG_FILE = "config_file" + TRACK_CONFIG_RATE = "config_rate" + TRACK_CONFIG_EXTENSION = "config_extension" + TRACK_CONFIG_EMAIL = "config_email" + TRACK_CONFIG_PRIVACY = "config_privacy" + TRACK_CONFIG_THEME = "config_theme" + TRACK_CONFIG_OAUTH = "config_oauth" + TRACK_CONFIG_LDAP = "config_ldap" + TRACK_CONFIG_COMPLIANCE = "config_compliance" + TRACK_CONFIG_LOCALIZATION = "config_localization" + TRACK_CONFIG_SAML = "config_saml" + TRACK_CONFIG_PASSWORD = "config_password" + TRACK_CONFIG_CLUSTER = "config_cluster" + TRACK_CONFIG_METRICS = "config_metrics" + TRACK_CONFIG_WEBRTC = "config_webrtc" + TRACK_CONFIG_SUPPORT = "config_support" + TRACK_CONFIG_NATIVEAPP = "config_nativeapp" + TRACK_CONFIG_EXPERIMENTAL = "config_experimental" + TRACK_CONFIG_ANALYTICS = "config_analytics" + TRACK_CONFIG_ANNOUNCEMENT = "config_announcement" + TRACK_CONFIG_ELASTICSEARCH = "config_elasticsearch" + TRACK_CONFIG_PLUGIN = "config_plugin" + TRACK_CONFIG_DATA_RETENTION = "config_data_retention" + TRACK_CONFIG_MESSAGE_EXPORT = "config_message_export" + TRACK_CONFIG_DISPLAY = "config_display" + TRACK_CONFIG_TIMEZONE = "config_timezone" + TRACK_PERMISSIONS_GENERAL = "permissions_general" + TRACK_PERMISSIONS_SYSTEM_SCHEME = "permissions_system_scheme" + TRACK_PERMISSIONS_TEAM_SCHEMES = "permissions_team_schemes" TRACK_ACTIVITY = "activity" TRACK_LICENSE = "license" @@ -63,6 +68,7 @@ func (a *App) SendDailyDiagnostics() { a.trackLicense() a.trackPlugins() a.trackServer() + a.trackPermissions() } } @@ -650,3 +656,97 @@ func (a *App) trackServer() { a.SendDiagnostic(TRACK_SERVER, data) } + +func (a *App) trackPermissions() { + phase1Complete := false + if ph1res := <-a.Srv.Store.System().GetByName(ADVANCED_PERMISSIONS_MIGRATION_KEY); ph1res.Err == nil { + phase1Complete = true + } + + phase2Complete := false + if ph2res := <-a.Srv.Store.System().GetByName(model.MIGRATION_KEY_ADVANCED_PERMISSIONS_PHASE_2); ph2res.Err == nil { + phase2Complete = true + } + + a.SendDiagnostic(TRACK_PERMISSIONS_GENERAL, map[string]interface{}{ + "phase_1_migration_complete": phase1Complete, + "phase_2_migration_complete": phase2Complete, + }) + + systemAdminPermissions := "" + if role, err := a.GetRoleByName(model.SYSTEM_ADMIN_ROLE_ID); err == nil { + systemAdminPermissions = strings.Join(role.Permissions, " ") + } + + systemUserPermissions := "" + if role, err := a.GetRoleByName(model.SYSTEM_USER_ROLE_ID); err == nil { + systemUserPermissions = strings.Join(role.Permissions, " ") + } + + teamAdminPermissions := "" + if role, err := a.GetRoleByName(model.TEAM_ADMIN_ROLE_ID); err == nil { + teamAdminPermissions = strings.Join(role.Permissions, " ") + } + + teamUserPermissions := "" + if role, err := a.GetRoleByName(model.TEAM_USER_ROLE_ID); err == nil { + teamUserPermissions = strings.Join(role.Permissions, " ") + } + + channelAdminPermissions := "" + if role, err := a.GetRoleByName(model.CHANNEL_ADMIN_ROLE_ID); err == nil { + channelAdminPermissions = strings.Join(role.Permissions, " ") + } + + channelUserPermissions := "" + if role, err := a.GetRoleByName(model.CHANNEL_USER_ROLE_ID); err == nil { + systemAdminPermissions = strings.Join(role.Permissions, " ") + } + + a.SendDiagnostic(TRACK_PERMISSIONS_SYSTEM_SCHEME, map[string]interface{}{ + "system_admin_permissions": systemAdminPermissions, + "system_user_permissions": systemUserPermissions, + "team_admin_permissions": teamAdminPermissions, + "team_user_permissions": teamUserPermissions, + "channel_admin_permissions": channelAdminPermissions, + "channel_user_permissions": channelUserPermissions, + }) + + if schemes, err := a.GetSchemes(model.SCHEME_SCOPE_TEAM, 0, 100); err == nil { + for _, scheme := range schemes { + teamAdminPermissions := "" + if role, err := a.GetRoleByName(scheme.DefaultTeamAdminRole); err == nil { + teamAdminPermissions = strings.Join(role.Permissions, " ") + } + + teamUserPermissions := "" + if role, err := a.GetRoleByName(scheme.DefaultTeamUserRole); err == nil { + teamUserPermissions = strings.Join(role.Permissions, " ") + } + + channelAdminPermissions := "" + if role, err := a.GetRoleByName(scheme.DefaultChannelAdminRole); err == nil { + channelAdminPermissions = strings.Join(role.Permissions, " ") + } + + channelUserPermissions := "" + if role, err := a.GetRoleByName(scheme.DefaultChannelUserRole); err == nil { + systemAdminPermissions = strings.Join(role.Permissions, " ") + } + + var count int64 = 0 + if res := <-a.Srv.Store.Team().AnalyticsGetTeamCountForScheme(scheme.Id); res.Err == nil { + count = res.Data.(int64) + } + + a.SendDiagnostic(TRACK_PERMISSIONS_TEAM_SCHEMES, map[string]interface{}{ + "scheme_id": scheme.Id, + "team_admin_permissions": teamAdminPermissions, + "team_user_permissions": teamUserPermissions, + "channel_admin_permissions": channelAdminPermissions, + "channel_user_permissions": channelUserPermissions, + "team_count": count, + }) + } + } +} diff --git a/app/import.go b/app/import.go index 078198dd4..496c6b7fc 100644 --- a/app/import.go +++ b/app/import.go @@ -100,6 +100,10 @@ func (a *App) BulkImport(fileReader io.Reader, dryRun bool, workers int) (*model return model.NewAppError("BulkImport", "app.import.bulk_import.file_scan.error", nil, err.Error(), http.StatusInternalServerError), 0 } + if err := a.finalizeImport(dryRun); err != nil { + return err, 0 + } + return nil, 0 } @@ -165,3 +169,14 @@ func (a *App) ImportLine(line LineImportData, dryRun bool) *model.AppError { return model.NewAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "", http.StatusBadRequest) } } + +func (a *App) finalizeImport(dryRun bool) *model.AppError { + if dryRun { + return nil + } + result := <-a.Srv.Store.Channel().ResetLastPostAt() + if result.Err != nil { + return result.Err + } + return nil +} diff --git a/app/plugin_api.go b/app/plugin_api.go index c3ab8fab2..503feabee 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -258,6 +258,18 @@ func (api *PluginAPI) CreatePost(post *model.Post) (*model.Post, *model.AppError return api.app.CreatePostMissingChannel(post, true) } +func (api *PluginAPI) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) { + return api.app.SaveReactionForPost(reaction) +} + +func (api *PluginAPI) RemoveReaction(reaction *model.Reaction) *model.AppError { + return api.app.DeleteReactionForPost(reaction) +} + +func (api *PluginAPI) GetReactions(postId string) ([]*model.Reaction, *model.AppError) { + return api.app.GetReactionsForPost(postId) +} + func (api *PluginAPI) SendEphemeralPost(userId string, post *model.Post) *model.Post { return api.app.SendEphemeralPost(userId, post) } @@ -279,6 +291,14 @@ func (api *PluginAPI) CopyFileInfos(userId string, fileIds []string) ([]string, return api.app.CopyFileInfos(userId, fileIds) } +func (api *PluginAPI) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { + return api.app.GetFileInfo(fileId) +} + +func (api *PluginAPI) ReadFile(path string) ([]byte, *model.AppError) { + return api.app.ReadFile(path) +} + func (api *PluginAPI) KVSet(key string, value []byte) *model.AppError { return api.app.SetPluginKey(api.id, key, value) } @@ -299,6 +319,18 @@ func (api *PluginAPI) PublishWebSocketEvent(event string, payload map[string]int }) } +func (api *PluginAPI) HasPermissionTo(userId string, permission *model.Permission) bool { + return api.app.HasPermissionTo(userId, permission) +} + +func (api *PluginAPI) HasPermissionToTeam(userId, teamId string, permission *model.Permission) bool { + return api.app.HasPermissionToTeam(userId, teamId, permission) +} + +func (api *PluginAPI) HasPermissionToChannel(userId, channelId string, permission *model.Permission) bool { + return api.app.HasPermissionToChannel(userId, channelId, permission) +} + func (api *PluginAPI) LogDebug(msg string, keyValuePairs ...interface{}) { api.logger.Debug(msg, keyValuePairs...) } diff --git a/app/post.go b/app/post.go index 0ce1bbe2d..0eed87280 100644 --- a/app/post.go +++ b/app/post.go @@ -869,6 +869,7 @@ func (a *App) DoPostAction(postId string, actionId string, userId string) *model request := &model.PostActionIntegrationRequest{ UserId: userId, + PostId: postId, Context: action.Integration.Context, } diff --git a/cmd/mattermost/commands/roles_test.go b/cmd/mattermost/commands/roles_test.go index 4f11ce7ed..da33a73cc 100644 --- a/cmd/mattermost/commands/roles_test.go +++ b/cmd/mattermost/commands/roles_test.go @@ -17,7 +17,7 @@ func TestAssignRole(t *testing.T) { CheckCommand(t, "roles", "system_admin", th.BasicUser.Email) if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { - t.Fatal() + t.Fatal(result.Err) } else { user := result.Data.(*model.User) if user.Roles != "system_user system_admin" { @@ -28,7 +28,7 @@ func TestAssignRole(t *testing.T) { CheckCommand(t, "roles", "member", th.BasicUser.Email) if result := <-th.App.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil { - t.Fatal() + t.Fatal(result.Err) } else { user := result.Data.(*model.User) if user.Roles != "system_user" { diff --git a/cmd/mattermost/commands/sampledata.go b/cmd/mattermost/commands/sampledata.go index 0983ab0df..ed550bf6b 100644 --- a/cmd/mattermost/commands/sampledata.go +++ b/cmd/mattermost/commands/sampledata.go @@ -59,6 +59,15 @@ func randomPastTime(seconds int) int64 { return (today.Unix() * 1000) - int64(rand.Intn(seconds*1000)) } +func sortedRandomDates(size int) []int64 { + dates := make([]int64, size) + for i := 0; i < size; i++ { + dates[i] = randomPastTime(50000) + } + sort.Slice(dates, func(a, b int) bool { return dates[a] < dates[b] }) + return dates +} + func randomEmoji() string { emojis := []string{"+1", "-1", "heart", "blush"} return emojis[rand.Intn(len(emojis))] @@ -274,8 +283,10 @@ func sampleDataCmdF(command *cobra.Command, args []string) error { for team, channels := range teamsAndChannels { for _, channel := range channels { + dates := sortedRandomDates(postsPerChannel) + for i := 0; i < postsPerChannel; i++ { - postLine := createPost(team, channel, allUsers) + postLine := createPost(team, channel, allUsers, dates[i]) encoder.Encode(postLine) } } @@ -286,8 +297,10 @@ func sampleDataCmdF(command *cobra.Command, args []string) error { user2 := allUsers[rand.Intn(len(allUsers))] channelLine := createDirectChannel([]string{user1, user2}) encoder.Encode(channelLine) + + dates := sortedRandomDates(postsPerDirectChannel) for j := 0; j < postsPerDirectChannel; j++ { - postLine := createDirectPost([]string{user1, user2}) + postLine := createDirectPost([]string{user1, user2}, dates[j]) encoder.Encode(postLine) } } @@ -303,8 +316,10 @@ func sampleDataCmdF(command *cobra.Command, args []string) error { } channelLine := createDirectChannel(users) encoder.Encode(channelLine) + + dates := sortedRandomDates(postsPerGroupChannel) for j := 0; j < postsPerGroupChannel; j++ { - postLine := createDirectPost(users) + postLine := createDirectPost(users, dates[j]) encoder.Encode(postLine) } } @@ -529,9 +544,9 @@ func createChannel(idx int, teamName string) app.LineImportData { } } -func createPost(team string, channel string, allUsers []string) app.LineImportData { +func createPost(team string, channel string, allUsers []string, createAt int64) app.LineImportData { message := randomMessage(allUsers) - create_at := randomPastTime(50000) + create_at := createAt user := allUsers[rand.Intn(len(allUsers))] // Some messages are flagged by an user @@ -589,9 +604,9 @@ func createDirectChannel(members []string) app.LineImportData { } } -func createDirectPost(members []string) app.LineImportData { +func createDirectPost(members []string, createAt int64) app.LineImportData { message := randomMessage(members) - create_at := randomPastTime(50000) + create_at := createAt user := members[rand.Intn(len(members))] // Some messages are flagged by an user diff --git a/cmd/mattermost/commands/user_test.go b/cmd/mattermost/commands/user_test.go index 69ca9ecb8..088893602 100644 --- a/cmd/mattermost/commands/user_test.go +++ b/cmd/mattermost/commands/user_test.go @@ -50,12 +50,10 @@ func TestCreateUserWithoutTeam(t *testing.T) { CheckCommand(t, "user", "create", "--email", email, "--password", "mypassword1", "--username", username) if result := <-th.App.Srv.Store.User().GetByEmail(email); result.Err != nil { - t.Fatal() + t.Fatal(result.Err) } else { user := result.Data.(*model.User) - if user.Email != email { - t.Fatal() - } + require.Equal(t, email, user.Email) } } @@ -92,7 +90,7 @@ func TestChangeUserEmail(t *testing.T) { t.Fatal("should've updated to the new email") } if result := <-th.App.Srv.Store.User().GetByEmail(newEmail); result.Err != nil { - t.Fatal() + t.Fatal(result.Err) } else { user := result.Data.(*model.User) if user.Email != newEmail { diff --git a/config/default.json b/config/default.json index 985d3f63a..884eceb98 100644 --- a/config/default.json +++ b/config/default.json @@ -105,7 +105,8 @@ "ExperimentalEnableAutomaticReplies": false, "ExperimentalHideTownSquareinLHS": false, "ExperimentalTownSquareIsReadOnly": false, - "ExperimentalPrimaryTeam": "" + "ExperimentalPrimaryTeam": "", + "ExperimentalDefaultChannels": "" }, "DisplaySettings": { "CustomUrlSchemes": [], diff --git a/i18n/en.json b/i18n/en.json index 19c92a0b4..b379e08d1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -156,6 +156,10 @@ "translation": "The channel has been archived or deleted" }, { + "id": "store.sql_team.analytics_get_team_count_for_scheme.app_error", + "translation": "We couldn't get the channel count for the scheme." + }, + { "id": "api.channel.delete_channel.type.invalid", "translation": "Cannot delete direct or group message channels" }, @@ -232,12 +236,12 @@ "translation": "Cannot remove user from the default channel {{.Channel}}" }, { - "id": "api.channel.remove_member.removed", - "translation": "%v removed from the channel." + "id": "api.channel.remove_channel_member.type.app_error", + "translation": "Cannot remove user from a channel." }, { - "id": "api.channel.remove_user_from_channel.deleted.app_error", - "translation": "The channel has been archived or deleted" + "id": "api.channel.remove_member.removed", + "translation": "%v removed from the channel." }, { "id": "api.channel.update_channel.deleted.app_error", @@ -1033,6 +1037,14 @@ "translation": "File attachments have been disabled on this server." }, { + "id": "api.file.file_exists.exists_local.app_error", + "translation": "Unable to know if the file exists. An error ocurred when trying to check file existency." + }, + { + "id": "api.file.file_exists.s3.app_error", + "translation": "Unable to know if the file exists. An error ocurred when trying to check file existency." + }, + { "id": "api.file.get_file.public_invalid.app_error", "translation": "The public link does not appear to be valid" }, @@ -2507,6 +2519,10 @@ "translation": "Import data line has type \"direct_post\" but the direct_post object is null." }, { + "id": "app.import.import_line.null_emoji.error", + "translation": "Import data line has type \"emoji\" but the emoji object is null." + }, + { "id": "app.import.import_line.null_post.error", "translation": "Import data line has type \"post\" but the post object is null." }, @@ -3019,10 +3035,6 @@ "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" }, { - "id": "app.plugin.activate.app_error", - "translation": "Unable to activate extracted plugin." - }, - { "id": "app.plugin.cluster.save_config.app_error", "translation": "The plugin configuration in your config.json file must be updated manually when using ReadOnlyConfig with clustering enabled." }, @@ -3087,18 +3099,10 @@ "translation": "Plugin is not installed" }, { - "id": "app.plugin.prepackaged.app_error", - "translation": "Cannot install prepackaged plugin" - }, - { "id": "app.plugin.remove.app_error", "translation": "Unable to delete plugin" }, { - "id": "app.plugin.set_plugin_status_state.app_error", - "translation": "Unable to set plugin status state." - }, - { "id": "app.plugin.upload_disabled.app_error", "translation": "Plugins and/or plugin uploads have been disabled." }, @@ -3219,6 +3223,10 @@ "translation": "Unable to open the temporary export file." }, { + "id": "ent.compliance.global_relay.rewind_temporary_file.appError", + "translation": "Unable to re-read the Global Relay temporary export file." + }, + { "id": "ent.compliance.licence_disable.app_error", "translation": "Compliance functionality disabled by current license. Please contact your system administrator about upgrading your enterprise license." }, @@ -3399,10 +3407,22 @@ "translation": "Invalid AD/LDAP Filter" }, { + "id": "ent.message_export.global_relay.attach_file.app_error", + "translation": "Unable to add attachment to the Global Relay export." + }, + { + "id": "ent.message_export.global_relay.close_zip_file.app_error", + "translation": "Unable to close properly the zip file." + }, + { "id": "ent.message_export.global_relay.create_file_in_zip.app_error", "translation": "Unable to create the eml file." }, { + "id": "ent.message_export.global_relay.generate_email.app_error", + "translation": "Unable to generate eml file data." + }, + { "id": "ent.message_export.global_relay_export.deliver.close.app_error", "translation": "Unable to deliver the email to Global Relay." }, @@ -4675,8 +4695,8 @@ "translation": "GitLab's Terms of Service have updated. Please go to gitlab.com to accept them and then try logging into Mattermost again." }, { - "id": "plugin.rpcplugin.invocation.error", - "translation": "Error invoking plugin RPC" + "id": "plugin.api.update_user_status.bad_status", + "translation": "Unable to set the user statys. Unknown user status." }, { "id": "store.sql.convert_string_array", @@ -4891,6 +4911,14 @@ "translation": "We could not reset the channel schemes" }, { + "id": "store.sql_channel.reset_last_post_at.app_error", + "translation": "We could not reset the channel last post at date" + }, + { + "id": "store.sql_channel.save.archived_channel.app_error", + "translation": "You can not modify an archived channel" + }, + { "id": "store.sql_channel.save.commit_transaction.app_error", "translation": "Unable to commit transaction" }, @@ -5119,6 +5147,10 @@ "translation": "We couldn't save the emoji" }, { + "id": "store.sql_file_info.PermanentDeleteByUser.app_error", + "translation": "We couldn't delete attachments of the user" + }, + { "id": "store.sql_file_info.attach_to_post.app_error", "translation": "We couldn't attach the file info to the post" }, @@ -5139,6 +5171,10 @@ "translation": "We couldn't get the file info for the post" }, { + "id": "store.sql_file_info.get_for_user_id.app_error", + "translation": "We couldn't get the file info for the user" + }, + { "id": "store.sql_file_info.permanent_delete.app_error", "translation": "We couldn't permanently delete the file info" }, diff --git a/model/access_test.go b/model/access_test.go index f0ed2da77..0f124a107 100644 --- a/model/access_test.go +++ b/model/access_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestAccessJson(t *testing.T) { @@ -26,9 +28,7 @@ func TestAccessJson(t *testing.T) { func TestAccessIsValid(t *testing.T) { ad := AccessData{} - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.ClientId = NewRandomString(28) if err := ad.IsValid(); err == nil { @@ -41,9 +41,7 @@ func TestAccessIsValid(t *testing.T) { } ad.ClientId = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.UserId = NewRandomString(28) if err := ad.IsValid(); err == nil { @@ -66,9 +64,7 @@ func TestAccessIsValid(t *testing.T) { } ad.Token = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.RefreshToken = NewRandomString(28) if err := ad.IsValid(); err == nil { @@ -76,9 +72,7 @@ func TestAccessIsValid(t *testing.T) { } ad.RefreshToken = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.RedirectUri = "" if err := ad.IsValid(); err == nil { diff --git a/model/authorize_test.go b/model/authorize_test.go index 81e059305..0775b06c1 100644 --- a/model/authorize_test.go +++ b/model/authorize_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestAuthJson(t *testing.T) { @@ -46,9 +48,7 @@ func TestAuthIsValid(t *testing.T) { ad := AuthData{} - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.ClientId = NewRandomString(28) if err := ad.IsValid(); err == nil { @@ -56,9 +56,7 @@ func TestAuthIsValid(t *testing.T) { } ad.ClientId = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.UserId = NewRandomString(28) if err := ad.IsValid(); err == nil { @@ -66,9 +64,7 @@ func TestAuthIsValid(t *testing.T) { } ad.UserId = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.Code = NewRandomString(129) if err := ad.IsValid(); err == nil { @@ -81,9 +77,7 @@ func TestAuthIsValid(t *testing.T) { } ad.Code = NewId() - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.ExpiresIn = 0 if err := ad.IsValid(); err == nil { @@ -91,9 +85,7 @@ func TestAuthIsValid(t *testing.T) { } ad.ExpiresIn = 1 - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.CreateAt = 0 if err := ad.IsValid(); err == nil { @@ -101,9 +93,7 @@ func TestAuthIsValid(t *testing.T) { } ad.CreateAt = 1 - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.State = NewRandomString(129) if err := ad.IsValid(); err == nil { @@ -121,9 +111,7 @@ func TestAuthIsValid(t *testing.T) { } ad.Scope = NewRandomString(128) - if err := ad.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, ad.IsValid()) ad.RedirectUri = "" if err := ad.IsValid(); err == nil { diff --git a/model/cluster_message_test.go b/model/cluster_message_test.go index 38603e577..e9225a5c5 100644 --- a/model/cluster_message_test.go +++ b/model/cluster_message_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestClusterMessage(t *testing.T) { @@ -17,9 +19,7 @@ func TestClusterMessage(t *testing.T) { json := m.ToJson() result := ClusterMessageFromJson(strings.NewReader(json)) - if result.Data != "hello" { - t.Fatal() - } + require.Equal(t, "hello", result.Data) badresult := ClusterMessageFromJson(strings.NewReader("junk")) if badresult != nil { diff --git a/model/compliance_post_test.go b/model/compliance_post_test.go index ff159ef1b..21044c128 100644 --- a/model/compliance_post_test.go +++ b/model/compliance_post_test.go @@ -5,25 +5,20 @@ package model import ( "testing" + + "github.com/stretchr/testify/require" ) func TestCompliancePostHeader(t *testing.T) { - if CompliancePostHeader()[0] != "TeamName" { - t.Fatal() - } + require.Equal(t, "TeamName", CompliancePostHeader()[0]) } func TestCompliancePost(t *testing.T) { o := CompliancePost{TeamName: "test", PostFileIds: "files", PostCreateAt: GetMillis()} r := o.Row() - if r[0] != "test" { - t.Fatal() - } - - if r[len(r)-1] != "files" { - t.Fatal() - } + require.Equal(t, "test", r[0]) + require.Equal(t, "files", r[len(r)-1]) } var cleanTests = []struct { diff --git a/model/emoji_test.go b/model/emoji_test.go index 50d741214..4539db873 100644 --- a/model/emoji_test.go +++ b/model/emoji_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestEmojiIsValid(t *testing.T) { @@ -23,61 +25,39 @@ func TestEmojiIsValid(t *testing.T) { } emoji.Id = "1234" - if err := emoji.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, emoji.IsValid()) emoji.Id = NewId() emoji.CreateAt = 0 - if err := emoji.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, emoji.IsValid()) emoji.CreateAt = 1234 emoji.UpdateAt = 0 - if err := emoji.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, emoji.IsValid()) emoji.UpdateAt = 1234 emoji.CreatorId = strings.Repeat("1", 27) - if err := emoji.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, emoji.IsValid()) emoji.CreatorId = NewId() emoji.Name = strings.Repeat("1", 65) - if err := emoji.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, emoji.IsValid()) emoji.Name = "" - if err := emoji.IsValid(); err == nil { - t.Fatal(err) - } + require.NotNil(t, emoji.IsValid()) emoji.Name = strings.Repeat("1", 64) - if err := emoji.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, emoji.IsValid()) emoji.Name = "name-" - if err := emoji.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, emoji.IsValid()) emoji.Name = "name_" - if err := emoji.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, emoji.IsValid()) emoji.Name = "name:" - if err := emoji.IsValid(); err == nil { - t.Fatal(err) - } + require.NotNil(t, emoji.IsValid()) emoji.Name = "croissant" - if err := emoji.IsValid(); err == nil { - t.Fatal(err) - } + require.NotNil(t, emoji.IsValid()) } diff --git a/model/oauth_test.go b/model/oauth_test.go index 5c0547717..cbed8a633 100644 --- a/model/oauth_test.go +++ b/model/oauth_test.go @@ -6,6 +6,8 @@ package model import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestOAuthAppJson(t *testing.T) { @@ -52,52 +54,32 @@ func TestOAuthAppPreUpdate(t *testing.T) { func TestOAuthAppIsValid(t *testing.T) { app := OAuthApp{} - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.Id = NewId() - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.CreateAt = 1 - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.UpdateAt = 1 - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.CreatorId = NewId() - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.ClientSecret = NewId() - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.Name = "TestOAuthApp" - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.CallbackUrls = []string{"https://nowhere.com"} - if err := app.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, app.IsValid()) app.Homepage = "https://nowhere.com" - if err := app.IsValid(); err != nil { - t.Fatal() - } + require.Nil(t, app.IsValid()) app.IconURL = "https://nowhere.com/icon_image.png" - if err := app.IsValid(); err != nil { - t.Fatal() - } + require.Nil(t, app.IsValid()) } diff --git a/model/post.go b/model/post.go index 1dd0a4db6..635493c9d 100644 --- a/model/post.go +++ b/model/post.go @@ -121,6 +121,7 @@ type PostActionIntegration struct { type PostActionIntegrationRequest struct { UserId string `json:"user_id"` + PostId string `json:"post_id"` Context StringInterface `json:"context,omitempty"` } @@ -351,6 +352,29 @@ func (r *PostActionIntegrationRequest) ToJson() string { return string(b) } +func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest { + var o *PostActionIntegrationRequest + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + +func (r *PostActionIntegrationResponse) ToJson() string { + b, _ := json.Marshal(r) + return string(b) +} + +func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { + var o *PostActionIntegrationResponse + err := json.NewDecoder(data).Decode(&o) + if err != nil { + return nil + } + return o +} + func (o *Post) Attachments() []*SlackAttachment { if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { return attachments diff --git a/model/post_test.go b/model/post_test.go index af350d76e..b15134c89 100644 --- a/model/post_test.go +++ b/model/post_test.go @@ -11,14 +11,46 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPostJson(t *testing.T) { +func TestPostToJson(t *testing.T) { o := Post{Id: NewId(), Message: NewId()} - json := o.ToJson() - ro := PostFromJson(strings.NewReader(json)) + j := o.ToJson() + ro := PostFromJson(strings.NewReader(j)) - if o.Id != ro.Id { - t.Fatal("Ids do not match") - } + assert.NotNil(t, ro) + assert.Equal(t, o, *ro) +} + +func TestPostFromJsonError(t *testing.T) { + ro := PostFromJson(strings.NewReader("")) + assert.Nil(t, ro) +} + +func TestPostActionIntegrationRequestToJson(t *testing.T) { + o := PostActionIntegrationRequest{UserId: NewId(), Context: StringInterface{"a": "abc"}} + j := o.ToJson() + ro := PostActionIntegrationRequesteFromJson(strings.NewReader(j)) + + assert.NotNil(t, ro) + assert.Equal(t, o, *ro) +} + +func TestPostActionIntegrationRequestFromJsonError(t *testing.T) { + ro := PostActionIntegrationRequesteFromJson(strings.NewReader("")) + assert.Nil(t, ro) +} + +func TestPostActionIntegrationResponseToJson(t *testing.T) { + o := PostActionIntegrationResponse{Update: &Post{Id: NewId(), Message: NewId()}, EphemeralText: NewId()} + j := o.ToJson() + ro := PostActionIntegrationResponseFromJson(strings.NewReader(j)) + + assert.NotNil(t, ro) + assert.Equal(t, o, *ro) +} + +func TestPostActionIntegrationResponseFromJsonError(t *testing.T) { + ro := PostActionIntegrationResponseFromJson(strings.NewReader("")) + assert.Nil(t, ro) } func TestPostIsValid(t *testing.T) { diff --git a/model/preference_test.go b/model/preference_test.go index c56d46e2c..d4035125a 100644 --- a/model/preference_test.go +++ b/model/preference_test.go @@ -7,6 +7,8 @@ import ( "encoding/json" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestPreferenceIsValid(t *testing.T) { @@ -16,54 +18,34 @@ func TestPreferenceIsValid(t *testing.T) { Name: NewId(), } - if err := preference.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, preference.IsValid()) preference.UserId = NewId() - if err := preference.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, preference.IsValid()) preference.Category = strings.Repeat("01234567890", 20) - if err := preference.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, preference.IsValid()) preference.Category = PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW - if err := preference.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, preference.IsValid()) preference.Name = strings.Repeat("01234567890", 20) - if err := preference.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, preference.IsValid()) preference.Name = NewId() - if err := preference.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, preference.IsValid()) preference.Value = strings.Repeat("01234567890", 201) - if err := preference.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, preference.IsValid()) preference.Value = "1234garbage" - if err := preference.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, preference.IsValid()) preference.Category = PREFERENCE_CATEGORY_THEME - if err := preference.IsValid(); err == nil { - t.Fatal() - } + require.NotNil(t, preference.IsValid()) preference.Value = `{"color": "#ff0000", "color2": "#faf"}` - if err := preference.IsValid(); err != nil { - t.Fatal(err) - } + require.Nil(t, preference.IsValid()) } func TestPreferencePreUpdate(t *testing.T) { @@ -79,11 +61,9 @@ func TestPreferencePreUpdate(t *testing.T) { t.Fatal(err) } - if props["color"] != "#ff0000" || props["color2"] != "#faf" || props["codeTheme"] != "github" { - t.Fatal("shouldn't have changed valid props") - } + require.Equal(t, "#ff0000", props["color"], "shouldn't have changed valid props") + require.Equal(t, "#faf", props["color2"], "shouldn't have changed valid props") + require.Equal(t, "github", props["codeTheme"], "shouldn't have changed valid props") - if props["invalid"] == "invalid" { - t.Fatal("should have changed invalid prop") - } + require.NotEqual(t, "invalid", props["invalid"], "should have changed invalid prop") } diff --git a/model/user_test.go b/model/user_test.go index b3aaad522..f86b52919 100644 --- a/model/user_test.go +++ b/model/user_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPasswordHash(t *testing.T) { @@ -117,41 +118,34 @@ func TestUserIsValid(t *testing.T) { } user.Id = NewId() - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "create_at", user.Id) { - t.Fatal(err) - } + err := user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "create_at", user.Id), "expected user is valid error: %s", err.Error()) user.CreateAt = GetMillis() - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "update_at", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "update_at", user.Id), "expected user is valid error: %s", err.Error()) user.UpdateAt = GetMillis() - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "username", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "username", user.Id), "expected user is valid error: %s", err.Error()) user.Username = NewId() + "^hello#" - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "username", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "username", user.Id), "expected user is valid error: %s", err.Error()) user.Username = NewId() - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "email", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "email", user.Id), "expected user is valid error: %s", err.Error()) user.Email = strings.Repeat("01234567890", 20) - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "email", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "email", user.Id), "expected user is valid error: %s", err.Error()) user.Email = "user@example.com" user.Nickname = strings.Repeat("a", 65) - if err := user.IsValid(); !HasExpectedUserIsValidError(err, "nickname", user.Id) { - t.Fatal(err) - } + err = user.IsValid() + require.True(t, HasExpectedUserIsValidError(err, "nickname", user.Id), "expected user is valid error: %s", err.Error()) user.Nickname = strings.Repeat("a", 64) if err := user.IsValid(); err != nil { @@ -331,28 +325,10 @@ func TestCleanUsername(t *testing.T) { } func TestRoles(t *testing.T) { - - if !IsValidUserRoles("team_user") { - t.Fatal() - } - - if IsValidUserRoles("system_admin") { - t.Fatal() - } - - if !IsValidUserRoles("system_user system_admin") { - t.Fatal() - } - - if IsInRole("system_admin junk", "admin") { - t.Fatal() - } - - if !IsInRole("system_admin junk", "system_admin") { - t.Fatal() - } - - if IsInRole("admin", "system_admin") { - t.Fatal() - } + require.True(t, IsValidUserRoles("team_user")) + require.False(t, IsValidUserRoles("system_admin")) + require.True(t, IsValidUserRoles("system_user system_admin")) + require.False(t, IsInRole("system_admin junk", "admin")) + require.True(t, IsInRole("system_admin junk", "system_admin")) + require.False(t, IsInRole("admin", "system_admin")) } diff --git a/model/utils.go b/model/utils.go index 0d8d359a6..574f43e06 100644 --- a/model/utils.go +++ b/model/utils.go @@ -263,7 +263,7 @@ func GetServerIpAddress() string { } else { for _, addr := range addrs { - if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() { + if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() && !ip.IP.IsLinkLocalMulticast() { if ip.IP.To4() != nil { return ip.IP.String() } diff --git a/model/utils_test.go b/model/utils_test.go index 9797c7090..d35146b30 100644 --- a/model/utils_test.go +++ b/model/utils_test.go @@ -34,18 +34,14 @@ func TestAppError(t *testing.T) { err := NewAppError("TestAppError", "message", nil, "", http.StatusInternalServerError) json := err.ToJson() rerr := AppErrorFromJson(strings.NewReader(json)) - if err.Message != rerr.Message { - t.Fatal() - } + require.Equal(t, err.Message, rerr.Message) t.Log(err.Error()) } func TestAppErrorJunk(t *testing.T) { rerr := AppErrorFromJson(strings.NewReader("<html><body>This is a broken test</body></html>")) - if "body: <html><body>This is a broken test</body></html>" != rerr.DetailedError { - t.Fatal() - } + require.Equal(t, "body: <html><body>This is a broken test</body></html>", rerr.DetailedError) } func TestCopyStringMap(t *testing.T) { @@ -173,9 +169,7 @@ func TestValidLower(t *testing.T) { func TestEtag(t *testing.T) { etag := Etag("hello", 24) - if len(etag) <= 0 { - t.Fatal() - } + require.NotEqual(t, "", etag) } var hashtags = map[string]string{ diff --git a/model/version_test.go b/model/version_test.go index 869ed8ad0..a06db326c 100644 --- a/model/version_test.go +++ b/model/version_test.go @@ -6,100 +6,59 @@ package model import ( "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestSplitVersion(t *testing.T) { major1, minor1, patch1 := SplitVersion("junk") - if major1 != 0 || minor1 != 0 || patch1 != 0 { - t.Fatal() - } + require.EqualValues(t, 0, major1) + require.EqualValues(t, 0, minor1) + require.EqualValues(t, 0, patch1) major2, minor2, patch2 := SplitVersion("1.2.3") - if major2 != 1 || minor2 != 2 || patch2 != 3 { - t.Fatal() - } + require.EqualValues(t, 1, major2) + require.EqualValues(t, 2, minor2) + require.EqualValues(t, 3, patch2) major3, minor3, patch3 := SplitVersion("1.2") - if major3 != 1 || minor3 != 2 || patch3 != 0 { - t.Fatal() - } + require.EqualValues(t, 1, major3) + require.EqualValues(t, 2, minor3) + require.EqualValues(t, 0, patch3) major4, minor4, patch4 := SplitVersion("1") - if major4 != 1 || minor4 != 0 || patch4 != 0 { - t.Fatal() - } + require.EqualValues(t, 1, major4) + require.EqualValues(t, 0, minor4) + require.EqualValues(t, 0, patch4) major5, minor5, patch5 := SplitVersion("1.2.3.junkgoeswhere") - if major5 != 1 || minor5 != 2 || patch5 != 3 { - t.Fatal() - } + require.EqualValues(t, 1, major5) + require.EqualValues(t, 2, minor5) + require.EqualValues(t, 3, patch5) } func TestGetPreviousVersion(t *testing.T) { - if GetPreviousVersion("1.3.0") != "1.2.0" { - t.Fatal() - } - - if GetPreviousVersion("1.2.1") != "1.1.0" { - t.Fatal() - } - - if GetPreviousVersion("1.1.0") != "1.0.0" { - t.Fatal() - } - - if GetPreviousVersion("1.0.0") != "0.7.0" { - t.Fatal() - } - - if GetPreviousVersion("0.7.1") != "0.6.0" { - t.Fatal() - } - - if GetPreviousVersion("0.5.0") != "" { - t.Fatal() - } + require.Equal(t, "1.2.0", GetPreviousVersion("1.3.0")) + require.Equal(t, "1.1.0", GetPreviousVersion("1.2.1")) + require.Equal(t, "1.0.0", GetPreviousVersion("1.1.0")) + require.Equal(t, "0.7.0", GetPreviousVersion("1.0.0")) + require.Equal(t, "0.6.0", GetPreviousVersion("0.7.1")) + require.Equal(t, "", GetPreviousVersion("0.5.0")) } func TestIsCurrentVersion(t *testing.T) { major, minor, patch := SplitVersion(CurrentVersion) - if !IsCurrentVersion(CurrentVersion) { - t.Fatal() - } - - if !IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor, patch+100)) { - t.Fatal() - } - - if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor+1, patch)) { - t.Fatal() - } - - if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major+1, minor, patch)) { - t.Fatal() - } + require.True(t, IsCurrentVersion(CurrentVersion)) + require.True(t, IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor, patch+100))) + require.False(t, IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor+1, patch))) + require.False(t, IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major+1, minor, patch))) } func TestIsPreviousVersionsSupported(t *testing.T) { - - if !IsPreviousVersionsSupported(versionsWithoutHotFixes[0]) { - t.Fatal() - } - - if !IsPreviousVersionsSupported(versionsWithoutHotFixes[1]) { - t.Fatal() - } - - if !IsPreviousVersionsSupported(versionsWithoutHotFixes[2]) { - t.Fatal() - } - - if IsPreviousVersionsSupported(versionsWithoutHotFixes[4]) { - t.Fatal() - } - - if IsPreviousVersionsSupported(versionsWithoutHotFixes[5]) { - t.Fatal() - } + require.True(t, IsPreviousVersionsSupported(versionsWithoutHotFixes[0])) + require.True(t, IsPreviousVersionsSupported(versionsWithoutHotFixes[1])) + require.True(t, IsPreviousVersionsSupported(versionsWithoutHotFixes[2])) + require.False(t, IsPreviousVersionsSupported(versionsWithoutHotFixes[4])) + require.False(t, IsPreviousVersionsSupported(versionsWithoutHotFixes[5])) } diff --git a/plugin/api.go b/plugin/api.go index 841a2c702..370c28268 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -143,6 +143,15 @@ type API interface { // CreatePost creates a post. CreatePost(post *model.Post) (*model.Post, *model.AppError) + // AddReaction add a reaction to a post. + AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) + + // RemoveReaction remove a reaction from a post. + RemoveReaction(reaction *model.Reaction) *model.AppError + + // GetReaction get the reactions of a post. + GetReactions(postId string) ([]*model.Reaction, *model.AppError) + // SendEphemeralPost creates an ephemeral post. SendEphemeralPost(userId string, post *model.Post) *model.Post @@ -163,6 +172,12 @@ type API interface { // actually duplicating the uploaded files. CopyFileInfos(userId string, fileIds []string) ([]string, *model.AppError) + // GetFileInfo gets a File Info for a specific fileId + GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) + + // ReadFileAtPath reads the file from the backend for a specific path + ReadFile(path string) ([]byte, *model.AppError) + // KVSet will store a key-value pair, unique per plugin. KVSet(key string, value []byte) *model.AppError @@ -178,6 +193,15 @@ type API interface { // broadcast determines to which users to send the event PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) + // HasPermissionTo check if the user has the permission at system scope. + HasPermissionTo(userId string, permission *model.Permission) bool + + // HasPermissionToTeam check if the user has the permission at team scope. + HasPermissionToTeam(userId, teamId string, permission *model.Permission) bool + + // HasPermissionToChannel check if the user has the permission at channel scope. + HasPermissionToChannel(userId, channelId string, permission *model.Permission) bool + // LogDebug writes a log message to the Mattermost server log file. // Appropriate context such as the plugin name will already be added as fields so plugins // do not need to add that info. diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 9106f2fad..ab41c66d9 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -1705,6 +1705,92 @@ func (s *apiRPCServer) CreatePost(args *Z_CreatePostArgs, returns *Z_CreatePostR return nil } +type Z_AddReactionArgs struct { + A *model.Reaction +} + +type Z_AddReactionReturns struct { + A *model.Reaction + B *model.AppError +} + +func (g *apiRPCClient) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) { + _args := &Z_AddReactionArgs{reaction} + _returns := &Z_AddReactionReturns{} + if err := g.client.Call("Plugin.AddReaction", _args, _returns); err != nil { + log.Printf("RPC call to AddReaction API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) AddReaction(args *Z_AddReactionArgs, returns *Z_AddReactionReturns) error { + if hook, ok := s.impl.(interface { + AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) + }); ok { + returns.A, returns.B = hook.AddReaction(args.A) + } else { + return fmt.Errorf("API AddReaction called but not implemented.") + } + return nil +} + +type Z_RemoveReactionArgs struct { + A *model.Reaction +} + +type Z_RemoveReactionReturns struct { + A *model.AppError +} + +func (g *apiRPCClient) RemoveReaction(reaction *model.Reaction) *model.AppError { + _args := &Z_RemoveReactionArgs{reaction} + _returns := &Z_RemoveReactionReturns{} + if err := g.client.Call("Plugin.RemoveReaction", _args, _returns); err != nil { + log.Printf("RPC call to RemoveReaction API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) RemoveReaction(args *Z_RemoveReactionArgs, returns *Z_RemoveReactionReturns) error { + if hook, ok := s.impl.(interface { + RemoveReaction(reaction *model.Reaction) *model.AppError + }); ok { + returns.A = hook.RemoveReaction(args.A) + } else { + return fmt.Errorf("API RemoveReaction called but not implemented.") + } + return nil +} + +type Z_GetReactionsArgs struct { + A string +} + +type Z_GetReactionsReturns struct { + A []*model.Reaction + B *model.AppError +} + +func (g *apiRPCClient) GetReactions(postId string) ([]*model.Reaction, *model.AppError) { + _args := &Z_GetReactionsArgs{postId} + _returns := &Z_GetReactionsReturns{} + if err := g.client.Call("Plugin.GetReactions", _args, _returns); err != nil { + log.Printf("RPC call to GetReactions API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetReactions(args *Z_GetReactionsArgs, returns *Z_GetReactionsReturns) error { + if hook, ok := s.impl.(interface { + GetReactions(postId string) ([]*model.Reaction, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetReactions(args.A) + } else { + return fmt.Errorf("API GetReactions called but not implemented.") + } + return nil +} + type Z_SendEphemeralPostArgs struct { A string B *model.Post @@ -1850,6 +1936,64 @@ func (s *apiRPCServer) CopyFileInfos(args *Z_CopyFileInfosArgs, returns *Z_CopyF return nil } +type Z_GetFileInfoArgs struct { + A string +} + +type Z_GetFileInfoReturns struct { + A *model.FileInfo + B *model.AppError +} + +func (g *apiRPCClient) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { + _args := &Z_GetFileInfoArgs{fileId} + _returns := &Z_GetFileInfoReturns{} + if err := g.client.Call("Plugin.GetFileInfo", _args, _returns); err != nil { + log.Printf("RPC call to GetFileInfo API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) GetFileInfo(args *Z_GetFileInfoArgs, returns *Z_GetFileInfoReturns) error { + if hook, ok := s.impl.(interface { + GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) + }); ok { + returns.A, returns.B = hook.GetFileInfo(args.A) + } else { + return fmt.Errorf("API GetFileInfo called but not implemented.") + } + return nil +} + +type Z_ReadFileArgs struct { + A string +} + +type Z_ReadFileReturns struct { + A []byte + B *model.AppError +} + +func (g *apiRPCClient) ReadFile(path string) ([]byte, *model.AppError) { + _args := &Z_ReadFileArgs{path} + _returns := &Z_ReadFileReturns{} + if err := g.client.Call("Plugin.ReadFile", _args, _returns); err != nil { + log.Printf("RPC call to ReadFile API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) ReadFile(args *Z_ReadFileArgs, returns *Z_ReadFileReturns) error { + if hook, ok := s.impl.(interface { + ReadFile(path string) ([]byte, *model.AppError) + }); ok { + returns.A, returns.B = hook.ReadFile(args.A) + } else { + return fmt.Errorf("API ReadFile called but not implemented.") + } + return nil +} + type Z_KVSetArgs struct { A string B []byte @@ -1965,6 +2109,95 @@ func (s *apiRPCServer) PublishWebSocketEvent(args *Z_PublishWebSocketEventArgs, return nil } +type Z_HasPermissionToArgs struct { + A string + B *model.Permission +} + +type Z_HasPermissionToReturns struct { + A bool +} + +func (g *apiRPCClient) HasPermissionTo(userId string, permission *model.Permission) bool { + _args := &Z_HasPermissionToArgs{userId, permission} + _returns := &Z_HasPermissionToReturns{} + if err := g.client.Call("Plugin.HasPermissionTo", _args, _returns); err != nil { + log.Printf("RPC call to HasPermissionTo API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) HasPermissionTo(args *Z_HasPermissionToArgs, returns *Z_HasPermissionToReturns) error { + if hook, ok := s.impl.(interface { + HasPermissionTo(userId string, permission *model.Permission) bool + }); ok { + returns.A = hook.HasPermissionTo(args.A, args.B) + } else { + return fmt.Errorf("API HasPermissionTo called but not implemented.") + } + return nil +} + +type Z_HasPermissionToTeamArgs struct { + A string + B string + C *model.Permission +} + +type Z_HasPermissionToTeamReturns struct { + A bool +} + +func (g *apiRPCClient) HasPermissionToTeam(userId, teamId string, permission *model.Permission) bool { + _args := &Z_HasPermissionToTeamArgs{userId, teamId, permission} + _returns := &Z_HasPermissionToTeamReturns{} + if err := g.client.Call("Plugin.HasPermissionToTeam", _args, _returns); err != nil { + log.Printf("RPC call to HasPermissionToTeam API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) HasPermissionToTeam(args *Z_HasPermissionToTeamArgs, returns *Z_HasPermissionToTeamReturns) error { + if hook, ok := s.impl.(interface { + HasPermissionToTeam(userId, teamId string, permission *model.Permission) bool + }); ok { + returns.A = hook.HasPermissionToTeam(args.A, args.B, args.C) + } else { + return fmt.Errorf("API HasPermissionToTeam called but not implemented.") + } + return nil +} + +type Z_HasPermissionToChannelArgs struct { + A string + B string + C *model.Permission +} + +type Z_HasPermissionToChannelReturns struct { + A bool +} + +func (g *apiRPCClient) HasPermissionToChannel(userId, channelId string, permission *model.Permission) bool { + _args := &Z_HasPermissionToChannelArgs{userId, channelId, permission} + _returns := &Z_HasPermissionToChannelReturns{} + if err := g.client.Call("Plugin.HasPermissionToChannel", _args, _returns); err != nil { + log.Printf("RPC call to HasPermissionToChannel API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) HasPermissionToChannel(args *Z_HasPermissionToChannelArgs, returns *Z_HasPermissionToChannelReturns) error { + if hook, ok := s.impl.(interface { + HasPermissionToChannel(userId, channelId string, permission *model.Permission) bool + }); ok { + returns.A = hook.HasPermissionToChannel(args.A, args.B, args.C) + } else { + return fmt.Errorf("API HasPermissionToChannel called but not implemented.") + } + return nil +} + type Z_LogDebugArgs struct { A string B []interface{} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index cf9ffa6a8..e84ceffd8 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -37,6 +37,31 @@ func (_m *API) AddChannelMember(channelId string, userId string) (*model.Channel return r0, r1 } +// AddReaction provides a mock function with given fields: reaction +func (_m *API) AddReaction(reaction *model.Reaction) (*model.Reaction, *model.AppError) { + ret := _m.Called(reaction) + + var r0 *model.Reaction + if rf, ok := ret.Get(0).(func(*model.Reaction) *model.Reaction); ok { + r0 = rf(reaction) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Reaction) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(*model.Reaction) *model.AppError); ok { + r1 = rf(reaction) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // CopyFileInfos provides a mock function with given fields: userId, fileIds func (_m *API) CopyFileInfos(userId string, fileIds []string) ([]string, *model.AppError) { ret := _m.Called(userId, fileIds) @@ -449,6 +474,31 @@ func (_m *API) GetDirectChannel(userId1 string, userId2 string) (*model.Channel, return r0, r1 } +// GetFileInfo provides a mock function with given fields: fileId +func (_m *API) GetFileInfo(fileId string) (*model.FileInfo, *model.AppError) { + ret := _m.Called(fileId) + + var r0 *model.FileInfo + if rf, ok := ret.Get(0).(func(string) *model.FileInfo); ok { + r0 = rf(fileId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FileInfo) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(fileId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetGroupChannel provides a mock function with given fields: userIds func (_m *API) GetGroupChannel(userIds []string) (*model.Channel, *model.AppError) { ret := _m.Called(userIds) @@ -524,6 +574,31 @@ func (_m *API) GetPublicChannelsForTeam(teamId string, offset int, limit int) (* return r0, r1 } +// GetReactions provides a mock function with given fields: postId +func (_m *API) GetReactions(postId string) ([]*model.Reaction, *model.AppError) { + ret := _m.Called(postId) + + var r0 []*model.Reaction + if rf, ok := ret.Get(0).(func(string) []*model.Reaction); ok { + r0 = rf(postId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Reaction) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(postId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetSession provides a mock function with given fields: sessionId func (_m *API) GetSession(sessionId string) (*model.Session, *model.AppError) { ret := _m.Called(sessionId) @@ -799,6 +874,48 @@ func (_m *API) GetUserStatusesByIds(userIds []string) ([]*model.Status, *model.A return r0, r1 } +// HasPermissionTo provides a mock function with given fields: userId, permission +func (_m *API) HasPermissionTo(userId string, permission *model.Permission) bool { + ret := _m.Called(userId, permission) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, *model.Permission) bool); ok { + r0 = rf(userId, permission) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// HasPermissionToChannel provides a mock function with given fields: userId, channelId, permission +func (_m *API) HasPermissionToChannel(userId string, channelId string, permission *model.Permission) bool { + ret := _m.Called(userId, channelId, permission) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string, *model.Permission) bool); ok { + r0 = rf(userId, channelId, permission) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// HasPermissionToTeam provides a mock function with given fields: userId, teamId, permission +func (_m *API) HasPermissionToTeam(userId string, teamId string, permission *model.Permission) bool { + ret := _m.Called(userId, teamId, permission) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string, *model.Permission) bool); ok { + r0 = rf(userId, teamId, permission) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // KVDelete provides a mock function with given fields: key func (_m *API) KVDelete(key string) *model.AppError { ret := _m.Called(key) @@ -907,6 +1024,31 @@ func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{ _m.Called(event, payload, broadcast) } +// ReadFile provides a mock function with given fields: path +func (_m *API) ReadFile(path string) ([]byte, *model.AppError) { + ret := _m.Called(path) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string) *model.AppError); ok { + r1 = rf(path) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // RegisterCommand provides a mock function with given fields: command func (_m *API) RegisterCommand(command *model.Command) error { ret := _m.Called(command) @@ -921,6 +1063,22 @@ func (_m *API) RegisterCommand(command *model.Command) error { return r0 } +// RemoveReaction provides a mock function with given fields: reaction +func (_m *API) RemoveReaction(reaction *model.Reaction) *model.AppError { + ret := _m.Called(reaction) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(*model.Reaction) *model.AppError); ok { + r0 = rf(reaction) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // SaveConfig provides a mock function with given fields: config func (_m *API) SaveConfig(config *model.Config) *model.AppError { ret := _m.Called(config) diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index e158ba5ea..97f60dda0 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -1921,3 +1921,11 @@ func (s SqlChannelStore) ClearAllCustomRoleAssignments() store.StoreChannel { } }) } + +func (s SqlChannelStore) ResetLastPostAt() store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + if _, err := s.GetMaster().Exec("UPDATE Channels SET LastPostAt = (SELECT UpdateAt FROM Posts WHERE ChannelId = Channels.Id ORDER BY UpdateAt DESC LIMIT 1);"); err != nil { + result.Err = model.NewAppError("SqlChannelStore.ResetLastPostAt", "store.sql_channel.reset_last_post_at.app_error", nil, err.Error(), http.StatusInternalServerError) + } + }) +} diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index 4277a0ba2..d9e33df76 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -913,3 +913,14 @@ func (s SqlTeamStore) ClearAllCustomRoleAssignments() store.StoreChannel { } }) } + +func (s SqlTeamStore) AnalyticsGetTeamCountForScheme(schemeId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + count, err := s.GetReplica().SelectInt("SELECT count(*) FROM Teams WHERE SchemeId = :SchemeId AND DeleteAt = 0", map[string]interface{}{"SchemeId": schemeId}) + if err != nil { + result.Err = model.NewAppError("SqlTeamStore.AnalyticsGetTeamCountForScheme", "store.sql_team.analytics_get_team_count_for_scheme.app_error", nil, "schemeId="+schemeId+" "+err.Error(), http.StatusInternalServerError) + return + } + result.Data = count + }) +} diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index cc4e5a0cc..ab3bd202b 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_5_3_0 = "5.3.0" VERSION_5_2_0 = "5.2.0" VERSION_5_1_0 = "5.1.0" VERSION_5_0_0 = "5.0.0" @@ -82,6 +83,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion50(sqlStore) UpgradeDatabaseToVersion51(sqlStore) UpgradeDatabaseToVersion52(sqlStore) + UpgradeDatabaseToVersion53(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -480,3 +482,11 @@ func UpgradeDatabaseToVersion52(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_5_2_0) } } + +func UpgradeDatabaseToVersion53(sqlStore SqlStore) { + // TODO: Uncomment following condition when version 5.3.0 is released + // if shouldPerformUpgrade(sqlStore, VERSION_5_2_0, VERSION_5_3_0) { + + // saveSchemaVersion(sqlStore, VERSION_5_3_0) + // } +} diff --git a/store/store.go b/store/store.go index 89adce188..0c89a0a91 100644 --- a/store/store.go +++ b/store/store.go @@ -110,6 +110,7 @@ type TeamStore interface { MigrateTeamMembers(fromTeamId string, fromUserId string) StoreChannel ResetAllTeamSchemes() StoreChannel ClearAllCustomRoleAssignments() StoreChannel + AnalyticsGetTeamCountForScheme(schemeId string) StoreChannel } type ChannelStore interface { @@ -171,6 +172,7 @@ type ChannelStore interface { MigrateChannelMembers(fromChannelId string, fromUserId string) StoreChannel ResetAllChannelSchemes() StoreChannel ClearAllCustomRoleAssignments() StoreChannel + ResetLastPostAt() StoreChannel } type ChannelMemberHistoryStore interface { diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go index a772f6e44..f7f095a00 100644 --- a/store/storetest/compliance_store.go +++ b/store/storetest/compliance_store.go @@ -10,6 +10,7 @@ import ( "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestComplianceStore(t *testing.T, ss store.Store) { @@ -35,9 +36,8 @@ func testComplianceStore(t *testing.T, ss store.Store) { result := <-c compliances := result.Data.(model.Compliances) - if compliances[0].Status != model.COMPLIANCE_STATUS_RUNNING && compliance2.Id != compliances[0].Id { - t.Fatal() - } + require.Equal(t, model.COMPLIANCE_STATUS_RUNNING, compliances[0].Status) + require.Equal(t, compliance2.Id, compliances[0].Id) compliance2.Status = model.COMPLIANCE_STATUS_FAILED store.Must(ss.Compliance().Update(compliance2)) @@ -46,17 +46,14 @@ func testComplianceStore(t *testing.T, ss store.Store) { result = <-c compliances = result.Data.(model.Compliances) - if compliances[0].Status != model.COMPLIANCE_STATUS_FAILED && compliance2.Id != compliances[0].Id { - t.Fatal() - } + require.Equal(t, model.COMPLIANCE_STATUS_FAILED, compliances[0].Status) + require.Equal(t, compliance2.Id, compliances[0].Id) c = ss.Compliance().GetAll(0, 1) result = <-c compliances = result.Data.(model.Compliances) - if len(compliances) != 1 { - t.Fatal("should only have returned 1") - } + require.Len(t, compliances, 1) c = ss.Compliance().GetAll(1, 1) result = <-c @@ -67,9 +64,7 @@ func testComplianceStore(t *testing.T, ss store.Store) { } rc2 := (<-ss.Compliance().Get(compliance2.Id)).Data.(*model.Compliance) - if rc2.Status != compliance2.Status { - t.Fatal() - } + require.Equal(t, compliance2.Status, rc2.Status) } func testComplianceExport(t *testing.T, ss store.Store) { diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 8adc98e10..747a844ec 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -711,6 +711,22 @@ func (_m *ChannelStore) ResetAllChannelSchemes() store.StoreChannel { return r0 } +// ResetLastPostAt provides a mock function with given fields: +func (_m *ChannelStore) ResetLastPostAt() store.StoreChannel { + ret := _m.Called() + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func() store.StoreChannel); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // Restore provides a mock function with given fields: channelId, time func (_m *ChannelStore) Restore(channelId string, time int64) store.StoreChannel { ret := _m.Called(channelId, time) diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index db5cb658f..8e27e3c05 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -13,6 +13,22 @@ type TeamStore struct { mock.Mock } +// AnalyticsGetTeamCountForScheme provides a mock function with given fields: schemeId +func (_m *TeamStore) AnalyticsGetTeamCountForScheme(schemeId string) store.StoreChannel { + ret := _m.Called(schemeId) + + var r0 store.StoreChannel + if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok { + r0 = rf(schemeId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.StoreChannel) + } + } + + return r0 +} + // AnalyticsTeamCount provides a mock function with given fields: func (_m *TeamStore) AnalyticsTeamCount() store.StoreChannel { ret := _m.Called() diff --git a/store/storetest/status_store.go b/store/storetest/status_store.go index b26be4c19..5231bc29a 100644 --- a/store/storetest/status_store.go +++ b/store/storetest/status_store.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" @@ -103,9 +104,7 @@ func testActiveUserCount(t *testing.T, ss store.Store) { t.Fatal(result.Err) } else { count := result.Data.(int64) - if count <= 0 { - t.Fatal() - } + require.True(t, count > 0, "expected count > 0, got %d", count) } } diff --git a/store/storetest/system_store.go b/store/storetest/system_store.go index a06b72a83..6dc1efe41 100644 --- a/store/storetest/system_store.go +++ b/store/storetest/system_store.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" @@ -25,9 +26,7 @@ func testSystemStore(t *testing.T, ss store.Store) { result := <-ss.System().Get() systems := result.Data.(model.StringMap) - if systems[system.Name] != system.Value { - t.Fatal() - } + require.Equal(t, system.Value, systems[system.Name]) system.Value = "value2" store.Must(ss.System().Update(system)) @@ -35,15 +34,11 @@ func testSystemStore(t *testing.T, ss store.Store) { result2 := <-ss.System().Get() systems2 := result2.Data.(model.StringMap) - if systems2[system.Name] != system.Value { - t.Fatal() - } + require.Equal(t, system.Value, systems2[system.Name]) result3 := <-ss.System().GetByName(system.Name) rsystem := result3.Data.(*model.System) - if rsystem.Value != system.Value { - t.Fatal() - } + require.Equal(t, system.Value, rsystem.Value) } func testSystemStoreSaveOrUpdate(t *testing.T, ss store.Store) { diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index 40d69a2f2..ede1a91d3 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -44,6 +44,7 @@ func TestTeamStore(t *testing.T, ss store.Store) { t.Run("MigrateTeamMembers", func(t *testing.T) { testTeamStoreMigrateTeamMembers(t, ss) }) t.Run("ResetAllTeamSchemes", func(t *testing.T) { testResetAllTeamSchemes(t, ss) }) t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testTeamStoreClearAllCustomRoleAssignments(t, ss) }) + t.Run("AnalyticsGetTeamCountForScheme", func(t *testing.T) { testTeamStoreAnalyticsGetTeamCountForScheme(t, ss) }) } func testTeamStoreSave(t *testing.T, ss store.Store) { @@ -590,10 +591,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { t.Fatal(r1.Err) } else { ms := r1.Data.([]*model.TeamMember) - - if len(ms) != 2 { - t.Fatal() - } + require.Len(t, ms, 2) } if r1 := <-ss.Team().GetMembers(teamId2, 0, 100); r1.Err != nil { @@ -601,14 +599,8 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 1 { - t.Fatal() - } - - if ms[0].UserId != m3.UserId { - t.Fatal() - - } + require.Len(t, ms, 1) + require.Equal(t, m3.UserId, ms[0].UserId) } if r1 := <-ss.Team().GetTeamsForUser(m1.UserId); r1.Err != nil { @@ -616,14 +608,8 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 1 { - t.Fatal() - } - - if ms[0].TeamId != m1.TeamId { - t.Fatal() - - } + require.Len(t, ms, 1) + require.Equal(t, m1.TeamId, ms[0].TeamId) } if r1 := <-ss.Team().RemoveMember(teamId1, m1.UserId); r1.Err != nil { @@ -635,14 +621,8 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 1 { - t.Fatal() - } - - if ms[0].UserId != m2.UserId { - t.Fatal() - - } + require.Len(t, ms, 1) + require.Equal(t, m2.UserId, ms[0].UserId) } store.Must(ss.Team().SaveMember(m1, -1)) @@ -656,9 +636,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 0 { - t.Fatal() - } + require.Len(t, ms, 0) } uid := model.NewId() @@ -672,9 +650,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 2 { - t.Fatal() - } + require.Len(t, ms, 2) } if r1 := <-ss.Team().RemoveAllMembersByUser(uid); r1.Err != nil { @@ -686,9 +662,7 @@ func testTeamMembers(t *testing.T, ss store.Store) { } else { ms := r1.Data.([]*model.TeamMember) - if len(ms) != 0 { - t.Fatal() - } + require.Len(t, ms, 0) } } @@ -1292,3 +1266,64 @@ func testTeamStoreClearAllCustomRoleAssignments(t *testing.T, ss store.Store) { require.Nil(t, r4.Err) assert.Equal(t, "", r4.Data.(*model.TeamMember).Roles) } + +func testTeamStoreAnalyticsGetTeamCountForScheme(t *testing.T, ss store.Store) { + s1 := &model.Scheme{ + DisplayName: model.NewId(), + Name: model.NewId(), + Description: model.NewId(), + Scope: model.SCHEME_SCOPE_TEAM, + } + s1 = (<-ss.Scheme().Save(s1)).Data.(*model.Scheme) + + count1 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) + assert.Equal(t, int64(0), count1) + + t1 := &model.Team{ + Name: model.NewId(), + DisplayName: model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + SchemeId: &s1.Id, + } + t1 = (<-ss.Team().Save(t1)).Data.(*model.Team) + + count2 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) + assert.Equal(t, int64(1), count2) + + t2 := &model.Team{ + Name: model.NewId(), + DisplayName: model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + SchemeId: &s1.Id, + } + t2 = (<-ss.Team().Save(t2)).Data.(*model.Team) + + count3 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) + assert.Equal(t, int64(2), count3) + + t3 := &model.Team{ + Name: model.NewId(), + DisplayName: model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + } + t3 = (<-ss.Team().Save(t3)).Data.(*model.Team) + + count4 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) + assert.Equal(t, int64(2), count4) + + t4 := &model.Team{ + Name: model.NewId(), + DisplayName: model.NewId(), + Email: MakeEmail(), + Type: model.TEAM_OPEN, + SchemeId: &s1.Id, + DeleteAt: model.GetMillis(), + } + t4 = (<-ss.Team().Save(t4)).Data.(*model.Team) + + count5 := (<-ss.Team().AnalyticsGetTeamCountForScheme(s1.Id)).Data.(int64) + assert.Equal(t, int64(2), count5) +} diff --git a/store/storetest/user_store.go b/store/storetest/user_store.go index 10fb6a4d9..d1a373f9b 100644 --- a/store/storetest/user_store.go +++ b/store/storetest/user_store.go @@ -246,9 +246,7 @@ func testUserCount(t *testing.T, ss store.Store) { t.Fatal(result.Err) } else { count := result.Data.(int64) - if count <= 0 { - t.Fatal() - } + require.False(t, count <= 0, "expected count > 0, got %d", count) } } diff --git a/utils/markdown/autolink.go b/utils/markdown/autolink.go index 16c40e609..7f7d1117f 100644 --- a/utils/markdown/autolink.go +++ b/utils/markdown/autolink.go @@ -16,27 +16,27 @@ var ( DefaultUrlSchemes = []string{"http", "https", "ftp", "mailto", "tel"} ) -// Given a string with a w at the given position, tries to parse and return a link starting with "www." +// Given a string with a w at the given position, tries to parse and return a range containing a www link. // if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to // www_match from the reference code. -func parseWWWAutolink(data string, position int) string { +func parseWWWAutolink(data string, position int) (Range, bool) { // Check that this isn't part of another word if position > 1 { prevChar := data[position-1] if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) { - return "" + return Range{}, false } } // Check that this starts with www if len(data)-position < 4 || !regexp.MustCompile(`^www\d{0,3}\.`).MatchString(data[position:]) { - return "" + return Range{}, false } end := checkDomain(data[position:], false) if end == 0 { - return "" + return Range{}, false } end += position @@ -47,12 +47,12 @@ func parseWWWAutolink(data string, position int) string { } // Trim trailing punctuation - link := trimTrailingCharactersFromLink(data[position:end]) - if link == "" { - return "" + end = trimTrailingCharactersFromLink(data, position, end) + if position == end { + return Range{}, false } - return link + return Range{position, end}, true } func isAllowedBeforeWWWLink(c byte) bool { @@ -64,13 +64,13 @@ func isAllowedBeforeWWWLink(c byte) bool { } } -// Given a string with a : at the given position, tried to parse and return a link starting with a URL scheme +// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme // if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to // url_match from the reference code. -func parseURLAutolink(data string, position int) string { +func parseURLAutolink(data string, position int) (Range, bool) { // Check that a :// exists. This doesn't match the clients that treat the slashes as optional. if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' { - return "" + return Range{}, false } start := position - 1 @@ -81,12 +81,12 @@ func parseURLAutolink(data string, position int) string { // Ensure that the URL scheme is allowed and that at least one character after the scheme is valid. scheme := data[start:position] if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) { - return "" + return Range{}, false } end := checkDomain(data[position+3:], true) if end == 0 { - return "" + return Range{}, false } end += position @@ -97,12 +97,12 @@ func parseURLAutolink(data string, position int) string { } // Trim trailing punctuation - link := trimTrailingCharactersFromLink(data[start:end]) - if link == "" { - return "" + end = trimTrailingCharactersFromLink(data, start, end) + if start == end { + return Range{}, false } - return link + return Range{start, end}, true } func isSchemeAllowed(scheme string) bool { @@ -166,9 +166,9 @@ func isValidHostCharacter(link string) bool { } // Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link. -// Equivalent to autolink_delim from the reference code. -func trimTrailingCharactersFromLink(link string) string { - runes := []rune(link) +// Returns a new end position for the link. Equivalent to autolink_delim from the reference code. +func trimTrailingCharactersFromLink(markdown string, start int, end int) int { + runes := []rune(markdown[start:end]) linkEnd := len(runes) // Cut off the link before an open angle bracket if it contains one @@ -240,7 +240,7 @@ func trimTrailingCharactersFromLink(link string) string { } } - return string(runes[:linkEnd]) + return start + len(string(runes[:linkEnd])) } func canEndAutolink(c rune) bool { diff --git a/utils/markdown/autolink_test.go b/utils/markdown/autolink_test.go index d0ea53fa4..997124338 100644 --- a/utils/markdown/autolink_test.go +++ b/utils/markdown/autolink_test.go @@ -134,7 +134,15 @@ func TestParseURLAutolink(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { - assert.Equal(t, testCase.Expected, parseURLAutolink(testCase.Input, testCase.Position)) + rawRange, ok := parseURLAutolink(testCase.Input, testCase.Position) + + if testCase.Expected == "" { + assert.False(t, ok) + assert.Equal(t, Range{0, 0}, rawRange) + } else { + assert.True(t, ok) + assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End]) + } }) } } @@ -264,89 +272,153 @@ func TestParseWWWAutolink(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { - assert.Equal(t, testCase.Expected, parseWWWAutolink(testCase.Input, testCase.Position)) + rawRange, ok := parseWWWAutolink(testCase.Input, testCase.Position) + + if testCase.Expected == "" { + assert.False(t, ok) + assert.Equal(t, Range{0, 0}, rawRange) + } else { + assert.True(t, ok) + assert.Equal(t, testCase.Expected, testCase.Input[rawRange.Position:rawRange.End]) + } }) } } func TestTrimTrailingCharactersFromLink(t *testing.T) { testCases := []struct { - Input string - Expected string + Input string + Start int + End int + ExpectedEnd int }{ { - Input: "http://www.example.com", - Expected: "http://www.example.com", + Input: "http://www.example.com", + ExpectedEnd: 22, + }, + { + Input: "http://www.example.com/abcd", + ExpectedEnd: 27, + }, + { + Input: "http://www.example.com/abcd/", + ExpectedEnd: 28, + }, + { + Input: "http://www.example.com/1234", + ExpectedEnd: 27, + }, + { + Input: "http://www.example.com/abcd?foo=bar", + ExpectedEnd: 35, }, { - Input: "http://www.example.com/abcd", - Expected: "http://www.example.com/abcd", + Input: "http://www.example.com/abcd#heading", + ExpectedEnd: 35, }, { - Input: "http://www.example.com/abcd/", - Expected: "http://www.example.com/abcd/", + Input: "http://www.example.com.", + ExpectedEnd: 22, }, { - Input: "http://www.example.com/1234", - Expected: "http://www.example.com/1234", + Input: "http://www.example.com,", + ExpectedEnd: 22, }, { - Input: "http://www.example.com/abcd?foo=bar", - Expected: "http://www.example.com/abcd?foo=bar", + Input: "http://www.example.com?", + ExpectedEnd: 22, }, { - Input: "http://www.example.com/abcd#heading", - Expected: "http://www.example.com/abcd#heading", + Input: "http://www.example.com)", + ExpectedEnd: 22, }, { - Input: "http://www.example.com.", - Expected: "http://www.example.com", + Input: "http://www.example.com", + ExpectedEnd: 22, }, { - Input: "http://www.example.com,", - Expected: "http://www.example.com", + Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)", + ExpectedEnd: 54, }, { - Input: "http://www.example.com?", - Expected: "http://www.example.com", + Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation", + ExpectedEnd: 53, }, { - Input: "http://www.example.com)", - Expected: "http://www.example.com", + Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation))", + ExpectedEnd: 54, }, { - Input: "http://www.example.com", - Expected: "http://www.example.com", + Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)", + ExpectedEnd: 71, }, { - Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)", - Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)", + Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))", + ExpectedEnd: 71, }, { - Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation", - Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation", + Input: "http://www.example.com"", + ExpectedEnd: 22, }, { - Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation))", - Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)", + Input: "this is a sentence containing http://www.example.com in it", + Start: 30, + End: 52, + ExpectedEnd: 52, }, { - Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)", - Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation)_(disambiguation)", + Input: "this is a sentence containing http://www.example.com???", + Start: 30, + End: 55, + ExpectedEnd: 52, }, { - Input: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))", - Expected: "https://en.wikipedia.org/wiki/Dolphin_(disambiguation_(disambiguation))", + Input: "http://google.com/å", + ExpectedEnd: len("http://google.com/å"), }, { - Input: "http://www.example.com"", - Expected: "http://www.example.com", + Input: "http://google.com/å...", + ExpectedEnd: len("http://google.com/å"), + }, + { + Input: "This is http://google.com/å, a link, and http://google.com/å", + Start: 8, + End: len("This is http://google.com/å,"), + ExpectedEnd: len("This is http://google.com/å"), + }, + { + Input: "This is http://google.com/å, a link, and http://google.com/å", + Start: 41, + End: len("This is http://google.com/å, a link, and http://google.com/å"), + ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"), + }, + { + Input: "This is http://google.com/å, a link, and http://google.com/å.", + Start: 41, + End: len("This is http://google.com/å, a link, and http://google.com/å."), + ExpectedEnd: len("This is http://google.com/å, a link, and http://google.com/å"), + }, + { + Input: "http://🍄.ga/ http://x🍄.ga/", + Start: 0, + End: len("http://🍄.ga/"), + ExpectedEnd: len("http://🍄.ga/"), + }, + { + Input: "http://🍄.ga/ http://x🍄.ga/", + Start: len("http://🍄.ga/ "), + End: len("http://🍄.ga/ http://x🍄.ga/"), + ExpectedEnd: len("http://🍄.ga/ http://x🍄.ga/"), }, } for _, testCase := range testCases { t.Run(testCase.Input, func(t *testing.T) { - assert.Equal(t, testCase.Expected, trimTrailingCharactersFromLink(testCase.Input)) + if testCase.End == 0 { + testCase.End = len(testCase.Input) - testCase.Start + } + + assert.Equal(t, testCase.ExpectedEnd, trimTrailingCharactersFromLink(testCase.Input, testCase.Start, testCase.End)) }) } } diff --git a/utils/markdown/commonmark_test.go b/utils/markdown/commonmark_test.go index 13e61f52d..d1381cee5 100644 --- a/utils/markdown/commonmark_test.go +++ b/utils/markdown/commonmark_test.go @@ -1000,7 +1000,7 @@ func TestCommonMarkReferenceStrings(t *testing.T) { } } -func TestCommonMarkRefernceAutolinks(t *testing.T) { +func TestCommonMarkReferenceAutolinks(t *testing.T) { // These tests are adapted from the GitHub-flavoured CommonMark extension tests located at // https://github.com/github/cmark/blob/master/test/extensions.txt for name, tc := range map[string]struct { diff --git a/utils/markdown/html.go b/utils/markdown/html.go index 1a857afed..afb72bff3 100644 --- a/utils/markdown/html.go +++ b/utils/markdown/html.go @@ -157,7 +157,7 @@ func RenderInlineHTML(inline Inline) (result string) { } result += "</a>" case *Autolink: - result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Link)) + `">` + result += `<a href="` + htmlEscaper.Replace(escapeURL(v.Destination())) + `">` for _, inline := range v.Children { result += RenderInlineHTML(inline) } diff --git a/utils/markdown/inlines.go b/utils/markdown/inlines.go index 453f4bbe5..a3abccef3 100644 --- a/utils/markdown/inlines.go +++ b/utils/markdown/inlines.go @@ -86,7 +86,19 @@ type Autolink struct { Children []Inline - Link string + RawDestination Range + + markdown string +} + +func (i *Autolink) Destination() string { + destination := Unescape(i.markdown[i.RawDestination.Position:i.RawDestination.End]) + + if strings.HasPrefix(destination, "www") { + destination = "http://" + destination + } + + return destination } type delimiterType int @@ -486,15 +498,18 @@ func (p *inlineParser) parseAutolink(c rune) bool { } } - link := "" - text := "" + var link Range if c == ':' { - text = parseURLAutolink(p.raw, p.position) - link = text + var ok bool + link, ok = parseURLAutolink(p.raw, p.position) + + if !ok { + return false + } // Since the current position is at the colon, we have to rewind the parsing slightly so that // we don't duplicate the URL scheme - rewind := strings.Index(text, ":") + rewind := strings.Index(p.raw[link.Position:link.End], ":") if rewind != -1 { lastInline := p.inlines[len(p.inlines)-1] lastText, ok := lastInline.(*Text) @@ -512,22 +527,30 @@ func (p *inlineParser) parseAutolink(c rune) bool { Range: Range{lastText.Range.Position, lastText.Range.End - rewind}, }) p.position -= rewind + } + } else if c == 'w' || c == 'W' { + var ok bool + link, ok = parseWWWAutolink(p.raw, p.position) + if !ok { + return false } - } else if c == 'w' { - text = parseWWWAutolink(p.raw, p.position) - link = "http://" + text } - if text == "" { - return false - } + linkMarkdownPosition := relativeToAbsolutePosition(p.ranges, link.Position) + linkRange := Range{linkMarkdownPosition, linkMarkdownPosition + link.End - link.Position} p.inlines = append(p.inlines, &Autolink{ - Link: link, - Children: []Inline{&Text{Text: text}}, + Children: []Inline{ + &Text{ + Text: p.raw[link.Position:link.End], + Range: linkRange, + }, + }, + RawDestination: linkRange, + markdown: p.markdown, }) - p.position += len(text) + p.position += (link.End - link.Position) return true } |