diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..b86d194ae8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,8 @@ +version: 2.1 + +jobs: + build: + docker: + - image: circleci/golang:1.12 + steps: + - run: echo "skipping build. PR \#11978 in progress." diff --git a/Makefile b/Makefile index da4c7f9b91..2dc873582c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build package run stop run-client run-server stop-client stop-server restart restart-server restart-client start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist prepare-enteprise run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows internal-test-web-client vet run-server-for-web-client-tests +.PHONY: build package run stop run-client run-server stop-client stop-server restart restart-server restart-client start-docker clean-dist clean nuke check-style check-client-style check-server-style check-unit-tests test dist prepare-enteprise run-client-tests setup-run-client-tests cleanup-run-client-tests test-client build-linux build-osx build-windows internal-test-web-client vet run-server-for-web-client-tests diff-config ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) @@ -87,7 +87,7 @@ PLUGIN_PACKAGES += mattermost-plugin-github-v0.10.2 PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.0.2 PLUGIN_PACKAGES += mattermost-plugin-antivirus-v0.1.1 -PLUGIN_PACKAGES += mattermost-plugin-jira-v2.1.0 +PLUGIN_PACKAGES += mattermost-plugin-jira-v2.1.1 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.0.0 @@ -361,11 +361,11 @@ ifeq ($(BUILDER_GOOS_GOARCH),"windows_amd64") wmic process where "Caption='go.exe' and CommandLine like '%go.exe run%'" call terminate wmic process where "Caption='mattermost.exe' and CommandLine like '%go-build%'" call terminate else - @for PID in $$(ps -ef | grep "[g]o run" | awk '{ print $$2 }'); do \ + @for PID in $$(ps -ef | grep "[g]o run" | grep "disableconfigwatch" | awk '{ print $$2 }'); do \ echo stopping go $$PID; \ kill $$PID; \ done - @for PID in $$(ps -ef | grep "[g]o-build" | awk '{ print $$2 }'); do \ + @for PID in $$(ps -ef | grep "[g]o-build" | grep "disableconfigwatch" | awk '{ print $$2 }'); do \ echo stopping mattermost $$PID; \ kill $$PID; \ done @@ -410,6 +410,9 @@ config-reset: ## Resets the config/config.json file to the default. rm -f config/config.json OUTPUT_CONFIG=$(PWD)/config/config.json go generate ./config +diff-config: ## Compares default configuration between two mattermost versions + @./scripts/diff-config.sh + clean: stop-docker ## Clean up everything except persistant server data. @echo Cleaning diff --git a/api4/apitestlib.go b/api4/apitestlib.go index f70bdbee3b..32aa279c67 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -25,8 +25,8 @@ import ( "github.com/mattermost/mattermost-server/web" "github.com/mattermost/mattermost-server/wsapi" - s3 "github.com/minio/minio-go" - "github.com/minio/minio-go/pkg/credentials" + s3 "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v6/pkg/credentials" ) type TestHelper struct { diff --git a/api4/channel.go b/api4/channel.go index bc7bb9c0b5..03edc36a7b 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -527,7 +527,13 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) { return } - stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount} + pinnedPostCount, err := c.App.GetChannelPinnedPostCount(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + stats := model.ChannelStats{ChannelId: c.Params.ChannelId, MemberCount: memberCount, GuestCount: guestCount, PinnedPostCount: pinnedPostCount} w.Write([]byte(stats.ToJson())) } diff --git a/api4/channel_test.go b/api4/channel_test.go index 8473f8c587..fb7ac11d15 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -1849,6 +1849,16 @@ func TestGetChannelStats(t *testing.T) { t.Fatal("couldnt't get extra info") } else if stats.MemberCount != 1 { t.Fatal("got incorrect member count") + } else if stats.PinnedPostCount != 0 { + t.Fatal("got incorrect pinned post count") + } + + th.CreatePinnedPostWithClient(th.Client, channel) + stats, resp = Client.GetChannelStats(channel.Id, "") + CheckNoError(t, resp) + + if stats.PinnedPostCount != 1 { + t.Fatal("should have returned 1 pinned post count") } _, resp = Client.GetChannelStats("junk", "") diff --git a/api4/system.go b/api4/system.go index fba02a2e6e..aa587b352c 100644 --- a/api4/system.go +++ b/api4/system.go @@ -27,6 +27,7 @@ func (api *API) InitSystem() { api.BaseRoutes.ApiRoot.Handle("/audits", api.ApiSessionRequired(getAudits)).Methods("GET") api.BaseRoutes.ApiRoot.Handle("/email/test", api.ApiSessionRequired(testEmail)).Methods("POST") + api.BaseRoutes.ApiRoot.Handle("/site_url/test", api.ApiSessionRequired(testSiteURL)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/file/s3_test", api.ApiSessionRequired(testS3)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/database/recycle", api.ApiSessionRequired(databaseRecycle)).Methods("POST") api.BaseRoutes.ApiRoot.Handle("/caches/invalidate", api.ApiSessionRequired(invalidateCaches)).Methods("POST") @@ -145,6 +146,32 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } +func testSiteURL(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin { + c.Err = model.NewAppError("testSiteURL", "api.restricted_system_admin", nil, "", http.StatusForbidden) + return + } + + props := model.MapFromJson(r.Body) + siteURL := props["site_url"] + if siteURL == "" { + c.SetInvalidParam("site_url") + return + } + err := c.App.TestSiteURL(siteURL) + if err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + func getAudits(c *Context, w http.ResponseWriter, r *http.Request) { if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) diff --git a/api4/system_test.go b/api4/system_test.go index 824cf0f86f..6e58b56413 100644 --- a/api4/system_test.go +++ b/api4/system_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "os" + "strings" "testing" "github.com/mattermost/mattermost-server/mlog" @@ -149,6 +150,47 @@ func TestEmailTest(t *testing.T) { }) } +func TestSiteURLTest(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + Client := th.Client + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/valid/api/v4/system/ping") { + w.WriteHeader(200) + } else { + w.WriteHeader(400) + } + })) + defer ts.Close() + + validSiteURL := ts.URL + "/valid" + invalidSiteURL := ts.URL + "/invalid" + + t.Run("as system admin", func(t *testing.T) { + _, resp := th.SystemAdminClient.TestSiteURL("") + CheckBadRequestStatus(t, resp) + + _, resp = th.SystemAdminClient.TestSiteURL(invalidSiteURL) + CheckBadRequestStatus(t, resp) + + _, resp = th.SystemAdminClient.TestSiteURL(validSiteURL) + CheckOKStatus(t, resp) + }) + + t.Run("as system user", func(t *testing.T) { + _, resp := Client.TestSiteURL(validSiteURL) + CheckForbiddenStatus(t, resp) + }) + + t.Run("as restricted system admin", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true }) + + _, resp := Client.TestSiteURL(validSiteURL) + CheckForbiddenStatus(t, resp) + }) +} + func TestDatabaseRecycle(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/api4/team.go b/api4/team.go index 73c3e8d5ba..5c0df4902c 100644 --- a/api4/team.go +++ b/api4/team.go @@ -729,7 +729,7 @@ func updateTeamMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.Requ } func getAllTeams(c *Context, w http.ResponseWriter, r *http.Request) { - var teams []*model.Team + teams := []*model.Team{} var err *model.AppError var teamsWithCount *model.TeamsWithCount @@ -740,9 +740,17 @@ func getAllTeams(c *Context, w http.ResponseWriter, r *http.Request) { teams, err = c.App.GetAllTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) } } else if c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_LIST_PRIVATE_TEAMS) { - teams, err = c.App.GetAllPrivateTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + if c.Params.IncludeTotalCount { + teamsWithCount, err = c.App.GetAllPrivateTeamsPageWithCount(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + } else { + teams, err = c.App.GetAllPrivateTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + } } else if c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_LIST_PUBLIC_TEAMS) { - teams, err = c.App.GetAllPublicTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + if c.Params.IncludeTotalCount { + teamsWithCount, err = c.App.GetAllPublicTeamsPageWithCount(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + } else { + teams, err = c.App.GetAllPublicTeamsPage(c.Params.Page*c.Params.PerPage, c.Params.PerPage) + } } if err != nil { diff --git a/api4/team_test.go b/api4/team_test.go index ff5b0d84eb..dce8f8b7d0 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -621,6 +621,10 @@ func TestGetAllTeams(t *testing.T) { team3, resp = Client.CreateTeam(team3) CheckNoError(t, resp) + team4 := &model.Team{DisplayName: "Name4", Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TEAM_OPEN, AllowOpenInvite: false} + team4, resp = Client.CreateTeam(team4) + CheckNoError(t, resp) + testCases := []struct { Name string Page int @@ -663,14 +667,14 @@ func TestGetAllTeams(t *testing.T) { Page: 0, PerPage: 10, Permissions: []string{model.PERMISSION_LIST_PRIVATE_TEAMS.Id}, - ExpectedTeams: []string{th.BasicTeam.Id, team3.Id}, + ExpectedTeams: []string{th.BasicTeam.Id, team3.Id, team4.Id}, }, { Name: "Get all teams", Page: 0, PerPage: 10, Permissions: []string{model.PERMISSION_LIST_PUBLIC_TEAMS.Id, model.PERMISSION_LIST_PRIVATE_TEAMS.Id}, - ExpectedTeams: []string{th.BasicTeam.Id, team1.Id, team2.Id, team3.Id}, + ExpectedTeams: []string{th.BasicTeam.Id, team1.Id, team2.Id, team3.Id, team4.Id}, }, { Name: "Get no teams because permissions", @@ -684,9 +688,27 @@ func TestGetAllTeams(t *testing.T) { Page: 0, PerPage: 10, Permissions: []string{model.PERMISSION_LIST_PUBLIC_TEAMS.Id, model.PERMISSION_LIST_PRIVATE_TEAMS.Id}, - ExpectedTeams: []string{th.BasicTeam.Id, team1.Id, team2.Id, team3.Id}, + ExpectedTeams: []string{th.BasicTeam.Id, team1.Id, team2.Id, team3.Id, team4.Id}, WithCount: true, - ExpectedCount: 4, + ExpectedCount: 5, + }, + { + Name: "Get all public teams with count", + Page: 0, + PerPage: 10, + Permissions: []string{model.PERMISSION_LIST_PUBLIC_TEAMS.Id}, + ExpectedTeams: []string{team1.Id, team2.Id}, + WithCount: true, + ExpectedCount: 2, + }, + { + Name: "Get all private teams with count", + Page: 0, + PerPage: 10, + Permissions: []string{model.PERMISSION_LIST_PRIVATE_TEAMS.Id}, + ExpectedTeams: []string{th.BasicTeam.Id, team3.Id, team4.Id}, + WithCount: true, + ExpectedCount: 3, }, } @@ -2310,7 +2332,9 @@ func TestInviteGuestsToTeam(t *testing.T) { defer func() { th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableEmailInvitations = &enableEmailInvitations }) th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.RestrictCreationToDomains = restrictCreationToDomains }) - th.App.UpdateConfig(func(cfg *model.Config) { cfg.GuestAccountsSettings.RestrictCreationToDomains = guestRestrictCreationToDomains }) + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.GuestAccountsSettings.RestrictCreationToDomains = guestRestrictCreationToDomains + }) th.App.UpdateConfig(func(cfg *model.Config) { cfg.GuestAccountsSettings.Enable = &enableGuestAccounts }) }() diff --git a/api4/user.go b/api4/user.go index e4589ae85f..927971c4f2 100644 --- a/api4/user.go +++ b/api4/user.go @@ -86,6 +86,8 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { return } + user.SanitizeInput() + tokenId := r.URL.Query().Get("t") inviteId := r.URL.Query().Get("iid") diff --git a/api4/user_test.go b/api4/user_test.go index 07fff6a122..505c654b1e 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -83,6 +83,70 @@ func TestCreateUser(t *testing.T) { assert.Equal(t, http.StatusBadRequest, r.StatusCode) } +func TestCreateUserInputFilter(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + t.Run("DomainRestriction", func(t *testing.T) { + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.EnableOpenServer = true + *cfg.TeamSettings.EnableUserCreation = true + *cfg.TeamSettings.RestrictCreationToDomains = "mattermost.com" + }) + + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.RestrictCreationToDomains = "" + }) + + t.Run("ValidUser", func(t *testing.T) { + user := &model.User{Email: "foobar+testdomainrestriction@mattermost.com", Password: "Password1", Username: GenerateTestUsername()} + _, resp := th.SystemAdminClient.CreateUser(user) + CheckNoError(t, resp) + }) + + t.Run("InvalidEmail", func(t *testing.T) { + user := &model.User{Email: "foobar+testdomainrestriction@mattermost.org", Password: "Password1", Username: GenerateTestUsername()} + _, resp := th.SystemAdminClient.CreateUser(user) + CheckBadRequestStatus(t, resp) + }) + + t.Run("AuthServiceFilter", func(t *testing.T) { + user := &model.User{Email: "foobar+testdomainrestriction@mattermost.org", Password: "Password1", Username: GenerateTestUsername(), AuthService: "ldap"} + _, resp := th.SystemAdminClient.CreateUser(user) + CheckBadRequestStatus(t, resp) + }) + }) + + t.Run("Roles", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.EnableOpenServer = true + *cfg.TeamSettings.EnableUserCreation = true + *cfg.TeamSettings.RestrictCreationToDomains = "" + }) + + t.Run("InvalidRole", func(t *testing.T) { + user := &model.User{Email: "foobar+testinvalidrole@mattermost.com", Password: "Password1", Username: GenerateTestUsername(), Roles: "system_user system_admin"} + _, resp := th.SystemAdminClient.CreateUser(user) + CheckNoError(t, resp) + ruser, err := th.App.GetUserByEmail("foobar+testinvalidrole@mattermost.com") + assert.Nil(t, err) + assert.NotEqual(t, ruser.Roles, "system_user system_admin") + }) + }) + + t.Run("InvalidId", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.TeamSettings.EnableOpenServer = true + *cfg.TeamSettings.EnableUserCreation = true + }) + + user := &model.User{Id: "AAAAAAAAAAAAAAAAAAAAAAAAAA", Email: "foobar+testinvalidid@mattermost.com", Password: "Password1", Username: GenerateTestUsername(), Roles: "system_user system_admin"} + _, resp := th.SystemAdminClient.CreateUser(user) + CheckBadRequestStatus(t, resp) + }) +} + func TestCreateUserWithToken(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/admin.go b/app/admin.go index f883a550dd..91898efe92 100644 --- a/app/admin.go +++ b/app/admin.go @@ -4,6 +4,7 @@ package app import ( + "fmt" "io" "os" "time" @@ -174,6 +175,16 @@ func (a *App) RecycleDatabaseConnection() { mlog.Warn("Finished recycling the database connection.") } +func (a *App) TestSiteURL(siteURL string) *model.AppError { + url := fmt.Sprintf("%s/api/v4/system/ping", siteURL) + res, err := http.Get(url) + if err != nil || res.StatusCode != 200 { + return model.NewAppError("testSiteURL", "app.admin.test_site_url.failure", nil, "", http.StatusBadRequest) + } + + return nil +} + func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError { if len(*cfg.EmailSettings.SMTPServer) == 0 { return model.NewAppError("testEmail", "api.admin.test_email.missing_server", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusBadRequest) diff --git a/app/authorization_test.go b/app/authorization_test.go index 0151985629..e8a32ebde6 100644 --- a/app/authorization_test.go +++ b/app/authorization_test.go @@ -4,6 +4,7 @@ package app import ( + "github.com/stretchr/testify/assert" "testing" "github.com/mattermost/mattermost-server/model" @@ -28,10 +29,8 @@ func TestCheckIfRolesGrantPermission(t *testing.T) { {[]string{model.TEAM_ADMIN_ROLE_ID, model.TEAM_USER_ROLE_ID}, model.PERMISSION_MANAGE_SLASH_COMMANDS.Id, true}, } - for testnum, testcase := range cases { - if th.App.RolesGrantPermission(testcase.roles, testcase.permissionId) != testcase.shouldGrant { - t.Fatal("Failed test case ", testnum) - } + for _, testcase := range cases { + assert.Equal(t, th.App.RolesGrantPermission(testcase.roles, testcase.permissionId), testcase.shouldGrant) } } diff --git a/app/channel.go b/app/channel.go index ac51f14e5b..ed800bf039 100644 --- a/app/channel.go +++ b/app/channel.go @@ -160,7 +160,7 @@ func (a *App) CreateChannelWithUser(channel *model.Channel, userId string) (*mod } // Get total number of channels on current team - count, err := a.GetNumberOfChannelsOnTeam(channel.TeamId) + count, err := a.GetNumberOfChannelsOnTeam(channel.TeamId, false) if err != nil { return nil, err } @@ -1330,6 +1330,10 @@ func (a *App) GetChannelGuestCount(channelId string) (int64, *model.AppError) { return a.Srv.Store.Channel().GetGuestCount(channelId, true) } +func (a *App) GetChannelPinnedPostCount(channelId string) (int64, *model.AppError) { + return a.Srv.Store.Channel().GetPinnedPostCount(channelId, true) +} + func (a *App) GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) { return a.Srv.Store.Channel().GetChannelCounts(teamId, userId) } @@ -1720,12 +1724,23 @@ func (a *App) RemoveUserFromChannel(userIdToRemove string, removerUserId string, return nil } -func (a *App) GetNumberOfChannelsOnTeam(teamId string) (int, *model.AppError) { +func (a *App) GetNumberOfChannelsOnTeam(teamId string, includeDeleted bool) (int, *model.AppError) { // Get total number of channels on current team list, err := a.Srv.Store.Channel().GetTeamChannels(teamId) if err != nil { return 0, err } + + if !includeDeleted { + count := 0 + for _, channel := range *list { + if channel.DeleteAt == 0 { + count++ + } + } + return count, nil + } + return len(*list), nil } diff --git a/app/channel_test.go b/app/channel_test.go index c44c6aa307..0e8446ce05 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -1042,6 +1042,21 @@ func TestSearchChannelsForUser(t *testing.T) { }) } +func TestGetNumberOfChannelsOnTeam(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.DeleteChannel(th.BasicChannel, th.BasicUser.Id) + + count, err := th.App.GetNumberOfChannelsOnTeam(th.BasicTeam.Id, true) + require.Nil(t, err) + assert.Equal(t, 3, count) + + count, err = th.App.GetNumberOfChannelsOnTeam(th.BasicTeam.Id, false) + require.Nil(t, err) + assert.Equal(t, 2, count) +} + func TestMarkChannelAsUnreadFromPost(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/command.go b/app/command.go index a69d8a3637..0d44b5dd1c 100644 --- a/app/command.go +++ b/app/command.go @@ -187,7 +187,7 @@ func (a *App) ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, * return nil, model.NewAppError("command", "api.command.execute_command.not_found.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusNotFound) } -// tryExecutePluginCommand attempts to run a built in command based on the given arguments. If no such command can be +// tryExecuteBuiltInCommand attempts to run a built in command based on the given arguments. If no such command can be // found, returns nil for all arguments. func (a *App) tryExecuteBuiltInCommand(args *model.CommandArgs, trigger string, message string) (*model.Command, *model.CommandResponse) { provider := GetCommandProvider(trigger) diff --git a/app/command_echo.go b/app/command_echo.go index 2197959114..9b489bda23 100644 --- a/app/command_echo.go +++ b/app/command_echo.go @@ -4,7 +4,6 @@ package app import ( - "fmt" "strconv" "strings" "time" @@ -90,7 +89,7 @@ func (me *EchoProvider) DoCommand(a *App, args *model.CommandArgs, message strin time.Sleep(time.Duration(delay) * time.Second) if _, err := a.CreatePostMissingChannel(post, true); err != nil { - mlog.Error(fmt.Sprintf("Unable to create /echo post, err=%v", err)) + mlog.Error("Unable to create /echo post.", mlog.Err(err)) } }) diff --git a/app/command_msg.go b/app/command_msg.go index 36a88ba1a1..492ed8c727 100644 --- a/app/command_msg.go +++ b/app/command_msg.go @@ -58,6 +58,15 @@ func (me *msgProvider) DoCommand(a *App, args *model.CommandArgs, message string return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } + canSee, err := a.UserCanSeeOtherUser(args.UserId, userProfile.Id) + if err != nil { + mlog.Error(err.Error()) + return &model.CommandResponse{Text: args.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + if !canSee { + return &model.CommandResponse{Text: args.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} + } + // Find the channel based on this user channelName := model.GetDMNameFromIds(args.UserId, userProfile.Id) diff --git a/app/command_msg_test.go b/app/command_msg_test.go index 6b5bbc6d46..c7054f4f94 100644 --- a/app/command_msg_test.go +++ b/app/command_msg_test.go @@ -62,4 +62,44 @@ func TestMsgProvider(t *testing.T) { assert.Equal(t, "", resp.Text) assert.Equal(t, "http://test.url/"+team.Name+"/channels/"+channelName, resp.GotoLocation) + + // Check that a guest user cannot message a user who is not in a channel/team with him + guest := th.CreateGuest() + user := th.CreateUser() + + th.LinkUserToTeam(user, team) + th.LinkUserToTeam(guest, th.BasicTeam) + th.AddUserToChannel(guest, th.BasicChannel) + + resp = cmd.DoCommand(th.App, &model.CommandArgs{ + T: i18n.IdentityTfunc(), + SiteURL: "http://test.url", + TeamId: th.BasicTeam.Id, + UserId: guest.Id, + Session: model.Session{ + Roles: model.SYSTEM_GUEST_ROLE_ID, + }, + }, "@"+user.Username+" hello") + + assert.Equal(t, "api.command_msg.missing.app_error", resp.Text) + assert.Equal(t, "", resp.GotoLocation) + + // Check that a guest user can message a user who is in a channel/team with him + th.LinkUserToTeam(user, th.BasicTeam) + th.AddUserToChannel(user, th.BasicChannel) + + resp = cmd.DoCommand(th.App, &model.CommandArgs{ + T: i18n.IdentityTfunc(), + SiteURL: "http://test.url", + TeamId: th.BasicTeam.Id, + UserId: guest.Id, + Session: model.Session{ + Roles: model.SYSTEM_GUEST_ROLE_ID, + }, + }, "@"+user.Username+" hello") + + channelName = model.GetDMNameFromIds(guest.Id, user.Id) + + assert.Equal(t, "", resp.Text) + assert.Equal(t, "http://test.url/"+th.BasicTeam.Name+"/channels/"+channelName, resp.GotoLocation) } diff --git a/app/diagnostics.go b/app/diagnostics.go index 4296c1e01e..98a8f4dece 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -586,13 +586,13 @@ func (a *App) trackConfig() { }) a.SendDiagnostic(TRACK_CONFIG_PLUGIN, map[string]interface{}{ - "enable_gitlab": pluginActivated(cfg.PluginSettings.PluginStates, "com.github.manland.mattermost-plugin-gitlab"), "enable_antivirus": pluginActivated(cfg.PluginSettings.PluginStates, "antivirus"), - "enable_jenkins": pluginActivated(cfg.PluginSettings.PluginStates, "jenkins"), "enable_autolink": pluginActivated(cfg.PluginSettings.PluginStates, "mattermost-autolink"), "enable_aws_sns": pluginActivated(cfg.PluginSettings.PluginStates, "com.mattermost.aws-sns"), "enable_custom_user_attributes": pluginActivated(cfg.PluginSettings.PluginStates, "com.mattermost.custom-attributes"), "enable_github": pluginActivated(cfg.PluginSettings.PluginStates, "github"), + "enable_gitlab": pluginActivated(cfg.PluginSettings.PluginStates, "com.github.manland.mattermost-plugin-gitlab"), + "enable_jenkins": pluginActivated(cfg.PluginSettings.PluginStates, "jenkins"), "enable_jira": pluginActivated(cfg.PluginSettings.PluginStates, "jira"), "enable_nps": pluginActivated(cfg.PluginSettings.PluginStates, "com.mattermost.nps"), "enable_nps_survey": pluginSetting(&cfg.PluginSettings, "com.mattermost.nps", "enablesurvey", true), diff --git a/app/helper_test.go b/app/helper_test.go index adfc0fbfe9..947f07888c 100644 --- a/app/helper_test.go +++ b/app/helper_test.go @@ -296,6 +296,26 @@ func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post { return post } +func (me *TestHelper) CreateMessagePost(channel *model.Channel, message string) *model.Post { + post := &model.Post{ + UserId: me.BasicUser.Id, + ChannelId: channel.Id, + Message: message, + CreateAt: model.GetMillis() - 10000, + } + + utils.DisableDebugLogForTest() + var err *model.AppError + if post, err = me.App.CreatePost(post, channel, false); err != nil { + mlog.Error(err.Error()) + + time.Sleep(time.Second) + panic(err) + } + utils.EnableDebugLogForTest() + return post +} + func (me *TestHelper) LinkUserToTeam(user *model.User, team *model.Team) { utils.DisableDebugLogForTest() diff --git a/app/notification.go b/app/notification.go index 1d4a2ccb49..375fcac4dd 100644 --- a/app/notification.go +++ b/app/notification.go @@ -593,7 +593,9 @@ func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, lookF for _, k := range splitKeys { // note that these are made lower case so that we can do a case insensitive check for them key := strings.ToLower(k) - keywords[key] = append(keywords[key], id) + if key != "" { + keywords[key] = append(keywords[key], id) + } } } @@ -784,3 +786,16 @@ func (e *ExplicitMentions) processText(text string, keywords map[string][]string } } } + +func (a *App) GetNotificationNameFormat(user *model.User) string { + if !*a.Config().PrivacySettings.ShowFullName { + return model.SHOW_USERNAME + } + + data, err := a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT) + if err != nil { + return *a.Config().TeamSettings.TeammateNameDisplay + } + + return data.Value +} diff --git a/app/notification_email.go b/app/notification_email.go index 07fba3449e..a2fbb6766d 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -6,6 +6,7 @@ package app import ( "fmt" "html" + "html/template" "net/url" "path/filepath" "strings" @@ -73,12 +74,7 @@ func (a *App) sendNotificationEmail(notification *postNotification, user *model. useMilitaryTime = data.Value == "true" } - var nameFormat string - if data, err := a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT); err != nil { - nameFormat = *a.Config().TeamSettings.TeammateNameDisplay - } else { - nameFormat = data.Value - } + nameFormat := a.GetNotificationNameFormat(user) channelName := notification.GetChannelName(nameFormat, "") senderName := notification.GetSenderName(nameFormat, *a.Config().ServiceSettings.EnablePostUsernameOverride) @@ -171,7 +167,10 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, var bodyPage *utils.HTMLTemplate if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale) - bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc) + postMessage := a.GetMessageForNotification(post, translateFunc) + postMessage = html.EscapeString(postMessage) + normalizedPostMessage := a.generateHyperlinkForChannels(postMessage, teamName, teamURL) + bodyPage.Props["PostMessage"] = template.HTML(normalizedPostMessage) } else { bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale) } @@ -283,6 +282,36 @@ func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bo } } +func (a *App) generateHyperlinkForChannels(postMessage, teamName, teamURL string) string { + team, err := a.GetTeamByName(teamName) + if err != nil { + mlog.Error("Encountered error while looking up team by name", mlog.String("Team Name", teamName), mlog.Err(err)) + return postMessage + } + + channelNames := model.ChannelMentions(postMessage) + if len(channelNames) == 0 { + return postMessage + } + + channels, err := a.GetChannelsByNames(channelNames, team.Id) + if err != nil { + mlog.Error("Encountered error while getting channels", mlog.Err(err)) + return postMessage + } + + visited := make(map[string]bool) + for _, ch := range channels { + if !visited[ch.Id] && ch.Type == model.CHANNEL_OPEN { + channelURL := teamURL + "/channels/" + ch.Name + channelHyperLink := fmt.Sprintf("%s", channelURL, "~"+ch.Name) + postMessage = strings.Replace(postMessage, "~"+ch.Name, channelHyperLink, -1) + visited[ch.Id] = true + } + } + return postMessage +} + func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { return post.Message diff --git a/app/notification_email_test.go b/app/notification_email_test.go index 1c920a9b0a..7dcf1ff874 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/services/timezones" "github.com/mattermost/mattermost-server/utils" @@ -25,7 +27,7 @@ func TestGetDirectMessageNotificationEmailSubject(t *testing.T) { CreateAt: 1501804801000, } translateFunc := utils.GetUserTranslations("en") - subject := getDirectMessageNotificationEmailSubject(user, post, translateFunc, "http://localhost:8065", "sender", true) + subject := getDirectMessageNotificationEmailSubject(user, post, translateFunc, "http://localhost:8065", "@sender", true) if !strings.HasPrefix(subject, expectedPrefix) { t.Fatal("Expected subject line prefix '" + expectedPrefix + "', got " + subject) } @@ -499,3 +501,177 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T) t.Fatal("Expected email text '" + teamURL + "'. Got " + body) } } + +func TestGetNotificationEmailEscapingChars(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + ch := &model.Channel{ + DisplayName: "ChannelName", + Type: model.CHANNEL_OPEN, + } + channelName := "ChannelName" + recipient := &model.User{} + message := "Bold Test" + post := &model.Post{ + Message: message, + } + + senderName := "sender" + teamName := "team" + teamURL := "http://localhost:8065/" + teamName + emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL + translateFunc := utils.GetUserTranslations("en") + + body := th.App.getNotificationEmailBody(recipient, post, ch, + channelName, senderName, teamName, teamURL, + emailNotificationContentsType, true, translateFunc) + + fmt.Println(body) + assert.NotContains(t, body, message) +} + +func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.BasicChannel + recipient := th.BasicUser2 + post := &model.Post{ + Message: "This is the message ~" + ch.Name, + } + + senderName := th.BasicUser.Username + teamName := th.BasicTeam.Name + teamURL := "http://localhost:8065/" + teamName + emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL + translateFunc := utils.GetUserTranslations("en") + + body := th.App.getNotificationEmailBody(recipient, post, ch, + ch.Name, senderName, teamName, teamURL, + emailNotificationContentsType, true, translateFunc) + channelURL := teamURL + "/channels/" + ch.Name + mention := "~" + ch.Name + assert.Contains(t, body, ""+mention+"") +} + +func TestGetNotificationEmailBodyMultiPublicChannelMention(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.BasicChannel + mention := "~" + ch.Name + + ch2 := th.CreateChannel(th.BasicTeam) + mention2 := "~" + ch2.Name + + ch3 := th.CreateChannel(th.BasicTeam) + mention3 := "~" + ch3.Name + + message := fmt.Sprintf("This is the message Channel1: %s; Channel2: %s;"+ + " Channel3: %s", mention, mention2, mention3) + recipient := th.BasicUser2 + post := &model.Post{ + Message: message, + } + + senderName := th.BasicUser.Username + teamName := th.BasicTeam.Name + teamURL := "http://localhost:8065/" + teamName + emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL + translateFunc := utils.GetUserTranslations("en") + + body := th.App.getNotificationEmailBody(recipient, post, ch, + ch.Name, senderName, teamName, teamURL, + emailNotificationContentsType, true, translateFunc) + channelURL := teamURL + "/channels/" + ch.Name + channelURL2 := teamURL + "/channels/" + ch2.Name + channelURL3 := teamURL + "/channels/" + ch3.Name + expMessage := fmt.Sprintf("This is the message Channel1: %s;"+ + " Channel2: %s; Channel3: %s", + channelURL, mention, channelURL2, mention2, channelURL3, mention3) + assert.Contains(t, body, expMessage) +} + +func TestGetNotificationEmailBodyPrivateChannelMention(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.CreatePrivateChannel(th.BasicTeam) + recipient := th.BasicUser2 + post := &model.Post{ + Message: "This is the message ~" + ch.Name, + } + + senderName := th.BasicUser.Username + teamName := ch.Name + teamURL := "http://localhost:8065/" + teamName + emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL + translateFunc := utils.GetUserTranslations("en") + + body := th.App.getNotificationEmailBody(recipient, post, ch, + ch.Name, senderName, teamName, teamURL, + emailNotificationContentsType, true, translateFunc) + channelURL := teamURL + "/channels/" + ch.Name + mention := "~" + ch.Name + assert.NotContains(t, body, ""+mention+"") +} + +func TestGenerateHyperlinkForChannelsPublic(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.BasicChannel + message := "This is the message " + mention := "~" + ch.Name + + teamName := th.BasicTeam.Name + teamURL := "http://localhost:8065/" + teamName + + outMessage := th.App.generateHyperlinkForChannels(message+mention, teamName, teamURL) + channelURL := teamURL + "/channels/" + ch.Name + assert.Equal(t, message+""+mention+"", outMessage) +} + +func TestGenerateHyperlinkForChannelsMultiPublic(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.BasicChannel + mention := "~" + ch.Name + + ch2 := th.CreateChannel(th.BasicTeam) + mention2 := "~" + ch2.Name + + ch3 := th.CreateChannel(th.BasicTeam) + mention3 := "~" + ch3.Name + + message := fmt.Sprintf("This is the message Channel1: %s; Channel2: %s;"+ + " Channel3: %s", mention, mention2, mention3) + + teamName := th.BasicTeam.Name + teamURL := "http://localhost:8065/" + teamName + + outMessage := th.App.generateHyperlinkForChannels(message, teamName, teamURL) + channelURL := teamURL + "/channels/" + ch.Name + channelURL2 := teamURL + "/channels/" + ch2.Name + channelURL3 := teamURL + "/channels/" + ch3.Name + expMessage := fmt.Sprintf("This is the message Channel1: %s;"+ + " Channel2: %s; Channel3: %s", + channelURL, mention, channelURL2, mention2, channelURL3, mention3) + assert.Equal(t, expMessage, outMessage) +} + +func TestGenerateHyperlinkForChannelsPrivate(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + ch := th.CreatePrivateChannel(th.BasicTeam) + message := "This is the message ~" + ch.Name + + teamName := th.BasicTeam.Name + teamURL := "http://localhost:8065/" + teamName + + outMessage := th.App.generateHyperlinkForChannels(message, teamName, teamURL) + assert.Equal(t, message, outMessage) +} diff --git a/app/notification_push.go b/app/notification_push.go index edcd7310c3..2b4e963186 100644 --- a/app/notification_push.go +++ b/app/notification_push.go @@ -53,58 +53,16 @@ func (hub *PushNotificationsHub) GetGoChannelFromUserId(userId string) chan Push } func (a *App) sendPushNotificationSync(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, - explicitMention, channelWideMention bool, replyToThreadType string) *model.AppError { - cfg := a.Config() + explicitMention bool, channelWideMention bool, replyToThreadType string) *model.AppError { sessions, err := a.getMobileAppSessions(user.Id) if err != nil { return err } - msg := model.PushNotification{ - Category: model.CATEGORY_CAN_REPLY, - Version: model.PUSH_MESSAGE_V2, - Type: model.PUSH_TYPE_MESSAGE, - TeamId: channel.TeamId, - ChannelId: channel.Id, - PostId: post.Id, - RootId: post.RootId, - SenderId: post.UserId, - } - - if unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id); err != nil { - msg.Badge = 1 - mlog.Error(fmt.Sprint("We could not get the unread message count for the user", user.Id, err), mlog.String("user_id", user.Id)) - } else { - msg.Badge = int(unreadCount) - } - - contentsConfig := *cfg.EmailSettings.PushNotificationContents - if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT { - msg.ChannelName = channelName - } - - msg.SenderName = senderName - if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { - msg.OverrideUsername = ou - msg.SenderName = ou - } - - if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { - msg.OverrideIconUrl = oi - } - - if fw, ok := post.Props["from_webhook"].(string); ok { - msg.FromWebhook = fw - } - - userLocale := utils.GetUserTranslations(user.Locale) - hasFiles := post.FileIds != nil && len(post.FileIds) > 0 - - msg.Message = a.getPushNotificationMessage(post.Message, explicitMention, channelWideMention, hasFiles, msg.SenderName, channelName, channel.Type, replyToThreadType, userLocale) + msg := a.BuildPushNotificationMessage(post, user, channel, channelName, senderName, explicitMention, channelWideMention, replyToThreadType) for _, session := range sessions { - if session.IsExpired() { continue } @@ -151,12 +109,7 @@ func (a *App) sendPushNotification(notification *postNotification, user *model.U channel := notification.channel post := notification.post - var nameFormat string - if data, err := a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT); err != nil { - nameFormat = *a.Config().TeamSettings.TeammateNameDisplay - } else { - nameFormat = data.Value - } + nameFormat := a.GetNotificationNameFormat(user) channelName := notification.GetChannelName(nameFormat, user.Id) senderName := notification.GetSenderName(nameFormat, *cfg.ServiceSettings.EnablePostUsernameOverride) @@ -475,3 +428,61 @@ func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *mo return false } + +func (a *App) BuildPushNotificationMessage(post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string, + explicitMention bool, channelWideMention bool, replyToThreadType string) model.PushNotification { + + msg := model.PushNotification{ + Category: model.CATEGORY_CAN_REPLY, + Version: model.PUSH_MESSAGE_V2, + Type: model.PUSH_TYPE_MESSAGE, + TeamId: channel.TeamId, + ChannelId: channel.Id, + PostId: post.Id, + RootId: post.RootId, + SenderId: post.UserId, + } + + if user.NotifyProps["push"] == "all" { + if unreadCount, err := a.Srv.Store.User().GetAnyUnreadPostCountForChannel(user.Id, channel.Id); err != nil { + msg.Badge = 1 + mlog.Error(fmt.Sprint("We could not get the unread message count for the user", user.Id, err), mlog.String("user_id", user.Id)) + } else { + msg.Badge = int(unreadCount) + } + } else { + if unreadCount, err := a.Srv.Store.User().GetUnreadCount(user.Id); err != nil { + msg.Badge = 1 + mlog.Error(fmt.Sprint("We could not get the unread message count for the user", user.Id, err), mlog.String("user_id", user.Id)) + } else { + msg.Badge = int(unreadCount) + } + } + + cfg := a.Config() + contentsConfig := *cfg.EmailSettings.PushNotificationContents + if contentsConfig != model.GENERIC_NO_CHANNEL_NOTIFICATION || channel.Type == model.CHANNEL_DIRECT { + msg.ChannelName = channelName + } + + msg.SenderName = senderName + if ou, ok := post.Props["override_username"].(string); ok && *cfg.ServiceSettings.EnablePostUsernameOverride { + msg.OverrideUsername = ou + msg.SenderName = ou + } + + if oi, ok := post.Props["override_icon_url"].(string); ok && *cfg.ServiceSettings.EnablePostIconOverride { + msg.OverrideIconUrl = oi + } + + if fw, ok := post.Props["from_webhook"].(string); ok { + msg.FromWebhook = fw + } + + userLocale := utils.GetUserTranslations(user.Locale) + hasFiles := post.FileIds != nil && len(post.FileIds) > 0 + + msg.Message = a.getPushNotificationMessage(post.Message, explicitMention, channelWideMention, hasFiles, msg.SenderName, channelName, channel.Type, replyToThreadType, userLocale) + + return msg +} diff --git a/app/notification_push_test.go b/app/notification_push_test.go index a7a7cf36d0..5efd7228a1 100644 --- a/app/notification_push_test.go +++ b/app/notification_push_test.go @@ -4,6 +4,7 @@ package app import ( + "fmt" "testing" "github.com/mattermost/mattermost-server/model" @@ -880,7 +881,7 @@ func TestGetPushNotificationMessage(t *testing.T) { *cfg.EmailSettings.PushNotificationContents = pushNotificationContents }) - if actualMessage := th.App.getPushNotificationMessage( + actualMessage := th.App.getPushNotificationMessage( tc.Message, tc.explicitMention, tc.channelWideMention, @@ -890,9 +891,59 @@ func TestGetPushNotificationMessage(t *testing.T) { tc.ChannelType, tc.replyToThreadType, utils.GetUserTranslations(locale), - ); actualMessage != tc.ExpectedMessage { - t.Fatalf("Received incorrect push notification message `%v`, expected `%v`", actualMessage, tc.ExpectedMessage) - } + ) + + assert.Equal(t, tc.ExpectedMessage, actualMessage) + }) + } +} + +func TestBuildPushNotificationMessage(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + team := th.CreateTeam() + sender := th.CreateUser() + receiver := th.CreateUser() + th.LinkUserToTeam(sender, team) + th.LinkUserToTeam(receiver, team) + channel := th.CreateChannel(team) + th.AddUserToChannel(sender, channel) + th.AddUserToChannel(receiver, channel) + + // Create three mention posts and two non-mention posts + th.CreateMessagePost(channel, "@channel Hello") + th.CreateMessagePost(channel, "@all Hello") + th.CreateMessagePost(channel, fmt.Sprintf("@%s Hello", receiver.Username)) + th.CreatePost(channel) + post := th.CreatePost(channel) + + for name, tc := range map[string]struct { + explicitMention bool + channelWideMention bool + replyToThreadType string + pushNotifyProps string + expectedBadge int + }{ + "only mentions included in badge count": { + explicitMention: false, + channelWideMention: true, + replyToThreadType: "", + pushNotifyProps: "mention", + expectedBadge: 3, + }, + "mentions and non-mentions included in badge count": { + explicitMention: false, + channelWideMention: true, + replyToThreadType: "", + pushNotifyProps: "all", + expectedBadge: 5, + }, + } { + t.Run(name, func(t *testing.T) { + receiver.NotifyProps["push"] = tc.pushNotifyProps + msg := th.App.BuildPushNotificationMessage(post, receiver, channel, channel.Name, sender.Username, tc.explicitMention, tc.channelWideMention, tc.replyToThreadType) + assert.Equal(t, tc.expectedBadge, msg.Badge) }) } } diff --git a/app/notification_test.go b/app/notification_test.go index 744b9738d4..13ee5ca588 100644 --- a/app/notification_test.go +++ b/app/notification_test.go @@ -1150,6 +1150,29 @@ func TestGetMentionKeywords(t *testing.T) { } else if _, ok := mentions["@here"]; ok { t.Fatal("should not have mentioned any user with @here") } + + // user with empty mention keys + userNoMentionKeys := &model.User{ + Id: model.NewId(), + FirstName: "First", + Username: "User", + NotifyProps: map[string]string{ + "mention_keys": ",", + }, + } + + channelMemberNotifyPropsMapEmptyOff := map[string]model.StringMap{ + userNoMentionKeys.Id: { + "ignore_channel_mentions": model.IGNORE_CHANNEL_MENTIONS_OFF, + }, + } + + profiles = map[string]*model.User{userNoMentionKeys.Id: userNoMentionKeys} + mentions = th.App.getMentionKeywordsInChannel(profiles, true, channelMemberNotifyPropsMapEmptyOff) + assert.Equal(t, 1, len(mentions), "should've returned one metion keyword") + ids, ok := mentions["@user"] + assert.True(t, ok) + assert.Equal(t, userNoMentionKeys.Id, ids[0], "should've returned mention key of @user") } func TestGetMentionsEnabledFields(t *testing.T) { @@ -1743,3 +1766,26 @@ func TestProcessText(t *testing.T) { }) } } + +func TestGetNotificationNameFormat(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + t.Run("show full name on", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PrivacySettings.ShowFullName = true + *cfg.TeamSettings.TeammateNameDisplay = model.SHOW_FULLNAME + }) + + assert.Equal(t, model.SHOW_FULLNAME, th.App.GetNotificationNameFormat(th.BasicUser)) + }) + + t.Run("show full name off", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PrivacySettings.ShowFullName = false + *cfg.TeamSettings.TeammateNameDisplay = model.SHOW_FULLNAME + }) + + assert.Equal(t, model.SHOW_USERNAME, th.App.GetNotificationNameFormat(th.BasicUser)) + }) +} diff --git a/app/opengraph_test.go b/app/opengraph_test.go index 867f02ea14..cd93fb7bca 100644 --- a/app/opengraph_test.go +++ b/app/opengraph_test.go @@ -4,6 +4,7 @@ package app import ( + "github.com/stretchr/testify/require" "strings" "testing" @@ -108,22 +109,17 @@ func TestMakeOpenGraphURLsAbsolute(t *testing.T) { } { t.Run(name, func(t *testing.T) { og := opengraph.NewOpenGraph() - if err := og.ProcessHTML(strings.NewReader(tc.HTML)); err != nil { - t.Fatal(err) - } + err := og.ProcessHTML(strings.NewReader(tc.HTML)) + require.Nil(t, err) makeOpenGraphURLsAbsolute(og, tc.RequestURL) - if og.URL != tc.URL { - t.Fatalf("incorrect url, expected %v, got %v", tc.URL, og.URL) - } + assert.Equalf(t, og.URL, tc.URL, "incorrect url, expected %v, got %v", tc.URL, og.URL) if len(og.Images) > 0 { - if og.Images[0].URL != tc.ImageURL { - t.Fatalf("incorrect image url, expected %v, got %v", tc.ImageURL, og.Images[0].URL) - } - } else if tc.ImageURL != "" { - t.Fatalf("missing image url, expected %v, got nothing", tc.ImageURL) + assert.Equalf(t, og.Images[0].URL, tc.ImageURL, "incorrect image url, expected %v, got %v", tc.ImageURL, og.Images[0].URL) + } else { + assert.Empty(t, tc.ImageURL, "missing image url, expected %v, got nothing", tc.ImageURL) } }) } diff --git a/app/plugin_api.go b/app/plugin_api.go index a99067fcb4..05ca69ab8b 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -81,6 +81,11 @@ func (api *PluginAPI) GetConfig() *model.Config { return api.app.GetSanitizedConfig() } +// GetUnsanitizedConfig gets the configuration for a system admin without removing secrets. +func (api *PluginAPI) GetUnsanitizedConfig() *model.Config { + return api.app.Config().Clone() +} + func (api *PluginAPI) SaveConfig(config *model.Config) *model.AppError { return api.app.SaveConfig(config, true) } diff --git a/app/plugin_api_test.go b/app/plugin_api_test.go index 5f3a756016..8277a2897b 100644 --- a/app/plugin_api_test.go +++ b/app/plugin_api_test.go @@ -1331,3 +1331,77 @@ func TestPluginCreatePostWithUploadedFile(t *testing.T) { require.Nil(t, err) assert.Equal(t, model.StringArray{fileInfo.Id}, actualPost.FileIds) } + +func TestPluginAPIGetConfig(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + config := api.GetConfig() + if config.LdapSettings.BindPassword != nil && len(*config.LdapSettings.BindPassword) > 0 { + assert.Equal(t, *config.LdapSettings.BindPassword, model.FAKE_SETTING) + } + + assert.Equal(t, *config.FileSettings.PublicLinkSalt, model.FAKE_SETTING) + + if len(*config.FileSettings.AmazonS3SecretAccessKey) > 0 { + assert.Equal(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FAKE_SETTING) + } + + if config.EmailSettings.SMTPPassword != nil && len(*config.EmailSettings.SMTPPassword) > 0 { + assert.Equal(t, *config.EmailSettings.SMTPPassword, model.FAKE_SETTING) + } + + if len(*config.GitLabSettings.Secret) > 0 { + assert.Equal(t, *config.GitLabSettings.Secret, model.FAKE_SETTING) + } + + assert.Equal(t, *config.SqlSettings.DataSource, model.FAKE_SETTING) + assert.Equal(t, *config.SqlSettings.AtRestEncryptKey, model.FAKE_SETTING) + assert.Equal(t, *config.ElasticsearchSettings.Password, model.FAKE_SETTING) + + for i := range config.SqlSettings.DataSourceReplicas { + assert.Equal(t, config.SqlSettings.DataSourceReplicas[i], model.FAKE_SETTING) + } + + for i := range config.SqlSettings.DataSourceSearchReplicas { + assert.Equal(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FAKE_SETTING) + } +} + +func TestPluginAPIGetUnsanitizedConfig(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + api := th.SetupPluginAPI() + + config := api.GetUnsanitizedConfig() + if config.LdapSettings.BindPassword != nil && len(*config.LdapSettings.BindPassword) > 0 { + assert.NotEqual(t, *config.LdapSettings.BindPassword, model.FAKE_SETTING) + } + + assert.NotEqual(t, *config.FileSettings.PublicLinkSalt, model.FAKE_SETTING) + + if len(*config.FileSettings.AmazonS3SecretAccessKey) > 0 { + assert.NotEqual(t, *config.FileSettings.AmazonS3SecretAccessKey, model.FAKE_SETTING) + } + + if config.EmailSettings.SMTPPassword != nil && len(*config.EmailSettings.SMTPPassword) > 0 { + assert.NotEqual(t, *config.EmailSettings.SMTPPassword, model.FAKE_SETTING) + } + + if len(*config.GitLabSettings.Secret) > 0 { + assert.NotEqual(t, *config.GitLabSettings.Secret, model.FAKE_SETTING) + } + + assert.NotEqual(t, *config.SqlSettings.DataSource, model.FAKE_SETTING) + assert.NotEqual(t, *config.SqlSettings.AtRestEncryptKey, model.FAKE_SETTING) + assert.NotEqual(t, *config.ElasticsearchSettings.Password, model.FAKE_SETTING) + + for i := range config.SqlSettings.DataSourceReplicas { + assert.NotEqual(t, config.SqlSettings.DataSourceReplicas[i], model.FAKE_SETTING) + } + + for i := range config.SqlSettings.DataSourceSearchReplicas { + assert.NotEqual(t, config.SqlSettings.DataSourceSearchReplicas[i], model.FAKE_SETTING) + } +} diff --git a/app/plugin_commands.go b/app/plugin_commands.go index 46842451d9..8e1df5fa42 100644 --- a/app/plugin_commands.go +++ b/app/plugin_commands.go @@ -97,20 +97,29 @@ func (a *App) tryExecutePluginCommand(args *model.CommandArgs) (*model.Command, trigger := parts[0][1:] trigger = strings.ToLower(trigger) + var matched *PluginCommand a.Srv.pluginCommandsLock.RLock() - defer a.Srv.pluginCommandsLock.RUnlock() - for _, pc := range a.Srv.pluginCommands { if (pc.Command.TeamId == "" || pc.Command.TeamId == args.TeamId) && pc.Command.Trigger == trigger { - if pluginsEnvironment := a.GetPluginsEnvironment(); pluginsEnvironment != nil { - pluginHooks, err := pluginsEnvironment.HooksForPlugin(pc.PluginId) - if err != nil { - return pc.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) - } - response, appErr := pluginHooks.ExecuteCommand(a.PluginContext(), args) - return pc.Command, response, appErr - } + matched = pc + break } } - return nil, nil, nil + a.Srv.pluginCommandsLock.RUnlock() + if matched == nil { + return nil, nil, nil + } + + pluginsEnvironment := a.GetPluginsEnvironment() + if pluginsEnvironment == nil { + return nil, nil, nil + } + + pluginHooks, err := pluginsEnvironment.HooksForPlugin(matched.PluginId) + if err != nil { + return matched.Command, nil, model.NewAppError("ExecutePluginCommand", "model.plugin_command.error.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + response, appErr := pluginHooks.ExecuteCommand(a.PluginContext(), args) + return matched.Command, response, appErr } diff --git a/app/plugin_commands_test.go b/app/plugin_commands_test.go index 830ef48980..06af46d1cd 100644 --- a/app/plugin_commands_test.go +++ b/app/plugin_commands_test.go @@ -5,6 +5,7 @@ package app import ( "testing" + "time" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" @@ -46,7 +47,7 @@ func TestPluginCommand(t *testing.T) { } type MyPlugin struct { - plugin.MattermostPlugin + plugin.MattermostPlugin configuration configuration } @@ -110,6 +111,110 @@ func TestPluginCommand(t *testing.T) { th.App.RemovePlugin(pluginIds[0]) }) + t.Run("re-entrant command registration on config change", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.Plugins["testloadpluginconfig"] = map[string]interface{}{ + "TeamId": args.TeamId, + } + }) + + tearDown, pluginIds, activationErrors := SetAppEnvironmentWithPlugins(t, []string{` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/model" + ) + + type configuration struct { + TeamId string + } + + type MyPlugin struct { + plugin.MattermostPlugin + + configuration configuration + } + + func (p *MyPlugin) OnConfigurationChange() error { + p.API.LogInfo("OnConfigurationChange") + err := p.API.LoadPluginConfiguration(&p.configuration); + if err != nil { + return err + } + + p.API.LogInfo("About to register") + err = p.API.RegisterCommand(&model.Command{ + TeamId: p.configuration.TeamId, + Trigger: "plugin", + DisplayName: "Plugin Command", + AutoComplete: true, + AutoCompleteDesc: "autocomplete", + }) + if err != nil { + p.API.LogInfo("Registered, with error", err, err.Error()) + return err + } + p.API.LogInfo("Registered, without error") + return nil + } + + func (p *MyPlugin) ExecuteCommand(c *plugin.Context, commandArgs *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + p.API.LogInfo("ExecuteCommand") + // Saving the plugin config eventually results in a call to + // OnConfigurationChange. This used to deadlock on account of + // effectively acquiring a RWLock reentrantly. + err := p.API.SavePluginConfig(map[string]interface{}{ + "TeamId": p.configuration.TeamId, + }) + if err != nil { + p.API.LogError("Failed to save plugin config", err, err.Error()) + return nil, err + } + p.API.LogInfo("ExecuteCommand, saved plugin config") + + return &model.CommandResponse{ + ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + Text: "text", + }, nil + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `}, th.App, th.App.NewPluginAPI) + defer tearDown() + + require.Len(t, activationErrors, 1) + require.Nil(t, nil, activationErrors[0]) + + wait := make(chan bool) + killed := false + go func() { + defer close(wait) + + resp, err := th.App.ExecuteCommand(args) + + // Ignore if we kill below. + if !killed { + require.Nil(t, err) + require.Equal(t, model.COMMAND_RESPONSE_TYPE_EPHEMERAL, resp.ResponseType) + require.Equal(t, "text", resp.Text) + } + }() + + select { + case <-wait: + case <-time.After(10 * time.Second): + killed = true + } + + th.App.RemovePlugin(pluginIds[0]) + if killed { + t.Fatal("execute command appears to have deadlocked") + } + }) + t.Run("error after plugin command unregistered", func(t *testing.T) { _, err := th.App.ExecuteCommand(args) require.NotNil(t, err) diff --git a/app/plugin_hooks_test.go b/app/plugin_hooks_test.go index a20a12a0bf..df88b99b9b 100644 --- a/app/plugin_hooks_test.go +++ b/app/plugin_hooks_test.go @@ -11,7 +11,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "strings" "testing" "time" @@ -47,6 +46,12 @@ func SetAppEnvironmentWithPlugins(t *testing.T, pluginCode []string, app *App, a _, _, activationErr := env.Activate(pluginId) pluginIds = append(pluginIds, pluginId) activationErrors = append(activationErrors, activationErr) + + app.UpdateConfig(func(cfg *model.Config) { + cfg.PluginSettings.PluginStates[pluginId] = &model.PluginState{ + Enable: true, + } + }) } return func() { @@ -172,9 +177,8 @@ func TestHookMessageWillBePosted(t *testing.T) { CreateAt: model.GetMillis() - 10000, } post, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) + assert.Equal(t, "message", post.Message) retrievedPost, errSingle := th.App.Srv.Store.Post().GetSingle(post.Id) require.Nil(t, errSingle) @@ -217,15 +221,12 @@ func TestHookMessageWillBePosted(t *testing.T) { CreateAt: model.GetMillis() - 10000, } post, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) + assert.Equal(t, "message_fromplugin", post.Message) - if retrievedPost, errSingle := th.App.Srv.Store.Post().GetSingle(post.Id); err != nil { - t.Fatal(errSingle) - } else { - assert.Equal(t, "message_fromplugin", retrievedPost.Message) - } + retrievedPost, errSingle := th.App.Srv.Store.Post().GetSingle(post.Id) + require.Nil(t, errSingle) + assert.Equal(t, "message_fromplugin", retrievedPost.Message) }) t.Run("multiple updated", func(t *testing.T) { @@ -286,9 +287,7 @@ func TestHookMessageWillBePosted(t *testing.T) { CreateAt: model.GetMillis() - 10000, } post, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) assert.Equal(t, "prefix_message_suffix", post.Message) }) } @@ -332,9 +331,7 @@ func TestHookMessageHasBeenPosted(t *testing.T) { CreateAt: model.GetMillis() - 10000, } _, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) } func TestHookMessageWillBeUpdated(t *testing.T) { @@ -373,15 +370,11 @@ func TestHookMessageWillBeUpdated(t *testing.T) { CreateAt: model.GetMillis() - 10000, } post, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) assert.Equal(t, "message_", post.Message) post.Message = post.Message + "edited_" post, err = th.App.UpdatePost(post, true) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) assert.Equal(t, "message_edited_fromplugin", post.Message) } @@ -425,15 +418,11 @@ func TestHookMessageHasBeenUpdated(t *testing.T) { CreateAt: model.GetMillis() - 10000, } post, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) assert.Equal(t, "message_", post.Message) post.Message = post.Message + "edited" _, err = th.App.UpdatePost(post, true) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) } func TestHookFileWillBeUploaded(t *testing.T) { @@ -582,8 +571,8 @@ func TestHookFileWillBeUploaded(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, response) assert.Equal(t, 1, len(response.FileInfos)) + fileId := response.FileInfos[0].Id - fileInfo, err := th.App.GetFileInfo(fileId) assert.Nil(t, err) assert.NotNil(t, fileInfo) @@ -678,11 +667,7 @@ func TestUserWillLogIn_Blocked(t *testing.T) { defer th.TearDown() err := th.App.UpdatePassword(th.BasicUser, "hunter2") - - if err != nil { - t.Errorf("Error updating user password: %s", err) - } - + assert.Nil(t, err, "Error updating user password: %s", err) tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{ ` @@ -711,9 +696,7 @@ func TestUserWillLogIn_Blocked(t *testing.T) { w := httptest.NewRecorder() _, err = th.App.DoLogin(w, r, th.BasicUser, "") - if !strings.HasPrefix(err.Id, "Login rejected by plugin") { - t.Errorf("Expected Login rejected by plugin, got %s", err.Id) - } + assert.Contains(t, err.Id, "Login rejected by plugin", "Expected Login rejected by plugin, got %s", err.Id) } func TestUserWillLogInIn_Passed(t *testing.T) { @@ -722,9 +705,7 @@ func TestUserWillLogInIn_Passed(t *testing.T) { err := th.App.UpdatePassword(th.BasicUser, "hunter2") - if err != nil { - t.Errorf("Error updating user password: %s", err) - } + assert.Nil(t, err, "Error updating user password: %s", err) tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{ @@ -754,13 +735,8 @@ func TestUserWillLogInIn_Passed(t *testing.T) { w := httptest.NewRecorder() session, err := th.App.DoLogin(w, r, th.BasicUser, "") - if err != nil { - t.Errorf("Expected nil, got %s", err) - } - - if session.UserId != th.BasicUser.Id { - t.Errorf("Expected %s, got %s", th.BasicUser.Id, session.UserId) - } + assert.Nil(t, err, "Expected nil, got %s", err) + assert.Equal(t, session.UserId, th.BasicUser.Id) } func TestUserHasLoggedIn(t *testing.T) { @@ -769,9 +745,7 @@ func TestUserHasLoggedIn(t *testing.T) { err := th.App.UpdatePassword(th.BasicUser, "hunter2") - if err != nil { - t.Errorf("Error updating user password: %s", err) - } + assert.Nil(t, err, "Error updating user password: %s", err) tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{ @@ -802,17 +776,13 @@ func TestUserHasLoggedIn(t *testing.T) { w := httptest.NewRecorder() _, err = th.App.DoLogin(w, r, th.BasicUser, "") - if err != nil { - t.Errorf("Expected nil, got %s", err) - } + assert.Nil(t, err, "Expected nil, got %s", err) time.Sleep(2 * time.Second) user, _ := th.App.GetUser(th.BasicUser.Id) - if user.FirstName != "plugin-callback-success" { - t.Errorf("Expected firstname overwrite, got default") - } + assert.Equal(t, user.FirstName, "plugin-callback-success", "Expected firstname overwrite, got default") } func TestUserHasBeenCreated(t *testing.T) { @@ -858,7 +828,6 @@ func TestUserHasBeenCreated(t *testing.T) { user, err = th.App.GetUser(user.Id) require.Nil(t, err) - require.Equal(t, "plugin-callback-success", user.Nickname) } @@ -988,7 +957,5 @@ func TestHookContext(t *testing.T) { CreateAt: model.GetMillis() - 10000, } _, err := th.App.CreatePost(post, th.BasicChannel, false) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) } diff --git a/app/plugin_shutdown_test.go b/app/plugin_shutdown_test.go new file mode 100644 index 0000000000..2f99ca8231 --- /dev/null +++ b/app/plugin_shutdown_test.go @@ -0,0 +1,69 @@ +package app + +import ( + "testing" + "time" +) + +func TestPluginShutdownTest(t *testing.T) { + if testing.Short() { + t.Skip("skipping test to verify forced shutdown of slow plugin") + } + + th := Setup(t).InitBasic() + defer th.TearDown() + + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, + []string{ + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `, + ` + package main + + import ( + "github.com/mattermost/mattermost-server/plugin" + ) + + type MyPlugin struct { + plugin.MattermostPlugin + } + + func (p *MyPlugin) OnDeactivate() error { + c := make(chan bool) + <-c + + return nil + } + + func main() { + plugin.ClientMain(&MyPlugin{}) + } + `, + }, th.App, th.App.NewPluginAPI) + defer tearDown() + + done := make(chan bool) + go func() { + defer close(done) + th.App.ShutDownPlugins() + }() + + select { + case <-done: + case <-time.After(15 * time.Second): + t.Fatal("failed to force plugin shutdown after 10 seconds") + } +} diff --git a/app/team.go b/app/team.go index cfb39d7cc4..3bc3e8409e 100644 --- a/app/team.go +++ b/app/team.go @@ -89,6 +89,9 @@ func (a *App) isTeamEmailAddressAllowed(email string, allowedDomains string) boo } func (a *App) isTeamEmailAllowed(user *model.User, team *model.Team) bool { + if user.IsBot { + return true + } email := strings.ToLower(user.Email) return a.isTeamEmailAddressAllowed(email, team.AllowedDomains) } @@ -482,7 +485,7 @@ func (a *App) AddUserToTeamByToken(userId string, tokenId string) (*model.Team, for _, channel := range channels { _, err := a.AddUserToChannel(user, channel) if err != nil { - mlog.Error(err.Error()) + mlog.Error("error adding user to channel", mlog.Err(err)) } } } @@ -614,7 +617,12 @@ func (a *App) JoinUserToTeam(team *model.Team, user *model.User, userRequestorId if !user.IsGuest() { // Soft error if there is an issue joining the default channels if err := a.JoinDefaultChannels(team.Id, user, shouldBeAdmin, userRequestorId); err != nil { - mlog.Error(fmt.Sprintf("Encountered an issue joining default channels err=%v", err), mlog.String("user_id", user.Id), mlog.String("team_id", team.Id)) + mlog.Error( + "Encountered an issue joining default channels.", + mlog.String("user_id", user.Id), + mlog.String("team_id", team.Id), + mlog.Err(err), + ) } } @@ -675,6 +683,18 @@ func (a *App) GetAllPrivateTeamsPage(offset int, limit int) ([]*model.Team, *mod return a.Srv.Store.Team().GetAllPrivateTeamPageListing(offset, limit) } +func (a *App) GetAllPrivateTeamsPageWithCount(offset int, limit int) (*model.TeamsWithCount, *model.AppError) { + totalCount, err := a.Srv.Store.Team().AnalyticsPrivateTeamCount() + if err != nil { + return nil, err + } + teams, err := a.Srv.Store.Team().GetAllPrivateTeamPageListing(offset, limit) + if err != nil { + return nil, err + } + return &model.TeamsWithCount{Teams: teams, TotalCount: totalCount}, nil +} + func (a *App) GetAllPublicTeams() ([]*model.Team, *model.AppError) { return a.Srv.Store.Team().GetAllTeamListing() } @@ -683,6 +703,18 @@ func (a *App) GetAllPublicTeamsPage(offset int, limit int) ([]*model.Team, *mode return a.Srv.Store.Team().GetAllTeamPageListing(offset, limit) } +func (a *App) GetAllPublicTeamsPageWithCount(offset int, limit int) (*model.TeamsWithCount, *model.AppError) { + totalCount, err := a.Srv.Store.Team().AnalyticsPublicTeamCount() + if err != nil { + return nil, err + } + teams, err := a.Srv.Store.Team().GetAllPublicTeamPageListing(offset, limit) + if err != nil { + return nil, err + } + return &model.TeamsWithCount{Teams: teams, TotalCount: totalCount}, nil +} + func (a *App) SearchAllTeams(term string) ([]*model.Team, *model.AppError) { return a.Srv.Store.Team().SearchAll(term) } @@ -940,11 +972,11 @@ func (a *App) LeaveTeam(team *model.Team, user *model.User, requestorId string) if *a.Config().ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages { if requestorId == user.Id { if err = a.postLeaveTeamMessage(user, channel); err != nil { - mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) + mlog.Error("Failed to post join/leave message", mlog.Err(err)) } } else { if err = a.postRemoveFromTeamMessage(user, channel); err != nil { - mlog.Error(fmt.Sprint("Failed to post join/leave message", err)) + mlog.Error("Failed to post join/leave message", mlog.Err(err)) } } } @@ -1299,7 +1331,7 @@ func (a *App) GetTeamIdFromQuery(query url.Values) (string, *model.AppError) { return team.Id, nil } // soft fail, so we still create user but don't auto-join team - mlog.Error(fmt.Sprintf("%v", err)) + mlog.Error("error getting team by inviteId.", mlog.String("invite_id", inviteId), mlog.Err(err)) } return "", nil diff --git a/app/team_test.go b/app/team_test.go index 88280cdce6..159c0fd61c 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -105,7 +105,7 @@ func TestAddUserToTeam(t *testing.T) { } }) - t.Run("block user by domain", func(t *testing.T) { + t.Run("block user by domain but allow bot", func(t *testing.T) { th.BasicTeam.AllowedDomains = "example.com" if _, err := th.App.UpdateTeam(th.BasicTeam); err != nil { t.Log(err) @@ -131,10 +131,20 @@ func TestAddUserToTeam(t *testing.T) { } defer th.App.PermanentDeleteUser(&user) - if _, err := th.App.AddUserToTeam(th.BasicTeam.Id, ruser.Id, ""); err == nil || err.Where != "JoinUserToTeam" { + if _, err = th.App.AddUserToTeam(th.BasicTeam.Id, ruser.Id, ""); err == nil || err.Where != "JoinUserToTeam" { t.Log(err) t.Fatal("Should not add authservice user") } + + bot, err := th.App.CreateBot(&model.Bot{ + Username: "somebot", + Description: "a bot", + OwnerId: th.BasicUser.Id, + }) + require.Nil(t, err) + + _, err = th.App.AddUserToTeam(th.BasicTeam.Id, bot.UserId, "") + assert.Nil(t, err, "should be able to add bot to domain restricted team") }) t.Run("block user with subdomain", func(t *testing.T) { diff --git a/app/user.go b/app/user.go index d2566bf5fd..2920eaefa8 100644 --- a/app/user.go +++ b/app/user.go @@ -2151,19 +2151,12 @@ func (a *App) GetViewUsersRestrictions(userId string) (*model.ViewUsersRestricti } teamIdsWithPermission := []string{} - teamIdsWithoutPermission := []string{} for _, teamId := range teamIds { if a.HasPermissionToTeam(userId, teamId, model.PERMISSION_VIEW_MEMBERS) { teamIdsWithPermission = append(teamIdsWithPermission, teamId) - } else { - teamIdsWithoutPermission = append(teamIdsWithoutPermission, teamId) } } - if len(teamIdsWithoutPermission) == 0 { - return &model.ViewUsersRestrictions{Teams: teamIdsWithPermission}, nil - } - userChannelMembers, err := a.Srv.Store.Channel().GetAllChannelMembersForUser(userId, true, true) if err != nil { return nil, err diff --git a/app/user_test.go b/app/user_test.go index f329815221..3f2aac4c06 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -899,7 +899,8 @@ func TestGetViewUsersRestrictions(t *testing.T) { assert.NotNil(t, restrictions) assert.NotNil(t, restrictions.Teams) - assert.Len(t, restrictions.Channels, 0) + assert.NotNil(t, restrictions.Channels) + assert.ElementsMatch(t, []string{team1townsquare.Id, team1offtopic.Id, team1channel1.Id, team1channel2.Id, team2townsquare.Id, team2offtopic.Id, team2channel1.Id}, restrictions.Channels) assert.ElementsMatch(t, []string{team1.Id, team2.Id}, restrictions.Teams) }) diff --git a/app/web_hub.go b/app/web_hub.go index babf49851b..8c5e828c1e 100644 --- a/app/web_hub.go +++ b/app/web_hub.go @@ -289,6 +289,7 @@ func (a *App) InvalidateCacheForChannelPosts(channelId string) { func (a *App) InvalidateCacheForChannelPostsSkipClusterSend(channelId string) { a.Srv.Store.Post().InvalidateLastPostTimeCache(channelId) + a.Srv.Store.Channel().InvalidatePinnedPostCount(channelId) } func (a *App) InvalidateCacheForUser(userId string) { diff --git a/app/webhook_test.go b/app/webhook_test.go index bf9a23a053..d56f45f14a 100644 --- a/app/webhook_test.go +++ b/app/webhook_test.go @@ -120,8 +120,6 @@ func TestCreateIncomingWebhookForChannel(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - assert := assert.New(t) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostUsernameOverride = tc.EnablePostUsernameOverride @@ -129,22 +127,22 @@ func TestCreateIncomingWebhookForChannel(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride }) createdHook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &tc.IncomingWebhook) - if tc.ExpectedError && err == nil { - t.Fatal("should have failed") - } else if !tc.ExpectedError && err != nil { - t.Fatalf("should not have failed: %v", err.Error()) + if tc.ExpectedError { + require.NotNil(t, err, "should have failed") + } else { + require.Nil(t, err, "should not have failed") } if createdHook != nil { defer th.App.DeleteIncomingWebhook(createdHook.Id) } if tc.ExpectedIncomingWebhook == nil { - assert.Nil(createdHook, "expected nil webhook") - } else if assert.NotNil(createdHook, "expected non-nil webhook") { - assert.Equal(tc.ExpectedIncomingWebhook.DisplayName, createdHook.DisplayName) - assert.Equal(tc.ExpectedIncomingWebhook.Description, createdHook.Description) - assert.Equal(tc.ExpectedIncomingWebhook.ChannelId, createdHook.ChannelId) - assert.Equal(tc.ExpectedIncomingWebhook.Username, createdHook.Username) - assert.Equal(tc.ExpectedIncomingWebhook.IconURL, createdHook.IconURL) + assert.Nil(t, createdHook, "expected nil webhook") + } else if assert.NotNil(t, createdHook, "expected non-nil webhook") { + assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, createdHook.DisplayName) + assert.Equal(t, tc.ExpectedIncomingWebhook.Description, createdHook.Description) + assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, createdHook.ChannelId) + assert.Equal(t, tc.ExpectedIncomingWebhook.Username, createdHook.Username) + assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, createdHook.IconURL) } }) } @@ -251,16 +249,12 @@ func TestUpdateIncomingWebhook(t *testing.T) { }, } { t.Run(name, func(t *testing.T) { - assert := assert.New(t) - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true }) hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ ChannelId: th.BasicChannel.Id, }) - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) defer th.App.DeleteIncomingWebhook(hook.Id) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = tc.EnableIncomingHooks }) @@ -270,19 +264,19 @@ func TestUpdateIncomingWebhook(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnablePostIconOverride = tc.EnablePostIconOverride }) updatedHook, err := th.App.UpdateIncomingWebhook(hook, &tc.IncomingWebhook) - if tc.ExpectedError && err == nil { - t.Fatal("should have failed") - } else if !tc.ExpectedError && err != nil { - t.Fatalf("should not have failed: %v", err.Error()) + if tc.ExpectedError { + require.NotNil(t, err, "should have failed") + } else { + require.Nil(t, err, "should not have failed") } if tc.ExpectedIncomingWebhook == nil { - assert.Nil(updatedHook, "expected nil webhook") - } else if assert.NotNil(updatedHook, "expected non-nil webhook") { - assert.Equal(tc.ExpectedIncomingWebhook.DisplayName, updatedHook.DisplayName) - assert.Equal(tc.ExpectedIncomingWebhook.Description, updatedHook.Description) - assert.Equal(tc.ExpectedIncomingWebhook.ChannelId, updatedHook.ChannelId) - assert.Equal(tc.ExpectedIncomingWebhook.Username, updatedHook.Username) - assert.Equal(tc.ExpectedIncomingWebhook.IconURL, updatedHook.IconURL) + assert.Nil(t, updatedHook, "expected nil webhook") + } else if assert.NotNil(t, updatedHook, "expected non-nil webhook") { + assert.Equal(t, tc.ExpectedIncomingWebhook.DisplayName, updatedHook.DisplayName) + assert.Equal(t, tc.ExpectedIncomingWebhook.Description, updatedHook.Description) + assert.Equal(t, tc.ExpectedIncomingWebhook.ChannelId, updatedHook.ChannelId) + assert.Equal(t, tc.ExpectedIncomingWebhook.Username, updatedHook.Username) + assert.Equal(t, tc.ExpectedIncomingWebhook.IconURL, updatedHook.IconURL) } }) } @@ -295,9 +289,7 @@ func TestCreateWebhookPost(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true }) hook, err := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id}) - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) defer th.App.DeleteIncomingWebhook(hook.Id) post, err := th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", model.StringInterface{ @@ -308,21 +300,14 @@ func TestCreateWebhookPost(t *testing.T) { }, "webhook_display_name": hook.DisplayName, }, model.POST_SLACK_ATTACHMENT, "") - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) - for _, k := range []string{"from_webhook", "attachments", "webhook_display_name"} { - if _, ok := post.Props[k]; !ok { - t.Log("missing one props: " + k) - t.Fatal(k) - } - } + assert.Contains(t, post.Props, "from_webhook", "missing from_webhook prop") + assert.Contains(t, post.Props, "attachments", "missing attachments prop") + assert.Contains(t, post.Props, "webhook_display_name", "missing webhook_display_name prop") _, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.POST_SYSTEM_GENERIC, "") - if err == nil { - t.Fatal("should have failed - bad post type") - } + require.NotNil(t, err, "Should have failed - bad post type") expectedText := "`<>|<>|`" post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{ @@ -333,9 +318,7 @@ func TestCreateWebhookPost(t *testing.T) { }, "webhook_display_name": hook.DisplayName, }, model.POST_SLACK_ATTACHMENT, "") - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) assert.Equal(t, expectedText, post.Message) expectedText = "< | \n|\n>" @@ -347,9 +330,7 @@ func TestCreateWebhookPost(t *testing.T) { }, "webhook_display_name": hook.DisplayName, }, model.POST_SLACK_ATTACHMENT, "") - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) assert.Equal(t, expectedText, post.Message) expectedText = `commit bc95839e4a430ace453e8b209a3723c000c1729a @@ -377,9 +358,7 @@ Date: Thu Mar 1 19:46:48 2018 +0300 }, "webhook_display_name": hook.DisplayName, }, model.POST_SLACK_ATTACHMENT, "") - if err != nil { - t.Fatal(err.Error()) - } + require.Nil(t, err) assert.Equal(t, expectedText, post.Message) } @@ -495,10 +474,7 @@ func TestCreateOutGoingWebhookWithUsernameAndIconURL(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOutgoingWebhooks = true }) createdHook, err := th.App.CreateOutgoingWebhook(&outgoingWebhook) - - if err != nil { - t.Fatalf("should not have failed: %v", err.Error()) - } + require.Nil(t, err) assert.NotNil(t, createdHook, "should not be null") diff --git a/cmd/mattermost/commands/integrity.go b/cmd/mattermost/commands/integrity.go new file mode 100644 index 0000000000..5f61aa4476 --- /dev/null +++ b/cmd/mattermost/commands/integrity.go @@ -0,0 +1,78 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "fmt" + "os" + "strings" + + "github.com/mattermost/mattermost-server/store" + "github.com/spf13/cobra" +) + +var IntegrityCmd = &cobra.Command{ + Use: "integrity", + Short: "Check database data integrity", + RunE: integrityCmdF, +} + +func init() { + IntegrityCmd.Flags().Bool("confirm", false, "Confirm you really want to run a complete integrity check that may temporarily harm system performance") + IntegrityCmd.Flags().BoolP("verbose", "v", false, "Show detailed information on integrity check results") + RootCmd.AddCommand(IntegrityCmd) +} + +func printRelationalIntegrityCheckResult(data store.RelationalIntegrityCheckData, verbose bool) { + fmt.Println(fmt.Sprintf("Found %d records in relation %s orphans of relation %s", + len(data.Records), data.ChildName, data.ParentName)) + if !verbose { + return + } + for _, record := range data.Records { + if record.ChildId != "" { + fmt.Println(fmt.Sprintf(" Child %s (%s.%s) is missing Parent %s (%s.%s)", record.ChildId, data.ChildName, data.ChildIdAttr, record.ParentId, data.ChildName, data.ParentIdAttr)) + } else { + fmt.Println(fmt.Sprintf(" Child is missing Parent %s (%s.%s)", record.ParentId, data.ChildName, data.ParentIdAttr)) + } + } +} + +func printIntegrityCheckResult(result store.IntegrityCheckResult, verbose bool) { + switch data := result.Data.(type) { + case store.RelationalIntegrityCheckData: + printRelationalIntegrityCheckResult(data, verbose) + } +} + +func integrityCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + confirmFlag, _ := command.Flags().GetBool("confirm") + if !confirmFlag { + var confirm string + fmt.Fprintf(os.Stdout, "This check may harm performance on live systems. Are you sure you want to proceed? (y/N): ") + fmt.Scanln(&confirm) + if !strings.EqualFold(confirm, "y") && !strings.EqualFold(confirm, "yes") { + fmt.Fprintf(os.Stderr, "Aborted.\n") + return nil + } + } + + verboseFlag, _ := command.Flags().GetBool("verbose") + results := a.Srv.Store.CheckIntegrity() + for result := range results { + if result.Err != nil { + fmt.Fprintf(os.Stderr, "%s\n", result.Err.Error()) + break + } + printIntegrityCheckResult(result, verboseFlag) + } + + return nil +} diff --git a/cmd/mattermost/commands/server.go b/cmd/mattermost/commands/server.go index e80313dcb0..b705671f4a 100644 --- a/cmd/mattermost/commands/server.go +++ b/cmd/mattermost/commands/server.go @@ -91,7 +91,7 @@ func runServer(configStore config.Store, disableConfigWatch bool, usedPlatform b // wait for kill signal before attempting to gracefully shutdown // the running service - signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE) <-interruptChan return nil diff --git a/go.mod b/go.mod index a51e2772ca..9d678fef9e 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mattn/go-sqlite3 v1.11.0 github.com/miekg/dns v1.1.15 // indirect - github.com/minio/minio-go v0.0.0-20190422205105-a8704b60278f + github.com/minio/minio-go/v6 v6.0.34 github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/muesli/smartcrop v0.3.0 // indirect github.com/olekukonko/tablewriter v0.0.1 // indirect @@ -100,5 +100,8 @@ require ( willnorris.com/go/imageproxy v0.9.0 ) -// Workaround for https://github.com/golang/go/issues/30831 and fallout. -replace github.com/golang/lint => github.com/golang/lint v0.0.0-20190227174305-8f45f776aaf1 +replace ( + git.apache.org/thrift.git => github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999 + // Workaround for https://github.com/golang/go/issues/30831 and fallout. + github.com/golang/lint => github.com/golang/lint v0.0.0-20190227174305-8f45f776aaf1 +) diff --git a/go.sum b/go.sum index 13c95d32a8..ad84326b89 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.37.1/go.mod h1:SAbnLi6YTSPKSI0dTUEOVLCkyPfKXK8n4ibqiMoj4ok= contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw= -git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/apache/thrift v0.0.0-20180902110319-2566ecd5d999/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -20,6 +20,7 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845/go.mod h1:c8Mh99Cw82nrsAnPgxQSZHkswVOJF7/MqZb1ZdvriLM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -65,6 +66,7 @@ github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjC github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8 h1:6muCmMJat6z7qptVrIf/+OWPxsjAfvhw5/6t+FwEkgg= github.com/dyatlov/go-opengraph v0.0.0-20180429202543-816b6608b3c8/go.mod h1:nYia/MIs9OyvXXYboPmNOj0gVWo97Wx0sde+ZuKkoM4= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -79,6 +81,7 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gernest/wow v0.1.0/go.mod h1:dEPabJRi5BneI1Nev1VWo0ZlcTWibHWp43qxKms4elY= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-gorp/gorp v2.0.0+incompatible h1:dIQPsBtl6/H1MjVseWuWPXa7ET4p6Dve4j3Hg+UjqYw= @@ -127,7 +130,6 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -202,7 +204,6 @@ github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -254,8 +255,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.15 h1:CSSIDtllwGLMoA6zjdKnaE6Tx6eVUxQ29LUgGetiDCI= github.com/miekg/dns v1.1.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/minio/minio-go v0.0.0-20190422205105-a8704b60278f h1:pIUObqY9ljwlPfUINTvfWhEuoJyy9dkCloS2f5Mf1Ks= -github.com/minio/minio-go v0.0.0-20190422205105-a8704b60278f/go.mod h1:/haSOWG8hQNx2+JOfLJ9GKp61EAmgPwRVw/Sac0NzaM= +github.com/minio/cli v1.20.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= +github.com/minio/minio-go/v6 v6.0.34 h1:ESPDlIg8Pe2BRvsxPomd0xB72uLmsXrkDNoze36yb90= +github.com/minio/minio-go/v6 v6.0.34/go.mod h1:vaNT59cWULS37E+E9zkuN/BVnKHyXtVGS+b04Boc66Y= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -355,11 +357,10 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -413,9 +414,10 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -446,6 +448,7 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190322120337-addf6b3196f6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= @@ -475,7 +478,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -545,7 +548,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0= gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= @@ -557,6 +560,7 @@ gopkg.in/olivere/elastic.v5 v5.0.81/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0F gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/i18n/en.json b/i18n/en.json index 5e29ce002a..c867424509 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2774,6 +2774,10 @@ "id": "app.admin.test_email.failure", "translation": "Connection unsuccessful: {{.Error}}" }, + { + "id": "app.admin.test_site_url.failure", + "translation": "This is not a valid live URL" + }, { "id": "app.channel.create_channel.no_team_id.app_error", "translation": "Must specify the team ID to create a channel" @@ -3424,7 +3428,7 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] New Direct Message from @{{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] New Direct Message from {{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}" }, { "id": "app.notification.subject.group_message.full", @@ -5662,6 +5666,10 @@ "id": "store.sql_channel.get_more_channels.get.app_error", "translation": "Unable to get the channels" }, + { + "id": "store.sql_channel.get_pinnedpost_count.app_error", + "translation": "Unable to get the channel pinned post count" + }, { "id": "store.sql_channel.get_public_channels.get.app_error", "translation": "Unable to get public channels" @@ -6678,6 +6686,14 @@ "id": "store.sql_team.analytics_get_team_count_for_scheme.app_error", "translation": "Unable to get the channel count for the scheme." }, + { + "id": "store.sql_team.analytics_private_team_count.app_error", + "translation": "Unable to count the private teams" + }, + { + "id": "store.sql_team.analytics_public_team_count.app_error", + "translation": "Unable to count the public teams" + }, { "id": "store.sql_team.analytics_team_count.app_error", "translation": "Unable to count the teams" diff --git a/jobs/schedulers.go b/jobs/schedulers.go index b0cb92fc5a..d415c85e1f 100644 --- a/jobs/schedulers.go +++ b/jobs/schedulers.go @@ -103,8 +103,7 @@ func (schedulers *Schedulers) Start() *Schedulers { if scheduler != nil { if scheduler.Enabled(cfg) { if _, err := schedulers.scheduleJob(cfg, scheduler); err != nil { - mlog.Warn(fmt.Sprintf("Failed to schedule job with scheduler: %v", scheduler.Name())) - mlog.Error(fmt.Sprint(err)) + mlog.Error("Failed to schedule job", mlog.String("scheduler", scheduler.Name()), mlog.Err(err)) } else { schedulers.setNextRunTime(cfg, idx, now, true) } @@ -148,7 +147,7 @@ func (schedulers *Schedulers) setNextRunTime(cfg *model.Config, idx int, now tim if !pendingJobs { if pj, err := schedulers.jobs.CheckForPendingJobsByType(scheduler.JobType()); err != nil { - mlog.Error("Failed to set next job run time: " + err.Error()) + mlog.Error("Failed to set next job run time", mlog.Err(err)) schedulers.nextRunTimes[idx] = nil return } else { @@ -158,13 +157,13 @@ func (schedulers *Schedulers) setNextRunTime(cfg *model.Config, idx int, now tim lastSuccessfulJob, err := schedulers.jobs.GetLastSuccessfulJobByType(scheduler.JobType()) if err != nil { - mlog.Error("Failed to set next job run time: " + err.Error()) + mlog.Error("Failed to set next job run time", mlog.Err(err)) schedulers.nextRunTimes[idx] = nil return } schedulers.nextRunTimes[idx] = scheduler.NextScheduleTime(cfg, now, pendingJobs, lastSuccessfulJob) - mlog.Debug(fmt.Sprintf("Next run time for scheduler %v: %v", scheduler.Name(), schedulers.nextRunTimes[idx])) + mlog.Debug("Next run time for scheduler", mlog.String("scheduler_name", scheduler.Name()), mlog.String("next_runtime", fmt.Sprintf("%v", schedulers.nextRunTimes[idx]))) } func (schedulers *Schedulers) scheduleJob(cfg *model.Config, scheduler model.Scheduler) (*model.Job, *model.AppError) { diff --git a/model/channel_stats.go b/model/channel_stats.go index 5eb26b7e29..63367fa48f 100644 --- a/model/channel_stats.go +++ b/model/channel_stats.go @@ -9,9 +9,10 @@ import ( ) type ChannelStats struct { - ChannelId string `json:"channel_id"` - MemberCount int64 `json:"member_count"` - GuestCount int64 `json:"guest_count"` + ChannelId string `json:"channel_id"` + MemberCount int64 `json:"member_count"` + GuestCount int64 `json:"guest_count"` + PinnedPostCount int64 `json:"pinnedpost_count"` } func (o *ChannelStats) ToJson() string { diff --git a/model/client4.go b/model/client4.go index 0195bef380..935d07e4f6 100644 --- a/model/client4.go +++ b/model/client4.go @@ -268,6 +268,10 @@ func (c *Client4) GetTestEmailRoute() string { return fmt.Sprintf("/email/test") } +func (c *Client4) GetTestSiteURLRoute() string { + return fmt.Sprintf("/site_url/test") +} + func (c *Client4) GetTestS3Route() string { return fmt.Sprintf("/file/s3_test") } @@ -2922,6 +2926,18 @@ func (c *Client4) TestEmail(config *Config) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } +// TestSiteURL will test the validity of a site URL. +func (c *Client4) TestSiteURL(siteURL string) (bool, *Response) { + requestBody := make(map[string]string) + requestBody["site_url"] = siteURL + r, err := c.DoApiPost(c.GetTestSiteURLRoute(), MapToJson(requestBody)) + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // TestS3Connection will attempt to connect to the AWS S3. func (c *Client4) TestS3Connection(config *Config) (bool, *Response) { r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson()) diff --git a/model/config.go b/model/config.go index b64a2f3215..9bc4376b5b 100644 --- a/model/config.go +++ b/model/config.go @@ -138,7 +138,7 @@ const ( SAML_SETTINGS_DEFAULT_LOCALE_ATTRIBUTE = "" SAML_SETTINGS_DEFAULT_POSITION_ATTRIBUTE = "" - NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK = "https://about.mattermost.com/downloads/" + NATIVEAPP_SETTINGS_DEFAULT_APP_DOWNLOAD_LINK = "https://mattermost.com/download/#mattermostApps" NATIVEAPP_SETTINGS_DEFAULT_ANDROID_APP_DOWNLOAD_LINK = "https://about.mattermost.com/mattermost-android-app/" NATIVEAPP_SETTINGS_DEFAULT_IOS_APP_DOWNLOAD_LINK = "https://about.mattermost.com/mattermost-ios-app/" @@ -1231,7 +1231,7 @@ func (s *EmailSettings) SetDefaults(isUpdate bool) { } if s.SMTPPort == nil || len(*s.SMTPPort) == 0 { - s.SMTPPort = NewString("2500") + s.SMTPPort = NewString("10025") } if s.ConnectionSecurity == nil || *s.ConnectionSecurity == CONN_SECURITY_PLAIN { diff --git a/model/user.go b/model/user.go index db0621e04e..ee8c8e7b6b 100644 --- a/model/user.go +++ b/model/user.go @@ -494,6 +494,18 @@ func (u *User) Sanitize(options map[string]bool) { } } +// Remove any input data from the user object that is not user controlled +func (u *User) SanitizeInput() { + u.AuthData = NewString("") + u.AuthService = "" + u.LastPasswordUpdate = 0 + u.LastPictureUpdate = 0 + u.FailedAttempts = 0 + u.EmailVerified = false + u.MfaActive = false + u.MfaSecret = "" +} + func (u *User) ClearNonProfileFields() { u.Password = "" u.AuthData = NewString("") diff --git a/model/version.go b/model/version.go index 0002932345..6fc625b95b 100644 --- a/model/version.go +++ b/model/version.go @@ -13,6 +13,7 @@ import ( // It should be maintained in chronological order with most current // release at the front of the list. var versions = []string{ + "5.15.0", "5.14.0", "5.13.0", "5.12.0", diff --git a/plugin/api.go b/plugin/api.go index fed0bbb3df..c799b47f57 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -31,6 +31,11 @@ type API interface { // GetConfig fetches the currently persisted config GetConfig() *model.Config + // GetUnsanitizedConfig fetches the currently persisted config without removing secrets. + // + // Minimum server version: 5.16 + GetUnsanitizedConfig() *model.Config + // SaveConfig sets the given config and persists the changes SaveConfig(config *model.Config) *model.AppError diff --git a/plugin/client.go b/plugin/client.go index e445f9e1e0..d74663d1ec 100644 --- a/plugin/client.go +++ b/plugin/client.go @@ -10,6 +10,7 @@ import ( const ( INTERNAL_KEY_PREFIX = "mmi_" BOT_USER_KEY = INTERNAL_KEY_PREFIX + "botid" + CHANNEL_KEY = INTERNAL_KEY_PREFIX + "channelid" ) // Starts the serving of a Mattermost plugin over net/rpc. gRPC is not yet supported. diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 22b577f3c5..e1bd6165ff 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -588,6 +588,33 @@ func (s *apiRPCServer) GetConfig(args *Z_GetConfigArgs, returns *Z_GetConfigRetu return nil } +type Z_GetUnsanitizedConfigArgs struct { +} + +type Z_GetUnsanitizedConfigReturns struct { + A *model.Config +} + +func (g *apiRPCClient) GetUnsanitizedConfig() *model.Config { + _args := &Z_GetUnsanitizedConfigArgs{} + _returns := &Z_GetUnsanitizedConfigReturns{} + if err := g.client.Call("Plugin.GetUnsanitizedConfig", _args, _returns); err != nil { + log.Printf("RPC call to GetUnsanitizedConfig API failed: %s", err.Error()) + } + return _returns.A +} + +func (s *apiRPCServer) GetUnsanitizedConfig(args *Z_GetUnsanitizedConfigArgs, returns *Z_GetUnsanitizedConfigReturns) error { + if hook, ok := s.impl.(interface { + GetUnsanitizedConfig() *model.Config + }); ok { + returns.A = hook.GetUnsanitizedConfig() + } else { + return encodableError(fmt.Errorf("API GetUnsanitizedConfig called but not implemented.")) + } + return nil +} + type Z_SaveConfigArgs struct { A *model.Config } diff --git a/plugin/environment.go b/plugin/environment.go index d301a8942e..e4410cf26a 100644 --- a/plugin/environment.go +++ b/plugin/environment.go @@ -284,16 +284,46 @@ func (env *Environment) RestartPlugin(id string) error { // Shutdown deactivates all plugins and gracefully shuts down the environment. func (env *Environment) Shutdown() { + if env.pluginHealthCheckJob != nil { + env.pluginHealthCheckJob.Cancel() + } + + var wg sync.WaitGroup env.registeredPlugins.Range(func(key, value interface{}) bool { rp := value.(*registeredPlugin) - if rp.supervisor != nil { + if rp.supervisor == nil { + return true + } + + wg.Add(1) + + done := make(chan bool) + go func() { + defer close(done) if err := rp.supervisor.Hooks().OnDeactivate(); err != nil { env.logger.Error("Plugin OnDeactivate() error", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id), mlog.Err(err)) } - rp.supervisor.Shutdown() - } + }() + go func() { + defer wg.Done() + + select { + case <-time.After(10 * time.Second): + env.logger.Warn("Plugin OnDeactivate() failed to complete in 10 seconds", mlog.String("plugin_id", rp.BundleInfo.Manifest.Id)) + case <-done: + } + + rp.supervisor.Shutdown() + }() + + return true + }) + + wg.Wait() + + env.registeredPlugins.Range(func(key, value interface{}) bool { env.registeredPlugins.Delete(key) return true diff --git a/plugin/health_check.go b/plugin/health_check.go index 7f9855e125..85989e4775 100644 --- a/plugin/health_check.go +++ b/plugin/health_check.go @@ -5,6 +5,7 @@ package plugin import ( "fmt" + "sync" "time" "github.com/mattermost/mattermost-server/mlog" @@ -19,9 +20,10 @@ const ( ) type PluginHealthCheckJob struct { - cancel chan struct{} - cancelled chan struct{} - env *Environment + cancel chan struct{} + cancelled chan struct{} + cancelOnce sync.Once + env *Environment } // InitPluginHealthCheckJob starts a new job if one is not running and is set to enabled, or kills an existing one if set to disabled. @@ -125,7 +127,9 @@ func newPluginHealthCheckJob(env *Environment) *PluginHealthCheckJob { } func (job *PluginHealthCheckJob) Cancel() { - close(job.cancel) + job.cancelOnce.Do(func() { + close(job.cancel) + }) <-job.cancelled } diff --git a/plugin/helpers_channels.go b/plugin/helpers_channels.go new file mode 100644 index 0000000000..710b022303 --- /dev/null +++ b/plugin/helpers_channels.go @@ -0,0 +1,111 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin + +import ( + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + "github.com/pkg/errors" +) + +func (p *HelpersImpl) EnsureChannel(channel *model.Channel) (retChannelId string, retErr error) { + // Must provide a channel with a name and teadId + if channel == nil || len(channel.Name) < 1 || len(channel.TeamId) < 1 { + return "", errors.New("passed a bad channel, nil or no name or no team id") + } + + // If we fail for any reason, this could be a race between creation of channel and + // retrieval from another EnsureChannel. Just try the basic retrieve existing again. + defer func() { + if retChannelId == "" || retErr != nil { + var err error + var channelIdBytes []byte + + err = utils.ProgressiveRetry(func() error { + channelIdBytes, err = p.API.KVGet(CHANNEL_KEY) + if err != nil { + return err + } + return nil + }) + + if err == nil && channelIdBytes != nil { + retChannelId = string(channelIdBytes) + retErr = nil + } + } + }() + + // Fetch channel ID from key value store + channelIdBytes, kvGetErr := p.API.KVGet(CHANNEL_KEY) + if kvGetErr != nil { + // Failed to retrive the value of channel + return "", errors.Wrap(kvGetErr, "failed to get channel ID") + } + + var existingChannel *model.Channel + var channelGetErr *model.AppError + + // If channel ID exists, get existing channel by ID else get it by Name + if channelIdBytes != nil { + existingChannel, channelGetErr = p.API.GetChannel(string(channelIdBytes)) + if channelGetErr != nil { + return "", errors.Wrap(channelGetErr, "failed to get channel by ID") + } + } else { + existingChannel, channelGetErr = p.API.GetChannelByName(channel.TeamId, channel.Name, false) + if channelGetErr != nil { + return "", errors.Wrap(channelGetErr, "failed to get channel by name") + } + } + + // If channel exists, update the metadata + if existingChannel != nil { + return updateChannel(p, existingChannel, channel) + } + + // Create a new channel + createdChannel, createChannelErr := p.API.CreateChannel(channel) + if createChannelErr != nil { + return "", errors.Wrap(createChannelErr, "failed to create channel") + } + + // Set the new channel id in key value store + if kvSetErr := p.API.KVSet(CHANNEL_KEY, []byte(createdChannel.Id)); kvSetErr != nil { + p.API.LogWarn("Failed to set created channel id.", "channelid", createdChannel.Id, "err", kvSetErr) + } + + return createdChannel.Id, nil +} + +func updateChannel(p *HelpersImpl, existing *model.Channel, new *model.Channel) (string, error) { + // Update metadata of the channel + if updateErr := updateChannelMeta(existing, new); updateErr != nil { + return "", errors.Wrap(updateErr, "Failed to update the metadata of existing channel") + } + + // Send the updates to API + updatedChannel, channelUpdateErr := p.API.UpdateChannel(existing) + if channelUpdateErr != nil { + return "", errors.Wrap(channelUpdateErr, "Failed to update the existing channel") + } + + // Channel exists! + return updatedChannel.Id, nil +} + +func updateChannelMeta(existing *model.Channel, new *model.Channel) error { + // Check if channels are of different types + if existing.Type != new.Type { + return errors.New("Channel type cannot be updated") + } + + // Update metadata of channel + existing.Name = new.Name + existing.DisplayName = new.DisplayName + existing.Purpose = new.Purpose + existing.Header = new.Header + + return nil +} diff --git a/plugin/helpers_channels_test.go b/plugin/helpers_channels_test.go new file mode 100644 index 0000000000..691466ed08 --- /dev/null +++ b/plugin/helpers_channels_test.go @@ -0,0 +1,297 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package plugin_test + +import ( + "testing" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/plugin" + "github.com/mattermost/mattermost-server/plugin/plugintest" + "github.com/stretchr/testify/assert" +) + +func TestEnsureChannel(t *testing.T) { + setupAPI := func() *plugintest.API { + return &plugintest.API{} + } + + testChannel := &model.Channel{ + Id: model.NewId(), + TeamId: model.NewId(), + Type: "public", + Name: "test_channel", + DisplayName: "Test Channel", + Purpose: "Testing EnsureChannel", + Header: "Testing EnsureChannel", + } + + t.Run("bad parameters", func(t *testing.T) { + t.Run("no channel", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(nil) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("empty name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + Name: "", + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("name without teamId", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + Name: "test_channel", + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("teamId without name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + TeamId: model.NewId(), + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("teamId with empty name", func(t *testing.T) { + p := &plugin.HelpersImpl{} + channelId, err := p.EnsureChannel(&model.Channel{ + TeamId: model.NewId(), + }) + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + }) + + t.Run("if channel already exists in Key Value store", func(t *testing.T) { + t.Run("should return an error if unable to get channel id", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return an error if unable to get channel", func(t *testing.T) { + expectedChannelId := model.NewId() + + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(expectedChannelId), nil) + api.On("GetChannel", expectedChannelId).Return(nil, &model.AppError{}) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return an error if unable to update channel", func(t *testing.T) { + expectedChannelId := model.NewId() + + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(expectedChannelId), nil) + api.On("GetChannel", expectedChannelId).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of existing channel if metadata is same", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(testChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if channel type is different from existing one", func(t *testing.T) { + privChannel := &model.Channel{ + Id: model.NewId(), + Type: "private", + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(privChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of updated channel if metadata is different", func(t *testing.T) { + updatedChannel := &model.Channel{ + Id: model.NewId(), + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return([]byte(testChannel.Id), nil) + api.On("GetChannel", testChannel.Id).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(updatedChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, updatedChannel.Id, channelId) + assert.Nil(t, err) + }) + }) + + t.Run("if channel is not in Key Value store but already exists", func(t *testing.T) { + t.Run("should return an error if unable to get channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of existing channel if metadata is same", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(testChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if failed to update the channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should return the Id of updated channel if metadata is different", func(t *testing.T) { + updatedChannel := &model.Channel{ + Id: model.NewId(), + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(testChannel, nil) + api.On("UpdateChannel", testChannel).Return(updatedChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, updatedChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if channel type is different from existing one", func(t *testing.T) { + privChannel := &model.Channel{ + Id: model.NewId(), + Type: "private", + TeamId: testChannel.TeamId, + Name: testChannel.Name, + } + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(privChannel, nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + }) + + t.Run("if channel does not exist", func(t *testing.T) { + t.Run("should create new channel and return the Id", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(testChannel, nil) + api.On("KVSet", plugin.CHANNEL_KEY, []byte(testChannel.Id)).Return(nil) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + t.Run("should return error if unable to create new channel", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(nil, &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, "", channelId) + assert.NotNil(t, err) + }) + t.Run("should log and return id if unable to write to Key Value store", func(t *testing.T) { + api := setupAPI() + api.On("KVGet", plugin.CHANNEL_KEY).Return(nil, nil) + api.On("GetChannelByName", testChannel.TeamId, testChannel.Name, false).Return(nil, nil) + api.On("CreateChannel", testChannel).Return(testChannel, nil) + api.On("KVSet", plugin.CHANNEL_KEY, []byte(testChannel.Id)).Return(&model.AppError{}) + api.On("LogWarn", "Failed to set created channel id.", "channelid", testChannel.Id, "err", &model.AppError{}) + defer api.AssertExpectations(t) + + p := &plugin.HelpersImpl{API: api} + + channelId, err := p.EnsureChannel(testChannel) + + assert.Equal(t, testChannel.Id, channelId) + assert.Nil(t, err) + }) + }) +} diff --git a/plugin/helpers_kv.go b/plugin/helpers_kv.go index b489ba7591..78efa31467 100644 --- a/plugin/helpers_kv.go +++ b/plugin/helpers_kv.go @@ -34,7 +34,12 @@ func (p *HelpersImpl) KVSetJSON(key string, value interface{}) error { return err } - return p.API.KVSet(key, data) + appErr := p.API.KVSet(key, data) + if appErr != nil { + return appErr + } + + return nil } // KVCompareAndSetJSON is a wrapper around KVCompareAndSet to simplify atomically writing a JSON object to the key value store. @@ -56,7 +61,12 @@ func (p *HelpersImpl) KVCompareAndSetJSON(key string, oldValue interface{}, newV } } - return p.API.KVCompareAndSet(key, oldData, newData) + set, appErr := p.API.KVCompareAndSet(key, oldData, newData) + if appErr != nil { + return set, appErr + } + + return set, nil } // KVCompareAndDeleteJSON is a wrapper around KVCompareAndDelete to simplify atomically deleting a JSON object from the key value store. @@ -71,7 +81,12 @@ func (p *HelpersImpl) KVCompareAndDeleteJSON(key string, oldValue interface{}) ( } } - return p.API.KVCompareAndDelete(key, oldData) + deleted, appErr := p.API.KVCompareAndDelete(key, oldData) + if appErr != nil { + return deleted, appErr + } + + return deleted, nil } // KVSetWithExpiryJSON is a wrapper around KVSetWithExpiry to simplify atomically writing a JSON object with expiry to the key value store. @@ -81,5 +96,10 @@ func (p *HelpersImpl) KVSetWithExpiryJSON(key string, value interface{}, expireI return err } - return p.API.KVSetWithExpiry(key, data, expireInSeconds) + appErr := p.API.KVSetWithExpiry(key, data, expireInSeconds) + if appErr != nil { + return appErr + } + + return nil } diff --git a/plugin/helpers_kv_test.go b/plugin/helpers_kv_test.go index 23a6d37fdf..f61033dacb 100644 --- a/plugin/helpers_kv_test.go +++ b/plugin/helpers_kv_test.go @@ -22,7 +22,7 @@ func TestKVGetJSON(t *testing.T) { ok, err := p.KVGetJSON("test-key", dat) api.AssertExpectations(t) assert.False(t, ok) - assert.NotNil(t, err) + assert.Error(t, err) assert.Nil(t, dat) }) @@ -38,7 +38,7 @@ func TestKVGetJSON(t *testing.T) { ok, err := p.KVGetJSON("test-key", dat) api.AssertExpectations(t) assert.False(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) assert.Nil(t, dat) }) @@ -54,7 +54,7 @@ func TestKVGetJSON(t *testing.T) { ok, err := p.KVGetJSON("test-key", &dat) api.AssertExpectations(t) assert.False(t, ok) - assert.NotNil(t, err) + assert.Error(t, err) assert.Nil(t, dat) }) @@ -70,7 +70,7 @@ func TestKVGetJSON(t *testing.T) { ok, err := p.KVGetJSON("test-key", &dat) assert.True(t, ok) api.AssertExpectations(t) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, map[string]interface{}{ "val-a": float64(10), }, dat) @@ -86,7 +86,21 @@ func TestKVSetJSON(t *testing.T) { err := p.KVSetJSON("test-key", func() { return }) api.AssertExpectations(t) - assert.NotNil(t, err) + assert.Error(t, err) + }) + + t.Run("KVSet error", func(t *testing.T) { + api := &plugintest.API{} + api.On("KVSet", "test-key", []byte(`{"val-a":10}`)).Return(&model.AppError{}) + + p := &plugin.HelpersImpl{API: api} + + err := p.KVSetJSON("test-key", map[string]interface{}{ + "val-a": float64(10), + }) + + api.AssertExpectations(t) + assert.Error(t, err) }) t.Run("marshallable struct", func(t *testing.T) { @@ -100,7 +114,7 @@ func TestKVSetJSON(t *testing.T) { }) api.AssertExpectations(t) - assert.Nil(t, err) + assert.NoError(t, err) }) } @@ -114,7 +128,7 @@ func TestKVCompareAndSetJSON(t *testing.T) { api.AssertExpectations(t) assert.Equal(t, false, ok) - assert.NotNil(t, err) + assert.Error(t, err) }) t.Run("new value JSON marshal error", func(t *testing.T) { @@ -127,7 +141,23 @@ func TestKVCompareAndSetJSON(t *testing.T) { api.AssertExpectations(t) assert.False(t, ok) - assert.NotNil(t, err) + assert.Error(t, err) + }) + + t.Run("KVCompareAndSet error", func(t *testing.T) { + api := &plugintest.API{} + api.On("KVCompareAndSet", "test-key", []byte(`{"val-a":10}`), []byte(`{"val-b":20}`)).Return(false, &model.AppError{}) + p := &plugin.HelpersImpl{API: api} + + ok, err := p.KVCompareAndSetJSON("test-key", map[string]interface{}{ + "val-a": 10, + }, map[string]interface{}{ + "val-b": 20, + }) + + api.AssertExpectations(t) + assert.False(t, ok) + assert.Error(t, err) }) t.Run("old value nil", func(t *testing.T) { @@ -141,7 +171,7 @@ func TestKVCompareAndSetJSON(t *testing.T) { api.AssertExpectations(t) assert.True(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) }) t.Run("old value non-nil", func(t *testing.T) { @@ -157,7 +187,7 @@ func TestKVCompareAndSetJSON(t *testing.T) { api.AssertExpectations(t) assert.True(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) }) t.Run("new value nil", func(t *testing.T) { @@ -171,7 +201,7 @@ func TestKVCompareAndSetJSON(t *testing.T) { api.AssertExpectations(t) assert.True(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) }) } @@ -185,7 +215,21 @@ func TestKVCompareAndDeleteJSON(t *testing.T) { api.AssertExpectations(t) assert.Equal(t, false, ok) - assert.NotNil(t, err) + assert.Error(t, err) + }) + + t.Run("KVCompareAndDelete error", func(t *testing.T) { + api := &plugintest.API{} + api.On("KVCompareAndDelete", "test-key", []byte(`{"val-a":10}`)).Return(false, &model.AppError{}) + p := &plugin.HelpersImpl{API: api} + + ok, err := p.KVCompareAndDeleteJSON("test-key", map[string]interface{}{ + "val-a": 10, + }) + + api.AssertExpectations(t) + assert.False(t, ok) + assert.Error(t, err) }) t.Run("old value nil", func(t *testing.T) { @@ -197,7 +241,7 @@ func TestKVCompareAndDeleteJSON(t *testing.T) { api.AssertExpectations(t) assert.True(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) }) t.Run("old value non-nil", func(t *testing.T) { @@ -211,7 +255,7 @@ func TestKVCompareAndDeleteJSON(t *testing.T) { api.AssertExpectations(t) assert.True(t, ok) - assert.Nil(t, err) + assert.NoError(t, err) }) } @@ -225,7 +269,20 @@ func TestKVSetWithExpiryJSON(t *testing.T) { err := p.KVSetWithExpiryJSON("test-key", func() { return }, 100) api.AssertExpectations(t) - assert.NotNil(t, err) + assert.Error(t, err) + }) + + t.Run("KVSetWithExpiry error", func(t *testing.T) { + api := &plugintest.API{} + api.On("KVSetWithExpiry", "test-key", []byte(`{"val-a":10}`), int64(100)).Return(&model.AppError{}) + p := &plugin.HelpersImpl{API: api} + + err := p.KVSetWithExpiryJSON("test-key", map[string]interface{}{ + "val-a": float64(10), + }, 100) + + api.AssertExpectations(t) + assert.Error(t, err) }) t.Run("wellformed JSON", func(t *testing.T) { @@ -239,6 +296,6 @@ func TestKVSetWithExpiryJSON(t *testing.T) { }, 100) api.AssertExpectations(t) - assert.Nil(t, err) + assert.NoError(t, err) }) } diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index f634748a1e..e5f92671b2 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -4,8 +4,10 @@ package plugintest -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // API is an autogenerated mock type for the API type type API struct { @@ -1636,6 +1638,22 @@ func (_m *API) GetTeamsUnreadForUser(userId string) ([]*model.TeamUnread, *model return r0, r1 } +// GetUnsanitizedConfig provides a mock function with given fields: +func (_m *API) GetUnsanitizedConfig() *model.Config { + ret := _m.Called() + + var r0 *model.Config + if rf, ok := ret.Get(0).(func() *model.Config); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Config) + } + } + + return r0 +} + // GetUser provides a mock function with given fields: userId func (_m *API) GetUser(userId string) (*model.User, *model.AppError) { ret := _m.Called(userId) diff --git a/plugin/plugintest/helpers.go b/plugin/plugintest/helpers.go index ba3f92b33a..066c0b2b8c 100644 --- a/plugin/plugintest/helpers.go +++ b/plugin/plugintest/helpers.go @@ -4,8 +4,10 @@ package plugintest -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // Helpers is an autogenerated mock type for the Helpers type type Helpers struct { diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go index 5ebeecc42e..ef317bf4e2 100644 --- a/plugin/plugintest/hooks.go +++ b/plugin/plugintest/hooks.go @@ -4,11 +4,16 @@ package plugintest -import http "net/http" -import io "io" -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" -import plugin "github.com/mattermost/mattermost-server/plugin" +import ( + io "io" + http "net/http" + + mock "github.com/stretchr/testify/mock" + + model "github.com/mattermost/mattermost-server/model" + + plugin "github.com/mattermost/mattermost-server/plugin" +) // Hooks is an autogenerated mock type for the Hooks type type Hooks struct { diff --git a/scripts/diff-config.sh b/scripts/diff-config.sh new file mode 100755 index 0000000000..6eb45c5120 --- /dev/null +++ b/scripts/diff-config.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +jq_cmd=jq +[[ $(type -P "$jq_cmd") ]] || { + echo "'$jq_cmd' command line JSON processor not found"; + echo "Please install on linux with 'sudo apt-get install jq'" + echo "Please install on mac with 'brew install jq'" + exit 1; +} + +if [ -z "$FROM" ] +then + echo "Missing FROM version. Usage: make diff-config FROM=1.1.1 TO=2.2.2" + exit 1 +fi + +if [ -z "$TO" ] +then + echo "Missing TO version. Usage: make diff-config FROM=1.1.1 TO=2.2.2" + exit 1 +fi + +# Returns the config file for a specific release +fetch_config() { + local url="https://releases.mattermost.com/$1/mattermost-$1-linux-amd64.tar.gz" + curl -sf "$url" | tar -xzOf - mattermost/config/config.json | jq -S . +} + +echo Fetching config files +from_config="$(fetch_config "$FROM")" +if [ -z "$from_config" ] +then + echo Invalid version "$FROM" + exit 1 +fi + +to_config=$(fetch_config "$TO") +if [ -z "$to_config" ] +then + echo Invalid version "$TO" + exit 1 +fi + +echo Comparing config files +diff -y <(echo "$from_config") <(echo "$to_config") + +# We ignore exits with 1 since it just means there's a difference, which is fine for us. +diff_exit=$? +if [ $diff_exit -eq 1 ]; then + exit 0 +else + exit $diff_exit +fi diff --git a/services/filesstore/s3store.go b/services/filesstore/s3store.go index 42113c1b43..2e80aab8b2 100644 --- a/services/filesstore/s3store.go +++ b/services/filesstore/s3store.go @@ -12,9 +12,9 @@ import ( "path/filepath" "strings" - s3 "github.com/minio/minio-go" - "github.com/minio/minio-go/pkg/credentials" - "github.com/minio/minio-go/pkg/encrypt" + s3 "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v6/pkg/credentials" + "github.com/minio/minio-go/v6/pkg/encrypt" "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" diff --git a/services/mailservice/mail.go b/services/mailservice/mail.go index 6cff92234d..6384257e4b 100644 --- a/services/mailservice/mail.go +++ b/services/mailservice/mail.go @@ -6,7 +6,6 @@ package mailservice import ( "crypto/tls" "errors" - "fmt" "io" "mime" "net" @@ -139,14 +138,14 @@ func ConnectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { func NewSMTPClientAdvanced(conn net.Conn, hostname string, connectionInfo *SmtpConnectionInfo) (*smtp.Client, *model.AppError) { c, err := smtp.NewClient(conn, connectionInfo.SmtpServerName+":"+connectionInfo.SmtpPort) if err != nil { - mlog.Error(fmt.Sprintf("Failed to open a connection to SMTP server %v", err)) + mlog.Error("Failed to open a connection to SMTP server", mlog.Err(err)) return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error(), http.StatusInternalServerError) } if hostname != "" { err = c.Hello(hostname) if err != nil { - mlog.Error(fmt.Sprintf("Failed to to set the HELO to SMTP server %v", err)) + mlog.Error("Failed to to set the HELO to SMTP server", mlog.Err(err)) return nil, model.NewAppError("SendMail", "utils.mail.connect_smtp.helo.app_error", nil, err.Error(), http.StatusInternalServerError) } } @@ -191,14 +190,14 @@ func TestConnection(config *model.Config) { conn, err1 := ConnectToSMTPServer(config) if err1 != nil { - mlog.Error(fmt.Sprintf("SMTP server settings do not appear to be configured properly err=%v details=%v", utils.T(err1.Message), err1.DetailedError)) + mlog.Error("SMTP server settings do not appear to be configured properly", mlog.Err(err1)) return } defer conn.Close() c, err2 := NewSMTPClient(conn, config) if err2 != nil { - mlog.Error(fmt.Sprintf("SMTP server settings do not appear to be configured properly err=%v details=%v", utils.T(err2.Message), err2.DetailedError)) + mlog.Error("SMTP server settings do not appear to be configured properly", mlog.Err(err2)) return } defer c.Quit() @@ -240,13 +239,13 @@ func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from, replyTo mail.Addre } func SendMail(c smtpClient, mimeTo, smtpTo string, from, replyTo mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, fileBackend filesstore.FileBackend, date time.Time) *model.AppError { - mlog.Debug(fmt.Sprintf("sending mail to %v with subject of '%v'", smtpTo, subject)) + mlog.Debug("sending mail", mlog.String("to", smtpTo), mlog.String("subject", subject)) htmlMessage := "\r\n" + htmlBody + "" txtBody, err := html2text.FromString(htmlBody) if err != nil { - mlog.Warn(fmt.Sprint(err)) + mlog.Warn("Unable to convert html body to text", mlog.Err(err)) txtBody = "" } diff --git a/store/layered_store.go b/store/layered_store.go index 26c8f0c9f2..9d9f3c9a47 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -217,6 +217,10 @@ func (s *LayeredStore) TotalSearchDbConnections() int { return s.DatabaseLayer.TotalSearchDbConnections() } +func (s *LayeredStore) CheckIntegrity() <-chan IntegrityCheckResult { + return s.DatabaseLayer.CheckIntegrity() +} + type LayeredRoleStore struct { *LayeredStore } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 0f00947392..7c4b02253f 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -35,6 +35,9 @@ const ( CHANNEL_GUESTS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE CHANNEL_GUESTS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SIZE = model.CHANNEL_CACHE_SIZE + CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SEC = 1800 // 30 mins + CHANNEL_CACHE_SEC = 900 // 15 mins ) @@ -281,6 +284,7 @@ type publicChannel struct { } var channelMemberCountsCache = utils.NewLru(CHANNEL_MEMBERS_COUNTS_CACHE_SIZE) +var channelPinnedPostCountsCache = utils.NewLru(CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SIZE) var channelGuestCountsCache = utils.NewLru(CHANNEL_GUESTS_COUNTS_CACHE_SIZE) var allChannelMembersForUserCache = utils.NewLru(ALL_CHANNEL_MEMBERS_FOR_USER_CACHE_SIZE) var allChannelMembersNotifyPropsForChannelCache = utils.NewLru(ALL_CHANNEL_MEMBERS_NOTIFY_PROPS_FOR_CHANNEL_CACHE_SIZE) @@ -289,6 +293,7 @@ var channelByNameCache = utils.NewLru(model.CHANNEL_CACHE_SIZE) func (s SqlChannelStore) ClearCaches() { channelMemberCountsCache.Purge() + channelPinnedPostCountsCache.Purge() channelGuestCountsCache.Purge() allChannelMembersForUserCache.Purge() allChannelMembersNotifyPropsForChannelCache.Purge() @@ -297,6 +302,7 @@ func (s SqlChannelStore) ClearCaches() { if s.metrics != nil { s.metrics.IncrementMemCacheInvalidationCounter("Channel Member Counts - Purge") + s.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Purge") s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members for User - Purge") s.metrics.IncrementMemCacheInvalidationCounter("All Channel Members Notify Props for Channel - Purge") s.metrics.IncrementMemCacheInvalidationCounter("Channel - Purge") @@ -1626,6 +1632,66 @@ func (s SqlChannelStore) GetMemberCount(channelId string, allowFromCache bool) ( return count, nil } +func (s SqlChannelStore) InvalidatePinnedPostCount(channelId string) { + channelPinnedPostCountsCache.Remove(channelId) + if s.metrics != nil { + s.metrics.IncrementMemCacheInvalidationCounter("Channel Pinned Post Counts - Remove by ChannelId") + } +} + +func (s SqlChannelStore) GetPinnedPostCountFromCache(channelId string) int64 { + if cacheItem, ok := channelPinnedPostCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Pinned Post Counts") + } + return cacheItem.(int64) + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Pinned Post Counts") + } + + count, err := s.GetPinnedPostCount(channelId, true) + if err != nil { + return 0 + } + + return count +} + +func (s SqlChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + if allowFromCache { + if cacheItem, ok := channelPinnedPostCountsCache.Get(channelId); ok { + if s.metrics != nil { + s.metrics.IncrementMemCacheHitCounter("Channel Pinned Post Counts") + } + return cacheItem.(int64), nil + } + } + + if s.metrics != nil { + s.metrics.IncrementMemCacheMissCounter("Channel Pinned Post Counts") + } + + count, err := s.GetReplica().SelectInt(` + SELECT count(*) + FROM Posts + WHERE + IsPinned = true + AND ChannelId = :ChannelId + AND DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId}) + + if err != nil { + return 0, model.NewAppError("SqlChannelStore.GetPinnedPostCount", "store.sql_channel.get_pinnedpost_count.app_error", nil, "channel_id="+channelId+", "+err.Error(), http.StatusInternalServerError) + } + + if allowFromCache { + channelPinnedPostCountsCache.AddWithExpiresInSecs(channelId, count, CHANNEL_PINNEDPOSTS_COUNTS_CACHE_SEC) + } + + return count, nil +} + func (s SqlChannelStore) InvalidateGuestCount(channelId string) { channelGuestCountsCache.Remove(channelId) if s.metrics != nil { diff --git a/store/sqlstore/integrity.go b/store/sqlstore/integrity.go new file mode 100644 index 0000000000..a09d2ec1c3 --- /dev/null +++ b/store/sqlstore/integrity.go @@ -0,0 +1,514 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "github.com/mattermost/mattermost-server/mlog" + "github.com/mattermost/mattermost-server/store" + + sq "github.com/Masterminds/squirrel" +) + +type relationalCheckConfig struct { + parentName string + parentIdAttr string + childName string + childIdAttr string + canParentIdBeEmpty bool + sortRecords bool +} + +func getOrphanedRecords(ss *SqlSupplier, cfg relationalCheckConfig) ([]store.OrphanedRecord, error) { + var records []store.OrphanedRecord + + sub := ss.getQueryBuilder(). + Select("TRUE"). + From(cfg.parentName). + Prefix("NOT EXISTS ("). + Suffix(")"). + Where(sq.Eq{"id": cfg.childName + "." + cfg.parentIdAttr}) + + main := ss.getQueryBuilder(). + Select(). + Column(cfg.parentIdAttr + " AS ParentId"). + From(cfg.childName). + Where(sub) + + if cfg.childIdAttr != "" { + main = main.Column(cfg.childIdAttr + " AS ChildId") + } + + if cfg.canParentIdBeEmpty { + main = main.Where(sq.NotEq{cfg.parentIdAttr: ""}) + } + + if cfg.sortRecords { + main = main.OrderBy(cfg.parentIdAttr) + } + + query, args, _ := main.ToSql() + _, err := ss.GetMaster().Select(&records, query, args...) + + return records, err +} + +func checkParentChildIntegrity(ss *SqlSupplier, config relationalCheckConfig) store.IntegrityCheckResult { + var result store.IntegrityCheckResult + var data store.RelationalIntegrityCheckData + + config.sortRecords = true + data.Records, result.Err = getOrphanedRecords(ss, config) + if result.Err != nil { + mlog.Error(result.Err.Error()) + return result + } + data.ParentName = config.parentName + data.ChildName = config.childName + data.ParentIdAttr = config.parentIdAttr + data.ChildIdAttr = config.childIdAttr + result.Data = data + + return result +} + +func checkChannelsCommandWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "CommandWebhooks", + childIdAttr: "Id", + }) +} + +func checkChannelsChannelMemberHistoryIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "ChannelMemberHistory", + childIdAttr: "", + }) +} + +func checkChannelsChannelMembersIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "ChannelMembers", + childIdAttr: "", + }) +} + +func checkChannelsIncomingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "IncomingWebhooks", + childIdAttr: "Id", + }) +} + +func checkChannelsOutgoingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "OutgoingWebhooks", + childIdAttr: "Id", + }) +} + +func checkChannelsPostsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Channels", + parentIdAttr: "ChannelId", + childName: "Posts", + childIdAttr: "Id", + }) +} + +func checkCommandsCommandWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Commands", + parentIdAttr: "CommandId", + childName: "CommandWebhooks", + childIdAttr: "Id", + }) +} + +func checkPostsFileInfoIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Posts", + parentIdAttr: "PostId", + childName: "FileInfo", + childIdAttr: "", + }) +} + +func checkPostsPostsParentIdIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Posts", + parentIdAttr: "ParentId", + childName: "Posts", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkPostsPostsRootIdIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Posts", + parentIdAttr: "RootId", + childName: "Posts", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkPostsReactionsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Posts", + parentIdAttr: "PostId", + childName: "Reactions", + childIdAttr: "", + }) +} + +func checkSchemesChannelsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Schemes", + parentIdAttr: "SchemeId", + childName: "Channels", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkSchemesTeamsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Schemes", + parentIdAttr: "SchemeId", + childName: "Teams", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkSessionsAuditsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Sessions", + parentIdAttr: "SessionId", + childName: "Audits", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkTeamsChannelsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "Channels", + childIdAttr: "Id", + }) +} + +func checkTeamsCommandsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "Commands", + childIdAttr: "Id", + }) +} + +func checkTeamsIncomingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "IncomingWebhooks", + childIdAttr: "Id", + }) +} + +func checkTeamsOutgoingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "OutgoingWebhooks", + childIdAttr: "Id", + }) +} + +func checkTeamsTeamMembersIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Teams", + parentIdAttr: "TeamId", + childName: "TeamMembers", + childIdAttr: "", + }) +} + +func checkUsersAuditsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Audits", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkUsersCommandWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "CommandWebhooks", + childIdAttr: "Id", + }) +} + +func checkUsersChannelMemberHistoryIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "ChannelMemberHistory", + childIdAttr: "", + }) +} + +func checkUsersChannelMembersIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "ChannelMembers", + childIdAttr: "", + }) +} + +func checkUsersChannelsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "CreatorId", + childName: "Channels", + childIdAttr: "Id", + canParentIdBeEmpty: true, + }) +} + +func checkUsersCommandsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "CreatorId", + childName: "Commands", + childIdAttr: "Id", + }) +} + +func checkUsersCompliancesIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Compliances", + childIdAttr: "Id", + }) +} + +func checkUsersEmojiIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "CreatorId", + childName: "Emoji", + childIdAttr: "Id", + }) +} + +func checkUsersFileInfoIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Posts", + parentIdAttr: "CreatorId", + childName: "FileInfo", + childIdAttr: "", + }) +} + +func checkUsersIncomingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "IncomingWebhooks", + childIdAttr: "Id", + }) +} + +func checkUsersOAuthAccessDataIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "OAuthAccessData", + childIdAttr: "Token", + }) +} + +func checkUsersOAuthAppsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "CreatorId", + childName: "OAuthApps", + childIdAttr: "Id", + }) +} + +func checkUsersOAuthAuthDataIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "OAuthAuthData", + childIdAttr: "Code", + }) +} + +func checkUsersOutgoingWebhooksIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "CreatorId", + childName: "OutgoingWebhooks", + childIdAttr: "Id", + }) +} + +func checkUsersPostsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Posts", + childIdAttr: "Id", + }) +} + +func checkUsersPreferencesIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Preferences", + childIdAttr: "", + }) +} + +func checkUsersReactionsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Reactions", + childIdAttr: "", + }) +} + +func checkUsersSessionsIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Sessions", + childIdAttr: "Id", + }) +} + +func checkUsersStatusIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "Status", + childIdAttr: "", + }) +} + +func checkUsersTeamMembersIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "TeamMembers", + childIdAttr: "", + }) +} + +func checkUsersUserAccessTokensIntegrity(ss *SqlSupplier) store.IntegrityCheckResult { + return checkParentChildIntegrity(ss, relationalCheckConfig{ + parentName: "Users", + parentIdAttr: "UserId", + childName: "UserAccessTokens", + childIdAttr: "Id", + }) +} + +func checkChannelsIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkChannelsCommandWebhooksIntegrity(ss) + results <- checkChannelsChannelMemberHistoryIntegrity(ss) + results <- checkChannelsChannelMembersIntegrity(ss) + results <- checkChannelsIncomingWebhooksIntegrity(ss) + results <- checkChannelsOutgoingWebhooksIntegrity(ss) + results <- checkChannelsPostsIntegrity(ss) +} + +func checkCommandsIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkCommandsCommandWebhooksIntegrity(ss) +} + +func checkPostsIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkPostsFileInfoIntegrity(ss) + results <- checkPostsPostsParentIdIntegrity(ss) + results <- checkPostsPostsRootIdIntegrity(ss) + results <- checkPostsReactionsIntegrity(ss) +} + +func checkSchemesIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkSchemesChannelsIntegrity(ss) + results <- checkSchemesTeamsIntegrity(ss) +} + +func checkSessionsIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkSessionsAuditsIntegrity(ss) +} + +func checkTeamsIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkTeamsChannelsIntegrity(ss) + results <- checkTeamsCommandsIntegrity(ss) + results <- checkTeamsIncomingWebhooksIntegrity(ss) + results <- checkTeamsOutgoingWebhooksIntegrity(ss) + results <- checkTeamsTeamMembersIntegrity(ss) +} + +func checkUsersIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + results <- checkUsersAuditsIntegrity(ss) + results <- checkUsersCommandWebhooksIntegrity(ss) + results <- checkUsersChannelMemberHistoryIntegrity(ss) + results <- checkUsersChannelMembersIntegrity(ss) + results <- checkUsersChannelsIntegrity(ss) + results <- checkUsersCommandsIntegrity(ss) + results <- checkUsersCompliancesIntegrity(ss) + results <- checkUsersEmojiIntegrity(ss) + results <- checkUsersFileInfoIntegrity(ss) + results <- checkUsersIncomingWebhooksIntegrity(ss) + results <- checkUsersOAuthAccessDataIntegrity(ss) + results <- checkUsersOAuthAppsIntegrity(ss) + results <- checkUsersOAuthAuthDataIntegrity(ss) + results <- checkUsersOutgoingWebhooksIntegrity(ss) + results <- checkUsersPostsIntegrity(ss) + results <- checkUsersPreferencesIntegrity(ss) + results <- checkUsersReactionsIntegrity(ss) + results <- checkUsersSessionsIntegrity(ss) + results <- checkUsersStatusIntegrity(ss) + results <- checkUsersTeamMembersIntegrity(ss) + results <- checkUsersUserAccessTokensIntegrity(ss) +} + +func CheckRelationalIntegrity(ss *SqlSupplier, results chan<- store.IntegrityCheckResult) { + mlog.Info("Starting relational integrity checks...") + checkChannelsIntegrity(ss, results) + checkCommandsIntegrity(ss, results) + checkPostsIntegrity(ss, results) + checkSchemesIntegrity(ss, results) + checkSessionsIntegrity(ss, results) + checkTeamsIntegrity(ss, results) + checkUsersIntegrity(ss, results) + mlog.Info("Done with relational integrity checks") + close(results) +} diff --git a/store/sqlstore/integrity_test.go b/store/sqlstore/integrity_test.go new file mode 100644 index 0000000000..02125a4cee --- /dev/null +++ b/store/sqlstore/integrity_test.go @@ -0,0 +1,1539 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package sqlstore + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/store" +) + +func createAudit(ss store.Store, userId, sessionId string) *model.Audit { + audit := model.Audit{ + UserId: userId, + SessionId: sessionId, + IpAddress: "ipaddress", + Action: "Action", + } + ss.Audit().Save(&audit) + return &audit +} + +func createChannel(ss store.Store, teamId, creatorId string) *model.Channel { + m := model.Channel{} + m.TeamId = teamId + m.CreatorId = creatorId + m.DisplayName = "Name" + m.Name = "zz" + model.NewId() + "b" + m.Type = model.CHANNEL_OPEN + c, _ := ss.Channel().Save(&m, -1) + return c +} + +func createChannelWithSchemeId(ss store.Store, schemeId *string) *model.Channel { + m := model.Channel{} + m.SchemeId = schemeId + m.TeamId = model.NewId() + m.CreatorId = model.NewId() + m.DisplayName = "Name" + m.Name = "zz" + model.NewId() + "b" + m.Type = model.CHANNEL_OPEN + c, _ := ss.Channel().Save(&m, -1) + return c +} + +func createCommand(ss store.Store, userId, teamId string) * model.Command { + m := model.Command{} + m.CreatorId = userId + m.Method = model.COMMAND_METHOD_POST + m.TeamId = teamId + m.URL = "http://nowhere.com/" + m.Trigger = "trigger" + cmd, _ := ss.Command().Save(&m) + return cmd +} + +func createChannelMember(ss store.Store, channelId, userId string) *model.ChannelMember { + m := model.ChannelMember{} + m.ChannelId = channelId + m.UserId = userId + m.NotifyProps = model.GetDefaultChannelNotifyProps() + cm, _ := ss.Channel().SaveMember(&m) + return cm +} + +func createChannelMemberHistory(ss store.Store, channelId, userId string) *model.ChannelMemberHistory { + m := model.ChannelMemberHistory{} + m.ChannelId = channelId + m.UserId = userId + ss.ChannelMemberHistory().LogJoinEvent(userId, channelId, model.GetMillis()) + return &m +} + +func createChannelWithTeamId(ss store.Store, id string) *model.Channel { + return createChannel(ss, id, model.NewId()); +} + +func createChannelWithCreatorId(ss store.Store, id string) *model.Channel { + return createChannel(ss, model.NewId(), id); +} + +func createChannelMemberWithChannelId(ss store.Store, id string) *model.ChannelMember { + return createChannelMember(ss, id, model.NewId()); +} + +func createChannelMemberWithUserId(ss store.Store, id string) *model.ChannelMember { + return createChannelMember(ss, model.NewId(), id); +} + +func createCommandWebhook(ss store.Store, commandId, userId, channelId string) *model.CommandWebhook { + m := model.CommandWebhook{} + m.CommandId = commandId + m.UserId = userId + m.ChannelId = channelId + cwh, _ := ss.CommandWebhook().Save(&m) + return cwh +} + +func createCompliance(ss store.Store, userId string) *model.Compliance { + m := model.Compliance{} + m.UserId = userId + m.Desc = "Audit" + m.Status = model.COMPLIANCE_STATUS_FAILED + m.StartAt = model.GetMillis() - 1 + m.EndAt = model.GetMillis() + 1 + m.Type = model.COMPLIANCE_TYPE_ADHOC + c, _ := ss.Compliance().Save(&m) + return c +} + +func createEmoji(ss store.Store, userId string) *model.Emoji { + m := model.Emoji{} + m.CreatorId = userId + m.Name = "emoji" + emoji, _ := ss.Emoji().Save(&m) + return emoji +} + +func createFileInfo(ss store.Store, postId, userId string) *model.FileInfo { + m := model.FileInfo{} + m.PostId = postId + m.CreatorId = userId + m.Path = "some/path/to/file" + info, _ := ss.FileInfo().Save(&m) + return info +} + +func createIncomingWebhook(ss store.Store, userId, channelId, teamId string) *model.IncomingWebhook { + m := model.IncomingWebhook{} + m.UserId = userId + m.ChannelId = channelId + m.TeamId = teamId + wh, _ := ss.Webhook().SaveIncoming(&m) + return wh +} + +func createOAuthAccessData(ss store.Store, userId string) *model.AccessData { + m := model.AccessData{} + m.ClientId = model.NewId() + m.UserId = userId + m.Token = model.NewId() + m.RefreshToken = model.NewId() + m.RedirectUri = "http://example.com" + ad, _ := ss.OAuth().SaveAccessData(&m) + return ad +} + +func createOAuthApp(ss store.Store, userId string) *model.OAuthApp { + m := model.OAuthApp{} + m.CreatorId = userId + m.CallbackUrls = []string{"https://nowhere.com"} + m.Homepage = "https://nowhere.com" + m.Id = "" + m.Name = "TestApp" + model.NewId() + app, _ := ss.OAuth().SaveApp(&m) + return app +} + +func createOAuthAuthData(ss store.Store, userId string) *model.AuthData { + m := model.AuthData{} + m.ClientId = model.NewId() + m.UserId = userId + m.Code = model.NewId() + m.RedirectUri = "http://example.com" + ad, _ := ss.OAuth().SaveAuthData(&m) + return ad +} + +func createOutgoingWebhook(ss store.Store, userId, channelId, teamId string) *model.OutgoingWebhook { + m := model.OutgoingWebhook{} + m.CreatorId = userId + m.ChannelId = channelId + m.TeamId = teamId + m.Token = model.NewId() + m.CallbackURLs = []string{"http://nowhere.com/"} + wh, _ := ss.Webhook().SaveOutgoing(&m) + return wh +} + +func createPost(ss store.Store, channelId, userId, rootId, parentId string) *model.Post { + m := model.Post{} + m.ChannelId = channelId + m.UserId = userId + m.RootId = rootId + m.ParentId = parentId + m.Message = "zz" + model.NewId() + "b" + p, _ := ss.Post().Save(&m) + return p +} + +func createPostWithChannelId(ss store.Store, id string) *model.Post { + return createPost(ss, id, model.NewId(), "", ""); +} + +func createPostWithUserId(ss store.Store, id string) *model.Post { + return createPost(ss, model.NewId(), id, "", ""); +} + +func createPreferences(ss store.Store, userId string) *model.Preferences { + preferences := model.Preferences{ + { + UserId: userId, + Name: model.NewId(), + Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, + Value: "somevalue", + }, + } + ss.Preference().Save(&preferences) + return &preferences +} + +func createReaction(ss store.Store, userId, postId string) *model.Reaction { + reaction := &model.Reaction{ + UserId: userId, + PostId: postId, + EmojiName: model.NewId(), + } + reaction, _ = ss.Reaction().Save(reaction) + return reaction +} + +func createDefaultRoles(ss store.Store) { + ss.Role().Save(&model.Role{ + Name: model.TEAM_ADMIN_ROLE_ID, + DisplayName: model.TEAM_ADMIN_ROLE_ID, + Permissions: []string{ + model.PERMISSION_DELETE_OTHERS_POSTS.Id, + }, + }) + + ss.Role().Save(&model.Role{ + Name: model.TEAM_USER_ROLE_ID, + DisplayName: model.TEAM_USER_ROLE_ID, + Permissions: []string{ + model.PERMISSION_VIEW_TEAM.Id, + model.PERMISSION_ADD_USER_TO_TEAM.Id, + }, + }) + + ss.Role().Save(&model.Role{ + Name: model.TEAM_GUEST_ROLE_ID, + DisplayName: model.TEAM_GUEST_ROLE_ID, + Permissions: []string{ + model.PERMISSION_VIEW_TEAM.Id, + }, + }) + + ss.Role().Save(&model.Role{ + Name: model.CHANNEL_ADMIN_ROLE_ID, + DisplayName: model.CHANNEL_ADMIN_ROLE_ID, + Permissions: []string{ + model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS.Id, + model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, + }, + }) + + ss.Role().Save(&model.Role{ + Name: model.CHANNEL_USER_ROLE_ID, + DisplayName: model.CHANNEL_USER_ROLE_ID, + Permissions: []string{ + model.PERMISSION_READ_CHANNEL.Id, + model.PERMISSION_CREATE_POST.Id, + }, + }) + + ss.Role().Save(&model.Role{ + Name: model.CHANNEL_GUEST_ROLE_ID, + DisplayName: model.CHANNEL_GUEST_ROLE_ID, + Permissions: []string{ + model.PERMISSION_READ_CHANNEL.Id, + model.PERMISSION_CREATE_POST.Id, + }, + }) +} + +func createScheme(ss store.Store) *model.Scheme { + m := model.Scheme{} + m.DisplayName = model.NewId() + m.Name = model.NewId() + m.Description = model.NewId() + m.Scope = model.SCHEME_SCOPE_CHANNEL + s, _ := ss.Scheme().Save(&m) + return s +} + +func createSession(ss store.Store, userId string) *model.Session { + m := model.Session{} + m.UserId = userId + s, _ := ss.Session().Save(&m) + return s +} + +func createStatus(ss store.Store, userId string) *model.Status { + m := model.Status{} + m.UserId = userId + m.Status = model.STATUS_ONLINE + ss.Status().SaveOrUpdate(&m) + return &m +} + +func createTeam(ss store.Store, userId string) *model.Team { + m := model.Team{} + m.DisplayName = "DisplayName" + m.Type = model.TEAM_OPEN + m.Email = "test@example.com" + m.Name = "z-z-z" + model.NewId() + "b" + t, _ := ss.Team().Save(&m) + return t +} + +func createTeamMember(ss store.Store, teamId, userId string) *model.TeamMember { + m := model.TeamMember{} + m.TeamId = teamId + m.UserId = userId + tm, _ := ss.Team().SaveMember(&m, -1) + return tm +} + +func createTeamWithSchemeId(ss store.Store, schemeId *string) *model.Team { + m := model.Team{} + m.SchemeId = schemeId + m.DisplayName = "DisplayName" + m.Type = model.TEAM_OPEN + m.Email = "test@example.com" + m.Name = "z-z-z" + model.NewId() + "b" + t, _ := ss.Team().Save(&m) + return t +} + +func createUser(ss store.Store) *model.User { + m := model.User{} + m.Username = model.NewId() + m.Email = "test@example.com" + user, _ := ss.User().Save(&m) + return user +} + +func createUserAccessToken(ss store.Store, userId string) *model.UserAccessToken { + m := model.UserAccessToken{} + m.UserId = userId + m.Token = model.NewId() + uat, _ := ss.UserAccessToken().Save(&m) + return uat +} + +func TestCheckIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + ss.DropAllTables() + t.Run("generate reports with no records", func(t *testing.T) { + results := ss.CheckIntegrity() + require.NotNil(t, results) + for result := range results { + require.IsType(t, store.IntegrityCheckResult{}, result) + require.Nil(t, result.Err) + switch data := result.Data.(type) { + case store.RelationalIntegrityCheckData: + require.Len(t, data.Records, 0) + } + } + }) + }) +} + +func TestCheckParentChildIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + t.Run("should receive an error", func(t *testing.T) { + config := relationalCheckConfig{ + parentName: "NotValid", + parentIdAttr: "NotValid", + childName: "NotValid", + childIdAttr: "NotValid", + } + result := checkParentChildIntegrity(supplier, config) + require.NotNil(t, result.Err) + require.Empty(t, result.Data) + }) + }) +} + +func TestCheckChannelsCommandWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channelId := model.NewId() + cwh := createCommandWebhook(ss, model.NewId(), model.NewId(), channelId) + result := checkChannelsCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: channelId, + ChildId: cwh.Id, + }, data.Records[0]) + dbmap.Delete(cwh) + }) + }) +} + +func TestCheckChannelsChannelMemberHistoryIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsChannelMemberHistoryIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channel := createChannel(ss, model.NewId(), model.NewId()) + user := createUser(ss) + cmh := createChannelMemberHistory(ss, channel.Id, user.Id) + dbmap.Delete(channel) + result := checkChannelsChannelMemberHistoryIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: cmh.ChannelId, + }, data.Records[0]) + dbmap.Delete(user) + dbmap.Exec(`DELETE FROM ChannelMemberHistory`) + }) + }) +} + +func TestCheckChannelsChannelMembersIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsChannelMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channel := createChannel(ss, model.NewId(), model.NewId()) + member := createChannelMemberWithChannelId(ss, channel.Id) + dbmap.Delete(channel) + result := checkChannelsChannelMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: member.ChannelId, + }, data.Records[0]) + ss.Channel().PermanentDeleteMembersByChannel(member.ChannelId) + }) + }) +} + +func TestCheckChannelsIncomingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channelId := model.NewId() + wh := createIncomingWebhook(ss, model.NewId(), channelId, model.NewId()) + result := checkChannelsIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: channelId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckChannelsOutgoingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channel := createChannel(ss, model.NewId(), model.NewId()) + channelId := channel.Id + wh := createOutgoingWebhook(ss, model.NewId(), channelId, model.NewId()) + dbmap.Delete(channel) + result := checkChannelsOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: channelId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckChannelsPostsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkChannelsPostsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + post := createPostWithChannelId(ss, model.NewId()) + result := checkChannelsPostsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: post.ChannelId, + ChildId: post.Id, + }, data.Records[0]) + dbmap.Delete(post) + }) + }) +} + +func TestCheckCommandsCommandWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkCommandsCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + commandId := model.NewId() + cwh := createCommandWebhook(ss, commandId, model.NewId(), model.NewId()) + result := checkCommandsCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: commandId, + ChildId: cwh.Id, + }, data.Records[0]) + dbmap.Delete(cwh) + }) + }) +} + +func TestCheckPostsFileInfoIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkPostsFileInfoIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + postId := model.NewId() + info := createFileInfo(ss, postId, model.NewId()) + result := checkPostsFileInfoIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: postId, + }, data.Records[0]) + dbmap.Delete(info) + }) + }) +} + +func TestCheckPostsPostsParentIdIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkPostsPostsParentIdIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + root := createPost(ss, model.NewId(), model.NewId(), "", "") + parent := createPost(ss, model.NewId(), model.NewId(), root.Id, root.Id) + parentId := parent.Id + post := createPost(ss, model.NewId(), model.NewId(), root.Id, parent.Id) + dbmap.Delete(parent) + result := checkPostsPostsParentIdIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: parentId, + ChildId: post.Id, + }, data.Records[0]) + dbmap.Delete(root) + dbmap.Delete(post) + }) + }) +} + +func TestCheckPostsPostsRootIdIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkPostsPostsRootIdIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + root := createPost(ss, model.NewId(), model.NewId(), "", "") + rootId := root.Id + post := createPost(ss, model.NewId(), model.NewId(), root.Id, root.Id) + dbmap.Delete(root) + result := checkPostsPostsRootIdIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: rootId, + ChildId: post.Id, + }, data.Records[0]) + dbmap.Delete(post) + }) + }) +} + +func TestCheckPostsReactionsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkPostsReactionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + postId := model.NewId() + reaction := createReaction(ss, model.NewId(), postId) + result := checkPostsReactionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: postId, + }, data.Records[0]) + dbmap.Delete(reaction) + }) + }) +} + +func TestCheckSchemesChannelsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkSchemesChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + createDefaultRoles(ss) + scheme := createScheme(ss) + schemeId := scheme.Id + channel := createChannelWithSchemeId(ss, &schemeId) + dbmap.Delete(scheme) + result := checkSchemesChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: schemeId, + ChildId: channel.Id, + }, data.Records[0]) + dbmap.Delete(channel) + }) + }) +} + +func TestCheckSchemesTeamsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkSchemesTeamsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + createDefaultRoles(ss) + scheme := createScheme(ss) + schemeId := scheme.Id + team := createTeamWithSchemeId(ss, &schemeId) + dbmap.Delete(scheme) + result := checkSchemesTeamsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: schemeId, + ChildId: team.Id, + }, data.Records[0]) + dbmap.Delete(team) + }) + }) +} + +func TestCheckSessionsAuditsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkSessionsAuditsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + session := createSession(ss, model.NewId()) + sessionId := session.Id + audit := createAudit(ss, userId, sessionId) + dbmap.Delete(session) + result := checkSessionsAuditsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: sessionId, + ChildId: audit.Id, + }, data.Records[0]) + ss.Audit().PermanentDeleteByUser(userId) + }) + }) +} + +func TestCheckTeamsChannelsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkTeamsChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channel := createChannelWithTeamId(ss, model.NewId()) + result := checkTeamsChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: channel.TeamId, + ChildId: channel.Id, + }, data.Records[0]) + dbmap.Delete(channel) + }) + }) +} + +func TestCheckTeamsCommandsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkTeamsCommandsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + teamId := model.NewId() + cmd := createCommand(ss, model.NewId(), teamId) + result := checkTeamsCommandsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: teamId, + ChildId: cmd.Id, + }, data.Records[0]) + dbmap.Delete(cmd) + }) + }) +} + +func TestCheckTeamsIncomingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkTeamsIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + teamId := model.NewId() + wh := createIncomingWebhook(ss, model.NewId(), model.NewId(), teamId) + result := checkTeamsIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: teamId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckTeamsOutgoingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkTeamsOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + teamId := model.NewId() + wh := createOutgoingWebhook(ss, model.NewId(), model.NewId(), teamId) + result := checkTeamsOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: teamId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckTeamsTeamMembersIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkTeamsTeamMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + team := createTeam(ss, model.NewId()) + member := createTeamMember(ss, team.Id, model.NewId()) + dbmap.Delete(team) + result := checkTeamsTeamMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: team.Id, + }, data.Records[0]) + ss.Team().RemoveAllMembersByTeam(member.TeamId) + }) + }) +} + +func TestCheckUsersAuditsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersAuditsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + audit := createAudit(ss, userId, model.NewId()) + dbmap.Delete(user) + result := checkUsersAuditsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: audit.Id, + }, data.Records[0]) + ss.Audit().PermanentDeleteByUser(userId) + }) + }) +} + +func TestCheckUsersCommandWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + cwh := createCommandWebhook(ss, model.NewId(), userId, model.NewId()) + result := checkUsersCommandWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: cwh.Id, + }, data.Records[0]) + dbmap.Delete(cwh) + }) + }) +} + +func TestCheckUsersChannelsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + channel := createChannelWithCreatorId(ss, model.NewId()) + result := checkUsersChannelsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: channel.CreatorId, + ChildId: channel.Id, + }, data.Records[0]) + dbmap.Delete(channel) + }) + }) +} + +func TestCheckUsersChannelMemberHistoryIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersChannelMemberHistoryIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + channel := createChannel(ss, model.NewId(), model.NewId()) + cmh := createChannelMemberHistory(ss, channel.Id, user.Id) + dbmap.Delete(user) + result := checkUsersChannelMemberHistoryIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: cmh.UserId, + }, data.Records[0]) + dbmap.Delete(channel) + dbmap.Exec(`DELETE FROM ChannelMemberHistory`) + }) + }) +} + +func TestCheckUsersChannelMembersIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersChannelMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + channel := createChannelWithCreatorId(ss, user.Id) + member := createChannelMember(ss, channel.Id, user.Id) + dbmap.Delete(user) + result := checkUsersChannelMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: member.UserId, + }, data.Records[0]) + dbmap.Delete(channel) + ss.Channel().PermanentDeleteMembersByUser(member.UserId) + }) + }) +} + +func TestCheckUsersCommandsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersCommandsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + cmd := createCommand(ss, userId, model.NewId()) + result := checkUsersCommandsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: cmd.Id, + }, data.Records[0]) + dbmap.Delete(cmd) + }) + }) +} + +func TestCheckUsersCompliancesIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersCompliancesIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + compliance := createCompliance(ss, userId) + dbmap.Delete(user) + result := checkUsersCompliancesIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: compliance.Id, + }, data.Records[0]) + dbmap.Delete(compliance) + }) + }) +} + +func TestCheckUsersEmojiIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersEmojiIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + emoji := createEmoji(ss, userId) + dbmap.Delete(user) + result := checkUsersEmojiIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: emoji.Id, + }, data.Records[0]) + dbmap.Delete(emoji) + }) + }) +} + +func TestCheckUsersFileInfoIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersFileInfoIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + info := createFileInfo(ss, model.NewId(), userId) + dbmap.Delete(user) + result := checkUsersFileInfoIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + }, data.Records[0]) + dbmap.Delete(info) + }) + }) +} + +func TestCheckUsersIncomingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + wh := createIncomingWebhook(ss, userId, model.NewId(), model.NewId()) + result := checkUsersIncomingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckUsersOAuthAccessDataIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersOAuthAccessDataIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + ad := createOAuthAccessData(ss, userId) + dbmap.Delete(user) + result := checkUsersOAuthAccessDataIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: ad.Token, + }, data.Records[0]) + ss.OAuth().RemoveAccessData(ad.Token) + }) + }) +} + +func TestCheckUsersOAuthAppsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersOAuthAppsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + app := createOAuthApp(ss, userId) + dbmap.Delete(user) + result := checkUsersOAuthAppsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: app.Id, + }, data.Records[0]) + ss.OAuth().DeleteApp(app.Id) + }) + }) +} + +func TestCheckUsersOAuthAuthDataIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersOAuthAuthDataIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + ad := createOAuthAuthData(ss, userId) + dbmap.Delete(user) + result := checkUsersOAuthAuthDataIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: ad.Code, + }, data.Records[0]) + ss.OAuth().RemoveAuthData(ad.Code) + }) + }) +} + +func TestCheckUsersOutgoingWebhooksIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + wh := createOutgoingWebhook(ss, userId, model.NewId(), model.NewId()) + result := checkUsersOutgoingWebhooksIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: wh.Id, + }, data.Records[0]) + dbmap.Delete(wh) + }) + }) +} + +func TestCheckUsersPostsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersPostsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + post := createPostWithUserId(ss, model.NewId()) + result := checkUsersPostsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: post.UserId, + ChildId: post.Id, + }, data.Records[0]) + dbmap.Delete(post) + }) + }) +} + +func TestCheckUsersPreferencesIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersPreferencesIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + preferences := createPreferences(ss, userId) + dbmap.Delete(user) + result := checkUsersPreferencesIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + }, data.Records[0]) + dbmap.Delete(preferences) + }) + }) +} + +func TestCheckUsersReactionsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersReactionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + reaction := createReaction(ss, user.Id, model.NewId()) + dbmap.Delete(user) + result := checkUsersReactionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + }, data.Records[0]) + dbmap.Delete(reaction) + }) + }) +} + +func TestCheckUsersSessionsIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersSessionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + userId := model.NewId() + session := createSession(ss, userId) + result := checkUsersSessionsIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: session.Id, + }, data.Records[0]) + dbmap.Delete(session) + }) + }) +} + +func TestCheckUsersStatusIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersStatusIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + status := createStatus(ss, user.Id) + dbmap.Delete(user) + result := checkUsersStatusIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + }, data.Records[0]) + dbmap.Delete(status) + }) + }) +} + +func TestCheckUsersTeamMembersIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersTeamMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + team := createTeam(ss, user.Id) + member := createTeamMember(ss, team.Id, user.Id) + dbmap.Delete(user) + result := checkUsersTeamMembersIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: member.UserId, + }, data.Records[0]) + ss.Team().RemoveAllMembersByTeam(member.TeamId) + dbmap.Delete(team) + }) + }) +} + +func TestCheckUsersUserAccessTokensIntegrity(t *testing.T) { + StoreTest(t, func(t *testing.T, ss store.Store) { + supplier := ss.(*store.LayeredStore).DatabaseLayer.(*SqlSupplier) + dbmap := supplier.GetMaster() + + t.Run("should generate a report with no records", func(t *testing.T) { + result := checkUsersUserAccessTokensIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 0) + }) + + t.Run("should generate a report with one record", func(t *testing.T) { + user := createUser(ss) + userId := user.Id + uat := createUserAccessToken(ss, user.Id) + dbmap.Delete(user) + result := checkUsersUserAccessTokensIntegrity(supplier) + require.Nil(t, result.Err) + data := result.Data.(store.RelationalIntegrityCheckData) + require.Len(t, data.Records, 1) + require.Equal(t, store.OrphanedRecord{ + ParentId: userId, + ChildId: uat.Id, + }, data.Records[0]) + ss.UserAccessToken().Delete(uat.Id) + }) + }) +} diff --git a/store/sqlstore/session_store.go b/store/sqlstore/session_store.go index 222ef4ba5e..c8c58b5546 100644 --- a/store/sqlstore/session_store.go +++ b/store/sqlstore/session_store.go @@ -4,7 +4,6 @@ package sqlstore import ( - "fmt" "net/http" "time" @@ -251,13 +250,13 @@ func (me SqlSessionStore) Cleanup(expiryTime int64, batchSize int64) { for rowsAffected > 0 { if sqlResult, err := me.GetMaster().Exec(query, map[string]interface{}{"ExpiresAt": expiryTime, "Limit": batchSize}); err != nil { - mlog.Error(fmt.Sprintf("Unable to cleanup session store. err=%v", err.Error())) + mlog.Error("Unable to cleanup session store.", mlog.Err(err)) return } else { var rowErr error rowsAffected, rowErr = sqlResult.RowsAffected() if rowErr != nil { - mlog.Error(fmt.Sprintf("Unable to cleanup session store. err=%v", err.Error())) + mlog.Error("Unable to cleanup session store.", mlog.Err(err)) return } } diff --git a/store/sqlstore/supplier.go b/store/sqlstore/supplier.go index 675b0029c9..d6a5cf9f83 100644 --- a/store/sqlstore/supplier.go +++ b/store/sqlstore/supplier.go @@ -159,14 +159,14 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter err := supplier.GetMaster().CreateTablesIfNotExists() if err != nil { - mlog.Critical(fmt.Sprintf("Error creating database tables: %v", err)) + mlog.Critical("Error creating database tables.", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_TABLE) } err = UpgradeDatabase(supplier, model.CurrentVersion) if err != nil { - mlog.Critical("Failed to upgrade database", mlog.Err(err)) + mlog.Critical("Failed to upgrade database.", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_GENERIC_FAILURE) } @@ -214,13 +214,13 @@ func (s *SqlSupplier) Next() store.LayeredStoreSupplier { func setupConnection(con_type string, dataSource string, settings *model.SqlSettings) *gorp.DbMap { db, err := dbsql.Open(*settings.DriverName, dataSource) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to open SQL connection to err:%v", err.Error())) + mlog.Critical("Failed to open SQL connection to err.", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_DB_OPEN) } for i := 0; i < DB_PING_ATTEMPTS; i++ { - mlog.Info(fmt.Sprintf("Pinging SQL %v database", con_type)) + mlog.Info("Pinging SQL", mlog.String("database", con_type)) ctx, cancel := context.WithTimeout(context.Background(), DB_PING_TIMEOUT_SECS*time.Second) defer cancel() err = db.PingContext(ctx) @@ -228,11 +228,11 @@ func setupConnection(con_type string, dataSource string, settings *model.SqlSett break } else { if i == DB_PING_ATTEMPTS-1 { - mlog.Critical(fmt.Sprintf("Failed to ping DB, server will exit err=%v", err)) + mlog.Critical("Failed to ping DB, server will exit.", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_PING) } else { - mlog.Error(fmt.Sprintf("Failed to ping DB retrying in %v seconds err=%v", DB_PING_TIMEOUT_SECS, err)) + mlog.Error("Failed to ping DB", mlog.Err(err), mlog.Int("retrying in seconds", DB_PING_TIMEOUT_SECS)) time.Sleep(DB_PING_TIMEOUT_SECS * time.Second) } } @@ -365,7 +365,7 @@ func (ss *SqlSupplier) DoesTableExist(tableName string) bool { ) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if table exists %v", err)) + mlog.Critical("Failed to check if table exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_TABLE_EXISTS) } @@ -387,7 +387,7 @@ func (ss *SqlSupplier) DoesTableExist(tableName string) bool { ) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if table exists %v", err)) + mlog.Critical("Failed to check if table exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_TABLE_EXISTS_MYSQL) } @@ -401,7 +401,7 @@ func (ss *SqlSupplier) DoesTableExist(tableName string) bool { ) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if table exists %v", err)) + mlog.Critical("Failed to check if table exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_TABLE_EXISTS_SQLITE) } @@ -433,7 +433,7 @@ func (ss *SqlSupplier) DoesColumnExist(tableName string, columnName string) bool return false } - mlog.Critical(fmt.Sprintf("Failed to check if column exists %v", err)) + mlog.Critical("Failed to check if column exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_DOES_COLUMN_EXISTS_POSTGRES) } @@ -456,7 +456,7 @@ func (ss *SqlSupplier) DoesColumnExist(tableName string, columnName string) bool ) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if column exists %v", err)) + mlog.Critical("Failed to check if column exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_DOES_COLUMN_EXISTS_MYSQL) } @@ -471,7 +471,7 @@ func (ss *SqlSupplier) DoesColumnExist(tableName string, columnName string) bool ) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if column exists %v", err)) + mlog.Critical("Failed to check if column exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_DOES_COLUMN_EXISTS_SQLITE) } @@ -498,7 +498,7 @@ func (ss *SqlSupplier) DoesTriggerExist(triggerName string) bool { `, triggerName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if trigger exists %v", err)) + mlog.Critical("Failed to check if trigger exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_GENERIC_FAILURE) } @@ -517,7 +517,7 @@ func (ss *SqlSupplier) DoesTriggerExist(triggerName string) bool { `, triggerName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check if trigger exists %v", err)) + mlog.Critical("Failed to check if trigger exists", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_GENERIC_FAILURE) } @@ -541,7 +541,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExists(tableName string, columnName stri if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType + " DEFAULT '" + defaultValue + "'") if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) + mlog.Critical("Failed to create column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_POSTGRES) } @@ -551,7 +551,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExists(tableName string, columnName stri } else if ss.DriverName() == model.DATABASE_DRIVER_MYSQL { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType + " DEFAULT '" + defaultValue + "'") if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) + mlog.Critical("Failed to create column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_MYSQL) } @@ -575,7 +575,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, column if ss.DriverName() == model.DATABASE_DRIVER_POSTGRES { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + postgresColType) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) + mlog.Critical("Failed to create column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_POSTGRES) } @@ -585,7 +585,7 @@ func (ss *SqlSupplier) CreateColumnIfNotExistsNoDefault(tableName string, column } else if ss.DriverName() == model.DATABASE_DRIVER_MYSQL { _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " ADD " + columnName + " " + mySqlColType) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create column %v", err)) + mlog.Critical("Failed to create column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_COLUMN_MYSQL) } @@ -608,7 +608,7 @@ func (ss *SqlSupplier) RemoveColumnIfExists(tableName string, columnName string) _, err := ss.GetMaster().ExecNoTimeout("ALTER TABLE " + tableName + " DROP COLUMN " + columnName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to drop column %v", err)) + mlog.Critical("Failed to drop column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_COLUMN) } @@ -623,7 +623,7 @@ func (ss *SqlSupplier) RemoveTableIfExists(tableName string) bool { _, err := ss.GetMaster().ExecNoTimeout("DROP TABLE " + tableName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to drop table %v", err)) + mlog.Critical("Failed to drop table", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_TABLE) } @@ -644,7 +644,7 @@ func (ss *SqlSupplier) RenameColumnIfExists(tableName string, oldColumnName stri } if err != nil { - mlog.Critical(fmt.Sprintf("Failed to rename column %v", err)) + mlog.Critical("Failed to rename column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_RENAME_COLUMN) } @@ -666,7 +666,7 @@ func (ss *SqlSupplier) GetMaxLengthOfColumnIfExists(tableName string, columnName } if err != nil { - mlog.Critical(fmt.Sprintf("Failed to get max length of column %v", err)) + mlog.Critical("Failed to get max length of column", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_MAX_COLUMN) } @@ -687,7 +687,7 @@ func (ss *SqlSupplier) AlterColumnTypeIfExists(tableName string, columnName stri } if err != nil { - mlog.Critical(fmt.Sprintf("Failed to alter column type %v", err)) + mlog.Critical("Failed to alter column type", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_ALTER_COLUMN) } @@ -736,7 +736,7 @@ func (ss *SqlSupplier) AlterColumnDefaultIfExists(tableName string, columnName s } if err != nil { - mlog.Critical(fmt.Sprintf("Failed to alter column %s.%s default %s: %v", tableName, columnName, defaultValue, err)) + mlog.Critical("Failed to alter column", mlog.String("table", tableName), mlog.String("column", columnName), mlog.String("default value", defaultValue), mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_GENERIC_FAILURE) return false @@ -790,7 +790,7 @@ func (ss *SqlSupplier) createIndexIfNotExists(indexName string, tableName string _, err := ss.GetMaster().ExecNoTimeout(query) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create index %v, %v", errExists, err)) + mlog.Critical("Failed to create index", mlog.Err(errExists), mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_INDEX_POSTGRES) } @@ -798,7 +798,7 @@ func (ss *SqlSupplier) createIndexIfNotExists(indexName string, tableName string count, err := ss.GetMaster().SelectInt("SELECT COUNT(0) AS index_exists FROM information_schema.statistics WHERE TABLE_SCHEMA = DATABASE() and table_name = ? AND index_name = ?", tableName, indexName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check index %v", err)) + mlog.Critical("Failed to check index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_INDEX_MYSQL) } @@ -814,14 +814,14 @@ func (ss *SqlSupplier) createIndexIfNotExists(indexName string, tableName string _, err = ss.GetMaster().ExecNoTimeout("CREATE " + uniqueStr + fullTextIndex + " INDEX " + indexName + " ON " + tableName + " (" + strings.Join(columnNames, ", ") + ")") if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create index %v", err)) + mlog.Critical("Failed to create index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_INDEX_FULL_MYSQL) } } else if ss.DriverName() == model.DATABASE_DRIVER_SQLITE { _, err := ss.GetMaster().ExecNoTimeout("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + tableName + " (" + strings.Join(columnNames, ", ") + ")") if err != nil { - mlog.Critical(fmt.Sprintf("Failed to create index %v", err)) + mlog.Critical("Failed to create index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_CREATE_INDEX_SQLITE) } @@ -845,7 +845,7 @@ func (ss *SqlSupplier) RemoveIndexIfExists(indexName string, tableName string) b _, err = ss.GetMaster().ExecNoTimeout("DROP INDEX " + indexName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to remove index %v", err)) + mlog.Critical("Failed to remove index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_INDEX_POSTGRES) } @@ -855,7 +855,7 @@ func (ss *SqlSupplier) RemoveIndexIfExists(indexName string, tableName string) b count, err := ss.GetMaster().SelectInt("SELECT COUNT(0) AS index_exists FROM information_schema.statistics WHERE TABLE_SCHEMA = DATABASE() and table_name = ? AND index_name = ?", tableName, indexName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to check index %v", err)) + mlog.Critical("Failed to check index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_INDEX_MYSQL) } @@ -866,14 +866,14 @@ func (ss *SqlSupplier) RemoveIndexIfExists(indexName string, tableName string) b _, err = ss.GetMaster().ExecNoTimeout("DROP INDEX " + indexName + " ON " + tableName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to remove index %v", err)) + mlog.Critical("Failed to remove index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_INDEX_MYSQL) } } else if ss.DriverName() == model.DATABASE_DRIVER_SQLITE { _, err := ss.GetMaster().ExecNoTimeout("DROP INDEX IF EXISTS " + indexName) if err != nil { - mlog.Critical(fmt.Sprintf("Failed to remove index %v", err)) + mlog.Critical("Failed to remove index", mlog.Err(err)) time.Sleep(time.Second) os.Exit(EXIT_REMOVE_INDEX_SQLITE) } @@ -1066,6 +1066,12 @@ func (ss *SqlSupplier) getQueryBuilder() sq.StatementBuilderType { return builder } +func (ss *SqlSupplier) CheckIntegrity() <-chan store.IntegrityCheckResult { + results := make(chan store.IntegrityCheckResult) + go CheckRelationalIntegrity(ss, results) + return results +} + type mattermConverter struct{} func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { diff --git a/store/sqlstore/supplier_reactions.go b/store/sqlstore/supplier_reactions.go index e607ed94c9..e3822af708 100644 --- a/store/sqlstore/supplier_reactions.go +++ b/store/sqlstore/supplier_reactions.go @@ -4,7 +4,6 @@ package sqlstore import ( - "fmt" "net/http" "github.com/mattermost/gorp" @@ -141,7 +140,7 @@ func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) *model.AppEr for _, reaction := range reactions { if _, err := s.GetMaster().Exec(UPDATE_POST_HAS_REACTIONS_ON_DELETE_QUERY, map[string]interface{}{"PostId": reaction.PostId, "UpdateAt": model.GetMillis()}); err != nil { - mlog.Warn(fmt.Sprintf("Unable to update Post.HasReactions while removing reactions post_id=%v, error=%v", reaction.PostId, err.Error())) + mlog.Warn("Unable to update Post.HasReactions while removing reactions", mlog.String("post_id", reaction.PostId), mlog.Err(err)) } } diff --git a/store/sqlstore/team_store.go b/store/sqlstore/team_store.go index 8b4c84ad52..ec3b3770f5 100644 --- a/store/sqlstore/team_store.go +++ b/store/sqlstore/team_store.go @@ -381,6 +381,21 @@ func (s SqlTeamStore) GetAllPrivateTeamListing() ([]*model.Team, *model.AppError return data, nil } +func (s SqlTeamStore) GetAllPublicTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) { + query := "SELECT * FROM Teams WHERE AllowOpenInvite = 1 ORDER BY DisplayName LIMIT :Limit OFFSET :Offset" + + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + query = "SELECT * FROM Teams WHERE AllowOpenInvite = true ORDER BY DisplayName LIMIT :Limit OFFSET :Offset" + } + + var data []*model.Team + if _, err := s.GetReplica().Select(&data, query, map[string]interface{}{"Offset": offset, "Limit": limit}); err != nil { + return nil, model.NewAppError("SqlTeamStore.GetAllPrivateTeamListing", "store.sql_team.get_all_private_team_listing.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return data, nil +} + func (s SqlTeamStore) GetAllPrivateTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) { query := "SELECT * FROM Teams WHERE AllowOpenInvite = 0 ORDER BY DisplayName LIMIT :Limit OFFSET :Offset" @@ -433,6 +448,35 @@ func (s SqlTeamStore) PermanentDelete(teamId string) *model.AppError { return nil } +func (s SqlTeamStore) AnalyticsPublicTeamCount() (int64, *model.AppError) { + + c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0 AND AllowOpenInvite = 1", map[string]interface{}{}) + + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + c, err = s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0 AND AllowOpenInvite = true", map[string]interface{}{}) + } + + if err != nil { + return int64(0), model.NewAppError("SqlTeamStore.AnalyticsPublicTeamCount", "store.sql_team.analytics_public_team_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return c, nil +} + +func (s SqlTeamStore) AnalyticsPrivateTeamCount() (int64, *model.AppError) { + c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0 AND AllowOpenInvite = 0", map[string]interface{}{}) + + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + c, err = s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0 AND AllowOpenInvite = false", map[string]interface{}{}) + } + + if err != nil { + return int64(0), model.NewAppError("SqlTeamStore.AnalyticsPrivateTeamCount", "store.sql_team.analytics_private_team_count.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return c, nil +} + func (s SqlTeamStore) AnalyticsTeamCount() (int64, *model.AppError) { c, err := s.GetReplica().SelectInt("SELECT COUNT(*) FROM Teams WHERE DeleteAt = 0", map[string]interface{}{}) diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 603b57ecfb..e118f4e2a6 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -729,11 +729,9 @@ func UpgradeDatabaseToVersion514(sqlStore SqlStore) { } func UpgradeDatabaseToVersion515(sqlStore SqlStore) { - // TODO: Uncomment following condition when version 5.15.0 is released - // if shouldPerformUpgrade(sqlStore, VERSION_5_14_0, VERSION_5_15_0) { - - // saveSchemaVersion(sqlStore, VERSION_5_15_0) - // } + if shouldPerformUpgrade(sqlStore, VERSION_5_14_0, VERSION_5_15_0) { + saveSchemaVersion(sqlStore, VERSION_5_15_0) + } } func UpgradeDatabaseToVersion516(sqlStore SqlStore) { diff --git a/store/store.go b/store/store.go index 3c666f4f23..64302fd40b 100644 --- a/store/store.go +++ b/store/store.go @@ -55,6 +55,7 @@ type Store interface { TotalMasterDbConnections() int TotalReadDbConnections() int TotalSearchDbConnections() int + CheckIntegrity() <-chan IntegrityCheckResult } type TeamStore interface { @@ -69,12 +70,15 @@ type TeamStore interface { GetAllPage(offset int, limit int) ([]*model.Team, *model.AppError) GetAllPrivateTeamListing() ([]*model.Team, *model.AppError) GetAllPrivateTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) + GetAllPublicTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) GetAllTeamListing() ([]*model.Team, *model.AppError) GetAllTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) GetTeamsByUserId(userId string) ([]*model.Team, *model.AppError) GetByInviteId(inviteId string) (*model.Team, *model.AppError) PermanentDelete(teamId string) *model.AppError AnalyticsTeamCount() (int64, *model.AppError) + AnalyticsPublicTeamCount() (int64, *model.AppError) + AnalyticsPrivateTeamCount() (int64, *model.AppError) SaveMember(member *model.TeamMember, maxUsersPerTeam int) (*model.TeamMember, *model.AppError) UpdateMember(member *model.TeamMember) (*model.TeamMember, *model.AppError) GetMember(teamId string, userId string) (*model.TeamMember, *model.AppError) @@ -147,6 +151,9 @@ type ChannelStore interface { InvalidateMemberCount(channelId string) GetMemberCountFromCache(channelId string) int64 GetMemberCount(channelId string, allowFromCache bool) (int64, *model.AppError) + InvalidatePinnedPostCount(channelId string) + GetPinnedPostCountFromCache(channelId string) int64 + GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) InvalidateGuestCount(channelId string) GetGuestCountFromCache(channelId string) int64 GetGuestCount(channelId string, allowFromCache bool) (int64, *model.AppError) @@ -628,3 +635,21 @@ type UserGetByIdsOpts struct { // Since filters the users based on their UpdateAt timestamp. Since int64 } + +type OrphanedRecord struct { + ParentId string + ChildId string +} + +type RelationalIntegrityCheckData struct { + ParentName string + ChildName string + ParentIdAttr string + ChildIdAttr string + Records []OrphanedRecord +} + +type IntegrityCheckResult struct { + Data interface{} + Err error +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index edf4531a75..e45a34ea06 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -75,6 +75,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) + t.Run("GetPinnedPostCount", func(t *testing.T) { testChannelStoreGetPinnedPostCount(t, ss) }) t.Run("MaxChannelsPerTeam", func(t *testing.T) { testChannelStoreMaxChannelsPerTeam(t, ss) }) t.Run("GetChannelsByScheme", func(t *testing.T) { testChannelStoreGetChannelsByScheme(t, ss) }) t.Run("MigrateChannelMembers", func(t *testing.T) { testChannelStoreMigrateChannelMembers(t, ss) }) @@ -3379,6 +3380,78 @@ func testChannelStoreGetPinnedPosts(t *testing.T, ss store.Store) { } } +func testChannelStoreGetPinnedPostCount(t *testing.T, ss store.Store) { + ch1 := &model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + + o1, err := ss.Channel().Save(ch1, -1) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o1.Id, + Message: "test", + IsPinned: true, + }) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o1.Id, + Message: "test", + IsPinned: true, + }) + require.Nil(t, err) + + if count, errGet := ss.Channel().GetPinnedPostCount(o1.Id, true); errGet != nil { + t.Fatal(errGet) + } else if count != 2 { + t.Fatal("didn't return right count") + } + + if ss.Channel().GetPinnedPostCountFromCache(o1.Id) != 2 { + t.Fatal("should have saved 2 pinned post count ") + } + + ch2 := &model.Channel{ + TeamId: model.NewId(), + DisplayName: "Name", + Name: "zz" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + + o2, err := ss.Channel().Save(ch2, -1) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o2.Id, + Message: "test", + }) + require.Nil(t, err) + + _, err = ss.Post().Save(&model.Post{ + UserId: model.NewId(), + ChannelId: o2.Id, + Message: "test", + }) + require.Nil(t, err) + + if count, errGet := ss.Channel().GetPinnedPostCount(o2.Id, true); errGet != nil { + t.Fatal(errGet) + } else if count != 0 { + t.Fatal("should return 0") + } + + if ss.Channel().GetPinnedPostCountFromCache(o2.Id) != 0 { + t.Fatal("should have saved 0 pinned post count ") + } +} + func testChannelStoreMaxChannelsPerTeam(t *testing.T, ss store.Store) { channel := &model.Channel{ TeamId: model.NewId(), diff --git a/store/storetest/mocks/AuditStore.go b/store/storetest/mocks/AuditStore.go index dd23d9c1ae..7740d16b0a 100644 --- a/store/storetest/mocks/AuditStore.go +++ b/store/storetest/mocks/AuditStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // AuditStore is an autogenerated mock type for the AuditStore type type AuditStore struct { diff --git a/store/storetest/mocks/BotStore.go b/store/storetest/mocks/BotStore.go index d4514ddbaf..c5d6f1d265 100644 --- a/store/storetest/mocks/BotStore.go +++ b/store/storetest/mocks/BotStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // BotStore is an autogenerated mock type for the BotStore type type BotStore struct { diff --git a/store/storetest/mocks/ChannelMemberHistoryStore.go b/store/storetest/mocks/ChannelMemberHistoryStore.go index 9ceac5cbff..0c65677b7b 100644 --- a/store/storetest/mocks/ChannelMemberHistoryStore.go +++ b/store/storetest/mocks/ChannelMemberHistoryStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // ChannelMemberHistoryStore is an autogenerated mock type for the ChannelMemberHistoryStore type type ChannelMemberHistoryStore struct { diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 54c870ac02..b0586aef3a 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -4,9 +4,11 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" -import store "github.com/mattermost/mattermost-server/store" +import ( + model "github.com/mattermost/mattermost-server/model" + store "github.com/mattermost/mattermost-server/store" + mock "github.com/stretchr/testify/mock" +) // ChannelStore is an autogenerated mock type for the ChannelStore type type ChannelStore struct { @@ -993,6 +995,43 @@ func (_m *ChannelStore) GetMoreChannels(teamId string, userId string, offset int return r0, r1 } +// GetPinnedPostCount provides a mock function with given fields: channelId, allowFromCache +func (_m *ChannelStore) GetPinnedPostCount(channelId string, allowFromCache bool) (int64, *model.AppError) { + ret := _m.Called(channelId, allowFromCache) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, bool) int64); ok { + r0 = rf(channelId, allowFromCache) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, bool) *model.AppError); ok { + r1 = rf(channelId, allowFromCache) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// GetPinnedPostCountFromCache provides a mock function with given fields: channelId +func (_m *ChannelStore) GetPinnedPostCountFromCache(channelId string) int64 { + ret := _m.Called(channelId) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string) int64); ok { + r0 = rf(channelId) + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + // GetPinnedPosts provides a mock function with given fields: channelId func (_m *ChannelStore) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) { ret := _m.Called(channelId) @@ -1139,6 +1178,11 @@ func (_m *ChannelStore) InvalidateMemberCount(channelId string) { _m.Called(channelId) } +// InvalidatePinnedPostCount provides a mock function with given fields: channelId +func (_m *ChannelStore) InvalidatePinnedPostCount(channelId string) { + _m.Called(channelId) +} + // IsUserInChannelUseCache provides a mock function with given fields: userId, channelId func (_m *ChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { ret := _m.Called(userId, channelId) diff --git a/store/storetest/mocks/ClusterDiscoveryStore.go b/store/storetest/mocks/ClusterDiscoveryStore.go index 7303faba31..1d36295689 100644 --- a/store/storetest/mocks/ClusterDiscoveryStore.go +++ b/store/storetest/mocks/ClusterDiscoveryStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // ClusterDiscoveryStore is an autogenerated mock type for the ClusterDiscoveryStore type type ClusterDiscoveryStore struct { diff --git a/store/storetest/mocks/CommandStore.go b/store/storetest/mocks/CommandStore.go index 5015c3d6c8..a60141d299 100644 --- a/store/storetest/mocks/CommandStore.go +++ b/store/storetest/mocks/CommandStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // CommandStore is an autogenerated mock type for the CommandStore type type CommandStore struct { diff --git a/store/storetest/mocks/CommandWebhookStore.go b/store/storetest/mocks/CommandWebhookStore.go index c388f2386d..b804a0ccd0 100644 --- a/store/storetest/mocks/CommandWebhookStore.go +++ b/store/storetest/mocks/CommandWebhookStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // CommandWebhookStore is an autogenerated mock type for the CommandWebhookStore type type CommandWebhookStore struct { diff --git a/store/storetest/mocks/ComplianceStore.go b/store/storetest/mocks/ComplianceStore.go index b175a9a7ba..e0b4bff3b2 100644 --- a/store/storetest/mocks/ComplianceStore.go +++ b/store/storetest/mocks/ComplianceStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // ComplianceStore is an autogenerated mock type for the ComplianceStore type type ComplianceStore struct { diff --git a/store/storetest/mocks/EmojiStore.go b/store/storetest/mocks/EmojiStore.go index 32cc6a49af..98429c68de 100644 --- a/store/storetest/mocks/EmojiStore.go +++ b/store/storetest/mocks/EmojiStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // EmojiStore is an autogenerated mock type for the EmojiStore type type EmojiStore struct { diff --git a/store/storetest/mocks/FileInfoStore.go b/store/storetest/mocks/FileInfoStore.go index 4a87217349..ef91923a22 100644 --- a/store/storetest/mocks/FileInfoStore.go +++ b/store/storetest/mocks/FileInfoStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // FileInfoStore is an autogenerated mock type for the FileInfoStore type type FileInfoStore struct { diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index 217bafd399..f630d27581 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // GroupStore is an autogenerated mock type for the GroupStore type type GroupStore struct { diff --git a/store/storetest/mocks/JobStore.go b/store/storetest/mocks/JobStore.go index 2c9908dece..1a8f34a660 100644 --- a/store/storetest/mocks/JobStore.go +++ b/store/storetest/mocks/JobStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // JobStore is an autogenerated mock type for the JobStore type type JobStore struct { diff --git a/store/storetest/mocks/LayeredStoreDatabaseLayer.go b/store/storetest/mocks/LayeredStoreDatabaseLayer.go index 360a5cf0c8..cb0ab438f5 100644 --- a/store/storetest/mocks/LayeredStoreDatabaseLayer.go +++ b/store/storetest/mocks/LayeredStoreDatabaseLayer.go @@ -4,10 +4,14 @@ package mocks -import context "context" -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" -import store "github.com/mattermost/mattermost-server/store" +import ( + context "context" + + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" + + store "github.com/mattermost/mattermost-server/store" +) // LayeredStoreDatabaseLayer is an autogenerated mock type for the LayeredStoreDatabaseLayer type type LayeredStoreDatabaseLayer struct { @@ -78,6 +82,22 @@ func (_m *LayeredStoreDatabaseLayer) ChannelMemberHistory() store.ChannelMemberH return r0 } +// CheckIntegrity provides a mock function with given fields: +func (_m *LayeredStoreDatabaseLayer) CheckIntegrity() <-chan store.IntegrityCheckResult { + ret := _m.Called() + + var r0 <-chan store.IntegrityCheckResult + if rf, ok := ret.Get(0).(func() <-chan store.IntegrityCheckResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan store.IntegrityCheckResult) + } + } + + return r0 +} + // Close provides a mock function with given fields: func (_m *LayeredStoreDatabaseLayer) Close() { _m.Called() diff --git a/store/storetest/mocks/LayeredStoreSupplier.go b/store/storetest/mocks/LayeredStoreSupplier.go index 45682da8f9..6d4e655e22 100644 --- a/store/storetest/mocks/LayeredStoreSupplier.go +++ b/store/storetest/mocks/LayeredStoreSupplier.go @@ -4,10 +4,14 @@ package mocks -import context "context" -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" -import store "github.com/mattermost/mattermost-server/store" +import ( + context "context" + + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" + + store "github.com/mattermost/mattermost-server/store" +) // LayeredStoreSupplier is an autogenerated mock type for the LayeredStoreSupplier type type LayeredStoreSupplier struct { diff --git a/store/storetest/mocks/LicenseStore.go b/store/storetest/mocks/LicenseStore.go index b769b39404..8a376037ef 100644 --- a/store/storetest/mocks/LicenseStore.go +++ b/store/storetest/mocks/LicenseStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // LicenseStore is an autogenerated mock type for the LicenseStore type type LicenseStore struct { diff --git a/store/storetest/mocks/LinkMetadataStore.go b/store/storetest/mocks/LinkMetadataStore.go index 5de2575969..2a7a12cef6 100644 --- a/store/storetest/mocks/LinkMetadataStore.go +++ b/store/storetest/mocks/LinkMetadataStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // LinkMetadataStore is an autogenerated mock type for the LinkMetadataStore type type LinkMetadataStore struct { diff --git a/store/storetest/mocks/OAuthStore.go b/store/storetest/mocks/OAuthStore.go index ab8f97b9ce..3ed44c34b3 100644 --- a/store/storetest/mocks/OAuthStore.go +++ b/store/storetest/mocks/OAuthStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // OAuthStore is an autogenerated mock type for the OAuthStore type type OAuthStore struct { diff --git a/store/storetest/mocks/PluginStore.go b/store/storetest/mocks/PluginStore.go index d618df0ce4..54812a66a6 100644 --- a/store/storetest/mocks/PluginStore.go +++ b/store/storetest/mocks/PluginStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // PluginStore is an autogenerated mock type for the PluginStore type type PluginStore struct { diff --git a/store/storetest/mocks/PostStore.go b/store/storetest/mocks/PostStore.go index 70818dd7d7..acb8adfe7f 100644 --- a/store/storetest/mocks/PostStore.go +++ b/store/storetest/mocks/PostStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // PostStore is an autogenerated mock type for the PostStore type type PostStore struct { diff --git a/store/storetest/mocks/PreferenceStore.go b/store/storetest/mocks/PreferenceStore.go index 293979210a..71828d62b0 100644 --- a/store/storetest/mocks/PreferenceStore.go +++ b/store/storetest/mocks/PreferenceStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // PreferenceStore is an autogenerated mock type for the PreferenceStore type type PreferenceStore struct { diff --git a/store/storetest/mocks/ReactionStore.go b/store/storetest/mocks/ReactionStore.go index 80cb0486c5..41117978bb 100644 --- a/store/storetest/mocks/ReactionStore.go +++ b/store/storetest/mocks/ReactionStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // ReactionStore is an autogenerated mock type for the ReactionStore type type ReactionStore struct { diff --git a/store/storetest/mocks/RoleStore.go b/store/storetest/mocks/RoleStore.go index e19635796c..64254eda52 100644 --- a/store/storetest/mocks/RoleStore.go +++ b/store/storetest/mocks/RoleStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // RoleStore is an autogenerated mock type for the RoleStore type type RoleStore struct { diff --git a/store/storetest/mocks/SchemeStore.go b/store/storetest/mocks/SchemeStore.go index a1e9d0779f..d1c1b1fbb8 100644 --- a/store/storetest/mocks/SchemeStore.go +++ b/store/storetest/mocks/SchemeStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // SchemeStore is an autogenerated mock type for the SchemeStore type type SchemeStore struct { diff --git a/store/storetest/mocks/SessionStore.go b/store/storetest/mocks/SessionStore.go index 4c0a97ecc9..88f3c817d5 100644 --- a/store/storetest/mocks/SessionStore.go +++ b/store/storetest/mocks/SessionStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // SessionStore is an autogenerated mock type for the SessionStore type type SessionStore struct { diff --git a/store/storetest/mocks/SqlStore.go b/store/storetest/mocks/SqlStore.go index 3c7c29c3b1..b4e22062bc 100644 --- a/store/storetest/mocks/SqlStore.go +++ b/store/storetest/mocks/SqlStore.go @@ -4,11 +4,14 @@ package mocks -import gorp "github.com/mattermost/gorp" -import mock "github.com/stretchr/testify/mock" +import ( + gorp "github.com/mattermost/gorp" + mock "github.com/stretchr/testify/mock" -import squirrel "github.com/Masterminds/squirrel" -import store "github.com/mattermost/mattermost-server/store" + squirrel "github.com/Masterminds/squirrel" + + store "github.com/mattermost/mattermost-server/store" +) // SqlStore is an autogenerated mock type for the SqlStore type type SqlStore struct { diff --git a/store/storetest/mocks/StatusStore.go b/store/storetest/mocks/StatusStore.go index 48d4e9a67f..bdfb0b40af 100644 --- a/store/storetest/mocks/StatusStore.go +++ b/store/storetest/mocks/StatusStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // StatusStore is an autogenerated mock type for the StatusStore type type StatusStore struct { diff --git a/store/storetest/mocks/Store.go b/store/storetest/mocks/Store.go index 63ca8cde83..c9b1cb1081 100644 --- a/store/storetest/mocks/Store.go +++ b/store/storetest/mocks/Store.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import store "github.com/mattermost/mattermost-server/store" +import ( + store "github.com/mattermost/mattermost-server/store" + mock "github.com/stretchr/testify/mock" +) // Store is an autogenerated mock type for the Store type type Store struct { @@ -76,6 +78,22 @@ func (_m *Store) ChannelMemberHistory() store.ChannelMemberHistoryStore { return r0 } +// CheckIntegrity provides a mock function with given fields: +func (_m *Store) CheckIntegrity() <-chan store.IntegrityCheckResult { + ret := _m.Called() + + var r0 <-chan store.IntegrityCheckResult + if rf, ok := ret.Get(0).(func() <-chan store.IntegrityCheckResult); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan store.IntegrityCheckResult) + } + } + + return r0 +} + // Close provides a mock function with given fields: func (_m *Store) Close() { _m.Called() diff --git a/store/storetest/mocks/SystemStore.go b/store/storetest/mocks/SystemStore.go index f9eeea7df2..1cd8f54a3e 100644 --- a/store/storetest/mocks/SystemStore.go +++ b/store/storetest/mocks/SystemStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // SystemStore is an autogenerated mock type for the SystemStore type type SystemStore struct { diff --git a/store/storetest/mocks/TeamStore.go b/store/storetest/mocks/TeamStore.go index e490238c37..b5afcea483 100644 --- a/store/storetest/mocks/TeamStore.go +++ b/store/storetest/mocks/TeamStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // TeamStore is an autogenerated mock type for the TeamStore type type TeamStore struct { @@ -35,6 +37,52 @@ func (_m *TeamStore) AnalyticsGetTeamCountForScheme(schemeId string) (int64, *mo return r0, r1 } +// AnalyticsPrivateTeamCount provides a mock function with given fields: +func (_m *TeamStore) AnalyticsPrivateTeamCount() (int64, *model.AppError) { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func() *model.AppError); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + +// AnalyticsPublicTeamCount provides a mock function with given fields: +func (_m *TeamStore) AnalyticsPublicTeamCount() (int64, *model.AppError) { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func() *model.AppError); ok { + r1 = rf() + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // AnalyticsTeamCount provides a mock function with given fields: func (_m *TeamStore) AnalyticsTeamCount() (int64, *model.AppError) { ret := _m.Called() @@ -252,6 +300,31 @@ func (_m *TeamStore) GetAllPrivateTeamPageListing(offset int, limit int) ([]*mod return r0, r1 } +// GetAllPublicTeamPageListing provides a mock function with given fields: offset, limit +func (_m *TeamStore) GetAllPublicTeamPageListing(offset int, limit int) ([]*model.Team, *model.AppError) { + ret := _m.Called(offset, limit) + + var r0 []*model.Team + if rf, ok := ret.Get(0).(func(int, int) []*model.Team); ok { + r0 = rf(offset, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Team) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(int, int) *model.AppError); ok { + r1 = rf(offset, limit) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetAllTeamListing provides a mock function with given fields: func (_m *TeamStore) GetAllTeamListing() ([]*model.Team, *model.AppError) { ret := _m.Called() diff --git a/store/storetest/mocks/TermsOfServiceStore.go b/store/storetest/mocks/TermsOfServiceStore.go index fca2d3f367..d107849e98 100644 --- a/store/storetest/mocks/TermsOfServiceStore.go +++ b/store/storetest/mocks/TermsOfServiceStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // TermsOfServiceStore is an autogenerated mock type for the TermsOfServiceStore type type TermsOfServiceStore struct { diff --git a/store/storetest/mocks/TokenStore.go b/store/storetest/mocks/TokenStore.go index b295467f20..a6a22c224f 100644 --- a/store/storetest/mocks/TokenStore.go +++ b/store/storetest/mocks/TokenStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // TokenStore is an autogenerated mock type for the TokenStore type type TokenStore struct { diff --git a/store/storetest/mocks/UserAccessTokenStore.go b/store/storetest/mocks/UserAccessTokenStore.go index 5995688d6a..0a3012f767 100644 --- a/store/storetest/mocks/UserAccessTokenStore.go +++ b/store/storetest/mocks/UserAccessTokenStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // UserAccessTokenStore is an autogenerated mock type for the UserAccessTokenStore type type UserAccessTokenStore struct { diff --git a/store/storetest/mocks/UserStore.go b/store/storetest/mocks/UserStore.go index 017b36cb2c..e901c81d70 100644 --- a/store/storetest/mocks/UserStore.go +++ b/store/storetest/mocks/UserStore.go @@ -4,9 +4,11 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" -import store "github.com/mattermost/mattermost-server/store" +import ( + model "github.com/mattermost/mattermost-server/model" + store "github.com/mattermost/mattermost-server/store" + mock "github.com/stretchr/testify/mock" +) // UserStore is an autogenerated mock type for the UserStore type type UserStore struct { diff --git a/store/storetest/mocks/UserTermsOfServiceStore.go b/store/storetest/mocks/UserTermsOfServiceStore.go index 387abb4c10..3689b7d212 100644 --- a/store/storetest/mocks/UserTermsOfServiceStore.go +++ b/store/storetest/mocks/UserTermsOfServiceStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // UserTermsOfServiceStore is an autogenerated mock type for the UserTermsOfServiceStore type type UserTermsOfServiceStore struct { diff --git a/store/storetest/mocks/WebhookStore.go b/store/storetest/mocks/WebhookStore.go index f6e3a61e4c..2c126cceb0 100644 --- a/store/storetest/mocks/WebhookStore.go +++ b/store/storetest/mocks/WebhookStore.go @@ -4,8 +4,10 @@ package mocks -import mock "github.com/stretchr/testify/mock" -import model "github.com/mattermost/mattermost-server/model" +import ( + model "github.com/mattermost/mattermost-server/model" + mock "github.com/stretchr/testify/mock" +) // WebhookStore is an autogenerated mock type for the WebhookStore type type WebhookStore struct { diff --git a/store/storetest/store.go b/store/storetest/store.go index fed9b6cede..1d09adf8f8 100644 --- a/store/storetest/store.go +++ b/store/storetest/store.go @@ -87,6 +87,9 @@ func (s *Store) TotalMasterDbConnections() int { return 1 } func (s *Store) TotalReadDbConnections() int { return 1 } func (s *Store) TotalSearchDbConnections() int { return 1 } func (s *Store) GetCurrentSchemaVersion() string { return "" } +func (s *Store) CheckIntegrity() <-chan store.IntegrityCheckResult { + return make(chan store.IntegrityCheckResult) +} func (s *Store) AssertExpectations(t mock.TestingT) bool { return mock.AssertExpectationsForObjects(t, diff --git a/store/storetest/team_store.go b/store/storetest/team_store.go index edf9fd0f55..8d161cbb66 100644 --- a/store/storetest/team_store.go +++ b/store/storetest/team_store.go @@ -15,6 +15,14 @@ import ( "github.com/mattermost/mattermost-server/store" ) +func cleanupTeamStore(t *testing.T, ss store.Store) { + allTeams, err := ss.Team().GetAll() + for _, team := range allTeams { + ss.Team().PermanentDelete(team.Id) + } + assert.Nil(t, err) +} + func TestTeamStore(t *testing.T, ss store.Store) { createDefaultRoles(t, ss) @@ -31,8 +39,11 @@ func TestTeamStore(t *testing.T, ss store.Store) { t.Run("GetAllTeamPageListing", func(t *testing.T) { testGetAllTeamPageListing(t, ss) }) t.Run("GetAllPrivateTeamListing", func(t *testing.T) { testGetAllPrivateTeamListing(t, ss) }) t.Run("GetAllPrivateTeamPageListing", func(t *testing.T) { testGetAllPrivateTeamPageListing(t, ss) }) + t.Run("GetAllPublicTeamPageListing", func(t *testing.T) { testGetAllPublicTeamPageListing(t, ss) }) t.Run("Delete", func(t *testing.T) { testDelete(t, ss) }) t.Run("TeamCount", func(t *testing.T) { testTeamCount(t, ss) }) + t.Run("TeamPublicCount", func(t *testing.T) { testPublicTeamCount(t, ss) }) + t.Run("TeamPrivateCount", func(t *testing.T) { testPrivateTeamCount(t, ss) }) t.Run("TeamMembers", func(t *testing.T) { testTeamMembers(t, ss) }) t.Run("SaveTeamMemberMaxMembers", func(t *testing.T) { testSaveTeamMemberMaxMembers(t, ss) }) t.Run("GetTeamMember", func(t *testing.T) { testGetTeamMember(t, ss) }) @@ -678,6 +689,66 @@ func testGetAllPrivateTeamPageListing(t *testing.T, ss store.Store) { } } +func testGetAllPublicTeamPageListing(t *testing.T, ss store.Store) { + cleanupTeamStore(t, ss) + + o1 := model.Team{} + o1.DisplayName = "DisplayName1" + o1.Name = "z-z-z" + model.NewId() + "b" + o1.Email = MakeEmail() + o1.Type = model.TEAM_OPEN + o1.AllowOpenInvite = true + t1, err := ss.Team().Save(&o1) + require.Nil(t, err) + + o2 := model.Team{} + o2.DisplayName = "DisplayName2" + o2.Name = "zz" + model.NewId() + "b" + o2.Email = MakeEmail() + o2.Type = model.TEAM_OPEN + o2.AllowOpenInvite = false + _, err = ss.Team().Save(&o2) + require.Nil(t, err) + + o3 := model.Team{} + o3.DisplayName = "DisplayName3" + o3.Name = "z-z-z" + model.NewId() + "b" + o3.Email = MakeEmail() + o3.Type = model.TEAM_INVITE + o3.AllowOpenInvite = true + t3, err := ss.Team().Save(&o3) + require.Nil(t, err) + + o4 := model.Team{} + o4.DisplayName = "DisplayName4" + o4.Name = "zz" + model.NewId() + "b" + o4.Email = MakeEmail() + o4.Type = model.TEAM_INVITE + o4.AllowOpenInvite = false + _, err = ss.Team().Save(&o4) + require.Nil(t, err) + + teams, err := ss.Team().GetAllPublicTeamPageListing(0, 10) + assert.Nil(t, err) + assert.Equal(t, []*model.Team{t1, t3}, teams) + + o5 := model.Team{} + o5.DisplayName = "DisplayName5" + o5.Name = "z-z-z" + model.NewId() + "b" + o5.Email = MakeEmail() + o5.Type = model.TEAM_OPEN + o5.AllowOpenInvite = true + t5, err := ss.Team().Save(&o5) + require.Nil(t, err) + + teams, err = ss.Team().GetAllPublicTeamPageListing(0, 4) + assert.Nil(t, err) + assert.Equal(t, []*model.Team{t1, t3, t5}, teams) + + teams, err = ss.Team().GetAllPublicTeamPageListing(1, 1) + assert.Nil(t, err) +} + func testDelete(t *testing.T, ss store.Store) { o1 := model.Team{} o1.DisplayName = "DisplayName" @@ -701,6 +772,76 @@ func testDelete(t *testing.T, ss store.Store) { } } +func testPublicTeamCount(t *testing.T, ss store.Store) { + cleanupTeamStore(t, ss) + + o1 := model.Team{} + o1.DisplayName = "DisplayName" + o1.Name = "z-z-z" + model.NewId() + "b" + o1.Email = MakeEmail() + o1.Type = model.TEAM_OPEN + o1.AllowOpenInvite = true + _, err := ss.Team().Save(&o1) + require.Nil(t, err) + + o2 := model.Team{} + o2.DisplayName = "DisplayName" + o2.Name = "z-z-z" + model.NewId() + "b" + o2.Email = MakeEmail() + o2.Type = model.TEAM_OPEN + o2.AllowOpenInvite = false + _, err = ss.Team().Save(&o2) + require.Nil(t, err) + + o3 := model.Team{} + o3.DisplayName = "DisplayName" + o3.Name = "z-z-z" + model.NewId() + "b" + o3.Email = MakeEmail() + o3.Type = model.TEAM_OPEN + o3.AllowOpenInvite = true + _, err = ss.Team().Save(&o3) + require.Nil(t, err) + + teamCount, err := ss.Team().AnalyticsPublicTeamCount() + require.Nil(t, err) + require.Equal(t, int64(2), teamCount, "should only be 1 team") +} + +func testPrivateTeamCount(t *testing.T, ss store.Store) { + cleanupTeamStore(t, ss) + + o1 := model.Team{} + o1.DisplayName = "DisplayName" + o1.Name = "z-z-z" + model.NewId() + "b" + o1.Email = MakeEmail() + o1.Type = model.TEAM_OPEN + o1.AllowOpenInvite = false + _, err := ss.Team().Save(&o1) + require.Nil(t, err) + + o2 := model.Team{} + o2.DisplayName = "DisplayName" + o2.Name = "z-z-z" + model.NewId() + "b" + o2.Email = MakeEmail() + o2.Type = model.TEAM_OPEN + o2.AllowOpenInvite = true + _, err = ss.Team().Save(&o2) + require.Nil(t, err) + + o3 := model.Team{} + o3.DisplayName = "DisplayName" + o3.Name = "z-z-z" + model.NewId() + "b" + o3.Email = MakeEmail() + o3.Type = model.TEAM_OPEN + o3.AllowOpenInvite = false + _, err = ss.Team().Save(&o3) + require.Nil(t, err) + + teamCount, err := ss.Team().AnalyticsPrivateTeamCount() + require.Nil(t, err) + require.Equal(t, int64(2), teamCount, "should only be 1 team") +} + func testTeamCount(t *testing.T, ss store.Store) { o1 := model.Team{} o1.DisplayName = "DisplayName" diff --git a/store/timer_layer.go b/store/timer_layer.go index 17848f300f..da3bcb5a8c 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -1382,6 +1382,23 @@ func (s *TimerLayerChannelStore) InvalidateMemberCount(channelId string) { return } +func (s *TimerLayerChannelStore) InvalidatePinnedPostCount(channelId string) { + start := timemodule.Now() + + s.ChannelStore.InvalidatePinnedPostCount(channelId) + + t := timemodule.Now() + elapsed := t.Sub(start) + if s.Root.Metrics != nil { + success := "false" + if true { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.InvalidatePinnedPostCount", success, float64(elapsed)) + } + return +} + func (s *TimerLayerChannelStore) IsUserInChannelUseCache(userId string, channelId string) bool { start := timemodule.Now() diff --git a/utils/test_files_compiler.go b/utils/test_files_compiler.go index 3ff81c2674..7e4bb4a4a7 100644 --- a/utils/test_files_compiler.go +++ b/utils/test_files_compiler.go @@ -49,6 +49,7 @@ func CompileGo(t *testing.T, sourceCode, outputPath string) { goMod(t, dir, "init", "mattermost.com/test") goMod(t, dir, "edit", "-require", "github.com/mattermost/mattermost-server@v0.0.0") goMod(t, dir, "edit", "-replace", fmt.Sprintf("github.com/mattermost/mattermost-server@v0.0.0=%s", mattermostServerPath)) + goMod(t, dir, "edit", "-replace", fmt.Sprintf("git.apache.org/thrift.git=%s", "github.com/apache/thrift@v0.0.0-20180902110319-2566ecd5d999")) } out := &bytes.Buffer{} diff --git a/vendor/github.com/minio/minio-go/.travis.yml b/vendor/github.com/minio/minio-go/.travis.yml deleted file mode 100644 index 7ed7df14e7..0000000000 --- a/vendor/github.com/minio/minio-go/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false -language: go - -os: -- linux - -env: -- ARCH=x86_64 -- ARCH=i686 - -go: -- 1.11.x -- tip - -matrix: - fast_finish: true - allow_failures: - - go: tip - -addons: - apt: - packages: - - devscripts - -script: -- diff -au <(gofmt -d .) <(printf "") -- diff -au <(licensecheck --check '.go$' --recursive --lines 0 * | grep -v -w 'Apache (v2.0)') <(printf "") -- make diff --git a/vendor/github.com/minio/minio-go/Makefile b/vendor/github.com/minio/minio-go/Makefile deleted file mode 100644 index 51c8ca266e..0000000000 --- a/vendor/github.com/minio/minio-go/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -all: checks - -checks: - @go get -t ./... - @go vet ./... - @SERVER_ENDPOINT=play.min.io:9000 ACCESS_KEY=Q3AM3UQ867SPQQA43P2F SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG ENABLE_HTTPS=1 MINT_MODE=full go test -race -v ./... - @go get github.com/dustin/go-humanize/... - @go get github.com/sirupsen/logrus/... - @SERVER_ENDPOINT=play.min.io:9000 ACCESS_KEY=Q3AM3UQ867SPQQA43P2F SECRET_KEY=zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG ENABLE_HTTPS=1 MINT_MODE=full go run functional_tests.go - @mkdir -p /tmp/examples && for i in $(echo examples/s3/*); do go build -o /tmp/examples/$(basename ${i:0:-3}) ${i}; done - @go get -u github.com/a8m/mark/... - @go get -u github.com/minio/cli/... - @go get -u golang.org/x/tools/cmd/goimports - @go get -u github.com/gernest/wow/... - @go build docs/validator.go && ./validator -m docs/API.md -t docs/checker.go.tpl diff --git a/vendor/github.com/minio/minio-go/go.mod b/vendor/github.com/minio/minio-go/go.mod deleted file mode 100644 index 59ef0a7943..0000000000 --- a/vendor/github.com/minio/minio-go/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/minio/minio-go - -require ( - github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect - github.com/jtolds/gls v4.2.1+incompatible // indirect - github.com/mitchellh/go-homedir v1.1.0 - github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect - github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect - golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b - golang.org/x/net v0.0.0-20190213061140-3a22650c66bd - golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect - golang.org/x/text v0.3.0 // indirect - gopkg.in/ini.v1 v1.41.0 -) diff --git a/vendor/github.com/minio/minio-go/go.sum b/vendor/github.com/minio/minio-go/go.sum deleted file mode 100644 index baf2f44e84..0000000000 --- a/vendor/github.com/minio/minio-go/go.sum +++ /dev/null @@ -1,20 +0,0 @@ -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= -github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= -github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b h1:Ib/yptP38nXZFMwqWSip+OKuMP9OkyDe3p+DssP8n9w= -golang.org/x/crypto v0.0.0-20190128193316-c7b33c32a30b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI= -golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= -gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/vendor/github.com/minio/minio-go/.gitignore b/vendor/github.com/minio/minio-go/v6/.gitignore similarity index 100% rename from vendor/github.com/minio/minio-go/.gitignore rename to vendor/github.com/minio/minio-go/v6/.gitignore diff --git a/vendor/github.com/minio/minio-go/v6/.travis.yml b/vendor/github.com/minio/minio-go/v6/.travis.yml new file mode 100644 index 0000000000..5dbda5e7de --- /dev/null +++ b/vendor/github.com/minio/minio-go/v6/.travis.yml @@ -0,0 +1,29 @@ +sudo: false +language: go + +os: +- linux + +env: +- ARCH=x86_64 + +go: +- 1.12.x +- tip + +matrix: + fast_finish: true + allow_failures: + - go: tip + +before_install: + - sudo apt-get install devscripts + - curl -O https://dl.minio.io/server/minio/release/linux-amd64/minio && chmod +x ./minio + - sudo cp testcerts/public.crt /usr/local/share/ca-certificates/ + - sudo update-ca-certificates + - MINIO_ACCESS_KEY=minio MINIO_SECRET_KEY=minio123 ./minio server --compat --quiet --certs-dir testcerts data 2>&1 > minio.log & + +script: + - diff -au <(gofmt -d .) <(printf "") + - diff -au <(licensecheck --check '.go$' --recursive --lines 0 * | grep -v -w 'Apache (v2.0)') <(printf "") + - make diff --git a/vendor/github.com/minio/minio-go/CONTRIBUTING.md b/vendor/github.com/minio/minio-go/v6/CONTRIBUTING.md similarity index 100% rename from vendor/github.com/minio/minio-go/CONTRIBUTING.md rename to vendor/github.com/minio/minio-go/v6/CONTRIBUTING.md diff --git a/vendor/github.com/minio/minio-go/LICENSE b/vendor/github.com/minio/minio-go/v6/LICENSE similarity index 100% rename from vendor/github.com/minio/minio-go/LICENSE rename to vendor/github.com/minio/minio-go/v6/LICENSE diff --git a/vendor/github.com/minio/minio-go/MAINTAINERS.md b/vendor/github.com/minio/minio-go/v6/MAINTAINERS.md similarity index 100% rename from vendor/github.com/minio/minio-go/MAINTAINERS.md rename to vendor/github.com/minio/minio-go/v6/MAINTAINERS.md diff --git a/vendor/github.com/minio/minio-go/v6/Makefile b/vendor/github.com/minio/minio-go/v6/Makefile new file mode 100644 index 0000000000..892f8cf507 --- /dev/null +++ b/vendor/github.com/minio/minio-go/v6/Makefile @@ -0,0 +1,20 @@ +all: checks + +.PHONY: examples docs + +checks: vet test examples docs functional-test + +vet: + @GO111MODULE=on go vet ./... + +test: + @GO111MODULE=on SERVER_ENDPOINT=localhost:9000 ACCESS_KEY=minio SECRET_KEY=minio123 ENABLE_HTTPS=1 MINT_MODE=full go test -race -v ./... + +examples: + @mkdir -p /tmp/examples && for i in $(echo examples/s3/*); do go build -o /tmp/examples/$(basename ${i:0:-3}) ${i}; done + +docs: + @(cd docs; GO111MODULE=on go build validator.go && ./validator -m ../docs/API.md -t checker.go.tpl) + +functional-test: + @GO111MODULE=on SERVER_ENDPOINT=localhost:9000 ACCESS_KEY=minio SECRET_KEY=minio123 ENABLE_HTTPS=1 MINT_MODE=full go run functional_tests.go diff --git a/vendor/github.com/minio/minio-go/NOTICE b/vendor/github.com/minio/minio-go/v6/NOTICE similarity index 100% rename from vendor/github.com/minio/minio-go/NOTICE rename to vendor/github.com/minio/minio-go/v6/NOTICE diff --git a/vendor/github.com/minio/minio-go/README.md b/vendor/github.com/minio/minio-go/v6/README.md similarity index 96% rename from vendor/github.com/minio/minio-go/README.md rename to vendor/github.com/minio/minio-go/v6/README.md index 5464390fa1..81320a14fb 100644 --- a/vendor/github.com/minio/minio-go/README.md +++ b/vendor/github.com/minio/minio-go/v6/README.md @@ -26,12 +26,12 @@ MinIO client requires the following four parameters specified to connect to an A package main import ( - "github.com/minio/minio-go" + "github.com/minio/minio-go/v6" "log" ) func main() { - endpoint := "play.min.io:9000" + endpoint := "play.min.io" accessKeyID := "Q3AM3UQ867SPQQA43P2F" secretAccessKey := "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" useSSL := true @@ -49,19 +49,19 @@ func main() { ## Quick Start Example - File Uploader This example program connects to an object storage server, creates a bucket and uploads a file to the bucket. -We will use the MinIO server running at [https://play.min.io:9000](https://play.min.io:9000) in this example. Feel free to use this service for testing and development. Access credentials shown in this example are open to the public. +We will use the MinIO server running at [https://play.min.io](https://play.min.io) in this example. Feel free to use this service for testing and development. Access credentials shown in this example are open to the public. ### FileUploader.go ```go package main import ( - "github.com/minio/minio-go" + "github.com/minio/minio-go/v6" "log" ) func main() { - endpoint := "play.min.io:9000" + endpoint := "play.min.io" accessKeyID := "Q3AM3UQ867SPQQA43P2F" secretAccessKey := "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" useSSL := true @@ -79,8 +79,8 @@ func main() { err = minioClient.MakeBucket(bucketName, location) if err != nil { // Check to see if we already own this bucket (which happens if you run this twice) - exists, err := minioClient.BucketExists(bucketName) - if err == nil && exists { + exists, errBucketExists := minioClient.BucketExists(bucketName) + if errBucketExists == nil && exists { log.Printf("We already own %s\n", bucketName) } else { log.Fatalln(err) diff --git a/vendor/github.com/minio/minio-go/README_zh_CN.md b/vendor/github.com/minio/minio-go/v6/README_zh_CN.md similarity index 97% rename from vendor/github.com/minio/minio-go/README_zh_CN.md rename to vendor/github.com/minio/minio-go/v6/README_zh_CN.md index 061f41743d..bc0c45c0fe 100644 --- a/vendor/github.com/minio/minio-go/README_zh_CN.md +++ b/vendor/github.com/minio/minio-go/v6/README_zh_CN.md @@ -38,12 +38,12 @@ MinIO client需要以下4个参数来连接与Amazon S3兼容的对象存储。 package main import ( - "github.com/minio/minio-go" + "github.com/minio/minio-go/v6" "log" ) func main() { - endpoint := "play.min.io:9000" + endpoint := "play.min.io" accessKeyID := "Q3AM3UQ867SPQQA43P2F" secretAccessKey := "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" useSSL := true @@ -61,19 +61,19 @@ func main() { ## 示例-文件上传 本示例连接到一个对象存储服务,创建一个存储桶并上传一个文件到存储桶中。 -我们在本示例中使用运行在 [https://play.min.io:9000](https://play.min.io:9000) 上的MinIO服务,你可以用这个服务来开发和测试。示例中的访问凭据是公开的。 +我们在本示例中使用运行在 [https://play.min.io](https://play.min.io) 上的MinIO服务,你可以用这个服务来开发和测试。示例中的访问凭据是公开的。 ### FileUploader.go ```go package main import ( - "github.com/minio/minio-go" + "github.com/minio/minio-go/v6" "log" ) func main() { - endpoint := "play.min.io:9000" + endpoint := "play.min.io" accessKeyID := "Q3AM3UQ867SPQQA43P2F" secretAccessKey := "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" useSSL := true diff --git a/vendor/github.com/minio/minio-go/api-compose-object.go b/vendor/github.com/minio/minio-go/v6/api-compose-object.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-compose-object.go rename to vendor/github.com/minio/minio-go/v6/api-compose-object.go index b4dddacb2d..748b558ce3 100644 --- a/vendor/github.com/minio/minio-go/api-compose-object.go +++ b/vendor/github.com/minio/minio-go/v6/api-compose-object.go @@ -28,8 +28,8 @@ import ( "strings" "time" - "github.com/minio/minio-go/pkg/encrypt" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // DestinationInfo - type with information about the object to be diff --git a/vendor/github.com/minio/minio-go/api-datatypes.go b/vendor/github.com/minio/minio-go/v6/api-datatypes.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-datatypes.go rename to vendor/github.com/minio/minio-go/v6/api-datatypes.go diff --git a/vendor/github.com/minio/minio-go/api-error-response.go b/vendor/github.com/minio/minio-go/v6/api-error-response.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-error-response.go rename to vendor/github.com/minio/minio-go/v6/api-error-response.go index 5dc8a0a90b..726fdd274a 100644 --- a/vendor/github.com/minio/minio-go/api-error-response.go +++ b/vendor/github.com/minio/minio-go/v6/api-error-response.go @@ -60,7 +60,7 @@ type ErrorResponse struct { // // For example: // -// import s3 "github.com/minio/minio-go" +// import s3 "github.com/minio/minio-go/v6" // ... // ... // reader, stat, err := s3.GetObject(...) diff --git a/vendor/github.com/minio/minio-go/api-get-lifecycle.go b/vendor/github.com/minio/minio-go/v6/api-get-lifecycle.go similarity index 97% rename from vendor/github.com/minio/minio-go/api-get-lifecycle.go rename to vendor/github.com/minio/minio-go/v6/api-get-lifecycle.go index 96ccfe9029..a24d03e370 100644 --- a/vendor/github.com/minio/minio-go/api-get-lifecycle.go +++ b/vendor/github.com/minio/minio-go/v6/api-get-lifecycle.go @@ -23,7 +23,7 @@ import ( "net/http" "net/url" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // GetBucketLifecycle - get bucket lifecycle. diff --git a/vendor/github.com/minio/minio-go/api-get-object-acl.go b/vendor/github.com/minio/minio-go/v6/api-get-object-acl.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-get-object-acl.go rename to vendor/github.com/minio/minio-go/v6/api-get-object-acl.go diff --git a/vendor/github.com/minio/minio-go/api-get-object-context.go b/vendor/github.com/minio/minio-go/v6/api-get-object-context.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-get-object-context.go rename to vendor/github.com/minio/minio-go/v6/api-get-object-context.go diff --git a/vendor/github.com/minio/minio-go/api-get-object-file.go b/vendor/github.com/minio/minio-go/v6/api-get-object-file.go similarity index 98% rename from vendor/github.com/minio/minio-go/api-get-object-file.go rename to vendor/github.com/minio/minio-go/v6/api-get-object-file.go index 98837bfa3e..9c82a7c4f9 100644 --- a/vendor/github.com/minio/minio-go/api-get-object-file.go +++ b/vendor/github.com/minio/minio-go/v6/api-get-object-file.go @@ -23,7 +23,7 @@ import ( "os" "path/filepath" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // FGetObjectWithContext - download contents of an object to a local file. diff --git a/vendor/github.com/minio/minio-go/api-get-object.go b/vendor/github.com/minio/minio-go/v6/api-get-object.go similarity index 98% rename from vendor/github.com/minio/minio-go/api-get-object.go rename to vendor/github.com/minio/minio-go/v6/api-get-object.go index a00ddaa895..5d03ea9bb7 100644 --- a/vendor/github.com/minio/minio-go/api-get-object.go +++ b/vendor/github.com/minio/minio-go/v6/api-get-object.go @@ -27,7 +27,7 @@ import ( "sync" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // GetObject - returns an seekable, readable object. @@ -321,6 +321,7 @@ func (o *Object) Read(b []byte) (n int, err error) { if o.prevErr != nil || o.isClosed { return 0, o.prevErr } + // Create a new request. readReq := getRequest{ isReadOp: true, @@ -403,10 +404,13 @@ func (o *Object) ReadAt(b []byte, offset int64) (n int, err error) { defer o.mutex.Unlock() // prevErr is error which was saved in previous operation. - if o.prevErr != nil || o.isClosed { + if o.prevErr != nil && o.prevErr != io.EOF || o.isClosed { return 0, o.prevErr } + // Set the current offset to ReadAt offset, because the current offset will be shifted at the end of this method. + o.currOffset = offset + // Can only compare offsets to size when size has been set. if o.objectInfoSet { // If offset is negative than we return io.EOF. @@ -476,11 +480,9 @@ func (o *Object) Seek(offset int64, whence int) (n int64, err error) { o.mutex.Lock() defer o.mutex.Unlock() - if o.prevErr != nil { - // At EOF seeking is legal allow only io.EOF, for any other errors we return. - if o.prevErr != io.EOF { - return 0, o.prevErr - } + // At EOF seeking is legal allow only io.EOF, for any other errors we return. + if o.prevErr != nil && o.prevErr != io.EOF { + return 0, o.prevErr } // Negative offset is valid for whence of '2'. diff --git a/vendor/github.com/minio/minio-go/api-get-options.go b/vendor/github.com/minio/minio-go/v6/api-get-options.go similarity index 98% rename from vendor/github.com/minio/minio-go/api-get-options.go rename to vendor/github.com/minio/minio-go/v6/api-get-options.go index 323b1c909a..538fd1a052 100644 --- a/vendor/github.com/minio/minio-go/api-get-options.go +++ b/vendor/github.com/minio/minio-go/v6/api-get-options.go @@ -22,7 +22,7 @@ import ( "net/http" "time" - "github.com/minio/minio-go/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/encrypt" ) // GetObjectOptions are used to specify additional headers or options diff --git a/vendor/github.com/minio/minio-go/api-get-policy.go b/vendor/github.com/minio/minio-go/v6/api-get-policy.go similarity index 97% rename from vendor/github.com/minio/minio-go/api-get-policy.go rename to vendor/github.com/minio/minio-go/v6/api-get-policy.go index 035685d044..bc1d10530a 100644 --- a/vendor/github.com/minio/minio-go/api-get-policy.go +++ b/vendor/github.com/minio/minio-go/v6/api-get-policy.go @@ -23,7 +23,7 @@ import ( "net/http" "net/url" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // GetBucketPolicy - get bucket policy at a given path. diff --git a/vendor/github.com/minio/minio-go/api-list.go b/vendor/github.com/minio/minio-go/v6/api-list.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-list.go rename to vendor/github.com/minio/minio-go/v6/api-list.go index 6c4259eb41..b9c0f5d8d0 100644 --- a/vendor/github.com/minio/minio-go/api-list.go +++ b/vendor/github.com/minio/minio-go/v6/api-list.go @@ -25,7 +25,7 @@ import ( "net/url" "strings" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // ListBuckets list all buckets owned by this authenticated user. diff --git a/vendor/github.com/minio/minio-go/api-notification.go b/vendor/github.com/minio/minio-go/v6/api-notification.go similarity index 93% rename from vendor/github.com/minio/minio-go/api-notification.go rename to vendor/github.com/minio/minio-go/v6/api-notification.go index 38a3f992ff..f35619541e 100644 --- a/vendor/github.com/minio/minio-go/api-notification.go +++ b/vendor/github.com/minio/minio-go/v6/api-notification.go @@ -21,12 +21,11 @@ import ( "bufio" "context" "encoding/json" - "io" "net/http" "net/url" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // GetBucketNotification - get bucket notification at a given path. @@ -164,13 +163,14 @@ func (c Client) ListenBucketNotification(bucketName, prefix, suffix string, even // Indicate to our routine to exit cleanly upon return. defer close(retryDoneCh) + // Prepare urlValues to pass into the request on every loop + urlValues := make(url.Values) + urlValues.Set("prefix", prefix) + urlValues.Set("suffix", suffix) + urlValues["events"] = events + // Wait on the jitter retry loop. for range c.newRetryTimerContinous(time.Second, time.Second*30, MaxJitter, retryDoneCh) { - urlValues := make(url.Values) - urlValues.Set("prefix", prefix) - urlValues.Set("suffix", suffix) - urlValues["events"] = events - // Execute GET on bucket to list objects. resp, err := c.executeMethod(context.Background(), "GET", requestMetadata{ bucketName: bucketName, @@ -196,30 +196,24 @@ func (c Client) ListenBucketNotification(bucketName, prefix, suffix string, even // Initialize a new bufio scanner, to read line by line. bio := bufio.NewScanner(resp.Body) - // Close the response body. - defer resp.Body.Close() - // Unmarshal each line, returns marshalled values. for bio.Scan() { var notificationInfo NotificationInfo if err = json.Unmarshal(bio.Bytes(), ¬ificationInfo); err != nil { + closeResponse(resp) continue } // Send notificationInfo select { case notificationInfoCh <- notificationInfo: case <-doneCh: + closeResponse(resp) return } } - // Look for any underlying errors. - if err = bio.Err(); err != nil { - // For an unexpected connection drop from server, we close the body - // and re-connect. - if err == io.ErrUnexpectedEOF { - resp.Body.Close() - } - } + + // Close current connection before looping further. + closeResponse(resp) } }(notificationInfoCh) diff --git a/vendor/github.com/minio/minio-go/api-presigned.go b/vendor/github.com/minio/minio-go/v6/api-presigned.go similarity index 98% rename from vendor/github.com/minio/minio-go/api-presigned.go rename to vendor/github.com/minio/minio-go/v6/api-presigned.go index 8ffcdd712e..e2d68b0ece 100644 --- a/vendor/github.com/minio/minio-go/api-presigned.go +++ b/vendor/github.com/minio/minio-go/v6/api-presigned.go @@ -23,8 +23,8 @@ import ( "net/url" "time" - "github.com/minio/minio-go/pkg/s3signer" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3signer" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // presignURL - Returns a presigned URL for an input 'method'. diff --git a/vendor/github.com/minio/minio-go/api-put-bucket.go b/vendor/github.com/minio/minio-go/v6/api-put-bucket.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-put-bucket.go rename to vendor/github.com/minio/minio-go/v6/api-put-bucket.go index 69d33e3151..28613a2291 100644 --- a/vendor/github.com/minio/minio-go/api-put-bucket.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-bucket.go @@ -26,7 +26,7 @@ import ( "net/url" "strings" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) /// Bucket operations diff --git a/vendor/github.com/minio/minio-go/api-put-object-common.go b/vendor/github.com/minio/minio-go/v6/api-put-object-common.go similarity index 96% rename from vendor/github.com/minio/minio-go/api-put-object-common.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-common.go index 00d98b2902..a786d2a8e0 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-common.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object-common.go @@ -23,7 +23,7 @@ import ( "math" "os" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // Verify if reader is *minio.Object @@ -64,7 +64,7 @@ func isReadAt(reader io.Reader) (ok bool) { // object storage it will have the following parameters as constants. // // maxPartsCount - 10000 -// minPartSize - 64MiB +// minPartSize - 128MiB // maxMultipartPutObjectSize - 5TiB // func optimalPartInfo(objectSize int64, configuredPartSize uint64) (totalPartsCount int, partSize int64, lastPartSize int64, err error) { @@ -105,7 +105,7 @@ func optimalPartInfo(objectSize int64, configuredPartSize uint64) (totalPartsCou configuredPartSize = minPartSize // Use floats for part size for all calculations to avoid // overflows during float64 to int64 conversions. - partSizeFlt = math.Ceil(float64(objectSize / maxPartsCount)) + partSizeFlt = float64(objectSize / maxPartsCount) partSizeFlt = math.Ceil(partSizeFlt/float64(configuredPartSize)) * float64(configuredPartSize) } diff --git a/vendor/github.com/minio/minio-go/api-put-object-context.go b/vendor/github.com/minio/minio-go/v6/api-put-object-context.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-put-object-context.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-context.go diff --git a/vendor/github.com/minio/minio-go/api-put-object-copy.go b/vendor/github.com/minio/minio-go/v6/api-put-object-copy.go similarity index 97% rename from vendor/github.com/minio/minio-go/api-put-object-copy.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-copy.go index c6ecf39d85..19e58add89 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-copy.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object-copy.go @@ -23,7 +23,7 @@ import ( "io/ioutil" "net/http" - "github.com/minio/minio-go/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/encrypt" ) // CopyObject - copy a source object into a new object diff --git a/vendor/github.com/minio/minio-go/api-put-object-file-context.go b/vendor/github.com/minio/minio-go/v6/api-put-object-file-context.go similarity index 97% rename from vendor/github.com/minio/minio-go/api-put-object-file-context.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-file-context.go index 3574a6b5b4..fb22c0d64b 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-file-context.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object-file-context.go @@ -23,7 +23,7 @@ import ( "os" "path/filepath" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // FPutObjectWithContext - Create an object in a bucket, with contents from file at filePath. Allows request cancellation. diff --git a/vendor/github.com/minio/minio-go/api-put-object-file.go b/vendor/github.com/minio/minio-go/v6/api-put-object-file.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-put-object-file.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-file.go diff --git a/vendor/github.com/minio/minio-go/api-put-object-multipart.go b/vendor/github.com/minio/minio-go/v6/api-put-object-multipart.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-put-object-multipart.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-multipart.go index 95a2599b0f..ab284f9869 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-multipart.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object-multipart.go @@ -33,8 +33,8 @@ import ( "strconv" "strings" - "github.com/minio/minio-go/pkg/encrypt" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/s3utils" ) func (c Client) putObjectMultipart(ctx context.Context, bucketName, objectName string, reader io.Reader, size int64, diff --git a/vendor/github.com/minio/minio-go/api-put-object-streaming.go b/vendor/github.com/minio/minio-go/v6/api-put-object-streaming.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-put-object-streaming.go rename to vendor/github.com/minio/minio-go/v6/api-put-object-streaming.go index 0035def3e7..0d3f2455a7 100644 --- a/vendor/github.com/minio/minio-go/api-put-object-streaming.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object-streaming.go @@ -25,7 +25,7 @@ import ( "sort" "strings" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // putObjectMultipartStream - upload a large object using @@ -75,7 +75,7 @@ type uploadPartReq struct { Part *ObjectPart // Size of the part uploaded. } -// putObjectMultipartFromReadAt - Uploads files bigger than 64MiB. +// putObjectMultipartFromReadAt - Uploads files bigger than 128MiB. // Supports all readers which implements io.ReaderAt interface // (ReadAt method). // diff --git a/vendor/github.com/minio/minio-go/api-put-object.go b/vendor/github.com/minio/minio-go/v6/api-put-object.go similarity index 96% rename from vendor/github.com/minio/minio-go/api-put-object.go rename to vendor/github.com/minio/minio-go/v6/api-put-object.go index 4ea9c495a5..8f33fca6cb 100644 --- a/vendor/github.com/minio/minio-go/api-put-object.go +++ b/vendor/github.com/minio/minio-go/v6/api-put-object.go @@ -26,8 +26,8 @@ import ( "runtime/debug" "sort" - "github.com/minio/minio-go/pkg/encrypt" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/s3utils" "golang.org/x/net/http/httpguts" ) @@ -124,9 +124,9 @@ func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].Part // // You must have WRITE permissions on a bucket to create an object. // -// - For size smaller than 64MiB PutObject automatically does a +// - For size smaller than 128MiB PutObject automatically does a // single atomic Put operation. -// - For size larger than 64MiB PutObject automatically does a +// - For size larger than 128MiB PutObject automatically does a // multipart Put operation. // - For size input as -1 PutObject does a multipart Put operation // until input stream reaches EOF. Maximum object size that can @@ -167,7 +167,7 @@ func (c Client) putObjectCommon(ctx context.Context, bucketName, objectName stri return c.putObjectNoChecksum(ctx, bucketName, objectName, reader, size, opts) } - // For all sizes greater than 64MiB do multipart. + // For all sizes greater than 128MiB do multipart. return c.putObjectMultipartStream(ctx, bucketName, objectName, reader, size, opts) } diff --git a/vendor/github.com/minio/minio-go/api-remove.go b/vendor/github.com/minio/minio-go/v6/api-remove.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-remove.go rename to vendor/github.com/minio/minio-go/v6/api-remove.go index 29f6f347ea..e919be17d1 100644 --- a/vendor/github.com/minio/minio-go/api-remove.go +++ b/vendor/github.com/minio/minio-go/v6/api-remove.go @@ -25,7 +25,7 @@ import ( "net/http" "net/url" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // RemoveBucket deletes the bucket name. diff --git a/vendor/github.com/minio/minio-go/api-s3-datatypes.go b/vendor/github.com/minio/minio-go/v6/api-s3-datatypes.go similarity index 100% rename from vendor/github.com/minio/minio-go/api-s3-datatypes.go rename to vendor/github.com/minio/minio-go/v6/api-s3-datatypes.go diff --git a/vendor/github.com/minio/minio-go/api-select.go b/vendor/github.com/minio/minio-go/v6/api-select.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-select.go rename to vendor/github.com/minio/minio-go/v6/api-select.go index 0b5762450f..b5ce131121 100644 --- a/vendor/github.com/minio/minio-go/api-select.go +++ b/vendor/github.com/minio/minio-go/v6/api-select.go @@ -31,8 +31,8 @@ import ( "net/url" "strings" - "github.com/minio/minio-go/pkg/encrypt" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // CSVFileHeaderInfo - is the parameter for whether to utilize headers. diff --git a/vendor/github.com/minio/minio-go/api-stat.go b/vendor/github.com/minio/minio-go/v6/api-stat.go similarity index 99% rename from vendor/github.com/minio/minio-go/api-stat.go rename to vendor/github.com/minio/minio-go/v6/api-stat.go index 15d0af39e4..c6fb47d63e 100644 --- a/vendor/github.com/minio/minio-go/api-stat.go +++ b/vendor/github.com/minio/minio-go/v6/api-stat.go @@ -24,7 +24,7 @@ import ( "strings" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // BucketExists verify if bucket exists and you have permission to access it. diff --git a/vendor/github.com/minio/minio-go/api.go b/vendor/github.com/minio/minio-go/v6/api.go similarity index 92% rename from vendor/github.com/minio/minio-go/api.go rename to vendor/github.com/minio/minio-go/v6/api.go index 0a15cb4d57..8e40219881 100644 --- a/vendor/github.com/minio/minio-go/api.go +++ b/vendor/github.com/minio/minio-go/v6/api.go @@ -41,9 +41,9 @@ import ( "golang.org/x/net/publicsuffix" - "github.com/minio/minio-go/pkg/credentials" - "github.com/minio/minio-go/pkg/s3signer" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/credentials" + "github.com/minio/minio-go/v6/pkg/s3signer" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // Client implements Amazon S3 compatible methods. @@ -73,8 +73,9 @@ type Client struct { bucketLocCache *bucketLocationCache // Advanced functionality. - isTraceEnabled bool - traceOutput io.Writer + isTraceEnabled bool + traceErrorsOnly bool + traceOutput io.Writer // S3 specific accelerated endpoint. s3AccelerateEndpoint string @@ -102,7 +103,7 @@ type Options struct { // Global constants. const ( libraryName = "minio-go" - libraryVersion = "v6.0.23" + libraryVersion = "v6.0.34" ) // User Agent should always following the below style. @@ -186,6 +187,12 @@ func NewWithOptions(endpoint string, opts *Options) (*Client, error) { return privateNew(endpoint, opts.Creds, opts.Secure, opts.Region, opts.BucketLookup) } +// EndpointURL returns the URL of the S3 endpoint. +func (c *Client) EndpointURL() *url.URL { + endpoint := *c.endpointURL // copy to prevent callers from modifying internal state + return &endpoint +} + // lockedRandSource provides protected rand source, implements rand.Source interface. type lockedRandSource struct { lk sync.Mutex @@ -263,7 +270,7 @@ func (c *Client) redirectHeaders(req *http.Request, via []*http.Request) error { case signerType.IsV2(): return errors.New("signature V2 cannot support redirection") case signerType.IsV4(): - req = s3signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, getDefaultLocation(*c.endpointURL, region)) + s3signer.SignV4(*req, accessKeyID, secretAccessKey, sessionToken, getDefaultLocation(*c.endpointURL, region)) } } return nil @@ -330,10 +337,6 @@ func privateNew(endpoint string, creds *credentials.Credentials, secure bool, re func (c *Client) SetAppInfo(appName string, appVersion string) { // if app name and version not set, we do not set a new user agent. if appName != "" && appVersion != "" { - c.appInfo = struct { - appName string - appVersion string - }{} c.appInfo.appName = appName c.appInfo.appVersion = appVersion } @@ -373,10 +376,23 @@ func (c *Client) TraceOn(outputStream io.Writer) { c.isTraceEnabled = true } +// TraceErrorsOnlyOn - same as TraceOn, but only errors will be traced. +func (c *Client) TraceErrorsOnlyOn(outputStream io.Writer) { + c.TraceOn(outputStream) + c.traceErrorsOnly = true +} + +// TraceErrorsOnlyOff - Turns off the errors only tracing and everything will be traced after this call. +// If all tracing needs to be turned off, call TraceOff(). +func (c *Client) TraceErrorsOnlyOff() { + c.traceErrorsOnly = false +} + // TraceOff - disable HTTP tracing. func (c *Client) TraceOff() { // Disable tracing. c.isTraceEnabled = false + c.traceErrorsOnly = false } // SetS3TransferAccelerate - turns s3 accelerated endpoint on or off for all your @@ -516,8 +532,9 @@ func (c Client) do(req *http.Request) (*http.Response, error) { return nil, ErrInvalidArgument(msg) } - // If trace is enabled, dump http request and response. - if c.isTraceEnabled { + // If trace is enabled, dump http request and response, + // except when the traceErrorsOnly enabled and the response's status code is ok + if c.isTraceEnabled && !(c.traceErrorsOnly && resp.StatusCode == http.StatusOK) { err = c.dumpHTTP(req, resp) if err != nil { return nil, err @@ -642,14 +659,30 @@ func (c Client) executeMethod(ctx context.Context, method string, metadata reque // // Additionally we should only retry if bucketLocation and custom // region is empty. - if metadata.bucketLocation == "" && c.region == "" { - if errResponse.Code == "AuthorizationHeaderMalformed" || errResponse.Code == "InvalidRegion" { + if c.region == "" { + switch errResponse.Code { + case "AuthorizationHeaderMalformed": + fallthrough + case "InvalidRegion": + fallthrough + case "AccessDenied": if metadata.bucketName != "" && errResponse.Region != "" { // Gather Cached location only if bucketName is present. - if _, cachedLocationError := c.bucketLocCache.Get(metadata.bucketName); cachedLocationError != false { + if _, cachedOk := c.bucketLocCache.Get(metadata.bucketName); cachedOk { c.bucketLocCache.Set(metadata.bucketName, errResponse.Region) continue // Retry. } + } else { + // Most probably for ListBuckets() + if errResponse.Region != metadata.bucketLocation { + // Retry if the error + // response has a + // different region + // than the request we + // just made. + metadata.bucketLocation = errResponse.Region + continue // Retry + } } } } @@ -683,13 +716,8 @@ func (c Client) newRequest(method string, metadata requestMetadata) (req *http.R // Gather location only if bucketName is present. location, err = c.getBucketLocation(metadata.bucketName) if err != nil { - if ToErrorResponse(err).Code != "AccessDenied" { - return nil, err - } + return nil, err } - // Upon AccessDenied error on fetching bucket location, default - // to possible locations based on endpoint URL. This can usually - // happen when GetBucketLocation() is disabled using IAM policies. } if location == "" { location = getDefaultLocation(*c.endpointURL, c.region) @@ -697,10 +725,14 @@ func (c Client) newRequest(method string, metadata requestMetadata) (req *http.R } // Look if target url supports virtual host. - isVirtualHost := c.isVirtualHostStyleRequest(*c.endpointURL, metadata.bucketName) + // We explicitly disallow MakeBucket calls to not use virtual DNS style, + // since the resolution may fail. + isMakeBucket := (metadata.objectName == "" && method == "PUT" && len(metadata.queryValues) == 0) + isVirtualHost := c.isVirtualHostStyleRequest(*c.endpointURL, metadata.bucketName) && !isMakeBucket // Construct a new target URL. - targetURL, err := c.makeTargetURL(metadata.bucketName, metadata.objectName, location, isVirtualHost, metadata.queryValues) + targetURL, err := c.makeTargetURL(metadata.bucketName, metadata.objectName, location, + isVirtualHost, metadata.queryValues) if err != nil { return nil, err } diff --git a/vendor/github.com/minio/minio-go/appveyor.yml b/vendor/github.com/minio/minio-go/v6/appveyor.yml similarity index 58% rename from vendor/github.com/minio/minio-go/appveyor.yml rename to vendor/github.com/minio/minio-go/v6/appveyor.yml index 48ea6e77dc..39a33d876d 100644 --- a/vendor/github.com/minio/minio-go/appveyor.yml +++ b/vendor/github.com/minio/minio-go/v6/appveyor.yml @@ -9,28 +9,24 @@ clone_folder: c:\gopath\src\github.com\minio\minio-go # environment variables environment: GOPATH: c:\gopath - GO15VENDOREXPERIMENT: 1 + GO111MODULE: on # scripts that run after cloning repository install: - set PATH=%GOPATH%\bin;c:\go\bin;%PATH% - go version - go env - - go get -u golang.org/x/lint/golint - - go get -u github.com/remyoudompheng/go-misc/deadcode - - go get -u github.com/gordonklaus/ineffassign - - go get -u golang.org/x/crypto/argon2 - - go get -t ./... + - go get golang.org/x/lint/golint + - go get honnef.co/go/tools/cmd/staticcheck # to run your custom scripts instead of automatic MSBuild build_script: - go vet ./... - gofmt -s -l . - - golint -set_exit_status github.com/minio/minio-go... - - deadcode - - ineffassign . - - go test -short -v - - go test -short -race -v + - golint -set_exit_status github.com/minio/minio-go/... + - staticcheck + - go test -short -v ./... + - go test -short -race -v ./... # to disable automatic tests test: off diff --git a/vendor/github.com/minio/minio-go/bucket-cache.go b/vendor/github.com/minio/minio-go/v6/bucket-cache.go similarity index 93% rename from vendor/github.com/minio/minio-go/bucket-cache.go rename to vendor/github.com/minio/minio-go/v6/bucket-cache.go index 4ea28e5690..7ba6cbb571 100644 --- a/vendor/github.com/minio/minio-go/bucket-cache.go +++ b/vendor/github.com/minio/minio-go/v6/bucket-cache.go @@ -24,9 +24,9 @@ import ( "path" "sync" - "github.com/minio/minio-go/pkg/credentials" - "github.com/minio/minio-go/pkg/s3signer" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/credentials" + "github.com/minio/minio-go/v6/pkg/s3signer" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // bucketLocationCache - Provides simple mechanism to hold bucket @@ -124,8 +124,16 @@ func processBucketLocationResponse(resp *http.Response, bucketName string) (buck // For access denied error, it could be an anonymous // request. Move forward and let the top level callers // succeed if possible based on their policy. - if errResp.Code == "AccessDenied" { - return "us-east-1", nil + switch errResp.Code { + case "AuthorizationHeaderMalformed": + fallthrough + case "InvalidRegion": + fallthrough + case "AccessDenied": + if errResp.Region == "" { + return "us-east-1", nil + } + return errResp.Region, nil } return "", err } @@ -162,7 +170,7 @@ func (c Client) getBucketLocationRequest(bucketName string) (*http.Request, erro urlValues.Set("location", "") // Set get bucket location always as path style. - targetURL := c.endpointURL + targetURL := *c.endpointURL // as it works in makeTargetURL method from api.go file if h, p, err := net.SplitHostPort(targetURL.Host); err == nil { diff --git a/vendor/github.com/minio/minio-go/bucket-notification.go b/vendor/github.com/minio/minio-go/v6/bucket-notification.go similarity index 99% rename from vendor/github.com/minio/minio-go/bucket-notification.go rename to vendor/github.com/minio/minio-go/v6/bucket-notification.go index 11fc299fff..4714eadad5 100644 --- a/vendor/github.com/minio/minio-go/bucket-notification.go +++ b/vendor/github.com/minio/minio-go/v6/bucket-notification.go @@ -20,7 +20,7 @@ package minio import ( "encoding/xml" - "github.com/minio/minio-go/pkg/set" + "github.com/minio/minio-go/v6/pkg/set" ) // NotificationEventType is a S3 notification event associated to the bucket notification configuration diff --git a/vendor/github.com/minio/minio-go/constants.go b/vendor/github.com/minio/minio-go/v6/constants.go similarity index 95% rename from vendor/github.com/minio/minio-go/constants.go rename to vendor/github.com/minio/minio-go/v6/constants.go index 252fc5fafa..ac472a631d 100644 --- a/vendor/github.com/minio/minio-go/constants.go +++ b/vendor/github.com/minio/minio-go/v6/constants.go @@ -23,9 +23,9 @@ package minio // a part in a multipart upload may not be uploaded. const absMinPartSize = 1024 * 1024 * 5 -// minPartSize - minimum part size 64MiB per object after which +// minPartSize - minimum part size 128MiB per object after which // putObject behaves internally as multipart. -const minPartSize = 1024 * 1024 * 64 +const minPartSize = 1024 * 1024 * 128 // maxPartsCount - maximum number of parts for a single multipart session. const maxPartsCount = 10000 diff --git a/vendor/github.com/minio/minio-go/core.go b/vendor/github.com/minio/minio-go/v6/core.go similarity index 61% rename from vendor/github.com/minio/minio-go/core.go rename to vendor/github.com/minio/minio-go/v6/core.go index afd4139eed..4f57193435 100644 --- a/vendor/github.com/minio/minio-go/core.go +++ b/vendor/github.com/minio/minio-go/v6/core.go @@ -22,7 +22,7 @@ import ( "io" "strings" - "github.com/minio/minio-go/pkg/encrypt" + "github.com/minio/minio-go/v6/pkg/encrypt" ) // Core - Inherits Client and adds new methods to expose the low level S3 APIs. @@ -55,9 +55,23 @@ func (c Core) ListObjectsV2(bucketName, objectPrefix, continuationToken string, return c.listObjectsV2Query(bucketName, objectPrefix, continuationToken, fetchOwner, delimiter, maxkeys, startAfter) } +// CopyObjectWithContext - copies an object from source object to destination object on server side. +func (c Core) CopyObjectWithContext(ctx context.Context, sourceBucket, sourceObject, destBucket, destObject string, metadata map[string]string) (ObjectInfo, error) { + return c.copyObjectDo(ctx, sourceBucket, sourceObject, destBucket, destObject, metadata) +} + // CopyObject - copies an object from source object to destination object on server side. func (c Core) CopyObject(sourceBucket, sourceObject, destBucket, destObject string, metadata map[string]string) (ObjectInfo, error) { - return c.copyObjectDo(context.Background(), sourceBucket, sourceObject, destBucket, destObject, metadata) + return c.CopyObjectWithContext(context.Background(), sourceBucket, sourceObject, destBucket, destObject, metadata) +} + +// CopyObjectPartWithContext - creates a part in a multipart upload by copying (a +// part of) an existing object. +func (c Core) CopyObjectPartWithContext(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string, + partID int, startOffset, length int64, metadata map[string]string) (p CompletePart, err error) { + + return c.copyObjectPartDo(ctx, srcBucket, srcObject, destBucket, destObject, uploadID, + partID, startOffset, length, metadata) } // CopyObjectPart - creates a part in a multipart upload by copying (a @@ -65,12 +79,12 @@ func (c Core) CopyObject(sourceBucket, sourceObject, destBucket, destObject stri func (c Core) CopyObjectPart(srcBucket, srcObject, destBucket, destObject string, uploadID string, partID int, startOffset, length int64, metadata map[string]string) (p CompletePart, err error) { - return c.copyObjectPartDo(context.Background(), srcBucket, srcObject, destBucket, destObject, uploadID, + return c.CopyObjectPartWithContext(context.Background(), srcBucket, srcObject, destBucket, destObject, uploadID, partID, startOffset, length, metadata) } -// PutObject - Upload object. Uploads using single PUT call. -func (c Core) PutObject(bucket, object string, data io.Reader, size int64, md5Base64, sha256Hex string, metadata map[string]string, sse encrypt.ServerSide) (ObjectInfo, error) { +// PutObjectWithContext - Upload object. Uploads using single PUT call. +func (c Core) PutObjectWithContext(ctx context.Context, bucket, object string, data io.Reader, size int64, md5Base64, sha256Hex string, metadata map[string]string, sse encrypt.ServerSide) (ObjectInfo, error) { opts := PutObjectOptions{} m := make(map[string]string) for k, v := range metadata { @@ -84,7 +98,7 @@ func (c Core) PutObject(bucket, object string, data io.Reader, size int64, md5Ba opts.ContentType = v } else if strings.ToLower(k) == "cache-control" { opts.CacheControl = v - } else if strings.ToLower(k) == strings.ToLower(amzWebsiteRedirectLocation) { + } else if strings.EqualFold(k, amzWebsiteRedirectLocation) { opts.WebsiteRedirectLocation = v } else { m[k] = metadata[k] @@ -92,7 +106,12 @@ func (c Core) PutObject(bucket, object string, data io.Reader, size int64, md5Ba } opts.UserMetadata = m opts.ServerSideEncryption = sse - return c.putObjectDo(context.Background(), bucket, object, data, md5Base64, sha256Hex, size, opts) + return c.putObjectDo(ctx, bucket, object, data, md5Base64, sha256Hex, size, opts) +} + +// PutObject - Upload object. Uploads using single PUT call. +func (c Core) PutObject(bucket, object string, data io.Reader, size int64, md5Base64, sha256Hex string, metadata map[string]string, sse encrypt.ServerSide) (ObjectInfo, error) { + return c.PutObjectWithContext(context.Background(), bucket, object, data, size, md5Base64, sha256Hex, metadata, sse) } // NewMultipartUpload - Initiates new multipart upload and returns the new uploadID. @@ -106,9 +125,14 @@ func (c Core) ListMultipartUploads(bucket, prefix, keyMarker, uploadIDMarker, de return c.listMultipartUploadsQuery(bucket, keyMarker, uploadIDMarker, prefix, delimiter, maxUploads) } +// PutObjectPartWithContext - Upload an object part. +func (c Core) PutObjectPartWithContext(ctx context.Context, bucket, object, uploadID string, partID int, data io.Reader, size int64, md5Base64, sha256Hex string, sse encrypt.ServerSide) (ObjectPart, error) { + return c.uploadPart(ctx, bucket, object, uploadID, data, partID, md5Base64, sha256Hex, size, sse) +} + // PutObjectPart - Upload an object part. func (c Core) PutObjectPart(bucket, object, uploadID string, partID int, data io.Reader, size int64, md5Base64, sha256Hex string, sse encrypt.ServerSide) (ObjectPart, error) { - return c.uploadPart(context.Background(), bucket, object, uploadID, data, partID, md5Base64, sha256Hex, size, sse) + return c.PutObjectPartWithContext(context.Background(), bucket, object, uploadID, partID, data, size, md5Base64, sha256Hex, sse) } // ListObjectParts - List uploaded parts of an incomplete upload.x @@ -116,17 +140,27 @@ func (c Core) ListObjectParts(bucket, object, uploadID string, partNumberMarker return c.listObjectPartsQuery(bucket, object, uploadID, partNumberMarker, maxParts) } -// CompleteMultipartUpload - Concatenate uploaded parts and commit to an object. -func (c Core) CompleteMultipartUpload(bucket, object, uploadID string, parts []CompletePart) (string, error) { - res, err := c.completeMultipartUpload(context.Background(), bucket, object, uploadID, completeMultipartUpload{ +// CompleteMultipartUploadWithContext - Concatenate uploaded parts and commit to an object. +func (c Core) CompleteMultipartUploadWithContext(ctx context.Context, bucket, object, uploadID string, parts []CompletePart) (string, error) { + res, err := c.completeMultipartUpload(ctx, bucket, object, uploadID, completeMultipartUpload{ Parts: parts, }) return res.ETag, err } +// CompleteMultipartUpload - Concatenate uploaded parts and commit to an object. +func (c Core) CompleteMultipartUpload(bucket, object, uploadID string, parts []CompletePart) (string, error) { + return c.CompleteMultipartUploadWithContext(context.Background(), bucket, object, uploadID, parts) +} + +// AbortMultipartUploadWithContext - Abort an incomplete upload. +func (c Core) AbortMultipartUploadWithContext(ctx context.Context, bucket, object, uploadID string) error { + return c.abortMultipartUpload(ctx, bucket, object, uploadID) +} + // AbortMultipartUpload - Abort an incomplete upload. func (c Core) AbortMultipartUpload(bucket, object, uploadID string) error { - return c.abortMultipartUpload(context.Background(), bucket, object, uploadID) + return c.AbortMultipartUploadWithContext(context.Background(), bucket, object, uploadID) } // GetBucketPolicy - fetches bucket access policy for a given bucket. @@ -139,15 +173,28 @@ func (c Core) PutBucketPolicy(bucket, bucketPolicy string) error { return c.putBucketPolicy(bucket, bucketPolicy) } +// GetObjectWithContext is a lower level API implemented to support reading +// partial objects and also downloading objects with special conditions +// matching etag, modtime etc. +func (c Core) GetObjectWithContext(ctx context.Context, bucketName, objectName string, opts GetObjectOptions) (io.ReadCloser, ObjectInfo, error) { + return c.getObject(ctx, bucketName, objectName, opts) +} + // GetObject is a lower level API implemented to support reading // partial objects and also downloading objects with special conditions // matching etag, modtime etc. func (c Core) GetObject(bucketName, objectName string, opts GetObjectOptions) (io.ReadCloser, ObjectInfo, error) { - return c.getObject(context.Background(), bucketName, objectName, opts) + return c.GetObjectWithContext(context.Background(), bucketName, objectName, opts) +} + +// StatObjectWithContext is a lower level API implemented to support special +// conditions matching etag, modtime on a request. +func (c Core) StatObjectWithContext(ctx context.Context, bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error) { + return c.statObject(ctx, bucketName, objectName, opts) } // StatObject is a lower level API implemented to support special // conditions matching etag, modtime on a request. func (c Core) StatObject(bucketName, objectName string, opts StatObjectOptions) (ObjectInfo, error) { - return c.statObject(context.Background(), bucketName, objectName, opts) + return c.StatObjectWithContext(context.Background(), bucketName, objectName, opts) } diff --git a/vendor/github.com/minio/minio-go/functional_tests.go b/vendor/github.com/minio/minio-go/v6/functional_tests.go similarity index 98% rename from vendor/github.com/minio/minio-go/functional_tests.go rename to vendor/github.com/minio/minio-go/v6/functional_tests.go index b412edb23f..463552ce12 100644 --- a/vendor/github.com/minio/minio-go/functional_tests.go +++ b/vendor/github.com/minio/minio-go/v6/functional_tests.go @@ -40,10 +40,10 @@ import ( "time" humanize "github.com/dustin/go-humanize" - minio "github.com/minio/minio-go" log "github.com/sirupsen/logrus" - "github.com/minio/minio-go/pkg/encrypt" + "github.com/minio/minio-go/v6" + "github.com/minio/minio-go/v6/pkg/encrypt" ) const letterBytes = "abcdefghijklmnopqrstuvwxyz01234569" @@ -184,9 +184,9 @@ func isErrNotImplemented(err error) bool { func init() { // If server endpoint is not set, all tests default to - // using https://play.min.io:9000 + // using https://play.min.io if os.Getenv(serverEndpoint) == "" { - os.Setenv(serverEndpoint, "play.min.io:9000") + os.Setenv(serverEndpoint, "play.min.io") os.Setenv(accessKey, "Q3AM3UQ867SPQQA43P2F") os.Setenv(secretKey, "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG") os.Setenv(enableHTTPS, "1") @@ -264,7 +264,7 @@ var dataFileMap = map[string]int{ "datafile-5-MB": 5 * humanize.MiByte, "datafile-6-MB": 6 * humanize.MiByte, "datafile-11-MB": 11 * humanize.MiByte, - "datafile-65-MB": 65 * humanize.MiByte, + "datafile-129-MB": 129 * humanize.MiByte, } func isFullMode() bool { @@ -531,8 +531,8 @@ func testPutObjectReadAt() { return } - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() // Save the data @@ -641,8 +641,8 @@ func testPutObjectWithMetadata() { return } - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() // Save the data @@ -1308,7 +1308,7 @@ func testFPutObjectMultipart() { } // Upload 4 parts to utilize all 3 'workers' in multipart and still have a part to upload. - var fileName = getMintDataDirFilePath("datafile-65-MB") + var fileName = getMintDataDirFilePath("datafile-129-MB") if fileName == "" { // Make a temp file with minPartSize bytes of data. file, err := ioutil.TempFile(os.TempDir(), "FPutObjectTest") @@ -1317,7 +1317,7 @@ func testFPutObjectMultipart() { return } // Upload 2 parts to utilize all 3 'workers' in multipart and still have a part to upload. - if _, err = io.Copy(file, getDataReader("datafile-65-MB")); err != nil { + if _, err = io.Copy(file, getDataReader("datafile-129-MB")); err != nil { logError(testName, function, args, startTime, "", "Copy failed", err) return } @@ -1328,7 +1328,7 @@ func testFPutObjectMultipart() { fileName = file.Name() args["fileName"] = fileName } - totalSize := dataFileMap["datafile-65-MB"] + totalSize := dataFileMap["datafile-129-MB"] // Set base object name objectName := bucketName + "FPutObject" + "-standard" args["objectName"] = objectName @@ -1426,7 +1426,7 @@ func testFPutObject() { // Upload 3 parts worth of data to use all 3 of multiparts 'workers' and have an extra part. // Use different data in part for multipart tests to check parts are uploaded in correct order. - var fName = getMintDataDirFilePath("datafile-65-MB") + var fName = getMintDataDirFilePath("datafile-129-MB") if fName == "" { // Make a temp file with minPartSize bytes of data. file, err := ioutil.TempFile(os.TempDir(), "FPutObjectTest") @@ -1436,7 +1436,7 @@ func testFPutObject() { } // Upload 3 parts to utilize all 3 'workers' in multipart and still have a part to upload. - if _, err = io.Copy(file, getDataReader("datafile-65-MB")); err != nil { + if _, err = io.Copy(file, getDataReader("datafile-129-MB")); err != nil { logError(testName, function, args, startTime, "", "File copy failed", err) return } @@ -1448,7 +1448,7 @@ func testFPutObject() { defer os.Remove(file.Name()) fName = file.Name() } - totalSize := dataFileMap["datafile-65-MB"] + totalSize := dataFileMap["datafile-129-MB"] // Set base object name function = "FPutObject(bucketName, objectName, fileName, opts)" @@ -2226,6 +2226,133 @@ func testGetObjectReadAtFunctional() { successLogger(testName, function, args, startTime).Info() } +// Reproduces issue https://github.com/minio/minio-go/issues/1137 +func testGetObjectReadAtWhenEOFWasReached() { + // initialize logging params + startTime := time.Now() + testName := getFuncName() + function := "GetObject(bucketName, objectName)" + args := map[string]interface{}{} + + // Seed random based on current time. + rand.Seed(time.Now().Unix()) + + // Instantiate new minio client object. + c, err := minio.New( + os.Getenv(serverEndpoint), + os.Getenv(accessKey), + os.Getenv(secretKey), + mustParseBool(os.Getenv(enableHTTPS)), + ) + if err != nil { + logError(testName, function, args, startTime, "", "MinIO client object creation failed", err) + return + } + + // Enable tracing, write to stderr. + // c.TraceOn(os.Stderr) + + // Set user agent. + c.SetAppInfo("MinIO-go-FunctionalTest", "0.1.0") + + // Generate a new random bucket name. + bucketName := randString(60, rand.NewSource(time.Now().UnixNano()), "minio-go-test-") + args["bucketName"] = bucketName + + // Make a new bucket. + err = c.MakeBucket(bucketName, "us-east-1") + if err != nil { + logError(testName, function, args, startTime, "", "MakeBucket failed", err) + return + } + + // Generate 33K of data. + bufSize := dataFileMap["datafile-33-kB"] + var reader = getDataReader("datafile-33-kB") + defer reader.Close() + + objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") + args["objectName"] = objectName + + buf, err := ioutil.ReadAll(reader) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAll failed", err) + return + } + + // Save the data + n, err := c.PutObject(bucketName, objectName, bytes.NewReader(buf), int64(len(buf)), minio.PutObjectOptions{ContentType: "binary/octet-stream"}) + if err != nil { + logError(testName, function, args, startTime, "", "PutObject failed", err) + return + } + + if n != int64(bufSize) { + logError(testName, function, args, startTime, "", "Number of bytes does not match, expected "+string(int64(bufSize))+", got "+string(n), err) + return + } + + // read the data back + r, err := c.GetObject(bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + logError(testName, function, args, startTime, "", "PutObject failed", err) + return + } + + // read directly + buf1 := make([]byte, n) + buf2 := make([]byte, 512) + + m, err := r.Read(buf1) + if err != nil { + if err != io.EOF { + logError(testName, function, args, startTime, "", "Read failed", err) + return + } + } + if m != len(buf1) { + logError(testName, function, args, startTime, "", "Read read shorter bytes before reaching EOF, expected "+string(len(buf1))+", got "+string(m), err) + return + } + if !bytes.Equal(buf1, buf) { + logError(testName, function, args, startTime, "", "Incorrect count of Read data", err) + return + } + + st, err := r.Stat() + if err != nil { + logError(testName, function, args, startTime, "", "Stat failed", err) + return + } + + if st.Size != int64(bufSize) { + logError(testName, function, args, startTime, "", "Number of bytes in stat does not match, expected "+string(int64(bufSize))+", got "+string(st.Size), err) + return + } + + m, err = r.ReadAt(buf2, 512) + if err != nil { + logError(testName, function, args, startTime, "", "ReadAt failed", err) + return + } + if m != len(buf2) { + logError(testName, function, args, startTime, "", "ReadAt read shorter bytes before reaching EOF, expected "+string(len(buf2))+", got "+string(m), err) + return + } + if !bytes.Equal(buf2, buf[512:1024]) { + logError(testName, function, args, startTime, "", "Incorrect count of ReadAt data", err) + return + } + + // Delete all objects and buckets + if err = cleanupBucket(bucketName, c); err != nil { + logError(testName, function, args, startTime, "", "Cleanup failed", err) + return + } + + successLogger(testName, function, args, startTime).Info() +} + // Test Presigned Post Policy func testPresignedPostPolicy() { // initialize logging params @@ -2706,9 +2833,9 @@ func testSSECEncryptedGetObjectReadSeekFunctional() { } }() - // Generate 65MiB of data. - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + // Generate 129MiB of data. + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") @@ -2894,9 +3021,9 @@ func testSSES3EncryptedGetObjectReadSeekFunctional() { } }() - // Generate 65MiB of data. - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + // Generate 129MiB of data. + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") @@ -3072,9 +3199,9 @@ func testSSECEncryptedGetObjectReadAtFunctional() { return } - // Generate 65MiB of data. - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + // Generate 129MiB of data. + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") @@ -3263,9 +3390,9 @@ func testSSES3EncryptedGetObjectReadAtFunctional() { return } - // Generate 65MiB of data. - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + // Generate 129MiB of data. + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() objectName := randString(60, rand.NewSource(time.Now().UnixNano()), "") @@ -8561,8 +8688,8 @@ func testPutObjectNoLengthV2() { objectName := bucketName + "unique" args["objectName"] = objectName - bufSize := dataFileMap["datafile-65-MB"] - var reader = getDataReader("datafile-65-MB") + bufSize := dataFileMap["datafile-129-MB"] + var reader = getDataReader("datafile-129-MB") defer reader.Close() args["size"] = bufSize @@ -10004,6 +10131,7 @@ func main() { testFPutObject() testGetObjectReadSeekFunctional() testGetObjectReadAtFunctional() + testGetObjectReadAtWhenEOFWasReached() testPresignedPostPolicy() testCopyObject() testComposeObjectErrorCases() diff --git a/vendor/github.com/minio/minio-go/v6/go.mod b/vendor/github.com/minio/minio-go/v6/go.mod new file mode 100644 index 0000000000..9d342468df --- /dev/null +++ b/vendor/github.com/minio/minio-go/v6/go.mod @@ -0,0 +1,16 @@ +module github.com/minio/minio-go/v6 + +go 1.12 + +require ( + github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gernest/wow v0.1.0 // indirect + github.com/minio/cli v1.20.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 + github.com/sirupsen/logrus v1.4.2 // indirect + github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect + golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f + golang.org/x/net v0.0.0-20190522155817-f3200d17e092 + gopkg.in/ini.v1 v1.42.0 +) diff --git a/vendor/github.com/minio/minio-go/v6/go.sum b/vendor/github.com/minio/minio-go/v6/go.sum new file mode 100644 index 0000000000..62a4f31fac --- /dev/null +++ b/vendor/github.com/minio/minio-go/v6/go.sum @@ -0,0 +1,48 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845 h1:hIjQrEARcc9LcH8igte3JBpWBZ7+SpinU70dOjU/afo= +github.com/a8m/mark v0.1.1-0.20170507133748-44f2db618845/go.mod h1:c8Mh99Cw82nrsAnPgxQSZHkswVOJF7/MqZb1ZdvriLM= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/gernest/wow v0.1.0 h1:g9xdwCwP0+xgVYlA2sopI0gZHqXe7HjI/7/LykG4fks= +github.com/gernest/wow v0.1.0/go.mod h1:dEPabJRi5BneI1Nev1VWo0ZlcTWibHWp43qxKms4elY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/minio/cli v1.20.0 h1:OVNIt8Rg5+mpYb8siWT2gBV5hvUyFbRvBikC+Ytvf5A= +github.com/minio/cli v1.20.0/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f h1:R423Cnkcp5JABoeemiGEPlt9tHXFfw5kvc0yqlxRPWo= +golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk= +gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/minio/minio-go/hook-reader.go b/vendor/github.com/minio/minio-go/v6/hook-reader.go similarity index 100% rename from vendor/github.com/minio/minio-go/hook-reader.go rename to vendor/github.com/minio/minio-go/v6/hook-reader.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/chain.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/chain.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/chain.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/chain.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/config.json.sample b/vendor/github.com/minio/minio-go/v6/pkg/credentials/config.json.sample similarity index 88% rename from vendor/github.com/minio/minio-go/pkg/credentials/config.json.sample rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/config.json.sample index 0affa58cf5..d793c9e0e9 100644 --- a/vendor/github.com/minio/minio-go/pkg/credentials/config.json.sample +++ b/vendor/github.com/minio/minio-go/v6/pkg/credentials/config.json.sample @@ -2,7 +2,7 @@ "version": "8", "hosts": { "play": { - "url": "https://play.min.io:9000", + "url": "https://play.min.io", "accessKey": "Q3AM3UQ867SPQQA43P2F", "secretKey": "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", "api": "S3v2" diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/credentials.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/credentials.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/credentials.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/credentials.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/credentials.sample b/vendor/github.com/minio/minio-go/v6/pkg/credentials/credentials.sample similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/credentials.sample rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/credentials.sample diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/doc.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/doc.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/doc.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/doc.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/env_aws.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/env_aws.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/env_aws.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/env_aws.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/env_minio.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/env_minio.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/env_minio.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/env_minio.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/file_aws_credentials.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/file_aws_credentials.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/file_aws_credentials.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/file_aws_credentials.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/file_minio_client.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/file_minio_client.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/file_minio_client.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/file_minio_client.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/iam_aws.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/iam_aws.go similarity index 99% rename from vendor/github.com/minio/minio-go/pkg/credentials/iam_aws.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/iam_aws.go index 310785209d..5732f2e4b6 100644 --- a/vendor/github.com/minio/minio-go/pkg/credentials/iam_aws.go +++ b/vendor/github.com/minio/minio-go/v6/pkg/credentials/iam_aws.go @@ -53,7 +53,7 @@ type IAM struct { const ( defaultIAMRoleEndpoint = "http://169.254.169.254" defaultECSRoleEndpoint = "http://169.254.170.2" - defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials" + defaultIAMSecurityCredsPath = "/latest/meta-data/iam/security-credentials/" ) // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/signature-type.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/signature-type.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/signature-type.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/signature-type.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/static.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/static.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/static.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/static.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/sts_client_grants.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/sts_client_grants.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/sts_client_grants.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/sts_client_grants.go diff --git a/vendor/github.com/minio/minio-go/pkg/credentials/sts_web_identity.go b/vendor/github.com/minio/minio-go/v6/pkg/credentials/sts_web_identity.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/credentials/sts_web_identity.go rename to vendor/github.com/minio/minio-go/v6/pkg/credentials/sts_web_identity.go diff --git a/vendor/github.com/minio/minio-go/pkg/encrypt/server-side.go b/vendor/github.com/minio/minio-go/v6/pkg/encrypt/server-side.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/encrypt/server-side.go rename to vendor/github.com/minio/minio-go/v6/pkg/encrypt/server-side.go diff --git a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-streaming.go b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-streaming.go similarity index 99% rename from vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-streaming.go rename to vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-streaming.go index c82e6bd372..810b47c4b3 100644 --- a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-streaming.go +++ b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-streaming.go @@ -285,7 +285,7 @@ func (s *StreamingReader) Read(buf []byte) (int, error) { // bytes read from baseReader different than // content length provided. if s.bytesRead != s.contentLen { - return 0, io.ErrUnexpectedEOF + return 0, fmt.Errorf("http: ContentLength=%d with Body length %d", s.contentLen, s.bytesRead) } // Sign the chunk and write it to s.buf. diff --git a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v2.go b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v2.go similarity index 99% rename from vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v2.go rename to vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v2.go index 919540690b..40ba07130a 100644 --- a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v2.go +++ b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v2.go @@ -30,7 +30,7 @@ import ( "strings" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // Signature and API related constants. diff --git a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v4.go b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v4.go similarity index 99% rename from vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v4.go rename to vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v4.go index e23c06baf6..ab96b58c52 100644 --- a/vendor/github.com/minio/minio-go/pkg/s3signer/request-signature-v4.go +++ b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/request-signature-v4.go @@ -26,7 +26,7 @@ import ( "strings" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // Signature and API related constants. diff --git a/vendor/github.com/minio/minio-go/pkg/s3signer/utils.go b/vendor/github.com/minio/minio-go/v6/pkg/s3signer/utils.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/s3signer/utils.go rename to vendor/github.com/minio/minio-go/v6/pkg/s3signer/utils.go diff --git a/vendor/github.com/minio/minio-go/pkg/s3utils/utils.go b/vendor/github.com/minio/minio-go/v6/pkg/s3utils/utils.go similarity index 98% rename from vendor/github.com/minio/minio-go/pkg/s3utils/utils.go rename to vendor/github.com/minio/minio-go/v6/pkg/s3utils/utils.go index c80cfba7eb..9af2997b74 100644 --- a/vendor/github.com/minio/minio-go/pkg/s3utils/utils.go +++ b/vendor/github.com/minio/minio-go/v6/pkg/s3utils/utils.go @@ -47,8 +47,8 @@ func IsValidDomain(host string) bool { if host[len(host)-1:] == "_" || host[:1] == "_" { return false } - // host cannot start or end with a "." - if host[len(host)-1:] == "." || host[:1] == "." { + // host cannot start with a "." + if host[:1] == "." { return false } // All non alphanumeric characters are invalid. @@ -282,7 +282,7 @@ func checkBucketNameCommon(bucketName string, strict bool) (err error) { if ipAddress.MatchString(bucketName) { return errors.New("Bucket name cannot be an ip address") } - if strings.Contains(bucketName, "..") { + if strings.Contains(bucketName, "..") || strings.Contains(bucketName, ".-") || strings.Contains(bucketName, "-.") { return errors.New("Bucket name contains invalid characters") } if strict { diff --git a/vendor/github.com/minio/minio-go/pkg/set/stringset.go b/vendor/github.com/minio/minio-go/v6/pkg/set/stringset.go similarity index 100% rename from vendor/github.com/minio/minio-go/pkg/set/stringset.go rename to vendor/github.com/minio/minio-go/v6/pkg/set/stringset.go diff --git a/vendor/github.com/minio/minio-go/post-policy.go b/vendor/github.com/minio/minio-go/v6/post-policy.go similarity index 100% rename from vendor/github.com/minio/minio-go/post-policy.go rename to vendor/github.com/minio/minio-go/v6/post-policy.go diff --git a/vendor/github.com/minio/minio-go/retry-continous.go b/vendor/github.com/minio/minio-go/v6/retry-continous.go similarity index 100% rename from vendor/github.com/minio/minio-go/retry-continous.go rename to vendor/github.com/minio/minio-go/v6/retry-continous.go diff --git a/vendor/github.com/minio/minio-go/retry.go b/vendor/github.com/minio/minio-go/v6/retry.go similarity index 95% rename from vendor/github.com/minio/minio-go/retry.go rename to vendor/github.com/minio/minio-go/v6/retry.go index bd16fbb5ad..2c608bab54 100644 --- a/vendor/github.com/minio/minio-go/retry.go +++ b/vendor/github.com/minio/minio-go/v6/retry.go @@ -111,6 +111,9 @@ func isHTTPReqErrorRetryable(err error) bool { } else if strings.Contains(err.Error(), "net/http: HTTP/1.x transport connection broken") { // If error is transport connection broken, retry. return true + } else if strings.Contains(err.Error(), "net/http: timeout awaiting response headers") { + // Retry errors due to server not sending the response before timeout + return true } } return false @@ -143,6 +146,7 @@ var retryableHTTPStatusCodes = map[int]struct{}{ http.StatusInternalServerError: {}, http.StatusBadGateway: {}, http.StatusServiceUnavailable: {}, + http.StatusGatewayTimeout: {}, // Add more HTTP status codes here. } diff --git a/vendor/github.com/minio/minio-go/s3-endpoints.go b/vendor/github.com/minio/minio-go/v6/s3-endpoints.go similarity index 97% rename from vendor/github.com/minio/minio-go/s3-endpoints.go rename to vendor/github.com/minio/minio-go/v6/s3-endpoints.go index ef4ae74b5e..989f58c7ec 100644 --- a/vendor/github.com/minio/minio-go/s3-endpoints.go +++ b/vendor/github.com/minio/minio-go/v6/s3-endpoints.go @@ -29,6 +29,7 @@ var awsS3EndpointMap = map[string]string{ "eu-west-3": "s3.dualstack.eu-west-3.amazonaws.com", "eu-central-1": "s3.dualstack.eu-central-1.amazonaws.com", "eu-north-1": "s3.dualstack.eu-north-1.amazonaws.com", + "ap-east-1": "s3.dualstack.ap-east-1.amazonaws.com", "ap-south-1": "s3.dualstack.ap-south-1.amazonaws.com", "ap-southeast-1": "s3.dualstack.ap-southeast-1.amazonaws.com", "ap-southeast-2": "s3.dualstack.ap-southeast-2.amazonaws.com", diff --git a/vendor/github.com/minio/minio-go/s3-error.go b/vendor/github.com/minio/minio-go/v6/s3-error.go similarity index 100% rename from vendor/github.com/minio/minio-go/s3-error.go rename to vendor/github.com/minio/minio-go/v6/s3-error.go diff --git a/vendor/github.com/minio/minio-go/v6/staticcheck.conf b/vendor/github.com/minio/minio-go/v6/staticcheck.conf new file mode 100644 index 0000000000..71cc6f536a --- /dev/null +++ b/vendor/github.com/minio/minio-go/v6/staticcheck.conf @@ -0,0 +1 @@ +checks = ["all", "-ST1005", "-ST1017", "-SA9004", "-ST1000", "-S1021"] \ No newline at end of file diff --git a/vendor/github.com/minio/minio-go/transport.go b/vendor/github.com/minio/minio-go/v6/transport.go similarity index 100% rename from vendor/github.com/minio/minio-go/transport.go rename to vendor/github.com/minio/minio-go/v6/transport.go diff --git a/vendor/github.com/minio/minio-go/utils.go b/vendor/github.com/minio/minio-go/v6/utils.go similarity index 98% rename from vendor/github.com/minio/minio-go/utils.go rename to vendor/github.com/minio/minio-go/v6/utils.go index dbad3b103b..fc30c1ab7c 100644 --- a/vendor/github.com/minio/minio-go/utils.go +++ b/vendor/github.com/minio/minio-go/v6/utils.go @@ -32,7 +32,7 @@ import ( "strings" "time" - "github.com/minio/minio-go/pkg/s3utils" + "github.com/minio/minio-go/v6/pkg/s3utils" ) // xmlDecoder provide decoded value in xml. @@ -229,7 +229,7 @@ var supportedHeaders = []string{ // isStorageClassHeader returns true if the header is a supported storage class header func isStorageClassHeader(headerKey string) bool { - return strings.ToLower(amzStorageClass) == strings.ToLower(headerKey) + return strings.EqualFold(amzStorageClass, headerKey) } // isStandardHeader returns true if header is a supported header and not a custom header diff --git a/vendor/modules.txt b/vendor/modules.txt index 386d6938bb..7a1125ff2a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -140,13 +140,13 @@ github.com/mattn/go-sqlite3 github.com/matttproud/golang_protobuf_extensions/pbutil # github.com/miekg/dns v1.1.15 github.com/miekg/dns -# github.com/minio/minio-go v0.0.0-20190422205105-a8704b60278f -github.com/minio/minio-go -github.com/minio/minio-go/pkg/credentials -github.com/minio/minio-go/pkg/encrypt -github.com/minio/minio-go/pkg/s3signer -github.com/minio/minio-go/pkg/s3utils -github.com/minio/minio-go/pkg/set +# github.com/minio/minio-go/v6 v6.0.34 +github.com/minio/minio-go/v6 +github.com/minio/minio-go/v6/pkg/credentials +github.com/minio/minio-go/v6/pkg/encrypt +github.com/minio/minio-go/v6/pkg/s3signer +github.com/minio/minio-go/v6/pkg/s3utils +github.com/minio/minio-go/v6/pkg/set # github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir # github.com/mitchellh/go-testing-interface v1.0.0 diff --git a/web/handlers.go b/web/handlers.go index a67c585ec1..9269a04e93 100644 --- a/web/handlers.go +++ b/web/handlers.go @@ -57,7 +57,7 @@ type Handler struct { func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { now := time.Now() - mlog.Debug(fmt.Sprintf("%v - %v", r.Method, r.URL.Path)) + mlog.Debug("request:", mlog.String("method", r.Method), mlog.String("url", r.URL.Path)) c := &Context{} c.App = app.New( diff --git a/web/oauth.go b/web/oauth.go index 9bcc61d0ce..5e971c9f5a 100644 --- a/web/oauth.go +++ b/web/oauth.go @@ -305,7 +305,6 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } if action == model.OAUTH_ACTION_MOBILE { - ReturnStatusOK(w) return }