diff options
214 files changed, 20822 insertions, 826 deletions
diff --git a/.gitignore b/.gitignore index b9a6a4bcd..e3646b174 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ web/static/js/libs*.js config/active.dat config/config.json +/plugins # Enteprise imports file imports/imports.go @@ -90,4 +91,4 @@ webapp/coverage tags .idea -debug
\ No newline at end of file +debug @@ -209,14 +209,20 @@ check-client-style: check-server-style: govet @echo Running GOFMT - $(eval GOFMT_OUTPUT := $(shell gofmt -d -s api/ model/ store/ utils/ manualtesting/ einterfaces/ cmd/platform/ 2>&1)) - @echo "$(GOFMT_OUTPUT)" - @if [ ! "$(GOFMT_OUTPUT)" ]; then \ - echo "gofmt success"; \ - else \ - echo "gofmt failure"; \ - exit 1; \ - fi + + @for package in $(TE_PACKAGES) $(EE_PACKAGES); do \ + echo "Checking "$$package; \ + files=$$(go list -f '{{range .GoFiles}}{{$$.Dir}}/{{.}} {{end}}' $$package); \ + if [ "$$files" ]; then \ + gofmt_output=$$(gofmt -d -s $$files 2>&1); \ + if [ "$$gofmt_output" ]; then \ + echo "$$gofmt_output"; \ + echo "gofmt failure"; \ + exit 1; \ + fi; \ + fi; \ + done + @echo "gofmt success"; \ check-style: check-client-style check-server-style @@ -535,37 +541,10 @@ setup-mac: govet: @echo Running GOVET - - $(GO) vet $(GOFLAGS) ./api || exit 1 - $(GO) vet $(GOFLAGS) ./api4 || exit 1 - $(GO) vet $(GOFLAGS) ./app || exit 1 - $(GO) vet $(GOFLAGS) ./app/plugin || exit 1 - $(GO) vet $(GOFLAGS) ./app/plugin/jira || exit 1 - $(GO) vet $(GOFLAGS) ./cmd/platform || exit 1 - $(GO) vet $(GOFLAGS) ./einterfaces || exit 1 - $(GO) vet $(GOFLAGS) ./jobs || exit 1 - $(GO) vet $(GOFLAGS) ./manualtesting || exit 1 - $(GO) vet $(GOFLAGS) ./model || exit 1 - $(GO) vet $(GOFLAGS) ./model/gitlab || exit 1 - $(GO) vet $(GOFLAGS) ./store || exit 1 - $(GO) vet $(GOFLAGS) ./utils || exit 1 - $(GO) vet $(GOFLAGS) ./web || exit 1 + $(GO) vet $(GOFLAGS) $(TE_PACKAGES) || exit 1 ifeq ($(BUILD_ENTERPRISE_READY),true) - $(GO) vet $(GOFLAGS) ./enterprise/account_migration || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/brand || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/cluster || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/compliance || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/data_retention || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/elasticsearch || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/emoji || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/imports || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/ldap || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/metrics || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/mfa || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/oauth/google || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/oauth/office365 || exit 1 - $(GO) vet $(GOFLAGS) ./enterprise/saml || exit 1 + $(GO) vet $(GOFLAGS) $(EE_PACKAGES) || exit 1 endif todo: diff --git a/api/apitestlib.go b/api/apitestlib.go index e857a5080..94cd5a926 100644 --- a/api/apitestlib.go +++ b/api/apitestlib.go @@ -39,7 +39,7 @@ func SetupEnterprise() *TestHelper { utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() - utils.License.Features.SetDefaults() + utils.License().Features.SetDefaults() app.NewServer() app.InitStores() InitRouter() diff --git a/api/channel.go b/api/channel.go index 2a56e7c93..50dc840ff 100644 --- a/api/channel.go +++ b/api/channel.go @@ -448,23 +448,14 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - var memberCount int64 - if memberCount, err = app.GetChannelMemberCount(id); err != nil { - c.Err = err + if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) { + c.SetPermissionError(model.PERMISSION_DELETE_PUBLIC_CHANNEL) return } - // Allow delete if user is the only member left in channel - if memberCount > 1 { - if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) { - c.SetPermissionError(model.PERMISSION_DELETE_PUBLIC_CHANNEL) - return - } - - if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) { - c.SetPermissionError(model.PERMISSION_DELETE_PRIVATE_CHANNEL) - return - } + if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) { + c.SetPermissionError(model.PERMISSION_DELETE_PRIVATE_CHANNEL) + return } err = app.DeleteChannel(channel, c.Session.UserId) diff --git a/api/channel_test.go b/api/channel_test.go index 6ed4d55fa..c6fa23282 100644 --- a/api/channel_test.go +++ b/api/channel_test.go @@ -95,23 +95,23 @@ func TestCreateChannel(t *testing.T) { t.Fatal("Should have errored out on direct channel type") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL utils.SetDefaultRolesBasedOnConfig() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() channel2 := &model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel3 := &model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -171,8 +171,8 @@ func TestCreateChannel(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() channel4 := model.Channel{DisplayName: "Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} @@ -351,22 +351,22 @@ func TestUpdateChannel(t *testing.T) { t.Fatal("should have failed - channel deleted") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel2 := th.CreateChannel(Client, team) @@ -388,9 +388,9 @@ func TestUpdateChannel(t *testing.T) { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_CHANNEL_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_CHANNEL_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() MakeUserChannelUser(th.BasicUser, channel2) MakeUserChannelUser(th.BasicUser, channel3) @@ -427,9 +427,9 @@ func TestUpdateChannel(t *testing.T) { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_TEAM_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.UpdateChannel(channel2); err == nil { @@ -453,9 +453,9 @@ func TestUpdateChannel(t *testing.T) { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_SYSTEM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.UpdateChannel(channel2); err == nil { @@ -475,8 +475,8 @@ func TestUpdateChannel(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() if _, err := Client.UpdateChannel(channel2); err != nil { @@ -586,22 +586,22 @@ func TestUpdateChannelHeader(t *testing.T) { t.Fatal("should have errored non-channel member trying to update header") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -697,8 +697,8 @@ func TestUpdateChannelHeader(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() if _, err := SystemAdminClient.UpdateChannelHeader(data2); err != nil { @@ -768,22 +768,22 @@ func TestUpdateChannelPurpose(t *testing.T) { t.Fatal("should have errored non-channel member trying to update purpose") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -879,8 +879,8 @@ func TestUpdateChannelPurpose(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() if _, err := SystemAdminClient.UpdateChannelHeader(data2); err != nil { t.Fatal(err) @@ -1324,22 +1324,22 @@ func TestDeleteChannel(t *testing.T) { t.Fatal("should have failed - channel already deleted") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginSystemAdmin() @@ -1362,9 +1362,9 @@ func TestDeleteChannel(t *testing.T) { t.Fatal(err) } - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.TeamSettings.RestrictPublicChannelDeletion = model.PERMISSIONS_CHANNEL_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelDeletion = model.PERMISSIONS_CHANNEL_ADMIN utils.SetDefaultRolesBasedOnConfig() @@ -1418,9 +1418,9 @@ func TestDeleteChannel(t *testing.T) { UpdateUserToNonTeamAdmin(th.BasicUser, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.TeamSettings.RestrictPublicChannelDeletion = model.PERMISSIONS_TEAM_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelDeletion = model.PERMISSIONS_TEAM_ADMIN utils.SetDefaultRolesBasedOnConfig() @@ -1453,9 +1453,9 @@ func TestDeleteChannel(t *testing.T) { t.Fatal(err) } - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.TeamSettings.RestrictPublicChannelDeletion = model.PERMISSIONS_SYSTEM_ADMIN *utils.Cfg.TeamSettings.RestrictPrivateChannelDeletion = model.PERMISSIONS_SYSTEM_ADMIN utils.SetDefaultRolesBasedOnConfig() @@ -1476,9 +1476,8 @@ func TestDeleteChannel(t *testing.T) { t.Fatal("should have errored not system admin") } - // Only one left in channel, should be able to delete - if _, err := Client.DeleteChannel(channel4.Id); err != nil { - t.Fatal(err) + if _, err := Client.DeleteChannel(channel4.Id); err == nil { + t.Fatal("Should not be able to delete channel, even though only one user is left") } th.LoginSystemAdmin() @@ -1491,8 +1490,8 @@ func TestDeleteChannel(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() channel2 = th.CreateChannel(Client, team) @@ -1599,17 +1598,17 @@ func TestAddChannelMember(t *testing.T) { } // Add a license - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() // Check that a regular channel user can add other users. @@ -1622,9 +1621,9 @@ func TestAddChannelMember(t *testing.T) { // Test with CHANNEL_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_CHANNEL_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel5 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -1636,9 +1635,9 @@ func TestAddChannelMember(t *testing.T) { MakeUserChannelAdmin(user1, channel5) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.AddChannelMember(channel5.Id, user2.Id); err != nil { @@ -1647,9 +1646,9 @@ func TestAddChannelMember(t *testing.T) { // Test with TEAM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel6 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -1661,9 +1660,9 @@ func TestAddChannelMember(t *testing.T) { UpdateUserToTeamAdmin(user1, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.AddChannelMember(channel6.Id, user2.Id); err != nil { @@ -1672,9 +1671,9 @@ func TestAddChannelMember(t *testing.T) { // Test with SYSTEM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_SYSTEM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel7 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -1773,17 +1772,17 @@ func TestRemoveChannelMember(t *testing.T) { } // Add a license - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() // Check that a regular channel user can remove other users. @@ -1797,9 +1796,9 @@ func TestRemoveChannelMember(t *testing.T) { // Test with CHANNEL_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_CHANNEL_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel5 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -1812,9 +1811,9 @@ func TestRemoveChannelMember(t *testing.T) { MakeUserChannelAdmin(user1, channel5) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() if _, err := Client.RemoveChannelMember(channel5.Id, user2.Id); err != nil { t.Fatal(err) @@ -1822,9 +1821,9 @@ func TestRemoveChannelMember(t *testing.T) { // Test with TEAM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel6 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} @@ -1837,9 +1836,9 @@ func TestRemoveChannelMember(t *testing.T) { UpdateUserToTeamAdmin(user1, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.RemoveChannelMember(channel6.Id, user2.Id); err != nil { @@ -1848,9 +1847,9 @@ func TestRemoveChannelMember(t *testing.T) { // Test with SYSTEM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_SYSTEM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel7 := &model.Channel{DisplayName: "A Test API Name", Name: "zz" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id} diff --git a/api/command_test.go b/api/command_test.go index 9e6696d64..dd4180b16 100644 --- a/api/command_test.go +++ b/api/command_test.go @@ -233,10 +233,13 @@ func TestTestCommand(t *testing.T) { channel1 := th.SystemAdminChannel enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() *utils.Cfg.ServiceSettings.EnableCommands = true + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" cmd1 := &model.Command{ URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX_V3 + "/teams/command_test", diff --git a/api/context.go b/api/context.go index d0036d077..286221092 100644 --- a/api/context.go +++ b/api/context.go @@ -149,7 +149,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host) w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) - w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed)) + w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed())) // Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking if !h.isApi { @@ -321,7 +321,7 @@ func (c *Context) UserRequired() { func (c *Context) MfaRequired() { // Must be licensed for MFA and have it configured for enforcement - if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { + if !utils.IsLicensed() || !*utils.License().Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { return } diff --git a/api/license.go b/api/license.go index 7a9e57677..162dd6762 100644 --- a/api/license.go +++ b/api/license.go @@ -97,7 +97,7 @@ func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) var clientLicense map[string]string if useSanitizedLicense { - clientLicense = utils.ClientLicense + clientLicense = utils.ClientLicense() } else { clientLicense = utils.GetSanitizedClientLicense() } diff --git a/api/license_test.go b/api/license_test.go index 978e044cc..15d3b9eef 100644 --- a/api/license_test.go +++ b/api/license_test.go @@ -29,7 +29,7 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("cache should be empty") } - utils.ClientLicense["IsLicensed"] = "true" + utils.SetClientLicense(map[string]string{"IsLicensed": "true"}) if cache_result, err := Client.GetClientLicenceConfig(result.Etag); err != nil { t.Fatal(err) @@ -37,7 +37,7 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("result should not be empty") } - utils.ClientLicense["SomeFeature"] = "true" + utils.SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) if cache_result, err := Client.GetClientLicenceConfig(result.Etag); err != nil { t.Fatal(err) @@ -45,6 +45,6 @@ func TestGetLicenceConfig(t *testing.T) { t.Fatal("result should not be empty") } - utils.ClientLicense = map[string]string{"IsLicensed": "false"} + utils.SetClientLicense(map[string]string{"IsLicensed": "false"}) } } diff --git a/api/post_test.go b/api/post_test.go index c7bd7a04c..29c1f791a 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -188,10 +188,13 @@ func testCreatePostWithOutgoingHook( channel := th.CreateChannel(Client, team) enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" var hook *model.OutgoingWebhook var post *model.Post @@ -406,15 +409,15 @@ func TestUpdatePost(t *testing.T) { } // Test licensed policy controls for edit post - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_NEVER @@ -948,15 +951,15 @@ func TestDeletePosts(t *testing.T) { } // Test licensed policy controls for delete post - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam) @@ -1011,8 +1014,8 @@ func TestDeletePosts(t *testing.T) { } // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() time.Sleep(10 * time.Millisecond) @@ -1359,10 +1362,13 @@ func TestGetOpenGraphMetadata(t *testing.T) { Client := th.BasicClient enableLinkPreviews := *utils.Cfg.ServiceSettings.EnableLinkPreviews + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { *utils.Cfg.ServiceSettings.EnableLinkPreviews = enableLinkPreviews + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() *utils.Cfg.ServiceSettings.EnableLinkPreviews = true + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" ogDataCacheMissCount := 0 diff --git a/api/team.go b/api/team.go index b1fcdfd78..4010a0ac5 100644 --- a/api/team.go +++ b/api/team.go @@ -118,7 +118,7 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) - if utils.IsLicensed && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_INVITE_USER) { + if utils.IsLicensed() && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_INVITE_USER) { errorId := "" if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_SYSTEM_ADMIN { errorId = "api.team.invite_members.restricted_system_admin.app_error" diff --git a/api/team_test.go b/api/team_test.go index 3c05588ce..99e54a904 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -95,12 +95,12 @@ func TestAddUserToTeam(t *testing.T) { // Restore config/license at end of test case. restrictTeamInvite := *utils.Cfg.TeamSettings.RestrictTeamInvite - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { *utils.Cfg.TeamSettings.RestrictTeamInvite = restrictTeamInvite - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() @@ -115,9 +115,9 @@ func TestAddUserToTeam(t *testing.T) { } // Add an EE license. - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() // Check that a regular user can't add someone to the team. @@ -130,9 +130,9 @@ func TestAddUserToTeam(t *testing.T) { UpdateUserToTeamAdmin(th.BasicUser, th.BasicTeam) app.InvalidateAllCaches() *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() user5 := th.CreateUser(th.BasicClient) @@ -386,16 +386,16 @@ func TestInviteMembers(t *testing.T) { t.Fatal(err) } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() if _, err := Client.InviteMembers(invites); err == nil { diff --git a/api/user.go b/api/user.go index bb63cc7e2..8843a3730 100644 --- a/api/user.go +++ b/api/user.go @@ -302,7 +302,7 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) { il.ClientCfg = utils.ClientCfg if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - il.LicenseCfg = utils.ClientLicense + il.LicenseCfg = utils.ClientLicense() } else { il.LicenseCfg = utils.GetSanitizedClientLicense() } @@ -1108,7 +1108,7 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) { } func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + if !utils.IsLicensed() || !*utils.License().Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { rdata := map[string]string{} rdata["mfa_required"] = "false" w.Write([]byte(model.MapToJson(rdata))) diff --git a/api/user_test.go b/api/user_test.go index f76403ee1..8669a7bbe 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -1836,18 +1836,18 @@ func TestUpdateMfa(t *testing.T) { th := Setup().InitBasic() Client := th.BasicClient - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() enableMfa := *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }() - utils.IsLicensed = false - utils.License = &model.License{Features: &model.Features{}} - if utils.License.Features.MFA == nil { - utils.License.Features.MFA = new(bool) + utils.SetIsLicensed(false) + utils.SetLicense(&model.License{Features: &model.Features{}}) + if utils.License().Features.MFA == nil { + utils.License().Features.MFA = new(bool) } team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -1874,8 +1874,8 @@ func TestUpdateMfa(t *testing.T) { t.Fatal("should have failed - not licensed") } - utils.IsLicensed = true - *utils.License.Features.MFA = true + utils.SetIsLicensed(true) + *utils.License().Features.MFA = true *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = true if _, err := Client.UpdateMfa(true, "123456"); err == nil { diff --git a/api4/api.go b/api4/api.go index be957d63b..6e9534d40 100644 --- a/api4/api.go +++ b/api4/api.go @@ -55,9 +55,8 @@ type Routes struct { PublicFile *mux.Router // 'files/{file_id:[A-Za-z0-9]+}/public' - Commands *mux.Router // 'api/v4/commands' - Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}' - CommandsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/commands' + Commands *mux.Router // 'api/v4/commands' + Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}' Hooks *mux.Router // 'api/v4/hooks' IncomingHooks *mux.Router // 'api/v4/hooks/incoming' @@ -149,7 +148,6 @@ func InitApi(full bool) { BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter() BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter() - BaseRoutes.CommandsForTeam = BaseRoutes.Team.PathPrefix("/commands").Subrouter() BaseRoutes.Hooks = BaseRoutes.ApiRoot.PathPrefix("/hooks").Subrouter() BaseRoutes.IncomingHooks = BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter() diff --git a/api4/apitestlib.go b/api4/apitestlib.go index 7f69b3690..b10e639e3 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -55,7 +55,7 @@ func SetupEnterprise() *TestHelper { utils.Cfg.EmailSettings.SMTPPort = "2500" utils.Cfg.EmailSettings.FeedbackEmail = "test@example.com" utils.DisableDebugLogForTest() - utils.License.Features.SetDefaults() + utils.License().Features.SetDefaults() app.NewServer() app.InitStores() InitRouter() diff --git a/api4/channel.go b/api4/channel.go index 604c47464..281fb6ac4 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -534,19 +534,12 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) { return } - var memberCount int64 - if memberCount, err = app.GetChannelMemberCount(c.Params.ChannelId); err != nil { - c.Err = err - return - } - if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) { c.SetPermissionError(model.PERMISSION_DELETE_PUBLIC_CHANNEL) return } - // Allow delete if there's only one member left in a private channel - if memberCount > 1 && channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) { + if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) { c.SetPermissionError(model.PERMISSION_DELETE_PRIVATE_CHANNEL) return } diff --git a/api4/channel_test.go b/api4/channel_test.go index a1c5d2ad8..1747d79b9 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -82,22 +82,22 @@ func TestCreateChannel(t *testing.T) { th.LoginBasic() // Check permissions with policy config changes - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelCreation restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelCreation defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelCreation = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelCreation = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelCreation = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelCreation = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() channel.Name = GenerateTestChannelName() @@ -165,8 +165,8 @@ func TestCreateChannel(t *testing.T) { CheckNoError(t, resp) // Check that if unlicensed the policy restriction is not enforced. - utils.IsLicensed = false - utils.License = nil + utils.SetIsLicensed(false) + utils.SetLicense(nil) utils.SetDefaultRolesBasedOnConfig() channel.Name = GenerateTestChannelName() @@ -879,22 +879,22 @@ func TestDeleteChannel(t *testing.T) { _, resp = th.SystemAdminClient.DeleteChannel(publicChannel5.Id) CheckNoError(t, resp) - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() restrictPublicChannel := *utils.Cfg.TeamSettings.RestrictPublicChannelManagement restrictPrivateChannel := *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement defer func() { *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = restrictPublicChannel *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = restrictPrivateChannel - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPublicChannelManagement = model.PERMISSIONS_ALL *utils.Cfg.TeamSettings.RestrictPrivateChannelManagement = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th = Setup().InitBasic().InitSystemAdmin() @@ -956,9 +956,9 @@ func TestDeleteChannel(t *testing.T) { // successful delete by team admin UpdateUserToTeamAdmin(user, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckNoError(t, resp) @@ -971,9 +971,9 @@ func TestDeleteChannel(t *testing.T) { utils.SetDefaultRolesBasedOnConfig() UpdateUserToNonTeamAdmin(user, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() // channels created by SystemAdmin publicChannel6 = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_OPEN) @@ -1003,9 +1003,9 @@ func TestDeleteChannel(t *testing.T) { // successful delete by team admin UpdateUserToTeamAdmin(th.BasicUser, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckNoError(t, resp) @@ -1045,9 +1045,9 @@ func TestDeleteChannel(t *testing.T) { // cannot delete by team admin UpdateUserToTeamAdmin(th.BasicUser, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() _, resp = Client.DeleteChannel(publicChannel6.Id) CheckForbiddenStatus(t, resp) @@ -1064,15 +1064,13 @@ func TestDeleteChannel(t *testing.T) { // last member of a public channel should have required permission to delete publicChannel6 = th.CreateChannelWithClient(th.Client, model.CHANNEL_OPEN) - _, resp = Client.DeleteChannel(publicChannel6.Id) CheckForbiddenStatus(t, resp) - // last member of a private channel should be able to delete it regardless of required permissions + // last member of a private channel should not be able to delete it if they don't have required permissions privateChannel7 = th.CreateChannelWithClient(th.Client, model.CHANNEL_PRIVATE) - _, resp = Client.DeleteChannel(privateChannel7.Id) - CheckNoError(t, resp) + CheckForbiddenStatus(t, resp) } func TestRestoreChannel(t *testing.T) { @@ -1798,17 +1796,17 @@ func TestAddChannelMember(t *testing.T) { Client.Logout() // Add a license - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() // Check that a regular channel user can add other users. @@ -1825,9 +1823,9 @@ func TestAddChannelMember(t *testing.T) { // Test with CHANNEL_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_CHANNEL_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() Client.Login(user2.Username, user2.Password) @@ -1843,9 +1841,9 @@ func TestAddChannelMember(t *testing.T) { MakeUserChannelAdmin(user, privateChannel) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() Client.Login(user.Username, user.Password) @@ -1855,9 +1853,9 @@ func TestAddChannelMember(t *testing.T) { // Test with TEAM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() Client.Login(user2.Username, user2.Password) @@ -1873,9 +1871,9 @@ func TestAddChannelMember(t *testing.T) { UpdateUserToTeamAdmin(user, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() Client.Login(user.Username, user.Password) @@ -1885,9 +1883,9 @@ func TestAddChannelMember(t *testing.T) { // Test with SYSTEM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_SYSTEM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() Client.Login(user2.Username, user2.Password) @@ -1982,17 +1980,17 @@ func TestRemoveChannelMember(t *testing.T) { CheckNoError(t, resp) // Add a license - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() // Check that a regular channel user can remove other users. @@ -2007,9 +2005,9 @@ func TestRemoveChannelMember(t *testing.T) { // Test with CHANNEL_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_CHANNEL_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() privateChannel = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) @@ -2023,18 +2021,18 @@ func TestRemoveChannelMember(t *testing.T) { MakeUserChannelAdmin(user1, privateChannel) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() _, resp = Client.RemoveUserFromChannel(privateChannel.Id, user2.Id) CheckNoError(t, resp) // Test with TEAM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() privateChannel = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) @@ -2048,18 +2046,18 @@ func TestRemoveChannelMember(t *testing.T) { UpdateUserToTeamAdmin(user1, team) app.InvalidateAllCaches() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() _, resp = Client.RemoveUserFromChannel(privateChannel.Id, user2.Id) CheckNoError(t, resp) // Test with SYSTEM_ADMIN level permission. *utils.Cfg.TeamSettings.RestrictPrivateChannelManageMembers = model.PERMISSIONS_SYSTEM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() privateChannel = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) diff --git a/api4/command_test.go b/api4/command_test.go index 467d45955..b0d5f4baa 100644 --- a/api4/command_test.go +++ b/api4/command_test.go @@ -388,10 +388,13 @@ func TestExecuteCommand(t *testing.T) { channel := th.BasicChannel enableCommands := *utils.Cfg.ServiceSettings.EnableCommands + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { utils.Cfg.ServiceSettings.EnableCommands = &enableCommands + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() *utils.Cfg.ServiceSettings.EnableCommands = true + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost" postCmd := &model.Command{ CreatorId: th.BasicUser.Id, diff --git a/api4/context.go b/api4/context.go index d72b3593d..69351a098 100644 --- a/api4/context.go +++ b/api4/context.go @@ -129,7 +129,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host) w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) - w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed)) + w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed())) w.Header().Set("Content-Type", "application/json") @@ -252,7 +252,7 @@ func (c *Context) SessionRequired() { func (c *Context) MfaRequired() { // Must be licensed for MFA and have it configured for enforcement - if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { + if !utils.IsLicensed() || !*utils.License().Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { return } diff --git a/api4/job_test.go b/api4/job_test.go index 3dcdbe58b..511386810 100644 --- a/api4/job_test.go +++ b/api4/job_test.go @@ -18,7 +18,7 @@ func TestCreateJob(t *testing.T) { job := &model.Job{ Type: model.JOB_TYPE_DATA_RETENTION, - Data: map[string]interface{}{ + Data: map[string]string{ "thing": "stuff", }, } diff --git a/api4/openGraph_test.go b/api4/openGraph_test.go index 958abf604..df1af66fc 100644 --- a/api4/openGraph_test.go +++ b/api4/openGraph_test.go @@ -19,10 +19,13 @@ func TestGetOpenGraphMetadata(t *testing.T) { Client := th.Client enableLinkPreviews := *utils.Cfg.ServiceSettings.EnableLinkPreviews + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { *utils.Cfg.ServiceSettings.EnableLinkPreviews = enableLinkPreviews + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() *utils.Cfg.ServiceSettings.EnableLinkPreviews = true + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" ogDataCacheMissCount := 0 diff --git a/api4/post_test.go b/api4/post_test.go index f136ba676..c09cb77d1 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -119,14 +119,17 @@ func testCreatePostWithOutgoingHook( enableOutgoingHooks := utils.Cfg.ServiceSettings.EnableOutgoingWebhooks enableAdminOnlyHooks := utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations + allowedInternalConnections := *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections defer func() { utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = enableOutgoingHooks utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = enableAdminOnlyHooks utils.SetDefaultRolesBasedOnConfig() + utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections }() utils.Cfg.ServiceSettings.EnableOutgoingWebhooks = true *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true utils.SetDefaultRolesBasedOnConfig() + *utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost 127.0.0.1" var hook *model.OutgoingWebhook var post *model.Post @@ -363,18 +366,18 @@ func TestUpdatePost(t *testing.T) { Client := th.Client channel := th.BasicChannel - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() allowEditPost := *utils.Cfg.ServiceSettings.AllowEditPost defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) *utils.Cfg.ServiceSettings.AllowEditPost = allowEditPost utils.SetDefaultRolesBasedOnConfig() }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_ALWAYS utils.SetDefaultRolesBasedOnConfig() @@ -442,18 +445,18 @@ func TestPatchPost(t *testing.T) { Client := th.Client channel := th.BasicChannel - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() allowEditPost := *utils.Cfg.ServiceSettings.AllowEditPost defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) *utils.Cfg.ServiceSettings.AllowEditPost = allowEditPost utils.SetDefaultRolesBasedOnConfig() }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_ALWAYS utils.SetDefaultRolesBasedOnConfig() diff --git a/api4/system.go b/api4/system.go index ff3aab0d0..0c0fc7d12 100644 --- a/api4/system.go +++ b/api4/system.go @@ -258,7 +258,7 @@ func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) { var clientLicense map[string]string if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { - clientLicense = utils.ClientLicense + clientLicense = utils.ClientLicense() } else { clientLicense = utils.GetSanitizedClientLicense() } diff --git a/api4/team_test.go b/api4/team_test.go index 72c5dd6e6..21c842f65 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -848,12 +848,12 @@ func TestAddTeamMember(t *testing.T) { // Check effects of config and license changes. restrictTeamInvite := *utils.Cfg.TeamSettings.RestrictTeamInvite - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { *utils.Cfg.TeamSettings.RestrictTeamInvite = restrictTeamInvite - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() @@ -867,9 +867,9 @@ func TestAddTeamMember(t *testing.T) { CheckNoError(t, resp) // Add an EE license. - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -881,9 +881,9 @@ func TestAddTeamMember(t *testing.T) { UpdateUserToTeamAdmin(th.BasicUser, th.BasicTeam) app.InvalidateAllCaches() *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -907,9 +907,9 @@ func TestAddTeamMember(t *testing.T) { UpdateUserToNonTeamAdmin(th.BasicUser, th.BasicTeam) app.InvalidateAllCaches() *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -919,8 +919,8 @@ func TestAddTeamMember(t *testing.T) { // Reset config and license. *utils.Cfg.TeamSettings.RestrictTeamInvite = restrictTeamInvite - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -1062,12 +1062,12 @@ func TestAddTeamMembers(t *testing.T) { // Check effects of config and license changes. restrictTeamInvite := *utils.Cfg.TeamSettings.RestrictTeamInvite - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() defer func() { *utils.Cfg.TeamSettings.RestrictTeamInvite = restrictTeamInvite - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) utils.SetDefaultRolesBasedOnConfig() }() @@ -1081,9 +1081,9 @@ func TestAddTeamMembers(t *testing.T) { CheckNoError(t, resp) // Add an EE license. - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -1095,9 +1095,9 @@ func TestAddTeamMembers(t *testing.T) { UpdateUserToTeamAdmin(th.BasicUser, th.BasicTeam) app.InvalidateAllCaches() *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_TEAM_ADMIN - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() @@ -1121,9 +1121,9 @@ func TestAddTeamMembers(t *testing.T) { UpdateUserToNonTeamAdmin(th.BasicUser, th.BasicTeam) app.InvalidateAllCaches() *utils.Cfg.TeamSettings.RestrictTeamInvite = model.PERMISSIONS_ALL - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() utils.SetDefaultRolesBasedOnConfig() th.LoginBasic() diff --git a/api4/user.go b/api4/user.go index 16c1f4a74..365248c0f 100644 --- a/api4/user.go +++ b/api4/user.go @@ -675,7 +675,7 @@ func checkUserMfa(c *Context, w http.ResponseWriter, r *http.Request) { resp := map[string]interface{}{} resp["mfa_required"] = false - if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + if !utils.IsLicensed() || !*utils.License().Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { w.Write([]byte(model.StringInterfaceToJson(resp))) return } @@ -926,6 +926,7 @@ func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { if sessionId == "" { c.SetInvalidParam("session_id") + return } if err := app.RevokeSessionById(sessionId); err != nil { diff --git a/api4/user_test.go b/api4/user_test.go index 37ecd660d..894187469 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -1513,17 +1513,17 @@ func TestGetUsersNotInChannel(t *testing.T) { defer TearDown() Client := th.Client - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() enableMfa := *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() + utils.IsLicensed()= true + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} rteam, _ := Client.CreateTeam(&team) @@ -1574,18 +1574,18 @@ func TestCheckUserMfa(t *testing.T) { t.Fatal("should be false - mfa not active") } - isLicensed := utils.IsLicensed - license := utils.License + isLicensed := utils.IsLicensed() + license := utils.License() enableMfa := *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication defer func() { - utils.IsLicensed = isLicensed - utils.License = license + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = enableMfa }() - utils.IsLicensed = true - utils.License = &model.License{Features: &model.Features{}} - utils.License.Features.SetDefaults() - *utils.License.Features.MFA = true + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() + *utils.License().Features.MFA = true *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication = true th.LoginBasic() diff --git a/api4/webhook_test.go b/api4/webhook_test.go index 96451f8a7..80328e373 100644 --- a/api4/webhook_test.go +++ b/api4/webhook_test.go @@ -4,8 +4,11 @@ package api4 import ( + "bytes" + "net/http" "testing" + "github.com/mattermost/platform/app" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) @@ -893,3 +896,40 @@ func TestDeleteOutgoingHook(t *testing.T) { CheckForbiddenStatus(t, resp) }) } + +func TestCommandWebhooks(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + Client := th.SystemAdminClient + + cmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: "http://nowhere.com", + Method: model.COMMAND_METHOD_POST, + Trigger: "delayed"} + + cmd, _ = Client.CreateCommand(cmd) + args := &model.CommandArgs{ + TeamId: th.BasicTeam.Id, + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + } + hook, err := app.CreateCommandWebhook(cmd.Id, args) + if err != nil { + t.Fatal(err) + } + + if resp, _ := http.Post(Client.Url+"/hooks/commands/123123123123", "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); resp.StatusCode != http.StatusNotFound { + t.Fatal("expected not-found for non-existent hook") + } + + for i := 0; i < 5; i++ { + if _, err := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); err != nil { + t.Fatal(err) + } + } + + if resp, _ := http.Post(Client.Url+"/hooks/commands/"+hook.Id, "application/json", bytes.NewBufferString("{\"text\":\"this is a test\"}")); resp.StatusCode != http.StatusBadRequest { + t.Fatal("expected error for sixth usage") + } +} diff --git a/app/admin.go b/app/admin.go index 6fbe150c4..50ed769b6 100644 --- a/app/admin.go +++ b/app/admin.go @@ -13,10 +13,10 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/jobs" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" - "github.com/mattermost/platform/jobs" ) func GetLogs(page, perPage int) ([]string, *model.AppError) { diff --git a/app/apptestlib.go b/app/apptestlib.go index 0c7086c64..3966bdeea 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -28,7 +28,7 @@ func SetupEnterprise() *TestHelper { utils.Cfg.TeamSettings.MaxUsersPerTeam = 50 *utils.Cfg.RateLimitSettings.Enable = false utils.DisableDebugLogForTest() - utils.License.Features.SetDefaults() + utils.License().Features.SetDefaults() NewServer() InitStores() StartServer() diff --git a/app/authentication.go b/app/authentication.go index 5e1b4461f..b09234d5f 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -102,7 +102,7 @@ func CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string } func CheckUserMfa(user *model.User, token string) *model.AppError { - if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { + if !user.MfaActive || !utils.IsLicensed() || !*utils.License().Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication { return nil } @@ -143,7 +143,7 @@ func checkUserNotDisabled(user *model.User) *model.AppError { } func authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) { - ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed() && *utils.License().Features.LDAP if user.AuthService == model.USER_AUTH_SERVICE_LDAP { if !ldapAvailable { diff --git a/app/command.go b/app/command.go index bfb97ae0c..83500cc1f 100644 --- a/app/command.go +++ b/app/command.go @@ -45,10 +45,9 @@ func CreateCommandPost(post *model.Post, teamId string, response *model.CommandR parseSlackAttachment(post, response.Attachments) } - switch response.ResponseType { - case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL: + if response.ResponseType == model.COMMAND_RESPONSE_TYPE_IN_CHANNEL { return CreatePost(post, teamId, true) - case model.COMMAND_RESPONSE_TYPE_EPHEMERAL: + } else if response.ResponseType == "" || response.ResponseType == model.COMMAND_RESPONSE_TYPE_EPHEMERAL { if response.Text == "" { return post, nil } @@ -196,7 +195,12 @@ func ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.App p.Set("command", "/"+trigger) p.Set("text", message) - p.Set("response_url", "not supported yet") + + if hook, err := CreateCommandWebhook(cmd.Id, args); err != nil { + return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError) + } else { + p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id) + } method := "POST" if cmd.Method == model.COMMAND_METHOD_GET { @@ -209,11 +213,11 @@ func ExecuteCommand(args *model.CommandArgs) (*model.CommandResponse, *model.App req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } - if resp, err := utils.HttpClient().Do(req); err != nil { + if resp, err := utils.HttpClient(false).Do(req); err != nil { return nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]interface{}{"Trigger": trigger}, err.Error(), http.StatusInternalServerError) } else { if resp.StatusCode == http.StatusOK { - response := model.CommandResponseFromJson(resp.Body) + response := model.CommandResponseFromHTTPBody(resp.Header.Get("Content-Type"), resp.Body) if response == nil { return nil, model.NewAppError("command", "api.command.execute_command.failed_empty.app_error", map[string]interface{}{"Trigger": trigger}, "", http.StatusInternalServerError) } else { diff --git a/app/compliance.go b/app/compliance.go index cb1eece70..5b4f9de67 100644 --- a/app/compliance.go +++ b/app/compliance.go @@ -12,7 +12,7 @@ import ( ) func GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) { - if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance { + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance { return nil, model.NewLocAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "") } @@ -24,7 +24,7 @@ func GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError } func SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) { - if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil { + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance || einterfaces.GetComplianceInterface() == nil { return nil, model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "") } @@ -41,7 +41,7 @@ func SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppE } func GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) { - if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil { + if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance || einterfaces.GetComplianceInterface() == nil { return nil, model.NewLocAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "") } diff --git a/app/diagnostics.go b/app/diagnostics.go index 983ce8077..1df5b1feb 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -49,7 +49,7 @@ const ( var client *analytics.Client func SendDailyDiagnostics() { - if *utils.Cfg.LogSettings.EnableDiagnostics { + if *utils.Cfg.LogSettings.EnableDiagnostics && utils.IsLeader() { initDiagnostics("") trackActivity() trackConfig() @@ -174,49 +174,50 @@ func trackActivity() { func trackConfig() { SendDiagnostic(TRACK_CONFIG_SERVICE, map[string]interface{}{ - "web_server_mode": *utils.Cfg.ServiceSettings.WebserverMode, - "enable_security_fix_alert": *utils.Cfg.ServiceSettings.EnableSecurityFixAlert, - "enable_insecure_outgoing_connections": *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections, - "enable_incoming_webhooks": utils.Cfg.ServiceSettings.EnableIncomingWebhooks, - "enable_outgoing_webhooks": utils.Cfg.ServiceSettings.EnableOutgoingWebhooks, - "enable_commands": *utils.Cfg.ServiceSettings.EnableCommands, - "enable_only_admin_integrations": *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations, - "enable_post_username_override": utils.Cfg.ServiceSettings.EnablePostUsernameOverride, - "enable_post_icon_override": utils.Cfg.ServiceSettings.EnablePostIconOverride, - "enable_apiv3": *utils.Cfg.ServiceSettings.EnableAPIv3, - "enable_user_access_tokens": *utils.Cfg.ServiceSettings.EnableUserAccessTokens, - "enable_custom_emoji": *utils.Cfg.ServiceSettings.EnableCustomEmoji, - "enable_emoji_picker": *utils.Cfg.ServiceSettings.EnableEmojiPicker, - "restrict_custom_emoji_creation": *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation, - "enable_testing": utils.Cfg.ServiceSettings.EnableTesting, - "enable_developer": *utils.Cfg.ServiceSettings.EnableDeveloper, - "enable_multifactor_authentication": *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication, - "enforce_multifactor_authentication": *utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication, - "enable_oauth_service_provider": utils.Cfg.ServiceSettings.EnableOAuthServiceProvider, - "connection_security": *utils.Cfg.ServiceSettings.ConnectionSecurity, - "uses_letsencrypt": *utils.Cfg.ServiceSettings.UseLetsEncrypt, - "forward_80_to_443": *utils.Cfg.ServiceSettings.Forward80To443, - "maximum_login_attempts": utils.Cfg.ServiceSettings.MaximumLoginAttempts, - "session_length_web_in_days": *utils.Cfg.ServiceSettings.SessionLengthWebInDays, - "session_length_mobile_in_days": *utils.Cfg.ServiceSettings.SessionLengthMobileInDays, - "session_length_sso_in_days": *utils.Cfg.ServiceSettings.SessionLengthSSOInDays, - "session_cache_in_minutes": *utils.Cfg.ServiceSettings.SessionCacheInMinutes, - "isdefault_site_url": isDefault(*utils.Cfg.ServiceSettings.SiteURL, model.SERVICE_SETTINGS_DEFAULT_SITE_URL), - "isdefault_tls_cert_file": isDefault(*utils.Cfg.ServiceSettings.TLSCertFile, model.SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE), - "isdefault_tls_key_file": isDefault(*utils.Cfg.ServiceSettings.TLSKeyFile, model.SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE), - "isdefault_read_timeout": isDefault(*utils.Cfg.ServiceSettings.ReadTimeout, model.SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT), - "isdefault_write_timeout": isDefault(*utils.Cfg.ServiceSettings.WriteTimeout, model.SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT), - "isdefault_google_developer_key": isDefault(utils.Cfg.ServiceSettings.GoogleDeveloperKey, ""), - "isdefault_allow_cors_from": isDefault(*utils.Cfg.ServiceSettings.AllowCorsFrom, model.SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM), - "restrict_post_delete": *utils.Cfg.ServiceSettings.RestrictPostDelete, - "allow_edit_post": *utils.Cfg.ServiceSettings.AllowEditPost, - "post_edit_time_limit": *utils.Cfg.ServiceSettings.PostEditTimeLimit, - "enable_user_typing_messages": *utils.Cfg.ServiceSettings.EnableUserTypingMessages, - "enable_channel_viewed_messages": *utils.Cfg.ServiceSettings.EnableChannelViewedMessages, - "time_between_user_typing_updates_milliseconds": *utils.Cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, - "cluster_log_timeout_milliseconds": *utils.Cfg.ServiceSettings.ClusterLogTimeoutMilliseconds, - "enable_post_search": *utils.Cfg.ServiceSettings.EnablePostSearch, - "enable_user_statuses": *utils.Cfg.ServiceSettings.EnableUserStatuses, + "web_server_mode": *utils.Cfg.ServiceSettings.WebserverMode, + "enable_security_fix_alert": *utils.Cfg.ServiceSettings.EnableSecurityFixAlert, + "enable_insecure_outgoing_connections": *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections, + "enable_incoming_webhooks": utils.Cfg.ServiceSettings.EnableIncomingWebhooks, + "enable_outgoing_webhooks": utils.Cfg.ServiceSettings.EnableOutgoingWebhooks, + "enable_commands": *utils.Cfg.ServiceSettings.EnableCommands, + "enable_only_admin_integrations": *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations, + "enable_post_username_override": utils.Cfg.ServiceSettings.EnablePostUsernameOverride, + "enable_post_icon_override": utils.Cfg.ServiceSettings.EnablePostIconOverride, + "enable_apiv3": *utils.Cfg.ServiceSettings.EnableAPIv3, + "enable_user_access_tokens": *utils.Cfg.ServiceSettings.EnableUserAccessTokens, + "enable_custom_emoji": *utils.Cfg.ServiceSettings.EnableCustomEmoji, + "enable_emoji_picker": *utils.Cfg.ServiceSettings.EnableEmojiPicker, + "restrict_custom_emoji_creation": *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation, + "enable_testing": utils.Cfg.ServiceSettings.EnableTesting, + "enable_developer": *utils.Cfg.ServiceSettings.EnableDeveloper, + "enable_multifactor_authentication": *utils.Cfg.ServiceSettings.EnableMultifactorAuthentication, + "enforce_multifactor_authentication": *utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication, + "enable_oauth_service_provider": utils.Cfg.ServiceSettings.EnableOAuthServiceProvider, + "connection_security": *utils.Cfg.ServiceSettings.ConnectionSecurity, + "uses_letsencrypt": *utils.Cfg.ServiceSettings.UseLetsEncrypt, + "forward_80_to_443": *utils.Cfg.ServiceSettings.Forward80To443, + "maximum_login_attempts": utils.Cfg.ServiceSettings.MaximumLoginAttempts, + "session_length_web_in_days": *utils.Cfg.ServiceSettings.SessionLengthWebInDays, + "session_length_mobile_in_days": *utils.Cfg.ServiceSettings.SessionLengthMobileInDays, + "session_length_sso_in_days": *utils.Cfg.ServiceSettings.SessionLengthSSOInDays, + "session_cache_in_minutes": *utils.Cfg.ServiceSettings.SessionCacheInMinutes, + "isdefault_site_url": isDefault(*utils.Cfg.ServiceSettings.SiteURL, model.SERVICE_SETTINGS_DEFAULT_SITE_URL), + "isdefault_tls_cert_file": isDefault(*utils.Cfg.ServiceSettings.TLSCertFile, model.SERVICE_SETTINGS_DEFAULT_TLS_CERT_FILE), + "isdefault_tls_key_file": isDefault(*utils.Cfg.ServiceSettings.TLSKeyFile, model.SERVICE_SETTINGS_DEFAULT_TLS_KEY_FILE), + "isdefault_read_timeout": isDefault(*utils.Cfg.ServiceSettings.ReadTimeout, model.SERVICE_SETTINGS_DEFAULT_READ_TIMEOUT), + "isdefault_write_timeout": isDefault(*utils.Cfg.ServiceSettings.WriteTimeout, model.SERVICE_SETTINGS_DEFAULT_WRITE_TIMEOUT), + "isdefault_google_developer_key": isDefault(utils.Cfg.ServiceSettings.GoogleDeveloperKey, ""), + "isdefault_allow_cors_from": isDefault(*utils.Cfg.ServiceSettings.AllowCorsFrom, model.SERVICE_SETTINGS_DEFAULT_ALLOW_CORS_FROM), + "isdefault_allowed_untrusted_internal_connections": isDefault(*utils.Cfg.ServiceSettings.AllowedUntrustedInternalConnections, ""), + "restrict_post_delete": *utils.Cfg.ServiceSettings.RestrictPostDelete, + "allow_edit_post": *utils.Cfg.ServiceSettings.AllowEditPost, + "post_edit_time_limit": *utils.Cfg.ServiceSettings.PostEditTimeLimit, + "enable_user_typing_messages": *utils.Cfg.ServiceSettings.EnableUserTypingMessages, + "enable_channel_viewed_messages": *utils.Cfg.ServiceSettings.EnableChannelViewedMessages, + "time_between_user_typing_updates_milliseconds": *utils.Cfg.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, + "cluster_log_timeout_milliseconds": *utils.Cfg.ServiceSettings.ClusterLogTimeoutMilliseconds, + "enable_post_search": *utils.Cfg.ServiceSettings.EnablePostSearch, + "enable_user_statuses": *utils.Cfg.ServiceSettings.EnableUserStatuses, }) SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{ @@ -422,17 +423,17 @@ func trackConfig() { } func trackLicense() { - if utils.IsLicensed { + if utils.IsLicensed() { data := map[string]interface{}{ - "customer_id": utils.License.Customer.Id, - "license_id": utils.License.Id, - "issued": utils.License.IssuedAt, - "start": utils.License.StartsAt, - "expire": utils.License.ExpiresAt, - "users": *utils.License.Features.Users, + "customer_id": utils.License().Customer.Id, + "license_id": utils.License().Id, + "issued": utils.License().IssuedAt, + "start": utils.License().StartsAt, + "expire": utils.License().ExpiresAt, + "users": *utils.License().Features.Users, } - features := utils.License.Features.ToMap() + features := utils.License().Features.ToMap() for featureName, featureValue := range features { data["feature_"+featureName] = featureValue } diff --git a/app/elasticsearch.go b/app/elasticsearch.go index 740042ba5..6f3a4e38b 100644 --- a/app/elasticsearch.go +++ b/app/elasticsearch.go @@ -6,8 +6,8 @@ package app import ( "net/http" - "github.com/mattermost/platform/model" "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" ) diff --git a/app/email_batching.go b/app/email_batching.go index a578daf04..b37963a94 100644 --- a/app/email_batching.go +++ b/app/email_batching.go @@ -196,7 +196,7 @@ func sendBatchedEmailNotification(userId string, notifications []*batchedNotific } emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL - if utils.IsLicensed && *utils.License.Features.EmailNotificationContents { + if utils.IsLicensed() && *utils.License().Features.EmailNotificationContents { emailNotificationContentsType = *utils.Cfg.EmailSettings.EmailNotificationContentsType } diff --git a/app/job.go b/app/job.go index 36c0b1992..005324999 100644 --- a/app/job.go +++ b/app/job.go @@ -4,8 +4,8 @@ package app import ( - "github.com/mattermost/platform/model" "github.com/mattermost/platform/jobs" + "github.com/mattermost/platform/model" ) func GetJob(id string) (*model.Job, *model.AppError) { diff --git a/app/ldap.go b/app/ldap.go index 1b823dc47..17c24db8f 100644 --- a/app/ldap.go +++ b/app/ldap.go @@ -14,7 +14,7 @@ import ( func SyncLdap() { go func() { - if utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable { + if utils.IsLicensed() && *utils.License().Features.LDAP && *utils.Cfg.LdapSettings.Enable { if ldapI := einterfaces.GetLdapInterface(); ldapI != nil { ldapI.SyncNow() } else { @@ -25,7 +25,7 @@ func SyncLdap() { } func TestLdap() *model.AppError { - if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable { + if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed() && *utils.License().Features.LDAP && *utils.Cfg.LdapSettings.Enable { if err := ldapI.RunTest(); err != nil { err.StatusCode = 500 return err diff --git a/app/license_test.go b/app/license_test.go index a7761b204..07805992a 100644 --- a/app/license_test.go +++ b/app/license_test.go @@ -13,7 +13,7 @@ func TestLoadLicense(t *testing.T) { Setup() LoadLicense() - if utils.IsLicensed { + if utils.IsLicensed() { t.Fatal("shouldn't have a valid license") } } diff --git a/app/notification.go b/app/notification.go index 2b8c0c8c4..21271c716 100644 --- a/app/notification.go +++ b/app/notification.go @@ -236,7 +236,7 @@ func SendNotifications(post *model.Post, team *model.Team, channel *model.Channe sendPushNotifications := false if *utils.Cfg.EmailSettings.SendPushNotifications { pushServer := *utils.Cfg.EmailSettings.PushNotificationServer - if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) { + if pushServer == model.MHPNS && (!utils.IsLicensed() || !*utils.License().Features.MHPNS) { l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn")) sendPushNotifications = false } else { @@ -359,7 +359,7 @@ func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Ch } emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL - if utils.IsLicensed && *utils.License.Features.EmailNotificationContents { + if utils.IsLicensed() && *utils.License().Features.EmailNotificationContents { emailNotificationContentsType = *utils.Cfg.EmailSettings.EmailNotificationContentsType } @@ -682,7 +682,7 @@ func sendToPushProxy(msg model.PushNotification, session *model.Session) { request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson())) - if resp, err := utils.HttpClient().Do(request); err != nil { + if resp, err := utils.HttpClient(true).Do(request); err != nil { l4g.Error("Device push reported as error for UserId=%v SessionId=%v message=%v", session.UserId, session.Id, err.Error()) } else { pushResponse := model.PushResponseFromJson(resp.Body) @@ -828,12 +828,14 @@ func GetExplicitMentions(message string, keywords map[string][]string) (map[stri // Case-sensitive check for first name if ids, match := keywords[splitWord]; match { addMentionedUsers(ids) - } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { - username := word[1:len(splitWord)] + } else if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") { + username := splitWord[1:] potentialOthersMentioned = append(potentialOthersMentioned, username) } } - } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { + } + + if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") { username := word[1:] potentialOthersMentioned = append(potentialOthersMentioned, username) } diff --git a/app/oauth.go b/app/oauth.go index d3633b7eb..f8ddc6076 100644 --- a/app/oauth.go +++ b/app/oauth.go @@ -673,7 +673,7 @@ func AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, s var ar *model.AccessResponse var bodyBytes []byte - if resp, err := utils.HttpClient().Do(req); err != nil { + if resp, err := utils.HttpClient(true).Do(req); err != nil { return nil, "", stateProps, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.token_failed.app_error", nil, err.Error()) } else { bodyBytes, _ = ioutil.ReadAll(resp.Body) @@ -702,7 +702,7 @@ func AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service, code, s req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+ar.AccessToken) - if resp, err := utils.HttpClient().Do(req); err != nil { + if resp, err := utils.HttpClient(true).Do(req); err != nil { return nil, "", stateProps, model.NewLocAppError("AuthorizeOAuthUser", "api.user.authorize_oauth_user.service.app_error", map[string]interface{}{"Service": service}, err.Error()) } else { diff --git a/app/post.go b/app/post.go index 055f98373..068266120 100644 --- a/app/post.go +++ b/app/post.go @@ -233,7 +233,7 @@ func UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model.AppError } else { oldPost = result.Data.(*model.PostList).Posts[post.Id] - if utils.IsLicensed { + if utils.IsLicensed() { if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER && post.Message != oldPost.Message { err := model.NewAppError("UpdatePost", "api.post.update_post.permissions_denied.app_error", nil, "", http.StatusForbidden) return nil, err @@ -255,7 +255,7 @@ func UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model.AppError return nil, err } - if utils.IsLicensed { + if utils.IsLicensed() { if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*utils.Cfg.ServiceSettings.PostEditTimeLimit*1000) && post.Message != oldPost.Message { err := model.NewAppError("UpdatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *utils.Cfg.ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest) return nil, err @@ -503,7 +503,7 @@ func SearchPostsInTeam(terms string, userId string, teamId string, isOrSearch bo paramsList := model.ParseSearchParams(terms) esInterface := einterfaces.GetElasticsearchInterface() - if esInterface != nil && *utils.Cfg.ElasticsearchSettings.EnableSearching && utils.IsLicensed && *utils.License.Features.Elasticsearch { + if esInterface != nil && *utils.Cfg.ElasticsearchSettings.EnableSearching && utils.IsLicensed() && *utils.License().Features.Elasticsearch { finalParamsList := []*model.SearchParams{} for _, params := range paramsList { @@ -619,7 +619,7 @@ func GetFileInfosForPost(postId string, readFromMaster bool) ([]*model.FileInfo, func GetOpenGraphMetadata(url string) *opengraph.OpenGraph { og := opengraph.NewOpenGraph() - res, err := utils.HttpClient().Get(url) + res, err := utils.HttpClient(false).Get(url) if err != nil { l4g.Error("GetOpenGraphMetadata request failed for url=%v with err=%v", url, err.Error()) return og diff --git a/app/slackimport_test.go b/app/slackimport_test.go index 38f5e2e9b..4090cc432 100644 --- a/app/slackimport_test.go +++ b/app/slackimport_test.go @@ -4,28 +4,25 @@ package app import ( - "github.com/mattermost/platform/model" "os" "strings" "testing" -) -func TestSlackConvertTimeStamp(t *testing.T) { + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - testTimeStamp := "1469785419.000033" - - result := SlackConvertTimeStamp(testTimeStamp) + "github.com/mattermost/platform/model" +) - if result != 1469785419000 { - t.Fatalf("Unexpected timestamp value %v returned.", result) - } +func TestSlackConvertTimeStamp(t *testing.T) { + assert.EqualValues(t, SlackConvertTimeStamp("1469785419.000033"), 1469785419000) } func TestSlackConvertChannelName(t *testing.T) { - var testData = []struct { + for _, tc := range []struct { nameInput string - idInput string - output string + idInput string + output string }{ {"test-channel", "C0G08DLQH", "test-channel"}, {"_test_channel_", "C0G04DLQH", "test_channel"}, @@ -33,12 +30,8 @@ func TestSlackConvertChannelName(t *testing.T) { {"-t", "C0G06DLQH", "slack-channel-t"}, {"a", "C0G05DLQH", "slack-channel-a"}, {"случайный", "C0G05DLQD", "c0g05dlqd"}, - } - - for _, td := range testData { - if td.output != SlackConvertChannelName(td.nameInput, td.idInput) { - t.Fatalf("Did not convert channel name correctly: %v", td.nameInput) - } + } { + assert.Equal(t, SlackConvertChannelName(tc.nameInput, tc.idInput), tc.output, "nameInput = %v", tc.nameInput) } } @@ -82,15 +75,7 @@ func TestSlackConvertUserMentions(t *testing.T) { }, } - convertedPosts := SlackConvertUserMentions(users, posts) - - for channelName, channelPosts := range convertedPosts { - for postIdx, post := range channelPosts { - if post.Text != expectedPosts[channelName][postIdx].Text { - t.Fatalf("Converted post text not as expected: %v", post.Text) - } - } - } + assert.Equal(t, expectedPosts, SlackConvertUserMentions(users, posts)) } func TestSlackConvertChannelMentions(t *testing.T) { @@ -117,69 +102,43 @@ func TestSlackConvertChannelMentions(t *testing.T) { Text: "Go to ~one.", }, { + User: "U00000A0A", Text: "Try ~two for this.", }, }, } - convertedPosts := SlackConvertChannelMentions(channels, posts) - - for channelName, channelPosts := range convertedPosts { - for postIdx, post := range channelPosts { - if post.Text != expectedPosts[channelName][postIdx].Text { - t.Fatalf("Converted post text not as expected: %v", post.Text) - } - } - } - + assert.Equal(t, expectedPosts, SlackConvertChannelMentions(channels, posts)) } func TestSlackParseChannels(t *testing.T) { file, err := os.Open("../tests/slack-import-test-channels.json") - if err != nil { - t.Fatalf("Failed to open data file: %v", err) - } + require.NoError(t, err) + defer file.Close() channels, err := SlackParseChannels(file) - if err != nil { - t.Fatalf("Error occurred parsing channels: %v", err) - } - - if len(channels) != 6 { - t.Fatalf("Unexpected number of channels: %v", len(channels)) - } + require.NoError(t, err) + assert.Equal(t, 6, len(channels)) } func TestSlackParseUsers(t *testing.T) { file, err := os.Open("../tests/slack-import-test-users.json") - if err != nil { - t.Fatalf("Failed to open data file: %v", err) - } + require.NoError(t, err) + defer file.Close() users, err := SlackParseUsers(file) - if err != nil { - t.Fatalf("Error occurred parsing users: %v", err) - } - - if len(users) != 11 { - t.Fatalf("Unexpected number of users: %v", len(users)) - } + require.NoError(t, err) + assert.Equal(t, 11, len(users)) } func TestSlackParsePosts(t *testing.T) { file, err := os.Open("../tests/slack-import-test-posts.json") - if err != nil { - t.Fatalf("Failed to open data file: %v", err) - } + require.NoError(t, err) + defer file.Close() posts, err := SlackParsePosts(file) - if err != nil { - t.Fatalf("Error occurred parsing posts: %v", err) - } - - if len(posts) != 8 { - t.Fatalf("Unexpected number of posts: %v", len(posts)) - } + require.NoError(t, err) + assert.Equal(t, 8, len(posts)) } func TestSlackSanitiseChannelProperties(t *testing.T) { @@ -191,9 +150,7 @@ func TestSlackSanitiseChannelProperties(t *testing.T) { } c1s := SlackSanitiseChannelProperties(c1) - if c1.DisplayName != c1s.DisplayName || c1.Name != c1s.Name || c1.Purpose != c1s.Purpose || c1.Header != c1s.Header { - t.Fatal("Unexpected alterations to the channel properties.") - } + assert.Equal(t, c1, c1s) c2 := model.Channel{ DisplayName: strings.Repeat("abcdefghij", 7), @@ -203,21 +160,12 @@ func TestSlackSanitiseChannelProperties(t *testing.T) { } c2s := SlackSanitiseChannelProperties(c2) - if c2s.DisplayName != strings.Repeat("abcdefghij", 6)+"abcd" { - t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.DisplayName) - } - - if c2s.Name != strings.Repeat("abcdefghij", 6)+"abcd" { - t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Name) - } - - if c2s.Purpose != strings.Repeat("0123456789", 25) { - t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Purpose) - } - - if c2s.Header != strings.Repeat("0123456789", 102)+"0123" { - t.Fatalf("Unexpected alterations to the channel properties: %v", c2s.Header) - } + assert.Equal(t, model.Channel{ + DisplayName: strings.Repeat("abcdefghij", 6) + "abcd", + Name: strings.Repeat("abcdefghij", 6) + "abcd", + Purpose: strings.Repeat("0123456789", 25), + Header: strings.Repeat("0123456789", 102) + "0123", + }, c2s) } func TestSlackConvertPostsMarkup(t *testing.T) { @@ -321,11 +269,5 @@ in this~.`, }, } - actualOutput := SlackConvertPostsMarkup(input) - - for i := range actualOutput["test"] { - if actualOutput["test"][i].Text != expectedOutput["test"][i].Text { - t.Errorf("Unexpected message after markup translation: %v", actualOutput["test"][i].Text) - } - } + assert.Equal(t, expectedOutput, SlackConvertPostsMarkup(input)) } diff --git a/app/team.go b/app/team.go index e4a71d7d5..b479ed134 100644 --- a/app/team.go +++ b/app/team.go @@ -735,7 +735,9 @@ func PermanentDeleteTeam(team *model.Team) *model.AppError { } if result := <-Srv.Store.Channel().GetTeamChannels(team.Id); result.Err != nil { - return result.Err + if result.Err.Id != "store.sql_channel.get_channels.not_found.app_error" { + return result.Err + } } else { channels := result.Data.(*model.ChannelList) for _, c := range *channels { diff --git a/app/team_test.go b/app/team_test.go index a410d6652..4e0ea82f5 100644 --- a/app/team_test.go +++ b/app/team_test.go @@ -152,4 +152,24 @@ func TestPermanentDeleteTeam(t *testing.T) { if command, err = GetCommand(command.Id); command != nil || err == nil { t.Fatal("command wasn't deleted") } + + // Test deleting a team with no channels. + team = th.CreateTeam() + defer func() { + PermanentDeleteTeam(team) + }() + + if channels, err := GetPublicChannelsForTeam(team.Id, 0, 1000); err != nil { + t.Fatal(err) + } else { + for _, channel := range *channels { + if err2 := PermanentDeleteChannel(channel); err2 != nil { + t.Fatal(err) + } + } + } + + if err := PermanentDeleteTeam(team); err != nil { + t.Fatal(err) + } } diff --git a/app/user.go b/app/user.go index 426a11bcb..813421a5c 100644 --- a/app/user.go +++ b/app/user.go @@ -376,7 +376,7 @@ func GetUserByAuth(authData *string, authService string) (*model.User, *model.Ap } func GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) { - ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP + ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed() && *utils.License().Features.LDAP if result := <-Srv.Store.User().GetForLogin( loginId, diff --git a/app/web_conn.go b/app/web_conn.go index 43047a881..24b7166c0 100644 --- a/app/web_conn.go +++ b/app/web_conn.go @@ -217,7 +217,7 @@ func (webCon *WebConn) IsAuthenticated() bool { func (webCon *WebConn) SendHello() { msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil) - msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed)) + msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.ClientCfgHash, utils.IsLicensed())) webCon.Send <- msg } diff --git a/app/webhook.go b/app/webhook.go index 29f642ea8..f84086d94 100644 --- a/app/webhook.go +++ b/app/webhook.go @@ -102,7 +102,7 @@ func TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model.OutgoingW req, _ := http.NewRequest("POST", url, body) req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", "application/json") - if resp, err := utils.HttpClient().Do(req); err != nil { + if resp, err := utils.HttpClient(false).Do(req); err != nil { l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error()) } else { defer CloseBody(resp) @@ -533,3 +533,54 @@ func HandleIncomingWebhook(hookId string, req *model.IncomingWebhookRequest) *mo return nil } + +func CreateCommandWebhook(commandId string, args *model.CommandArgs) (*model.CommandWebhook, *model.AppError) { + hook := &model.CommandWebhook{ + CommandId: commandId, + UserId: args.UserId, + ChannelId: args.ChannelId, + RootId: args.RootId, + ParentId: args.ParentId, + } + + if result := <-Srv.Store.CommandWebhook().Save(hook); result.Err != nil { + return nil, result.Err + } else { + return result.Data.(*model.CommandWebhook), nil + } +} + +func HandleCommandWebhook(hookId string, response *model.CommandResponse) *model.AppError { + if response == nil { + return model.NewAppError("HandleCommandWebhook", "web.command_webhook.parse.app_error", nil, "", http.StatusBadRequest) + } + + var hook *model.CommandWebhook + if result := <-Srv.Store.CommandWebhook().Get(hookId); result.Err != nil { + return model.NewAppError("HandleCommandWebhook", "web.command_webhook.invalid.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode) + } else { + hook = result.Data.(*model.CommandWebhook) + } + + var cmd *model.Command + if result := <-Srv.Store.Command().Get(hook.CommandId); result.Err != nil { + return model.NewAppError("HandleCommandWebhook", "web.command_webhook.command.app_error", nil, "err="+result.Err.Message, http.StatusBadRequest) + } else { + cmd = result.Data.(*model.Command) + } + + args := &model.CommandArgs{ + UserId: hook.UserId, + ChannelId: hook.ChannelId, + TeamId: cmd.TeamId, + RootId: hook.RootId, + ParentId: hook.ParentId, + } + + if result := <-Srv.Store.CommandWebhook().TryUse(hook.Id, 5); result.Err != nil { + return model.NewAppError("HandleCommandWebhook", "web.command_webhook.invalid.app_error", nil, "err="+result.Err.Message, result.Err.StatusCode) + } + + _, err := HandleCommandResponse(cmd, args, response, false) + return err +} diff --git a/app/webrtc.go b/app/webrtc.go index 4d44234a8..f8361bbb2 100644 --- a/app/webrtc.go +++ b/app/webrtc.go @@ -59,7 +59,7 @@ func GetWebrtcToken(sessionId string) (string, *model.AppError) { rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) rq.Header.Set("Content-Type", "application/json") - if rp, err := utils.HttpClient().Do(rq); err != nil { + if rp, err := utils.HttpClient(true).Do(rq); err != nil { return "", model.NewAppError("WebRTC.Token", "model.client.connecting.app_error", nil, err.Error(), http.StatusInternalServerError) } else if rp.StatusCode >= 300 { defer CloseBody(rp) @@ -80,3 +80,18 @@ func GenerateTurnPassword(username string, secret string) string { h.Write([]byte(username)) return base64.StdEncoding.EncodeToString(h.Sum(nil)) } + +func RevokeWebrtcToken(sessionId string) { + token := base64.StdEncoding.EncodeToString([]byte(sessionId)) + data := make(map[string]string) + data["janus"] = "remove_token" + data["token"] = token + data["transaction"] = model.NewId() + data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret + + rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) + rq.Header.Set("Content-Type", "application/json") + + // we do not care about the response + utils.HttpClient(true).Do(rq) +} diff --git a/app/webtrc.go b/app/webtrc.go deleted file mode 100644 index 76e173651..000000000 --- a/app/webtrc.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -package app - -import ( - "encoding/base64" - "net/http" - "strings" - - "github.com/mattermost/platform/model" - "github.com/mattermost/platform/utils" -) - -func RevokeWebrtcToken(sessionId string) { - token := base64.StdEncoding.EncodeToString([]byte(sessionId)) - data := make(map[string]string) - data["janus"] = "remove_token" - data["token"] = token - data["transaction"] = model.NewId() - data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret - - rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data))) - rq.Header.Set("Content-Type", "application/json") - - // we do not care about the response - utils.HttpClient().Do(rq) -} diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go index 0ff2a1b5f..ed8a0b817 100644 --- a/cmd/platform/channel.go +++ b/cmd/platform/channel.go @@ -106,7 +106,7 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error { return err } - if !utils.IsLicensed { + if !utils.IsLicensed() { return errors.New(utils.T("cli.license.critical")) } @@ -158,7 +158,7 @@ func removeChannelUsersCmdF(cmd *cobra.Command, args []string) error { return err } - if !utils.IsLicensed { + if !utils.IsLicensed() { return errors.New(utils.T("cli.license.critical")) } @@ -194,7 +194,7 @@ func addChannelUsersCmdF(cmd *cobra.Command, args []string) error { return err } - if !utils.IsLicensed { + if !utils.IsLicensed() { return errors.New(utils.T("cli.license.critical")) } @@ -292,7 +292,7 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error { return err } - if !utils.IsLicensed { + if !utils.IsLicensed() { return errors.New(utils.T("cli.license.critical")) } @@ -329,7 +329,7 @@ func restoreChannelsCmdF(cmd *cobra.Command, args []string) error { return err } - if !utils.IsLicensed { + if !utils.IsLicensed() { return errors.New(utils.T("cli.license.critical")) } diff --git a/cmd/platform/server.go b/cmd/platform/server.go index 8695129b7..77fedccf4 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -81,12 +81,12 @@ func runServer(configFileLocation string) { app.LoadLicense() } - if !utils.IsLicensed && len(utils.Cfg.SqlSettings.DataSourceReplicas) > 1 { + if !utils.IsLicensed() && len(utils.Cfg.SqlSettings.DataSourceReplicas) > 1 { l4g.Warn(utils.T("store.sql.read_replicas_not_licensed.critical")) utils.Cfg.SqlSettings.DataSourceReplicas = utils.Cfg.SqlSettings.DataSourceReplicas[:1] } - if !utils.IsLicensed { + if !utils.IsLicensed() { utils.Cfg.TeamSettings.MaxNotificationsPerChannel = &MaxNotificationsPerChannelDefault } @@ -107,6 +107,7 @@ func runServer(configFileLocation string) { go runDiagnosticsJob() go runTokenCleanupJob() + go runCommandWebhookCleanupJob() if complianceI := einterfaces.GetComplianceInterface(); complianceI != nil { complianceI.StartComplianceDailyJob() @@ -170,6 +171,11 @@ func runTokenCleanupJob() { model.CreateRecurringTask("Token Cleanup", doTokenCleanup, time.Hour*1) } +func runCommandWebhookCleanupJob() { + doCommandWebhookCleanup() + model.CreateRecurringTask("Command Hook Cleanup", doCommandWebhookCleanup, time.Hour*1) +} + func resetStatuses() { if result := <-app.Srv.Store.Status().ResetAll(); result.Err != nil { l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error()) @@ -204,3 +210,7 @@ func doDiagnostics() { func doTokenCleanup() { app.Srv.Store.Token().Cleanup() } + +func doCommandWebhookCleanup() { + app.Srv.Store.CommandWebhook().Cleanup() +} diff --git a/config/config.json b/config/config.json index 0619e3500..e7e67855a 100644 --- a/config/config.json +++ b/config/config.json @@ -27,6 +27,7 @@ "EnableDeveloper": false, "EnableSecurityFixAlert": true, "EnableInsecureOutgoingConnections": false, + "AllowedUntrustedInternalConnections": "", "EnableMultifactorAuthentication": false, "EnforceMultifactorAuthentication": false, "EnableUserAccessTokens": false, @@ -295,7 +296,9 @@ "EnableSearching": false, "Sniff": true, "PostIndexReplicas": 1, - "PostIndexShards": 1 + "PostIndexShards": 1, + "AggregatePostsAfterDays": 365, + "PostsAggregatorJobStartTime": "03:00" }, "DataRetentionSettings": { "Enable": false diff --git a/einterfaces/cluster.go b/einterfaces/cluster.go index 096a775fe..9d61b7503 100644 --- a/einterfaces/cluster.go +++ b/einterfaces/cluster.go @@ -14,6 +14,7 @@ type ClusterInterface interface { StopInterNodeCommunication() RegisterClusterMessageHandler(event string, crm ClusterMessageHandler) GetClusterId() string + IsLeader() bool GetClusterInfos() []*model.ClusterInfo SendClusterMessage(cluster *model.ClusterMessage) NotifyMsg(buf []byte) diff --git a/einterfaces/jobs/elasticsearch.go b/einterfaces/jobs/elasticsearch.go index 6d6dbe893..ca05b2ef3 100644 --- a/einterfaces/jobs/elasticsearch.go +++ b/einterfaces/jobs/elasticsearch.go @@ -20,3 +20,18 @@ func RegisterElasticsearchIndexerInterface(newInterface ElasticsearchIndexerInte func GetElasticsearchIndexerInterface() ElasticsearchIndexerInterface { return theElasticsearchIndexerInterface } + +type ElasticsearchAggregatorInterface interface { + MakeWorker() model.Worker + MakeScheduler() model.Scheduler +} + +var theElasticsearchAggregatorInterface ElasticsearchAggregatorInterface + +func RegisterElasticsearchAggregatorInterface(newInterface ElasticsearchAggregatorInterface) { + theElasticsearchAggregatorInterface = newInterface +} + +func GetElasticsearchAggregatorInterface() ElasticsearchAggregatorInterface { + return theElasticsearchAggregatorInterface +} diff --git a/glide.lock b/glide.lock index 2a59822c9..50de62789 100644 --- a/glide.lock +++ b/glide.lock @@ -177,6 +177,11 @@ imports: version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 - name: github.com/spf13/viper version: c1de95864d73a5465492829d7cb2dd422b19ac96 +- name: github.com/stretchr/testify + version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0 + subpackages: + - assert + - require - name: github.com/tylerb/graceful version: 4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb - name: github.com/xenolf/lego diff --git a/glide.yaml b/glide.yaml index 4bfd9e468..1ba91daa4 100644 --- a/glide.yaml +++ b/glide.yaml @@ -86,3 +86,8 @@ import: version: 995ddf2264c4ad45fbaf342f7500e4787ebae84a - package: github.com/go-redis/redis version: v6.5.2 +- package: github.com/stretchr/testify + version: ~1.1.4 + subpackages: + - assert + - require diff --git a/i18n/en.json b/i18n/en.json index 542a538f5..1b3691ecf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3788,6 +3788,30 @@ "translation": "Compliance export started for job '{{.JobName}}' at '{{.FilePath}}'" }, { + "id": "ent.elasticsearch.aggregator_worker.get_indexes.error", + "translation": "Elasticsearch aggregator worker failed to get indexes" + }, + { + "id": "ent.elasticsearch.aggregator_worker.create_index_job.error", + "translation": "Elasticsearch aggregator worker failed to create the indexing job" + }, + { + "id": "ent.elasticsearch.aggregator_worker.delete_indexes.error", + "translation": "Elasticsearch aggregator worker failed to delete the indexes" + }, + { + "id": "ent.elasticsearch.aggregator_worker.index_job_failed.error", + "translation": "Elasticsearch aggregator worker failed due to the indexing job failing" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_start_time.error", + "translation": "Elasticsearch indexing worker failed to parse the start time" + }, + { + "id": "ent.elasticsearch.indexer.do_job.parse_end_time.error", + "translation": "Elasticsearch indexing worker failed to parse the end time" + }, + { "id": "ent.elasticsearch.create_client.connect_failed", "translation": "Setting up Elasticsearch Client Failed" }, @@ -4436,6 +4460,14 @@ "translation": "Elastic Search Username setting must be provided when Elastic Search indexing is enabled." }, { + "id": "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", + "translation": "Elasticsearch AggregatePostsAfterDays setting must be a number greater than or equal to 1" + }, + { + "id": "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", + "translation": "Elasticsearch PostsAggregatorJobStartTime setting must be a time in the format \"hh:mm\"" + }, + { "id": "model.config.is_valid.email_batching_buffer_size.app_error", "translation": "Invalid email batching buffer size for email settings. Must be zero or a positive number." }, @@ -4784,6 +4816,34 @@ "translation": "Invalid job type" }, { + "id": "model.command_hook.id.app_error", + "translation": "Invalid command hook id" + }, + { + "id": "model.command_hook.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.command_hook.command_id.app_error", + "translation": "Invalid command id" + }, + { + "id": "model.command_hook.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.command_hook.channel_id.app_error", + "translation": "Invalid channel id" + }, + { + "id": "model.command_hook.root_id.app_error", + "translation": "Invalid root id" + }, + { + "id": "model.command_hook.parent_id.app_error", + "translation": "Invalid parent id" + }, + { "id": "model.oauth.is_valid.app_id.app_error", "translation": "Invalid app id" }, @@ -6504,6 +6564,26 @@ "translation": "We couldn't update the webhook" }, { + "id": "store.sql_command_webhooks.save.existing.app_error", + "translation": "You cannot update an existing CommandWebhook" + }, + { + "id": "store.sql_command_webhooks.save.app_error", + "translation": "We couldn't save the CommandWebhook" + }, + { + "id": "store.sql_command_webhooks.get.app_error", + "translation": "We couldn't get the webhook" + }, + { + "id": "store.sql_command_webhooks.try_use.app_error", + "translation": "Unable to use the webhook" + }, + { + "id": "store.sql_command_webhooks.try_use.invalid.app_error", + "translation": "Invalid webhook" + }, + { "id": "system.message.name", "translation": "System" }, @@ -6728,6 +6808,18 @@ "translation": "Couldn't find the user" }, { + "id": "web.command_webhook.parse.app_error", + "translation": "Unable to parse incoming data" + }, + { + "id": "web.command_webhook.invalid.app_error", + "translation": "Invalid webhook" + }, + { + "id": "web.command_webhook.command.app_error", + "translation": "Couldn't find the command" + }, + { "id": "web.init.debug", "translation": "Initializing web routes" }, diff --git a/jobs/jobs.go b/jobs/jobs.go index 1986b22b6..e478c5a19 100644 --- a/jobs/jobs.go +++ b/jobs/jobs.go @@ -16,7 +16,7 @@ const ( CANCEL_WATCHER_POLLING_INTERVAL = 5000 ) -func CreateJob(jobType string, jobData map[string]interface{}) (*model.Job, *model.AppError) { +func CreateJob(jobType string, jobData map[string]string) (*model.Job, *model.AppError) { job := model.Job{ Id: model.NewId(), Type: jobType, @@ -70,9 +70,9 @@ func SetJobError(job *model.Job, jobError *model.AppError) *model.AppError { job.Status = model.JOB_STATUS_ERROR job.Progress = -1 if job.Data == nil { - job.Data = make(map[string]interface{}) + job.Data = make(map[string]string) } - job.Data["error"] = jobError + job.Data["error"] = jobError.Error() if result := <-Srv.Store.Job().UpdateOptimistically(job, model.JOB_STATUS_IN_PROGRESS); result.Err != nil { return result.Err diff --git a/jobs/jobs_watcher.go b/jobs/jobs_watcher.go index 5892e3b1c..83d4249eb 100644 --- a/jobs/jobs_watcher.go +++ b/jobs/jobs_watcher.go @@ -38,7 +38,7 @@ func (watcher *Watcher) Start() { rand.Seed(time.Now().UTC().UnixNano()) _ = <-time.After(time.Duration(rand.Intn(WATCHER_POLLING_INTERVAL)) * time.Millisecond) - defer func(){ + defer func() { l4g.Debug("Watcher Finished") watcher.stopped <- true }() @@ -64,25 +64,27 @@ func (watcher *Watcher) PollAndNotify() { if result := <-Srv.Store.Job().GetAllByStatus(model.JOB_STATUS_PENDING); result.Err != nil { l4g.Error("Error occured getting all pending statuses: %v", result.Err.Error()) } else { - jobStatuses := result.Data.([]*model.Job) + jobs := result.Data.([]*model.Job) - for _, js := range jobStatuses { - j := model.Job{ - Type: js.Type, - Id: js.Id, - } - - if js.Type == model.JOB_TYPE_DATA_RETENTION { + for _, job := range jobs { + if job.Type == model.JOB_TYPE_DATA_RETENTION { if watcher.workers.DataRetention != nil { select { - case watcher.workers.DataRetention.JobChannel() <- j: + case watcher.workers.DataRetention.JobChannel() <- *job: default: } } - } else if js.Type == model.JOB_TYPE_ELASTICSEARCH_POST_INDEXING { + } else if job.Type == model.JOB_TYPE_ELASTICSEARCH_POST_INDEXING { if watcher.workers.ElasticsearchIndexing != nil { select { - case watcher.workers.ElasticsearchIndexing.JobChannel() <- j: + case watcher.workers.ElasticsearchIndexing.JobChannel() <- *job: + default: + } + } + } else if job.Type == model.JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION { + if watcher.workers.ElasticsearchAggregation != nil { + select { + case watcher.workers.ElasticsearchAggregation.JobChannel() <- *job: default: } } diff --git a/jobs/schedulers.go b/jobs/schedulers.go index 73ec6661a..2f4e18001 100644 --- a/jobs/schedulers.go +++ b/jobs/schedulers.go @@ -16,7 +16,8 @@ import ( type Schedulers struct { startOnce sync.Once - DataRetention model.Scheduler + DataRetention model.Scheduler + ElasticsearchAggregation model.Scheduler listenerId string } @@ -28,6 +29,10 @@ func InitSchedulers() *Schedulers { schedulers.DataRetention = dataRetentionInterface.MakeScheduler() } + if elasticsearchAggregatorInterface := ejobs.GetElasticsearchAggregatorInterface(); elasticsearchAggregatorInterface != nil { + schedulers.ElasticsearchAggregation = elasticsearchAggregatorInterface.MakeScheduler() + } + return schedulers } @@ -38,6 +43,10 @@ func (schedulers *Schedulers) Start() *Schedulers { if schedulers.DataRetention != nil && *utils.Cfg.DataRetentionSettings.Enable { go schedulers.DataRetention.Run() } + + if schedulers.ElasticsearchAggregation != nil && *utils.Cfg.ElasticsearchSettings.EnableIndexing { + go schedulers.ElasticsearchAggregation.Run() + } }) schedulers.listenerId = utils.AddConfigListener(schedulers.handleConfigChange) @@ -53,6 +62,14 @@ func (schedulers *Schedulers) handleConfigChange(oldConfig *model.Config, newCon schedulers.DataRetention.Stop() } } + + if schedulers.ElasticsearchAggregation != nil { + if !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing { + go schedulers.ElasticsearchAggregation.Run() + } else if *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing { + schedulers.ElasticsearchAggregation.Stop() + } + } } func (schedulers *Schedulers) Stop() *Schedulers { @@ -62,6 +79,10 @@ func (schedulers *Schedulers) Stop() *Schedulers { schedulers.DataRetention.Stop() } + if schedulers.ElasticsearchAggregation != nil && *utils.Cfg.ElasticsearchSettings.EnableIndexing { + schedulers.ElasticsearchAggregation.Stop() + } + l4g.Info("Stopped schedulers") return schedulers diff --git a/jobs/testscheduler.go b/jobs/testscheduler.go index 31b5d144c..d7345f67f 100644 --- a/jobs/testscheduler.go +++ b/jobs/testscheduler.go @@ -28,7 +28,7 @@ func MakeTestScheduler(name string, jobType string) *TestScheduler { func (scheduler *TestScheduler) Run() { l4g.Debug("Scheduler %v: Started", scheduler.name) - defer func(){ + defer func() { l4g.Debug("Scheduler %v: Finished", scheduler.name) scheduler.stopped <- true }() diff --git a/jobs/workers.go b/jobs/workers.go index 592c001fb..fe38641e7 100644 --- a/jobs/workers.go +++ b/jobs/workers.go @@ -16,16 +16,15 @@ type Workers struct { startOnce sync.Once watcher *Watcher - DataRetention model.Worker - ElasticsearchIndexing model.Worker + DataRetention model.Worker + ElasticsearchIndexing model.Worker + ElasticsearchAggregation model.Worker listenerId string } func InitWorkers() *Workers { - workers := &Workers{ - // SearchIndexing: MakeTestJob(s, "SearchIndexing"), - } + workers := &Workers{} workers.watcher = MakeWatcher(workers) if dataRetentionInterface := ejobs.GetDataRetentionInterface(); dataRetentionInterface != nil { @@ -36,6 +35,10 @@ func InitWorkers() *Workers { workers.ElasticsearchIndexing = elasticsearchIndexerInterface.MakeWorker() } + if elasticsearchAggregatorInterface := ejobs.GetElasticsearchAggregatorInterface(); elasticsearchAggregatorInterface != nil { + workers.ElasticsearchAggregation = elasticsearchAggregatorInterface.MakeWorker() + } + return workers } @@ -51,6 +54,10 @@ func (workers *Workers) Start() *Workers { go workers.ElasticsearchIndexing.Run() } + if workers.ElasticsearchAggregation != nil && *utils.Cfg.ElasticsearchSettings.EnableIndexing { + go workers.ElasticsearchAggregation.Run() + } + go workers.watcher.Start() }) @@ -75,6 +82,14 @@ func (workers *Workers) handleConfigChange(oldConfig *model.Config, newConfig *m workers.ElasticsearchIndexing.Stop() } } + + if workers.ElasticsearchAggregation != nil { + if !*oldConfig.ElasticsearchSettings.EnableIndexing && *newConfig.ElasticsearchSettings.EnableIndexing { + go workers.ElasticsearchAggregation.Run() + } else if *oldConfig.ElasticsearchSettings.EnableIndexing && !*newConfig.ElasticsearchSettings.EnableIndexing { + workers.ElasticsearchAggregation.Stop() + } + } } func (workers *Workers) Stop() *Workers { @@ -90,6 +105,10 @@ func (workers *Workers) Stop() *Workers { workers.ElasticsearchIndexing.Stop() } + if workers.ElasticsearchAggregation != nil && *utils.Cfg.ElasticsearchSettings.EnableIndexing { + workers.ElasticsearchAggregation.Stop() + } + l4g.Info("Stopped workers") return workers diff --git a/model/cluster_info.go b/model/cluster_info.go index 1e468044e..c4f7e89a6 100644 --- a/model/cluster_info.go +++ b/model/cluster_info.go @@ -10,6 +10,7 @@ import ( ) type ClusterInfo struct { + Id string `json:"id"` Version string `json:"version"` ConfigHash string `json:"config_hash"` IpAddress string `json:"ipaddress"` diff --git a/model/command_response.go b/model/command_response.go index 27d39e173..0b80b297b 100644 --- a/model/command_response.go +++ b/model/command_response.go @@ -6,6 +6,7 @@ package model import ( "encoding/json" "io" + "io/ioutil" ) const ( @@ -31,6 +32,22 @@ func (o *CommandResponse) ToJson() string { } } +func CommandResponseFromHTTPBody(contentType string, body io.Reader) *CommandResponse { + if contentType == "application/json" { + return CommandResponseFromJson(body) + } + if b, err := ioutil.ReadAll(body); err == nil { + return CommandResponseFromPlainText(string(b)) + } + return nil +} + +func CommandResponseFromPlainText(text string) *CommandResponse { + return &CommandResponse{ + Text: text, + } +} + func CommandResponseFromJson(data io.Reader) *CommandResponse { decoder := json.NewDecoder(data) var o CommandResponse diff --git a/model/command_response_test.go b/model/command_response_test.go index df478ff2c..19be796b8 100644 --- a/model/command_response_test.go +++ b/model/command_response_test.go @@ -18,6 +18,30 @@ func TestCommandResponseJson(t *testing.T) { } } +func TestCommandResponseFromHTTPBody(t *testing.T) { + for _, test := range []struct { + ContentType string + Body string + ExpectedText string + }{ + {"", "foo", "foo"}, + {"text/plain", "foo", "foo"}, + {"application/json", `{"text": "foo"}`, "foo"}, + } { + response := CommandResponseFromHTTPBody(test.ContentType, strings.NewReader(test.Body)) + if response.Text != test.ExpectedText { + t.Fatal() + } + } +} + +func TestCommandResponseFromPlainText(t *testing.T) { + response := CommandResponseFromPlainText("foo") + if response.Text != "foo" { + t.Fatal("text should be foo") + } +} + func TestCommandResponseFromJson(t *testing.T) { json := `{ "response_type": "ephemeral", diff --git a/model/command_webhook.go b/model/command_webhook.go new file mode 100644 index 000000000..0b00e00b6 --- /dev/null +++ b/model/command_webhook.go @@ -0,0 +1,65 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "net/http" +) + +type CommandWebhook struct { + Id string + CreateAt int64 + CommandId string + UserId string + ChannelId string + RootId string + ParentId string + UseCount int +} + +const ( + COMMAND_WEBHOOK_LIFETIME = 1000 * 60 * 30 +) + +func (o *CommandWebhook) PreSave() { + if o.Id == "" { + o.Id = NewId() + } + + if o.CreateAt == 0 { + o.CreateAt = GetMillis() + } +} + +func (o *CommandWebhook) IsValid() *AppError { + if len(o.Id) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.id.app_error", nil, "", http.StatusBadRequest) + } + + if o.CreateAt == 0 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) + } + + if len(o.CommandId) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.command_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.UserId) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.user_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ChannelId) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.channel_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.RootId) != 0 && len(o.RootId) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.root_id.app_error", nil, "", http.StatusBadRequest) + } + + if len(o.ParentId) != 0 && len(o.ParentId) != 26 { + return NewAppError("CommandWebhook.IsValid", "model.command_hook.parent_id.app_error", nil, "", http.StatusBadRequest) + } + + return nil +} diff --git a/model/command_webhook_test.go b/model/command_webhook_test.go new file mode 100644 index 000000000..629bbdaa7 --- /dev/null +++ b/model/command_webhook_test.go @@ -0,0 +1,54 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package model + +import ( + "testing" +) + +func TestCommandWebhookPreSave(t *testing.T) { + h := CommandWebhook{} + h.PreSave() + if len(h.Id) != 26 { + t.Fatal("Id should be generated") + } + if h.CreateAt == 0 { + t.Fatal("CreateAt should be set") + } +} + +func TestCommandWebhookIsValid(t *testing.T) { + h := CommandWebhook{} + h.Id = NewId() + h.CreateAt = GetMillis() + h.CommandId = NewId() + h.UserId = NewId() + h.ChannelId = NewId() + + for _, test := range []struct { + Transform func() + ExpectedError string + }{ + {func() {}, ""}, + {func() { h.Id = "asd" }, "model.command_hook.id.app_error"}, + {func() { h.CreateAt = 0 }, "model.command_hook.create_at.app_error"}, + {func() { h.CommandId = "asd" }, "model.command_hook.command_id.app_error"}, + {func() { h.UserId = "asd" }, "model.command_hook.user_id.app_error"}, + {func() { h.ChannelId = "asd" }, "model.command_hook.channel_id.app_error"}, + {func() { h.RootId = "asd" }, "model.command_hook.root_id.app_error"}, + {func() { h.RootId = NewId() }, ""}, + {func() { h.ParentId = "asd" }, "model.command_hook.parent_id.app_error"}, + {func() { h.ParentId = NewId() }, ""}, + } { + tmp := h + test.Transform() + err := h.IsValid() + if test.ExpectedError == "" && err != nil { + t.Fatal("hook should be valid") + } else if test.ExpectedError != "" && test.ExpectedError != err.Id { + t.Fatal("expected " + test.ExpectedError + " error") + } + h = tmp + } +} diff --git a/model/config.go b/model/config.go index 1717d61a0..9906723e7 100644 --- a/model/config.go +++ b/model/config.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "time" ) const ( @@ -123,11 +124,13 @@ const ( ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_COLOR = "#f2a93b" ANNOUNCEMENT_SETTINGS_DEFAULT_BANNER_TEXT_COLOR = "#333333" - ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL = "" - ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME = "" - ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD = "" - ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS = 1 - ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_CONNECTION_URL = "" + ELASTICSEARCH_SETTINGS_DEFAULT_USERNAME = "" + ELASTICSEARCH_SETTINGS_DEFAULT_PASSWORD = "" + ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_REPLICAS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS = 1 + ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS = 365 + ELASTICSEARCH_SETTINGS_DEFAULT_POSTS_AGGREGATOR_JOB_START_TIME = "03:00" ) type ServiceSettings struct { @@ -158,6 +161,7 @@ type ServiceSettings struct { EnableDeveloper *bool EnableSecurityFixAlert *bool EnableInsecureOutgoingConnections *bool + AllowedUntrustedInternalConnections *string EnableMultifactorAuthentication *bool EnforceMultifactorAuthentication *bool EnableUserAccessTokens *bool @@ -440,14 +444,16 @@ type WebrtcSettings struct { } type ElasticsearchSettings struct { - ConnectionUrl *string - Username *string - Password *string - EnableIndexing *bool - EnableSearching *bool - Sniff *bool - PostIndexReplicas *int - PostIndexShards *int + ConnectionUrl *string + Username *string + Password *string + EnableIndexing *bool + EnableSearching *bool + Sniff *bool + PostIndexReplicas *int + PostIndexShards *int + AggregatePostsAfterDays *int + PostsAggregatorJobStartTime *string } type DataRetentionSettings struct { @@ -629,6 +635,10 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.EnableInsecureOutgoingConnections = false } + if o.ServiceSettings.AllowedUntrustedInternalConnections == nil { + o.ServiceSettings.AllowedUntrustedInternalConnections = new(string) + } + if o.ServiceSettings.EnableMultifactorAuthentication == nil { o.ServiceSettings.EnableMultifactorAuthentication = new(bool) *o.ServiceSettings.EnableMultifactorAuthentication = false @@ -1447,6 +1457,16 @@ func (o *Config) SetDefaults() { *o.ElasticsearchSettings.PostIndexShards = ELASTICSEARCH_SETTINGS_DEFAULT_POST_INDEX_SHARDS } + if o.ElasticsearchSettings.AggregatePostsAfterDays == nil { + o.ElasticsearchSettings.AggregatePostsAfterDays = new(int) + *o.ElasticsearchSettings.AggregatePostsAfterDays = ELASTICSEARCH_SETTINGS_DEFAULT_AGGREGATE_POSTS_AFTER_DAYS + } + + if o.ElasticsearchSettings.PostsAggregatorJobStartTime == nil { + o.ElasticsearchSettings.PostsAggregatorJobStartTime = new(string) + *o.ElasticsearchSettings.PostsAggregatorJobStartTime = ELASTICSEARCH_SETTINGS_DEFAULT_POSTS_AGGREGATOR_JOB_START_TIME + } + if o.DataRetentionSettings.Enable == nil { o.DataRetentionSettings.Enable = new(bool) *o.DataRetentionSettings.Enable = false @@ -1695,6 +1715,14 @@ func (o *Config) IsValid() *AppError { return NewLocAppError("Config.IsValid", "model.config.is_valid.elastic_search.enable_searching.app_error", nil, "") } + if *o.ElasticsearchSettings.AggregatePostsAfterDays < 1 { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.aggregate_posts_after_days.app_error", nil, "", http.StatusBadRequest) + } + + if _, err := time.Parse("03:04", *o.ElasticsearchSettings.PostsAggregatorJobStartTime); err != nil { + return NewAppError("Config.IsValid", "model.config.is_valid.elastic_search.posts_aggregator_job_start_time.app_error", nil, err.Error(), http.StatusBadRequest) + } + return nil } diff --git a/model/job.go b/model/job.go index 004331a1f..258fa2bd3 100644 --- a/model/job.go +++ b/model/job.go @@ -10,8 +10,9 @@ import ( ) const ( - JOB_TYPE_DATA_RETENTION = "data_retention" - JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing" + JOB_TYPE_DATA_RETENTION = "data_retention" + JOB_TYPE_ELASTICSEARCH_POST_INDEXING = "elasticsearch_post_indexing" + JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION = "elasticsearch_post_aggregation" JOB_STATUS_PENDING = "pending" JOB_STATUS_IN_PROGRESS = "in_progress" @@ -22,15 +23,15 @@ const ( ) type Job struct { - Id string `json:"id"` - Type string `json:"type"` - Priority int64 `json:"priority"` - CreateAt int64 `json:"create_at"` - StartAt int64 `json:"start_at"` - LastActivityAt int64 `json:"last_activity_at"` - Status string `json:"status"` - Progress int64 `json:"progress"` - Data map[string]interface{} `json:"data"` + Id string `json:"id"` + Type string `json:"type"` + Priority int64 `json:"priority"` + CreateAt int64 `json:"create_at"` + StartAt int64 `json:"start_at"` + LastActivityAt int64 `json:"last_activity_at"` + Status string `json:"status"` + Progress int64 `json:"progress"` + Data map[string]string `json:"data"` } func (j *Job) IsValid() *AppError { @@ -45,6 +46,7 @@ func (j *Job) IsValid() *AppError { switch j.Type { case JOB_TYPE_DATA_RETENTION: case JOB_TYPE_ELASTICSEARCH_POST_INDEXING: + case JOB_TYPE_ELASTICSEARCH_POST_AGGREGATION: default: return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest) } diff --git a/plugin/api.go b/plugin/api.go new file mode 100644 index 000000000..c62ae0f55 --- /dev/null +++ b/plugin/api.go @@ -0,0 +1,7 @@ +package plugin + +type API interface { + // LoadPluginConfiguration loads the plugin's configuration. dest should be a pointer to a + // struct that the configuration JSON can be unmarshalled to. + LoadPluginConfiguration(dest interface{}) error +} diff --git a/plugin/bundle_info.go b/plugin/bundle_info.go new file mode 100644 index 000000000..9dc47ceea --- /dev/null +++ b/plugin/bundle_info.go @@ -0,0 +1,20 @@ +package plugin + +type BundleInfo struct { + Path string + + Manifest *Manifest + ManifestPath string + ManifestError error +} + +// Returns bundle info for the given path. The return value is never nil. +func BundleInfoForPath(path string) *BundleInfo { + m, mpath, err := FindManifest(path) + return &BundleInfo{ + Path: path, + Manifest: m, + ManifestPath: mpath, + ManifestError: err, + } +} diff --git a/plugin/bundle_info_test.go b/plugin/bundle_info_test.go new file mode 100644 index 000000000..94a0c624f --- /dev/null +++ b/plugin/bundle_info_test.go @@ -0,0 +1,30 @@ +package plugin + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBundleInfoForPath(t *testing.T) { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, "plugin.json") + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.WriteString(`{"id": "foo"}`) + f.Close() + require.NoError(t, err) + + info := BundleInfoForPath(dir) + assert.Equal(t, info.Path, dir) + assert.NotNil(t, info.Manifest) + assert.Equal(t, info.ManifestPath, path) + assert.Nil(t, info.ManifestError) +} diff --git a/plugin/hooks.go b/plugin/hooks.go new file mode 100644 index 000000000..28a762a1a --- /dev/null +++ b/plugin/hooks.go @@ -0,0 +1,10 @@ +package plugin + +type Hooks interface { + // OnActivate is invoked when the plugin is activated. + OnActivate(API) error + + // OnDeactivate is invoked when the plugin is deactivated. This is the plugin's last chance to + // use the API, and the plugin will be terminated shortly after this invocation. + OnDeactivate() error +} diff --git a/plugin/manifest.go b/plugin/manifest.go new file mode 100644 index 000000000..15b7f0555 --- /dev/null +++ b/plugin/manifest.go @@ -0,0 +1,70 @@ +package plugin + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +type Manifest struct { + Id string `json:"id" yaml:"id"` + Backend *ManifestBackend `json:"backend,omitempty" yaml:"backend,omitempty"` +} + +type ManifestBackend struct { + Executable string `json:"executable" yaml:"executable"` +} + +// FindManifest will find and parse the manifest in a given directory. +// +// In all cases other than a does-not-exist error, path is set to the path of the manifest file that was +// found. +// +// Manifests are JSON or YAML files named plugin.json, plugin.yaml, or plugin.yml. +func FindManifest(dir string) (manifest *Manifest, path string, err error) { + for _, name := range []string{"plugin.yml", "plugin.yaml"} { + path = filepath.Join(dir, name) + f, ferr := os.Open(path) + if ferr != nil { + if !os.IsNotExist(ferr) { + err = ferr + return + } + continue + } + b, ioerr := ioutil.ReadAll(f) + f.Close() + if ioerr != nil { + err = ioerr + return + } + var parsed Manifest + err = yaml.Unmarshal(b, &parsed) + if err != nil { + return + } + manifest = &parsed + return + } + + path = filepath.Join(dir, "plugin.json") + f, ferr := os.Open(path) + if ferr != nil { + if os.IsNotExist(ferr) { + path = "" + } + err = ferr + return + } + defer f.Close() + var parsed Manifest + err = json.NewDecoder(f).Decode(&parsed) + if err != nil { + return + } + manifest = &parsed + return +} diff --git a/plugin/manifest_test.go b/plugin/manifest_test.go new file mode 100644 index 000000000..5dae4fbaa --- /dev/null +++ b/plugin/manifest_test.go @@ -0,0 +1,97 @@ +package plugin + +import ( + "encoding/json" + "gopkg.in/yaml.v2" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindManifest(t *testing.T) { + for _, tc := range []struct { + Filename string + Contents string + ExpectError bool + ExpectNotExist bool + }{ + {"foo", "bar", true, true}, + {"plugin.json", "bar", true, false}, + {"plugin.json", `{"id": "foo"}`, false, false}, + {"plugin.yaml", `id: foo`, false, false}, + {"plugin.yaml", "bar", true, false}, + {"plugin.yml", `id: foo`, false, false}, + {"plugin.yml", "bar", true, false}, + } { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, tc.Filename) + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.WriteString(tc.Contents) + f.Close() + require.NoError(t, err) + + m, mpath, err := FindManifest(dir) + assert.True(t, (err != nil) == tc.ExpectError, tc.Filename) + assert.True(t, (err != nil && os.IsNotExist(err)) == tc.ExpectNotExist, tc.Filename) + if !tc.ExpectNotExist { + assert.Equal(t, path, mpath, tc.Filename) + } else { + assert.Empty(t, mpath, tc.Filename) + } + if !tc.ExpectError { + require.NotNil(t, m, tc.Filename) + assert.NotEmpty(t, m.Id, tc.Filename) + } + } +} + +func TestManifestUnmarshal(t *testing.T) { + expected := Manifest{ + Id: "theid", + Backend: &ManifestBackend{ + Executable: "theexecutable", + }, + } + + var yamlResult Manifest + require.NoError(t, yaml.Unmarshal([]byte(` +id: theid +backend: + executable: theexecutable +`), &yamlResult)) + assert.Equal(t, expected, yamlResult) + + var jsonResult Manifest + require.NoError(t, json.Unmarshal([]byte(`{ + "id": "theid", + "backend": { + "executable": "theexecutable" + } + }`), &jsonResult)) + assert.Equal(t, expected, jsonResult) +} + +func TestFindManifest_FileErrors(t *testing.T) { + for _, tc := range []string{"plugin.yaml", "plugin.json"} { + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer os.RemoveAll(dir) + + path := filepath.Join(dir, tc) + require.NoError(t, os.Mkdir(path, 0700)) + + m, mpath, err := FindManifest(dir) + assert.Nil(t, m) + assert.Equal(t, path, mpath) + assert.Error(t, err, tc) + assert.False(t, os.IsNotExist(err), tc) + } +} diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go new file mode 100644 index 000000000..36a8c6e76 --- /dev/null +++ b/plugin/pluginenv/environment.go @@ -0,0 +1,123 @@ +// Package pluginenv provides high level functionality for discovering and launching plugins. +package pluginenv + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/mattermost/platform/plugin" +) + +type APIProviderFunc func(*plugin.Manifest) (plugin.API, error) +type SupervisorProviderFunc func(*plugin.BundleInfo) (plugin.Supervisor, error) + +// Environment represents an environment that plugins are discovered and launched in. +type Environment struct { + searchPath string + apiProvider APIProviderFunc + supervisorProvider SupervisorProviderFunc + activePlugins map[string]plugin.Supervisor +} + +type Option func(*Environment) + +// Creates a new environment. At a minimum, the APIProvider and SearchPath options are required. +func New(options ...Option) (*Environment, error) { + env := &Environment{ + activePlugins: make(map[string]plugin.Supervisor), + } + for _, opt := range options { + opt(env) + } + if env.supervisorProvider == nil { + env.supervisorProvider = DefaultSupervisorProvider + } + if env.searchPath == "" { + return nil, fmt.Errorf("a search path must be provided") + } else if env.apiProvider == nil { + return nil, fmt.Errorf("an api provider must be provided") + } + return env, nil +} + +// Returns a list of all plugins found within the environment. +func (env *Environment) Plugins() ([]*plugin.BundleInfo, error) { + return ScanSearchPath(env.searchPath) +} + +// Returns the ids of the currently active plugins. +func (env *Environment) ActivePluginIds() (ids []string) { + for id := range env.activePlugins { + ids = append(ids, id) + } + return +} + +// Activates the plugin with the given id. +func (env *Environment) ActivatePlugin(id string) error { + if _, ok := env.activePlugins[id]; ok { + return fmt.Errorf("plugin already active: %v", id) + } + plugins, err := ScanSearchPath(env.searchPath) + if err != nil { + return err + } + var plugin *plugin.BundleInfo + for _, p := range plugins { + if p.Manifest != nil && p.Manifest.Id == id { + if plugin != nil { + return fmt.Errorf("multiple plugins found: %v", id) + } + plugin = p + } + } + if plugin == nil { + return fmt.Errorf("plugin not found: %v", id) + } + supervisor, err := env.supervisorProvider(plugin) + if err != nil { + return errors.Wrapf(err, "unable to create supervisor for plugin: %v", id) + } + api, err := env.apiProvider(plugin.Manifest) + if err != nil { + return errors.Wrapf(err, "unable to get api for plugin: %v", id) + } + if err := supervisor.Start(); err != nil { + return errors.Wrapf(err, "unable to start plugin: %v", id) + } + if err := supervisor.Hooks().OnActivate(api); err != nil { + supervisor.Stop() + return errors.Wrapf(err, "unable to activate plugin: %v", id) + } + env.activePlugins[id] = supervisor + return nil +} + +// Deactivates the plugin with the given id. +func (env *Environment) DeactivatePlugin(id string) error { + if supervisor, ok := env.activePlugins[id]; !ok { + return fmt.Errorf("plugin not active: %v", id) + } else { + delete(env.activePlugins, id) + err := supervisor.Hooks().OnDeactivate() + if serr := supervisor.Stop(); err == nil { + err = serr + } + return err + } +} + +// Deactivates all plugins and gracefully shuts down the environment. +func (env *Environment) Shutdown() (errs []error) { + for _, supervisor := range env.activePlugins { + if err := supervisor.Hooks().OnDeactivate(); err != nil { + errs = append(errs, err) + } + if err := supervisor.Stop(); err != nil { + errs = append(errs, err) + } + } + env.activePlugins = make(map[string]plugin.Supervisor) + return +} diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go new file mode 100644 index 000000000..d933c8696 --- /dev/null +++ b/plugin/pluginenv/environment_test.go @@ -0,0 +1,291 @@ +package pluginenv + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/plugintest" +) + +type MockProvider struct { + mock.Mock +} + +func (m *MockProvider) API(manifest *plugin.Manifest) (plugin.API, error) { + ret := m.Called() + if ret.Get(0) == nil { + return nil, ret.Error(1) + } + return ret.Get(0).(plugin.API), ret.Error(1) +} + +func (m *MockProvider) Supervisor(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { + ret := m.Called() + if ret.Get(0) == nil { + return nil, ret.Error(1) + } + return ret.Get(0).(plugin.Supervisor), ret.Error(1) +} + +type MockSupervisor struct { + mock.Mock +} + +func (m *MockSupervisor) Start() error { + return m.Called().Error(0) +} + +func (m *MockSupervisor) Stop() error { + return m.Called().Error(0) +} + +func (m *MockSupervisor) Hooks() plugin.Hooks { + return m.Called().Get(0).(plugin.Hooks) +} + +func initTmpDir(t *testing.T, files map[string]string) string { + success := false + dir, err := ioutil.TempDir("", "mm-plugin-test") + require.NoError(t, err) + defer func() { + if !success { + os.RemoveAll(dir) + } + }() + + for name, contents := range files { + path := filepath.Join(dir, name) + parent := filepath.Dir(path) + require.NoError(t, os.MkdirAll(parent, 0700)) + f, err := os.Create(path) + require.NoError(t, err) + _, err = f.WriteString(contents) + f.Close() + require.NoError(t, err) + } + + success = true + return dir +} + +func TestNew_MissingOptions(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + "foo/plugin.json": `{"id": "foo"}`, + }) + defer os.RemoveAll(dir) + + var provider MockProvider + defer provider.AssertExpectations(t) + + env, err := New( + APIProvider(provider.API), + ) + assert.Nil(t, env) + assert.Error(t, err) + + env, err = New( + SearchPath(dir), + ) + assert.Nil(t, env) + assert.Error(t, err) +} + +func TestEnvironment(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + ".foo/plugin.json": `{"id": "foo"}`, + "foo/bar": "asdf", + "foo/plugin.json": `{"id": "foo"}`, + "bar/zxc": "qwer", + "baz/plugin.yaml": "id: baz", + "bad/plugin.json": "asd", + "qwe": "asd", + }) + defer os.RemoveAll(dir) + + var provider MockProvider + defer provider.AssertExpectations(t) + + env, err := New( + SearchPath(dir), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + plugins, err := env.Plugins() + assert.NoError(t, err) + assert.Len(t, plugins, 3) + + assert.Error(t, env.ActivatePlugin("x")) + + var api struct{ plugin.API } + var supervisor MockSupervisor + defer supervisor.AssertExpectations(t) + var hooks plugintest.Hooks + defer hooks.AssertExpectations(t) + + provider.On("API").Return(&api, nil) + provider.On("Supervisor").Return(&supervisor, nil) + + supervisor.On("Start").Return(nil) + supervisor.On("Stop").Return(nil) + supervisor.On("Hooks").Return(&hooks) + + hooks.On("OnActivate", &api).Return(nil) + + assert.NoError(t, env.ActivatePlugin("foo")) + assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) + assert.Error(t, env.ActivatePlugin("foo")) + + hooks.On("OnDeactivate").Return(nil) + assert.NoError(t, env.DeactivatePlugin("foo")) + assert.Error(t, env.DeactivatePlugin("foo")) + + assert.NoError(t, env.ActivatePlugin("foo")) + assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) + assert.Empty(t, env.Shutdown()) +} + +func TestEnvironment_DuplicatePluginError(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + "foo/plugin.json": `{"id": "foo"}`, + "foo2/plugin.json": `{"id": "foo"}`, + }) + defer os.RemoveAll(dir) + + var provider MockProvider + defer provider.AssertExpectations(t) + + env, err := New( + SearchPath(dir), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + assert.Error(t, env.ActivatePlugin("foo")) + assert.Empty(t, env.ActivePluginIds()) +} + +func TestEnvironment_BadSearchPathError(t *testing.T) { + var provider MockProvider + defer provider.AssertExpectations(t) + + env, err := New( + SearchPath("thissearchpathshouldnotexist!"), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + assert.Error(t, env.ActivatePlugin("foo")) + assert.Empty(t, env.ActivePluginIds()) +} + +func TestEnvironment_ActivatePluginErrors(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + "foo/plugin.json": `{"id": "foo"}`, + }) + defer os.RemoveAll(dir) + + var provider MockProvider + + env, err := New( + SearchPath(dir), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + var api struct{ plugin.API } + var supervisor MockSupervisor + var hooks plugintest.Hooks + + for name, setup := range map[string]func(){ + "SupervisorProviderError": func() { + provider.On("Supervisor").Return(nil, fmt.Errorf("test error")) + }, + "APIProviderError": func() { + provider.On("API").Return(plugin.API(nil), fmt.Errorf("test error")) + provider.On("Supervisor").Return(&supervisor, nil) + }, + "SupervisorError": func() { + provider.On("API").Return(&api, nil) + provider.On("Supervisor").Return(&supervisor, nil) + + supervisor.On("Start").Return(fmt.Errorf("test error")) + }, + "HooksError": func() { + provider.On("API").Return(&api, nil) + provider.On("Supervisor").Return(&supervisor, nil) + + supervisor.On("Start").Return(nil) + supervisor.On("Stop").Return(nil) + supervisor.On("Hooks").Return(&hooks) + + hooks.On("OnActivate", &api).Return(fmt.Errorf("test error")) + }, + } { + t.Run(name, func(t *testing.T) { + supervisor.Mock = mock.Mock{} + hooks.Mock = mock.Mock{} + provider.Mock = mock.Mock{} + setup() + assert.Error(t, env.ActivatePlugin("foo")) + assert.Empty(t, env.ActivePluginIds()) + supervisor.AssertExpectations(t) + hooks.AssertExpectations(t) + provider.AssertExpectations(t) + }) + } +} + +func TestEnvironment_ShutdownError(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + "foo/plugin.json": `{"id": "foo"}`, + }) + defer os.RemoveAll(dir) + + var provider MockProvider + defer provider.AssertExpectations(t) + + env, err := New( + SearchPath(dir), + APIProvider(provider.API), + SupervisorProvider(provider.Supervisor), + ) + require.NoError(t, err) + defer env.Shutdown() + + var api struct{ plugin.API } + var supervisor MockSupervisor + defer supervisor.AssertExpectations(t) + var hooks plugintest.Hooks + defer hooks.AssertExpectations(t) + + provider.On("API").Return(&api, nil) + provider.On("Supervisor").Return(&supervisor, nil) + + supervisor.On("Start").Return(nil) + supervisor.On("Stop").Return(fmt.Errorf("test error")) + supervisor.On("Hooks").Return(&hooks) + + hooks.On("OnActivate", &api).Return(nil) + hooks.On("OnDeactivate").Return(fmt.Errorf("test error")) + + assert.NoError(t, env.ActivatePlugin("foo")) + assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) + assert.Len(t, env.Shutdown(), 2) +} diff --git a/plugin/pluginenv/options.go b/plugin/pluginenv/options.go new file mode 100644 index 000000000..3f83228fb --- /dev/null +++ b/plugin/pluginenv/options.go @@ -0,0 +1,42 @@ +package pluginenv + +import ( + "fmt" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/rpcplugin" +) + +// APIProvider specifies a function that provides an API implementation to each plugin. +func APIProvider(provider APIProviderFunc) Option { + return func(env *Environment) { + env.apiProvider = provider + } +} + +// SupervisorProvider specifies a function that provides a Supervisor implementation to each plugin. +// If unspecified, DefaultSupervisorProvider is used. +func SupervisorProvider(provider SupervisorProviderFunc) Option { + return func(env *Environment) { + env.supervisorProvider = provider + } +} + +// SearchPath specifies a directory that contains the plugins to launch. +func SearchPath(path string) Option { + return func(env *Environment) { + env.searchPath = path + } +} + +// DefaultSupervisorProvider chooses a supervisor based on the plugin's manifest contents. E.g. if +// the manifest specifies a backend executable, it will be given an rpcplugin.Supervisor. +func DefaultSupervisorProvider(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { + if bundle.Manifest == nil { + return nil, fmt.Errorf("a manifest is required") + } + if bundle.Manifest.Backend == nil { + return nil, fmt.Errorf("invalid manifest: at this time, only backend plugins are supported") + } + return rpcplugin.SupervisorProvider(bundle) +} diff --git a/plugin/pluginenv/options_test.go b/plugin/pluginenv/options_test.go new file mode 100644 index 000000000..4f8d411bd --- /dev/null +++ b/plugin/pluginenv/options_test.go @@ -0,0 +1,32 @@ +package pluginenv + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/rpcplugin" +) + +func TestDefaultSupervisorProvider(t *testing.T) { + _, err := DefaultSupervisorProvider(&plugin.BundleInfo{}) + assert.Error(t, err) + + _, err = DefaultSupervisorProvider(&plugin.BundleInfo{ + Manifest: &plugin.Manifest{}, + }) + assert.Error(t, err) + + supervisor, err := DefaultSupervisorProvider(&plugin.BundleInfo{ + Manifest: &plugin.Manifest{ + Backend: &plugin.ManifestBackend{ + Executable: "foo", + }, + }, + }) + require.NoError(t, err) + _, ok := supervisor.(*rpcplugin.Supervisor) + assert.True(t, ok) +} diff --git a/plugin/pluginenv/search_path.go b/plugin/pluginenv/search_path.go new file mode 100644 index 000000000..daebdb0d3 --- /dev/null +++ b/plugin/pluginenv/search_path.go @@ -0,0 +1,32 @@ +package pluginenv + +import ( + "io/ioutil" + "path/filepath" + + "github.com/mattermost/platform/plugin" +) + +// Performs a full scan of the given path. +// +// This function will return info for all subdirectories that appear to be plugins (i.e. all +// subdirectories containing plugin manifest files, regardless of whether they could actually be +// parsed). +// +// Plugins are found non-recursively and paths beginning with a dot are always ignored. +func ScanSearchPath(path string) ([]*plugin.BundleInfo, error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + var ret []*plugin.BundleInfo + for _, file := range files { + if !file.IsDir() || file.Name()[0] == '.' { + continue + } + if info := plugin.BundleInfoForPath(filepath.Join(path, file.Name())); info.ManifestPath != "" { + ret = append(ret, info) + } + } + return ret, nil +} diff --git a/plugin/pluginenv/search_path_test.go b/plugin/pluginenv/search_path_test.go new file mode 100644 index 000000000..d9a18cf56 --- /dev/null +++ b/plugin/pluginenv/search_path_test.go @@ -0,0 +1,62 @@ +package pluginenv + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/platform/plugin" +) + +func TestScanSearchPath(t *testing.T) { + dir := initTmpDir(t, map[string]string{ + ".foo/plugin.json": `{"id": "foo"}`, + "foo/bar": "asdf", + "foo/plugin.json": `{"id": "foo"}`, + "bar/zxc": "qwer", + "baz/plugin.yaml": "id: baz", + "bad/plugin.json": "asd", + "qwe": "asd", + }) + defer os.RemoveAll(dir) + + plugins, err := ScanSearchPath(dir) + require.NoError(t, err) + assert.Len(t, plugins, 3) + assert.Contains(t, plugins, &plugin.BundleInfo{ + Path: filepath.Join(dir, "foo"), + ManifestPath: filepath.Join(dir, "foo", "plugin.json"), + Manifest: &plugin.Manifest{ + Id: "foo", + }, + }) + assert.Contains(t, plugins, &plugin.BundleInfo{ + Path: filepath.Join(dir, "baz"), + ManifestPath: filepath.Join(dir, "baz", "plugin.yaml"), + Manifest: &plugin.Manifest{ + Id: "baz", + }, + }) + foundError := false + for _, x := range plugins { + if x.ManifestError != nil { + assert.Equal(t, x.Path, filepath.Join(dir, "bad")) + assert.Equal(t, x.ManifestPath, filepath.Join(dir, "bad", "plugin.json")) + syntexError, ok := x.ManifestError.(*json.SyntaxError) + assert.True(t, ok) + assert.EqualValues(t, 1, syntexError.Offset) + foundError = true + } + } + assert.True(t, foundError) +} + +func TestScanSearchPath_Error(t *testing.T) { + plugins, err := ScanSearchPath("not a valid path!") + assert.Nil(t, plugins) + assert.Error(t, err) +} diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go new file mode 100644 index 000000000..2f1db88cf --- /dev/null +++ b/plugin/plugintest/api.go @@ -0,0 +1,17 @@ +package plugintest + +import ( + "github.com/stretchr/testify/mock" + + "github.com/mattermost/platform/plugin" +) + +type API struct { + mock.Mock +} + +var _ plugin.API = (*API)(nil) + +func (m *API) LoadPluginConfiguration(dest interface{}) error { + return m.Called(dest).Error(0) +} diff --git a/plugin/plugintest/hooks.go b/plugin/plugintest/hooks.go new file mode 100644 index 000000000..057c705c9 --- /dev/null +++ b/plugin/plugintest/hooks.go @@ -0,0 +1,21 @@ +package plugintest + +import ( + "github.com/stretchr/testify/mock" + + "github.com/mattermost/platform/plugin" +) + +type Hooks struct { + mock.Mock +} + +var _ plugin.Hooks = (*Hooks)(nil) + +func (m *Hooks) OnActivate(api plugin.API) error { + return m.Called(api).Error(0) +} + +func (m *Hooks) OnDeactivate() error { + return m.Called().Error(0) +} diff --git a/plugin/rpcplugin/api.go b/plugin/rpcplugin/api.go new file mode 100644 index 000000000..a807d0837 --- /dev/null +++ b/plugin/rpcplugin/api.go @@ -0,0 +1,62 @@ +package rpcplugin + +import ( + "encoding/json" + "io" + "net/rpc" + + "github.com/mattermost/platform/plugin" +) + +type LocalAPI struct { + api plugin.API + muxer *Muxer +} + +func (h *LocalAPI) LoadPluginConfiguration(args struct{}, reply *[]byte) error { + var config interface{} + if err := h.api.LoadPluginConfiguration(&config); err != nil { + return err + } + b, err := json.Marshal(config) + if err != nil { + return err + } + *reply = b + return nil +} + +type RemoteAPI struct { + client *rpc.Client + muxer *Muxer +} + +func ServeAPI(api plugin.API, conn io.ReadWriteCloser, muxer *Muxer) { + server := rpc.NewServer() + server.Register(&LocalAPI{ + api: api, + muxer: muxer, + }) + server.ServeConn(conn) +} + +var _ plugin.API = (*RemoteAPI)(nil) + +func (h *RemoteAPI) LoadPluginConfiguration(dest interface{}) error { + var config []byte + if err := h.client.Call("LocalAPI.LoadPluginConfiguration", struct{}{}, &config); err != nil { + return err + } + return json.Unmarshal(config, dest) +} + +func (h *RemoteAPI) Close() error { + return h.client.Close() +} + +func ConnectAPI(conn io.ReadWriteCloser, muxer *Muxer) *RemoteAPI { + return &RemoteAPI{ + client: rpc.NewClient(conn), + muxer: muxer, + } +} diff --git a/plugin/rpcplugin/api_test.go b/plugin/rpcplugin/api_test.go new file mode 100644 index 000000000..e55433556 --- /dev/null +++ b/plugin/rpcplugin/api_test.go @@ -0,0 +1,57 @@ +package rpcplugin + +import ( + "encoding/json" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/plugintest" +) + +func testAPIRPC(api plugin.API, f func(plugin.API)) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + c1 := NewMuxer(NewReadWriteCloser(r1, w2), false) + defer c1.Close() + + c2 := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer c2.Close() + + id, server := c1.Serve() + go ServeAPI(api, server, c1) + + remote := ConnectAPI(c2.Connect(id), c2) + defer remote.Close() + + f(remote) +} + +func TestAPI(t *testing.T) { + var api plugintest.API + defer api.AssertExpectations(t) + + type Config struct { + Foo string + Bar struct { + Baz string + } + } + + api.On("LoadPluginConfiguration", mock.MatchedBy(func(x interface{}) bool { return true })).Run(func(args mock.Arguments) { + dest := args.Get(0).(interface{}) + json.Unmarshal([]byte(`{"Foo": "foo", "Bar": {"Baz": "baz"}}`), dest) + }).Return(nil) + + testAPIRPC(&api, func(remote plugin.API) { + var config Config + assert.NoError(t, remote.LoadPluginConfiguration(&config)) + + assert.Equal(t, "foo", config.Foo) + assert.Equal(t, "baz", config.Bar.Baz) + }) +} diff --git a/plugin/rpcplugin/hooks.go b/plugin/rpcplugin/hooks.go new file mode 100644 index 000000000..008730402 --- /dev/null +++ b/plugin/rpcplugin/hooks.go @@ -0,0 +1,77 @@ +package rpcplugin + +import ( + "io" + "net/rpc" + + "github.com/mattermost/platform/plugin" +) + +type LocalHooks struct { + hooks plugin.Hooks + muxer *Muxer + remoteAPI *RemoteAPI +} + +func (h *LocalHooks) OnActivate(args int64, reply *struct{}) error { + stream := h.muxer.Connect(args) + if h.remoteAPI != nil { + h.remoteAPI.Close() + } + h.remoteAPI = ConnectAPI(stream, h.muxer) + return h.hooks.OnActivate(h.remoteAPI) +} + +func (h *LocalHooks) OnDeactivate(args, reply *struct{}) error { + err := h.hooks.OnDeactivate() + if h.remoteAPI != nil { + h.remoteAPI.Close() + h.remoteAPI = nil + } + return err +} + +type RemoteHooks struct { + client *rpc.Client + muxer *Muxer + apiCloser io.Closer +} + +func ServeHooks(hooks plugin.Hooks, conn io.ReadWriteCloser, muxer *Muxer) { + server := rpc.NewServer() + server.Register(&LocalHooks{ + hooks: hooks, + muxer: muxer, + }) + server.ServeConn(conn) +} + +var _ plugin.Hooks = (*RemoteHooks)(nil) + +func (h *RemoteHooks) OnActivate(api plugin.API) error { + id, stream := h.muxer.Serve() + if h.apiCloser != nil { + h.apiCloser.Close() + } + h.apiCloser = stream + go ServeAPI(api, stream, h.muxer) + return h.client.Call("LocalHooks.OnActivate", id, nil) +} + +func (h *RemoteHooks) OnDeactivate() error { + return h.client.Call("LocalHooks.OnDeactivate", struct{}{}, nil) +} + +func (h *RemoteHooks) Close() error { + if h.apiCloser != nil { + h.apiCloser.Close() + } + return h.client.Close() +} + +func ConnectHooks(conn io.ReadWriteCloser, muxer *Muxer) *RemoteHooks { + return &RemoteHooks{ + client: rpc.NewClient(conn), + muxer: muxer, + } +} diff --git a/plugin/rpcplugin/hooks_test.go b/plugin/rpcplugin/hooks_test.go new file mode 100644 index 000000000..fbbbbedeb --- /dev/null +++ b/plugin/rpcplugin/hooks_test.go @@ -0,0 +1,58 @@ +package rpcplugin + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/plugintest" +) + +func testHooksRPC(hooks plugin.Hooks, f func(plugin.Hooks)) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + c1 := NewMuxer(NewReadWriteCloser(r1, w2), false) + defer c1.Close() + + c2 := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer c2.Close() + + id, server := c1.Serve() + go ServeHooks(hooks, server, c1) + + remote := ConnectHooks(c2.Connect(id), c2) + defer remote.Close() + + f(remote) +} + +func TestHooks(t *testing.T) { + var api plugintest.API + var hooks plugintest.Hooks + defer hooks.AssertExpectations(t) + + testHooksRPC(&hooks, func(remote plugin.Hooks) { + hooks.On("OnActivate", mock.AnythingOfType("*rpcplugin.RemoteAPI")).Return(nil) + assert.NoError(t, remote.OnActivate(&api)) + + hooks.On("OnDeactivate").Return(nil) + assert.NoError(t, remote.OnDeactivate()) + }) +} + +func BenchmarkOnDeactivate(b *testing.B) { + var hooks plugintest.Hooks + hooks.On("OnDeactivate").Return(nil) + + testHooksRPC(&hooks, func(remote plugin.Hooks) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + remote.OnDeactivate() + } + b.StopTimer() + }) +} diff --git a/plugin/rpcplugin/io.go b/plugin/rpcplugin/io.go new file mode 100644 index 000000000..f1b2f3c35 --- /dev/null +++ b/plugin/rpcplugin/io.go @@ -0,0 +1,23 @@ +package rpcplugin + +import ( + "io" +) + +type rwc struct { + io.ReadCloser + io.WriteCloser +} + +func (rwc *rwc) Close() error { + rerr := rwc.ReadCloser.Close() + werr := rwc.WriteCloser.Close() + if rerr != nil { + return rerr + } + return werr +} + +func NewReadWriteCloser(r io.ReadCloser, w io.WriteCloser) io.ReadWriteCloser { + return &rwc{r, w} +} diff --git a/plugin/rpcplugin/ipc.go b/plugin/rpcplugin/ipc.go new file mode 100644 index 000000000..3e6c89c4f --- /dev/null +++ b/plugin/rpcplugin/ipc.go @@ -0,0 +1,28 @@ +package rpcplugin + +import ( + "io" + "os" +) + +// Returns a new IPC for the parent process and a set of files to pass on to the child. +// +// The returned files must be closed after the child process is started. +func NewIPC() (io.ReadWriteCloser, []*os.File, error) { + parentReader, childWriter, err := os.Pipe() + if err != nil { + return nil, nil, err + } + childReader, parentWriter, err := os.Pipe() + if err != nil { + parentReader.Close() + childWriter.Close() + return nil, nil, err + } + return NewReadWriteCloser(parentReader, parentWriter), []*os.File{childReader, childWriter}, nil +} + +// Returns the IPC instance inherited by the process from its parent. +func InheritedIPC(fd0, fd1 uintptr) (io.ReadWriteCloser, error) { + return NewReadWriteCloser(os.NewFile(fd0, ""), os.NewFile(fd1, "")), nil +} diff --git a/plugin/rpcplugin/ipc_test.go b/plugin/rpcplugin/ipc_test.go new file mode 100644 index 000000000..bf4df3017 --- /dev/null +++ b/plugin/rpcplugin/ipc_test.go @@ -0,0 +1,61 @@ +package rpcplugin + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIPC(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + pingpong := filepath.Join(dir, "pingpong") + compileGo(t, ` + package main + + import ( + "log" + + "github.com/mattermost/platform/plugin/rpcplugin" + ) + + func main() { + ipc, err := rpcplugin.InheritedProcessIPC() + if err != nil { + log.Fatal("unable to get inherited ipc") + } + defer ipc.Close() + _, err = ipc.Write([]byte("ping")) + if err != nil { + log.Fatal("unable to write to ipc") + } + b := make([]byte, 10) + n, err := ipc.Read(b) + if err != nil { + log.Fatal("unable to read from ipc") + } + if n != 4 || string(b[:4]) != "pong" { + log.Fatal("unexpected response") + } + } + `, pingpong) + + p, ipc, err := NewProcess(context.Background(), pingpong) + require.NoError(t, err) + defer ipc.Close() + b := make([]byte, 10) + n, err := ipc.Read(b) + require.NoError(t, err) + assert.Equal(t, 4, n) + assert.Equal(t, "ping", string(b[:4])) + _, err = ipc.Write([]byte("pong")) + require.NoError(t, err) + require.NoError(t, p.Wait()) +} diff --git a/plugin/rpcplugin/main.go b/plugin/rpcplugin/main.go new file mode 100644 index 000000000..36177954b --- /dev/null +++ b/plugin/rpcplugin/main.go @@ -0,0 +1,46 @@ +package rpcplugin + +import ( + "bufio" + "encoding/binary" + "fmt" + "log" + "os" + + "github.com/mattermost/platform/plugin" +) + +// Makes a set of hooks available via RPC. This function never returns. +func Main(hooks plugin.Hooks) { + ipc, err := InheritedProcessIPC() + if err != nil { + log.Fatal(err.Error()) + } + muxer := NewMuxer(ipc, true) + id, conn := muxer.Serve() + buf := make([]byte, 11) + buf[0] = 0 + n := binary.PutVarint(buf[1:], id) + if _, err := muxer.Write(buf[:1+n]); err != nil { + log.Fatal(err.Error()) + } + ServeHooks(hooks, conn, muxer) + os.Exit(0) +} + +// Returns the hooks being served by a call to Main. +func ConnectMain(muxer *Muxer) (*RemoteHooks, error) { + buf := make([]byte, 1) + if _, err := muxer.Read(buf); err != nil { + return nil, err + } else if buf[0] != 0 { + return nil, fmt.Errorf("unexpected control byte") + } + reader := bufio.NewReader(muxer) + id, err := binary.ReadVarint(reader) + if err != nil { + return nil, err + } + + return ConnectHooks(muxer.Connect(id), muxer), nil +} diff --git a/plugin/rpcplugin/main_test.go b/plugin/rpcplugin/main_test.go new file mode 100644 index 000000000..b54364bad --- /dev/null +++ b/plugin/rpcplugin/main_test.go @@ -0,0 +1,58 @@ +package rpcplugin + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/platform/plugin/plugintest" +) + +func TestMain(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + plugin := filepath.Join(dir, "plugin") + compileGo(t, ` + package main + + import ( + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/rpcplugin" + ) + + type MyPlugin struct {} + + func (p *MyPlugin) OnActivate(api plugin.API) error { + return nil + } + + func (p *MyPlugin) OnDeactivate() error { + return nil + } + + func main() { + rpcplugin.Main(&MyPlugin{}) + } + `, plugin) + + p, ipc, err := NewProcess(context.Background(), plugin) + require.NoError(t, err) + defer p.Wait() + + muxer := NewMuxer(ipc, false) + defer muxer.Close() + + var api plugintest.API + + hooks, err := ConnectMain(muxer) + require.NoError(t, err) + assert.NoError(t, hooks.OnActivate(&api)) + assert.NoError(t, hooks.OnDeactivate()) +} diff --git a/plugin/rpcplugin/muxer.go b/plugin/rpcplugin/muxer.go new file mode 100644 index 000000000..a2bfbf8b6 --- /dev/null +++ b/plugin/rpcplugin/muxer.go @@ -0,0 +1,253 @@ +package rpcplugin + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "sync" + "sync/atomic" +) + +// Muxer allows multiple bidirectional streams to be transmitted over a single connection. +// +// Muxer is safe for use by multiple goroutines. +// +// Streams opened on the muxer must be periodically drained in order to reclaim read buffer memory. +// In other words, readers must consume incoming data as it comes in. +type Muxer struct { + // writeMutex guards conn writes + writeMutex sync.Mutex + conn io.ReadWriteCloser + + // didCloseConn is a boolean (0 or 1) used from multiple goroutines via atomic operations + didCloseConn int32 + + // streamsMutex guards streams and nextId + streamsMutex sync.Mutex + nextId int64 + streams map[int64]*muxerStream + + stream0Reader *io.PipeReader + stream0Writer *io.PipeWriter + result chan error +} + +// Creates a new Muxer. +// +// conn must be safe for simultaneous reads by one goroutine and writes by another. +// +// For two muxers communicating with each other via a connection, parity must be true for exactly +// one of them. +func NewMuxer(conn io.ReadWriteCloser, parity bool) *Muxer { + s0r, s0w := io.Pipe() + muxer := &Muxer{ + conn: conn, + streams: make(map[int64]*muxerStream), + result: make(chan error, 1), + nextId: 1, + stream0Reader: s0r, + stream0Writer: s0w, + } + if parity { + muxer.nextId = 2 + } + go muxer.run() + return muxer +} + +// Opens a new stream with a unique id. +// +// Writes made to the stream before the other end calls Connect will be discarded. +func (m *Muxer) Serve() (int64, io.ReadWriteCloser) { + m.streamsMutex.Lock() + id := m.nextId + m.nextId += 2 + m.streamsMutex.Unlock() + return id, m.Connect(id) +} + +// Opens a remotely opened stream. +func (m *Muxer) Connect(id int64) io.ReadWriteCloser { + m.streamsMutex.Lock() + defer m.streamsMutex.Unlock() + mutex := &sync.Mutex{} + stream := &muxerStream{ + id: id, + muxer: m, + mutex: mutex, + readWake: sync.NewCond(mutex), + } + m.streams[id] = stream + return stream +} + +// Calling Read on the muxer directly performs a read on a dedicated, always-open channel. +func (m *Muxer) Read(p []byte) (int, error) { + return m.stream0Reader.Read(p) +} + +// Calling Write on the muxer directly performs a write on a dedicated, always-open channel. +func (m *Muxer) Write(p []byte) (int, error) { + return m.write(p, 0) +} + +// Closes the muxer. +func (m *Muxer) Close() error { + if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) { + m.conn.Close() + } + m.stream0Reader.Close() + m.stream0Writer.Close() + <-m.result + return nil +} + +func (m *Muxer) IsClosed() bool { + return atomic.LoadInt32(&m.didCloseConn) > 0 +} + +func (m *Muxer) write(p []byte, sid int64) (int, error) { + m.writeMutex.Lock() + defer m.writeMutex.Unlock() + if m.IsClosed() { + return 0, fmt.Errorf("muxer closed") + } + buf := make([]byte, 10) + n := binary.PutVarint(buf, sid) + if _, err := m.conn.Write(buf[:n]); err != nil { + m.shutdown(err) + return 0, err + } + n = binary.PutVarint(buf, int64(len(p))) + if _, err := m.conn.Write(buf[:n]); err != nil { + m.shutdown(err) + return 0, err + } + if _, err := m.conn.Write(p); err != nil { + m.shutdown(err) + return 0, err + } + return len(p), nil +} + +func (m *Muxer) rm(sid int64) { + m.streamsMutex.Lock() + defer m.streamsMutex.Unlock() + delete(m.streams, sid) +} + +func (m *Muxer) run() { + m.shutdown(m.loop()) +} + +func (m *Muxer) loop() error { + reader := bufio.NewReader(m.conn) + + for { + sid, err := binary.ReadVarint(reader) + if err != nil { + return err + } + len, err := binary.ReadVarint(reader) + if err != nil { + return err + } + + if sid == 0 { + if _, err := io.CopyN(m.stream0Writer, reader, len); err != nil { + return err + } + continue + } + + m.streamsMutex.Lock() + stream, ok := m.streams[sid] + m.streamsMutex.Unlock() + if !ok { + if _, err := reader.Discard(int(len)); err != nil { + return err + } + continue + } + + stream.mutex.Lock() + if stream.isClosed { + stream.mutex.Unlock() + if _, err := reader.Discard(int(len)); err != nil { + return err + } + continue + } + _, err = io.CopyN(&stream.readBuf, reader, len) + stream.mutex.Unlock() + if err != nil { + return err + } + stream.readWake.Signal() + } +} + +func (m *Muxer) shutdown(err error) { + if atomic.CompareAndSwapInt32(&m.didCloseConn, 0, 1) { + m.conn.Close() + } + go func() { + m.streamsMutex.Lock() + for _, stream := range m.streams { + stream.mutex.Lock() + stream.readWake.Signal() + stream.mutex.Unlock() + } + m.streams = make(map[int64]*muxerStream) + m.streamsMutex.Unlock() + }() + m.result <- err +} + +type muxerStream struct { + id int64 + muxer *Muxer + readBuf bytes.Buffer + mutex *sync.Mutex + readWake *sync.Cond + isClosed bool + closeErr error +} + +func (s *muxerStream) Read(p []byte) (int, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + for { + if s.muxer.IsClosed() { + return 0, fmt.Errorf("muxer closed") + } else if s.isClosed { + return 0, io.EOF + } else if s.readBuf.Len() > 0 { + n, err := s.readBuf.Read(p) + return n, err + } + s.readWake.Wait() + } +} + +func (s *muxerStream) Write(p []byte) (int, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + if s.isClosed { + return 0, fmt.Errorf("stream closed") + } + return s.muxer.write(p, s.id) +} + +func (s *muxerStream) Close() error { + s.mutex.Lock() + defer s.mutex.Unlock() + if !s.isClosed { + s.isClosed = true + s.muxer.rm(s.id) + } + s.readWake.Signal() + return nil +} diff --git a/plugin/rpcplugin/muxer_test.go b/plugin/rpcplugin/muxer_test.go new file mode 100644 index 000000000..7bb63d4f8 --- /dev/null +++ b/plugin/rpcplugin/muxer_test.go @@ -0,0 +1,169 @@ +package rpcplugin + +import ( + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMuxer(t *testing.T) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + alice := NewMuxer(NewReadWriteCloser(r1, w2), false) + defer func() { assert.NoError(t, alice.Close()) }() + + bob := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer func() { assert.NoError(t, bob.Close()) }() + + id1, alice1 := alice.Serve() + defer func() { assert.NoError(t, alice1.Close()) }() + + id2, bob2 := bob.Serve() + defer func() { assert.NoError(t, bob2.Close()) }() + + done1 := make(chan bool) + done2 := make(chan bool) + + go func() { + bob1 := bob.Connect(id1) + defer func() { assert.NoError(t, bob1.Close()) }() + + n, err := bob1.Write([]byte("ping1.0")) + require.NoError(t, err) + assert.Equal(t, n, 7) + + n, err = bob1.Write([]byte("ping1.1")) + require.NoError(t, err) + assert.Equal(t, n, 7) + }() + + go func() { + alice2 := alice.Connect(id2) + defer func() { assert.NoError(t, alice2.Close()) }() + + n, err := alice2.Write([]byte("ping2.0")) + require.NoError(t, err) + assert.Equal(t, n, 7) + + buf := make([]byte, 20) + n, err = alice2.Read(buf) + require.NoError(t, err) + assert.Equal(t, n, 7) + assert.Equal(t, []byte("pong2.0"), buf[:n]) + + done2 <- true + }() + + go func() { + buf := make([]byte, 7) + n, err := io.ReadFull(alice1, buf) + require.NoError(t, err) + assert.Equal(t, n, 7) + assert.Equal(t, []byte("ping1.0"), buf[:n]) + + n, err = alice1.Read(buf) + require.NoError(t, err) + assert.Equal(t, n, 7) + assert.Equal(t, []byte("ping1.1"), buf[:n]) + + done1 <- true + }() + + go func() { + buf := make([]byte, 20) + n, err := bob2.Read(buf) + require.NoError(t, err) + assert.Equal(t, n, 7) + assert.Equal(t, []byte("ping2.0"), buf[:n]) + + n, err = bob2.Write([]byte("pong2.0")) + require.NoError(t, err) + assert.Equal(t, n, 7) + }() + + <-done1 + <-done2 +} + +// Closing a muxer during a read should unblock, but return an error. +func TestMuxer_CloseDuringRead(t *testing.T) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + alice := NewMuxer(NewReadWriteCloser(r1, w2), false) + + bob := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer func() { assert.NoError(t, bob.Close()) }() + + _, s := alice.Serve() + + go alice.Close() + buf := make([]byte, 20) + n, err := s.Read(buf) + assert.Equal(t, 0, n) + assert.NotNil(t, err) + assert.NotEqual(t, io.EOF, err) +} + +// Closing a stream during a read should unblock and return io.EOF since this is the way to +// gracefully close a connection. +func TestMuxer_StreamCloseDuringRead(t *testing.T) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + alice := NewMuxer(NewReadWriteCloser(r1, w2), false) + defer func() { assert.NoError(t, alice.Close()) }() + + bob := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer func() { assert.NoError(t, bob.Close()) }() + + _, s := alice.Serve() + + go s.Close() + buf := make([]byte, 20) + n, err := s.Read(buf) + assert.Equal(t, 0, n) + assert.Equal(t, io.EOF, err) +} + +// Closing a muxer during a write should unblock, but return an error. +func TestMuxer_CloseDuringWrite(t *testing.T) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + alice := NewMuxer(NewReadWriteCloser(r1, w2), false) + + // Don't connect bob to let writes will block forever. + defer r2.Close() + defer w1.Close() + + _, s := alice.Serve() + + go alice.Close() + buf := make([]byte, 20) + n, err := s.Write(buf) + assert.Equal(t, 0, n) + assert.NotNil(t, err) + assert.NotEqual(t, io.EOF, err) +} + +func TestMuxer_ReadWrite(t *testing.T) { + r1, w1 := io.Pipe() + r2, w2 := io.Pipe() + + alice := NewMuxer(NewReadWriteCloser(r1, w2), false) + defer func() { assert.NoError(t, alice.Close()) }() + + bob := NewMuxer(NewReadWriteCloser(r2, w1), true) + defer func() { assert.NoError(t, bob.Close()) }() + + go alice.Write([]byte("hello")) + buf := make([]byte, 20) + n, err := bob.Read(buf) + assert.Equal(t, 5, n) + assert.Nil(t, err) + assert.Equal(t, []byte("hello"), buf[:n]) +} diff --git a/plugin/rpcplugin/process.go b/plugin/rpcplugin/process.go new file mode 100644 index 000000000..4b3362d68 --- /dev/null +++ b/plugin/rpcplugin/process.go @@ -0,0 +1,23 @@ +package rpcplugin + +import ( + "context" + "io" +) + +type Process interface { + // Waits for the process to exit and returns an error if a problem occurred or the process exited + // with a non-zero status. + Wait() error +} + +// NewProcess launches an RPC executable in a new process and returns an IPC that can be used to +// communicate with it. +func NewProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { + return newProcess(ctx, path) +} + +// When called on a process launched with NewProcess, returns the inherited IPC. +func InheritedProcessIPC() (io.ReadWriteCloser, error) { + return inheritedProcessIPC() +} diff --git a/plugin/rpcplugin/process_test.go b/plugin/rpcplugin/process_test.go new file mode 100644 index 000000000..b7984ad0a --- /dev/null +++ b/plugin/rpcplugin/process_test.go @@ -0,0 +1,64 @@ +package rpcplugin + +import ( + "context" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func compileGo(t *testing.T, sourceCode, outputPath string) { + dir, err := ioutil.TempDir(".", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "main.go"), []byte(sourceCode), 0600)) + cmd := exec.Command("go", "build", "-o", outputPath, "main.go") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run()) +} + +func TestProcess(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + ping := filepath.Join(dir, "ping") + compileGo(t, ` + package main + + import ( + "log" + + "github.com/mattermost/platform/plugin/rpcplugin" + ) + + func main() { + ipc, err := rpcplugin.InheritedProcessIPC() + if err != nil { + log.Fatal("unable to get inherited ipc") + } + defer ipc.Close() + _, err = ipc.Write([]byte("ping")) + if err != nil { + log.Fatal("unable to write to ipc") + } + } + `, ping) + + p, ipc, err := NewProcess(context.Background(), ping) + require.NoError(t, err) + defer ipc.Close() + b := make([]byte, 10) + n, err := ipc.Read(b) + require.NoError(t, err) + assert.Equal(t, 4, n) + assert.Equal(t, "ping", string(b[:4])) + require.NoError(t, p.Wait()) +} diff --git a/plugin/rpcplugin/process_unix.go b/plugin/rpcplugin/process_unix.go new file mode 100644 index 000000000..f196e34f8 --- /dev/null +++ b/plugin/rpcplugin/process_unix.go @@ -0,0 +1,45 @@ +// +build !windows + +package rpcplugin + +import ( + "context" + "io" + "os" + "os/exec" +) + +type process struct { + command *exec.Cmd +} + +func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { + ipc, childFiles, err := NewIPC() + if err != nil { + return nil, nil, err + } + defer childFiles[0].Close() + defer childFiles[1].Close() + + cmd := exec.CommandContext(ctx, path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.ExtraFiles = childFiles + err = cmd.Start() + if err != nil { + ipc.Close() + return nil, nil, err + } + + return &process{ + command: cmd, + }, ipc, nil +} + +func (p *process) Wait() error { + return p.command.Wait() +} + +func inheritedProcessIPC() (io.ReadWriteCloser, error) { + return InheritedIPC(3, 4) +} diff --git a/plugin/rpcplugin/process_windows.go b/plugin/rpcplugin/process_windows.go new file mode 100644 index 000000000..7be03cacd --- /dev/null +++ b/plugin/rpcplugin/process_windows.go @@ -0,0 +1,17 @@ +package rpcplugin + +import ( + "context" + "fmt" + "io" +) + +func newProcess(ctx context.Context, path string) (Process, io.ReadWriteCloser, error) { + // TODO + return nil, nil, fmt.Errorf("not yet supported") +} + +func inheritedProcessIPC() (*IPC, error) { + // TODO + return nil, fmt.Errorf("not yet supported") +} diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go new file mode 100644 index 000000000..9316d7186 --- /dev/null +++ b/plugin/rpcplugin/supervisor.go @@ -0,0 +1,128 @@ +package rpcplugin + +import ( + "context" + "fmt" + "path/filepath" + "sync/atomic" + "time" + + "github.com/mattermost/platform/plugin" +) + +// Supervisor implements a plugin.Supervisor that launches the plugin in a separate process and +// communicates via RPC. +// +// If the plugin unexpectedly exists, the supervisor will relaunch it after a short delay. +type Supervisor struct { + executable string + hooks atomic.Value + done chan bool + cancel context.CancelFunc +} + +var _ plugin.Supervisor = (*Supervisor)(nil) + +// Starts the plugin. This method will block until the plugin is successfully launched for the first +// time and will return an error if the plugin cannot be launched at all. +func (s *Supervisor) Start() error { + ctx, cancel := context.WithCancel(context.Background()) + s.done = make(chan bool, 1) + start := make(chan error, 1) + go s.run(ctx, start) + + select { + case <-time.After(time.Second * 3): + cancel() + <-s.done + return fmt.Errorf("timed out waiting for plugin") + case err := <-start: + s.cancel = cancel + return err + } +} + +// Stops the plugin. +func (s *Supervisor) Stop() error { + s.cancel() + <-s.done + return nil +} + +// Returns the hooks used to communicate with the plugin. The hooks may change if the plugin is +// restarted, so the return value should not be cached. +func (s *Supervisor) Hooks() plugin.Hooks { + return s.hooks.Load().(plugin.Hooks) +} + +func (s *Supervisor) run(ctx context.Context, start chan<- error) { + defer func() { + s.done <- true + }() + done := ctx.Done() + for { + s.runPlugin(ctx, start) + select { + case <-done: + return + default: + start = nil + time.Sleep(time.Second) + } + } +} + +func (s *Supervisor) runPlugin(ctx context.Context, start chan<- error) error { + p, ipc, err := NewProcess(ctx, s.executable) + if err != nil { + if start != nil { + start <- err + } + return err + } + + muxer := NewMuxer(ipc, false) + closeMuxer := make(chan bool, 1) + muxerClosed := make(chan error, 1) + go func() { + select { + case <-ctx.Done(): + break + case <-closeMuxer: + break + } + muxerClosed <- muxer.Close() + }() + + hooks, err := ConnectMain(muxer) + if err != nil { + if start != nil { + start <- err + } + closeMuxer <- true + <-muxerClosed + p.Wait() + return err + } + + s.hooks.Store(hooks) + if start != nil { + start <- nil + } + p.Wait() + closeMuxer <- true + <-muxerClosed + + return nil +} + +func SupervisorProvider(bundle *plugin.BundleInfo) (plugin.Supervisor, error) { + if bundle.Manifest == nil { + return nil, fmt.Errorf("no manifest available") + } else if bundle.Manifest.Backend == nil || bundle.Manifest.Backend.Executable == "" { + return nil, fmt.Errorf("no backend executable specified") + } + return &Supervisor{ + executable: filepath.Join(bundle.Path, bundle.Manifest.Backend.Executable), + }, nil +} diff --git a/plugin/rpcplugin/supervisor_test.go b/plugin/rpcplugin/supervisor_test.go new file mode 100644 index 000000000..1d046bf82 --- /dev/null +++ b/plugin/rpcplugin/supervisor_test.go @@ -0,0 +1,130 @@ +package rpcplugin + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/mattermost/platform/plugin" +) + +func TestSupervisor(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend") + compileGo(t, ` + package main + + import ( + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/rpcplugin" + ) + + type MyPlugin struct {} + + func (p *MyPlugin) OnActivate(api plugin.API) error { + return nil + } + + func (p *MyPlugin) OnDeactivate() error { + return nil + } + + func main() { + rpcplugin.Main(&MyPlugin{}) + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600) + + bundle := plugin.BundleInfoForPath(dir) + supervisor, err := SupervisorProvider(bundle) + require.NoError(t, err) + require.NoError(t, supervisor.Start()) + require.NoError(t, supervisor.Hooks().OnActivate(nil)) + require.NoError(t, supervisor.Stop()) +} + +// If plugin development goes really wrong, let's make sure plugin activation won't block forever. +func TestSupervisor_StartTimeout(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend") + compileGo(t, ` + package main + + func main() { + for { + } + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600) + + bundle := plugin.BundleInfoForPath(dir) + supervisor, err := SupervisorProvider(bundle) + require.NoError(t, err) + require.Error(t, supervisor.Start()) +} + +// Crashed plugins should be relaunched. +func TestSupervisor_PluginCrash(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + backend := filepath.Join(dir, "backend") + compileGo(t, ` + package main + + import ( + "os" + + "github.com/mattermost/platform/plugin" + "github.com/mattermost/platform/plugin/rpcplugin" + ) + + type MyPlugin struct {} + + func (p *MyPlugin) OnActivate(api plugin.API) error { + os.Exit(1) + return nil + } + + func (p *MyPlugin) OnDeactivate() error { + return nil + } + + func main() { + rpcplugin.Main(&MyPlugin{}) + } + `, backend) + + ioutil.WriteFile(filepath.Join(dir, "plugin.json"), []byte(`{"id": "foo", "backend": {"executable": "backend"}}`), 0600) + + bundle := plugin.BundleInfoForPath(dir) + supervisor, err := SupervisorProvider(bundle) + require.NoError(t, err) + require.NoError(t, supervisor.Start()) + require.Error(t, supervisor.Hooks().OnActivate(nil)) + + recovered := false + for i := 0; i < 30; i++ { + if supervisor.Hooks().OnDeactivate() == nil { + recovered = true + break + } + time.Sleep(time.Millisecond * 100) + } + assert.True(t, recovered) + require.NoError(t, supervisor.Stop()) +} diff --git a/plugin/supervisor.go b/plugin/supervisor.go new file mode 100644 index 000000000..5ddf5f169 --- /dev/null +++ b/plugin/supervisor.go @@ -0,0 +1,8 @@ +package plugin + +// Supervisor provides the interface for an object that controls the execution of a plugin. +type Supervisor interface { + Start() error + Stop() error + Hooks() Hooks +} diff --git a/store/layered_store.go b/store/layered_store.go index 4eb908659..d215cb2f0 100644 --- a/store/layered_store.go +++ b/store/layered_store.go @@ -107,6 +107,10 @@ func (s *LayeredStore) Command() CommandStore { return s.DatabaseLayer.Command() } +func (s *LayeredStore) CommandWebhook() CommandWebhookStore { + return s.DatabaseLayer.CommandWebhook() +} + func (s *LayeredStore) Preference() PreferenceStore { return s.DatabaseLayer.Preference() } diff --git a/store/sql_command_webhook_store.go b/store/sql_command_webhook_store.go new file mode 100644 index 000000000..af4b298b1 --- /dev/null +++ b/store/sql_command_webhook_store.go @@ -0,0 +1,125 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "net/http" + + "database/sql" + + l4g "github.com/alecthomas/log4go" + + "github.com/mattermost/platform/model" +) + +type SqlCommandWebhookStore struct { + SqlStore +} + +func NewSqlCommandWebhookStore(sqlStore SqlStore) CommandWebhookStore { + s := &SqlCommandWebhookStore{sqlStore} + + for _, db := range sqlStore.GetAllConns() { + tablec := db.AddTableWithName(model.CommandWebhook{}, "CommandWebhooks").SetKeys(false, "Id") + tablec.ColMap("Id").SetMaxSize(26) + tablec.ColMap("CommandId").SetMaxSize(26) + tablec.ColMap("UserId").SetMaxSize(26) + tablec.ColMap("ChannelId").SetMaxSize(26) + tablec.ColMap("RootId").SetMaxSize(26) + tablec.ColMap("ParentId").SetMaxSize(26) + } + + return s +} + +func (s SqlCommandWebhookStore) CreateIndexesIfNotExists() { + s.CreateIndexIfNotExists("idx_command_webhook_create_at", "CommandWebhooks", "CreateAt") +} + +func (s SqlCommandWebhookStore) Save(webhook *model.CommandWebhook) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + if len(webhook.Id) > 0 { + result.Err = model.NewLocAppError("SqlCommandWebhookStore.Save", "store.sql_command_webhooks.save.existing.app_error", nil, "id="+webhook.Id) + storeChannel <- result + close(storeChannel) + return + } + + webhook.PreSave() + if result.Err = webhook.IsValid(); result.Err != nil { + storeChannel <- result + close(storeChannel) + return + } + + if err := s.GetMaster().Insert(webhook); err != nil { + result.Err = model.NewLocAppError("SqlCommandWebhookStore.Save", "store.sql_command_webhooks.save.app_error", nil, "id="+webhook.Id+", "+err.Error()) + } else { + result.Data = webhook + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandWebhookStore) Get(id string) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + var webhook model.CommandWebhook + + exptime := model.GetMillis() - model.COMMAND_WEBHOOK_LIFETIME + if err := s.GetReplica().SelectOne(&webhook, "SELECT * FROM CommandWebhooks WHERE Id = :Id AND CreateAt > :ExpTime", map[string]interface{}{"Id": id, "ExpTime": exptime}); err != nil { + result.Err = model.NewLocAppError("SqlCommandWebhookStore.Get", "store.sql_command_webhooks.get.app_error", nil, "id="+id+", err="+err.Error()) + if err == sql.ErrNoRows { + result.Err.StatusCode = http.StatusNotFound + } + } + + result.Data = &webhook + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandWebhookStore) TryUse(id string, limit int) StoreChannel { + storeChannel := make(StoreChannel, 1) + + go func() { + result := StoreResult{} + + if sqlResult, err := s.GetMaster().Exec("UPDATE CommandWebhooks SET UseCount = UseCount + 1 WHERE Id = :Id AND UseCount < :UseLimit", map[string]interface{}{"Id": id, "UseLimit": limit}); err != nil { + result.Err = model.NewLocAppError("SqlCommandWebhookStore.TryUse", "store.sql_command_webhooks.try_use.app_error", nil, "id="+id+", err="+err.Error()) + } else if rows, _ := sqlResult.RowsAffected(); rows == 0 { + result.Err = model.NewAppError("SqlCommandWebhookStore.TryUse", "store.sql_command_webhooks.try_use.invalid.app_error", nil, "id="+id, http.StatusBadRequest) + } + + result.Data = id + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlCommandWebhookStore) Cleanup() { + l4g.Debug("Cleaning up command webhook store.") + exptime := model.GetMillis() - model.COMMAND_WEBHOOK_LIFETIME + if _, err := s.GetMaster().Exec("DELETE FROM CommandWebhooks WHERE CreateAt < :ExpTime", map[string]interface{}{"ExpTime": exptime}); err != nil { + l4g.Error("Unable to cleanup command webhook store.") + } +} diff --git a/store/sql_command_webhook_store_test.go b/store/sql_command_webhook_store_test.go new file mode 100644 index 000000000..2215a4263 --- /dev/null +++ b/store/sql_command_webhook_store_test.go @@ -0,0 +1,65 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package store + +import ( + "testing" + + "net/http" + + "github.com/mattermost/platform/model" +) + +func TestCommandWebhookStore(t *testing.T) { + Setup() + + cws := store.CommandWebhook() + + h1 := &model.CommandWebhook{} + h1.CommandId = model.NewId() + h1.UserId = model.NewId() + h1.ChannelId = model.NewId() + h1 = (<-cws.Save(h1)).Data.(*model.CommandWebhook) + + if r1 := <-cws.Get(h1.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if *r1.Data.(*model.CommandWebhook) != *h1 { + t.Fatal("invalid returned webhook") + } + } + + if err := (<-cws.Get("123")).Err; err.StatusCode != http.StatusNotFound { + t.Fatal("Should have set the status as not found for missing id") + } + + h2 := &model.CommandWebhook{} + h2.CreateAt = model.GetMillis() - 2*model.COMMAND_WEBHOOK_LIFETIME + h2.CommandId = model.NewId() + h2.UserId = model.NewId() + h2.ChannelId = model.NewId() + h2 = (<-cws.Save(h2)).Data.(*model.CommandWebhook) + + if err := (<-cws.Get(h2.Id)).Err; err == nil || err.StatusCode != http.StatusNotFound { + t.Fatal("Should have set the status as not found for expired webhook") + } + + cws.Cleanup() + + if err := (<-cws.Get(h1.Id)).Err; err != nil { + t.Fatal("Should have no error getting unexpired webhook") + } + + if err := (<-cws.Get(h2.Id)).Err; err.StatusCode != http.StatusNotFound { + t.Fatal("Should have set the status as not found for expired webhook") + } + + if err := (<-cws.TryUse(h1.Id, 1)).Err; err != nil { + t.Fatal("Should be able to use webhook once") + } + + if err := (<-cws.TryUse(h1.Id, 1)).Err; err == nil || err.StatusCode != http.StatusBadRequest { + t.Fatal("Should be able to use webhook once") + } +} diff --git a/store/sql_job_store_test.go b/store/sql_job_store_test.go index 97e95ab92..394a09192 100644 --- a/store/sql_job_store_test.go +++ b/store/sql_job_store_test.go @@ -17,9 +17,9 @@ func TestJobSaveGet(t *testing.T) { Id: model.NewId(), Type: model.NewId(), Status: model.NewId(), - Data: map[string]interface{}{ - "Processed": 0, - "Total": 12345, + Data: map[string]string{ + "Processed": "0", + "Total": "12345", "LastProcessed": "abcd", }, } @@ -36,6 +36,8 @@ func TestJobSaveGet(t *testing.T) { t.Fatal(result.Err) } else if received := result.Data.(*model.Job); received.Id != job.Id { t.Fatal("received incorrect job after save") + } else if received.Data["Total"] != "12345" { + t.Fatal("data field was not retrieved successfully:", received.Data) } } @@ -184,6 +186,9 @@ func TestJobGetAllByStatus(t *testing.T) { Type: jobType, CreateAt: 1000, Status: status, + Data: map[string]string{ + "test": "data", + }, }, { Id: model.NewId(), @@ -214,10 +219,10 @@ func TestJobGetAllByStatus(t *testing.T) { t.Fatal(result.Err) } else if received := result.Data.([]*model.Job); len(received) != 3 { t.Fatal("received wrong number of jobs") - } else if received[0].Id != jobs[0].Id && received[1].Id != jobs[0].Id { - t.Fatal("should've received first jobs") - } else if received[0].Id != jobs[1].Id && received[1].Id != jobs[1].Id { - t.Fatal("should've received second jobs") + } else if received[0].Id != jobs[1].Id || received[1].Id != jobs[0].Id || received[2].Id != jobs[2].Id { + t.Fatal("should've received jobs ordered by CreateAt time") + } else if received[1].Data["test"] != "data" { + t.Fatal("should've received job data field back as saved") } } @@ -237,7 +242,7 @@ func TestJobUpdateOptimistically(t *testing.T) { job.LastActivityAt = model.GetMillis() job.Status = model.JOB_STATUS_IN_PROGRESS job.Progress = 50 - job.Data = map[string]interface{}{ + job.Data = map[string]string{ "Foo": "Bar", } diff --git a/store/sql_store.go b/store/sql_store.go index 817f3fb0f..488b44522 100644 --- a/store/sql_store.go +++ b/store/sql_store.go @@ -72,6 +72,7 @@ type SqlStore interface { System() SystemStore Webhook() WebhookStore Command() CommandStore + CommandWebhook() CommandWebhookStore Preference() PreferenceStore License() LicenseStore Token() TokenStore diff --git a/store/sql_supplier.go b/store/sql_supplier.go index 5997a1339..f56a9f448 100644 --- a/store/sql_supplier.go +++ b/store/sql_supplier.go @@ -70,6 +70,7 @@ type SqlSupplierOldStores struct { system SystemStore webhook WebhookStore command CommandStore + commandWebhook CommandWebhookStore preference PreferenceStore license LicenseStore token TokenStore @@ -111,6 +112,7 @@ func NewSqlSupplier() *SqlSupplier { supplier.oldStores.system = NewSqlSystemStore(supplier) supplier.oldStores.webhook = NewSqlWebhookStore(supplier) supplier.oldStores.command = NewSqlCommandStore(supplier) + supplier.oldStores.commandWebhook = NewSqlCommandWebhookStore(supplier) supplier.oldStores.preference = NewSqlPreferenceStore(supplier) supplier.oldStores.license = NewSqlLicenseStore(supplier) supplier.oldStores.token = NewSqlTokenStore(supplier) @@ -142,6 +144,7 @@ func NewSqlSupplier() *SqlSupplier { supplier.oldStores.system.(*SqlSystemStore).CreateIndexesIfNotExists() supplier.oldStores.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists() supplier.oldStores.command.(*SqlCommandStore).CreateIndexesIfNotExists() + supplier.oldStores.commandWebhook.(*SqlCommandWebhookStore).CreateIndexesIfNotExists() supplier.oldStores.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists() supplier.oldStores.license.(*SqlLicenseStore).CreateIndexesIfNotExists() supplier.oldStores.token.(*SqlTokenStore).CreateIndexesIfNotExists() @@ -732,6 +735,10 @@ func (ss *SqlSupplier) Command() CommandStore { return ss.oldStores.command } +func (ss *SqlSupplier) CommandWebhook() CommandWebhookStore { + return ss.oldStores.commandWebhook +} + func (ss *SqlSupplier) Preference() PreferenceStore { return ss.oldStores.preference } @@ -779,6 +786,8 @@ func (me mattermConverter) ToDb(val interface{}) (interface{}, error) { switch t := val.(type) { case model.StringMap: return model.MapToJson(t), nil + case map[string]string: + return model.MapToJson(model.StringMap(t)), nil case model.StringArray: return model.ArrayToJson(t), nil case model.StringInterface: @@ -802,6 +811,16 @@ func (me mattermConverter) FromDb(target interface{}) (gorp.CustomScanner, bool) return json.Unmarshal(b, target) } return gorp.CustomScanner{Holder: new(string), Target: target, Binder: binder}, true + case *map[string]string: + binder := func(holder, target interface{}) error { + s, ok := holder.(*string) + if !ok { + return errors.New(utils.T("store.sql.convert_string_map")) + } + b := []byte(*s) + return json.Unmarshal(b, target) + } + return gorp.CustomScanner{Holder: new(string), Target: target, Binder: binder}, true case *model.StringArray: binder := func(holder, target interface{}) error { s, ok := holder.(*string) diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go index 26ff9a164..5f4ed2a4e 100644 --- a/store/sql_upgrade.go +++ b/store/sql_upgrade.go @@ -15,6 +15,7 @@ import ( ) const ( + VERSION_4_2_0 = "4.2.0" VERSION_4_1_0 = "4.1.0" VERSION_4_0_0 = "4.0.0" VERSION_3_10_0 = "3.10.0" @@ -52,6 +53,7 @@ func UpgradeDatabase(sqlStore SqlStore) { UpgradeDatabaseToVersion310(sqlStore) UpgradeDatabaseToVersion40(sqlStore) UpgradeDatabaseToVersion41(sqlStore) + UpgradeDatabaseToVersion42(sqlStore) // If the SchemaVersion is empty this this is the first time it has ran // so lets set it to the current version. @@ -292,3 +294,10 @@ func UpgradeDatabaseToVersion41(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_4_1_0) } } + +func UpgradeDatabaseToVersion42(sqlStore SqlStore) { + // TODO: Uncomment following condition when version 4.1.0 is released + // if shouldPerformUpgrade(sqlStore, VERSION_4_1_0, VERSION_4_2_0) { + // saveSchemaVersion(sqlStore, VERSION_4_2_0) + // } +} diff --git a/store/store.go b/store/store.go index d883ea5a2..e86b5f116 100644 --- a/store/store.go +++ b/store/store.go @@ -41,6 +41,7 @@ type Store interface { System() SystemStore Webhook() WebhookStore Command() CommandStore + CommandWebhook() CommandWebhookStore Preference() PreferenceStore License() LicenseStore Token() TokenStore @@ -326,6 +327,13 @@ type CommandStore interface { AnalyticsCommandCount(teamId string) StoreChannel } +type CommandWebhookStore interface { + Save(webhook *model.CommandWebhook) StoreChannel + Get(id string) StoreChannel + TryUse(id string, limit int) StoreChannel + Cleanup() +} + type PreferenceStore interface { Save(preferences *model.Preferences) StoreChannel Get(userId string, category string, name string) StoreChannel diff --git a/utils/authorization.go b/utils/authorization.go index 6f01d7820..e0e587d71 100644 --- a/utils/authorization.go +++ b/utils/authorization.go @@ -11,7 +11,7 @@ func SetDefaultRolesBasedOnConfig() { // Reset the roles to default to make this logic easier model.InitalizeRoles() - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPublicChannelCreation { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -33,7 +33,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPublicChannelManagement { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -65,7 +65,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPublicChannelDeletion { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -97,7 +97,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPrivateChannelCreation { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -119,7 +119,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPrivateChannelManagement { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -151,7 +151,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPrivateChannelDeletion { case model.PERMISSIONS_ALL: model.ROLE_TEAM_USER.Permissions = append( @@ -184,7 +184,7 @@ func SetDefaultRolesBasedOnConfig() { } // Restrict permissions for Private Channel Manage Members - if IsLicensed { + if IsLicensed() { switch *Cfg.TeamSettings.RestrictPrivateChannelManageMembers { case model.PERMISSIONS_ALL: model.ROLE_CHANNEL_USER.Permissions = append( @@ -229,7 +229,7 @@ func SetDefaultRolesBasedOnConfig() { } // Grant permissions for inviting and adding users to a team. - if IsLicensed { + if IsLicensed() { if *Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_TEAM_ADMIN { model.ROLE_TEAM_ADMIN.Permissions = append( model.ROLE_TEAM_ADMIN.Permissions, @@ -251,7 +251,7 @@ func SetDefaultRolesBasedOnConfig() { ) } - if IsLicensed { + if IsLicensed() { switch *Cfg.ServiceSettings.RestrictPostDelete { case model.PERMISSIONS_DELETE_POST_ALL: model.ROLE_CHANNEL_USER.Permissions = append( diff --git a/utils/config.go b/utils/config.go index c0c7ecc20..36474c921 100644 --- a/utils/config.go +++ b/utils/config.go @@ -489,7 +489,10 @@ func getClientConfig(c *model.Config) map[string]string { props["DiagnosticId"] = CfgDiagnosticId props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics) - if IsLicensed { + if IsLicensed() { + + License := License() + if *License.Features.CustomBrand { props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) props["CustomBrandText"] = *c.TeamSettings.CustomBrandText @@ -650,3 +653,11 @@ func Desanitize(cfg *model.Config) { cfg.SqlSettings.DataSourceSearchReplicas[i] = Cfg.SqlSettings.DataSourceSearchReplicas[i] } } + +func IsLeader() bool { + if IsLicensed() && *Cfg.ClusterSettings.Enable && einterfaces.GetClusterInterface() != nil { + return einterfaces.GetClusterInterface().IsLeader() + } else { + return true + } +} diff --git a/utils/httpclient.go b/utils/httpclient.go index a2355d827..afa717637 100644 --- a/utils/httpclient.go +++ b/utils/httpclient.go @@ -4,9 +4,12 @@ package utils import ( + "context" "crypto/tls" + "errors" "net" "net/http" + "strings" "time" ) @@ -15,6 +18,11 @@ const ( requestTimeout = 30 * time.Second ) +var secureHttpClient *http.Client +var secureUntrustedHttpClient *http.Client +var insecureHttpClient *http.Client +var insecureUntrustedHttpClient *http.Client + // HttpClient returns a variation the default implementation of Client. // It uses a Transport with the same settings as the default Transport // but with the following modifications: @@ -24,27 +32,147 @@ const ( // "requestTimeout") // - skipping server certificate check if specified in "config.json" // via "ServiceSettings.EnableInsecureOutgoingConnections" -func HttpClient() *http.Client { - if Cfg.ServiceSettings.EnableInsecureOutgoingConnections != nil && *Cfg.ServiceSettings.EnableInsecureOutgoingConnections { +func HttpClient(trustURLs bool) *http.Client { + insecure := Cfg.ServiceSettings.EnableInsecureOutgoingConnections != nil && *Cfg.ServiceSettings.EnableInsecureOutgoingConnections + switch { + case insecure && trustURLs: return insecureHttpClient + case insecure: + return insecureUntrustedHttpClient + case trustURLs: + return secureHttpClient + default: + return secureUntrustedHttpClient } - return secureHttpClient } -var ( - secureHttpClient = createHttpClient(false) - insecureHttpClient = createHttpClient(true) -) +var reservedIPRanges []*net.IPNet + +func isReserved(ip net.IP) bool { + for _, ipRange := range reservedIPRanges { + if ipRange.Contains(ip) { + return true + } + } + return false +} + +func init() { + for _, cidr := range []string{ + // See https://tools.ietf.org/html/rfc6890 + "0.0.0.0/8", // This host on this network + "10.0.0.0/8", // Private-Use + "127.0.0.0/8", // Loopback + "169.254.0.0/16", // Link Local + "172.16.0.0/12", // Private-Use Networks + "192.168.0.0/16", // Private-Use Networks + "::/128", // Unspecified Address + "::1/128", // Loopback Address + "fc00::/7", // Unique-Local + "fe80::/10", // Linked-Scoped Unicast + } { + _, parsed, err := net.ParseCIDR(cidr) + if err != nil { + panic(err) + } + reservedIPRanges = append(reservedIPRanges, parsed) + } + + allowHost := func(host string) bool { + if Cfg.ServiceSettings.AllowedUntrustedInternalConnections == nil { + return false + } + for _, allowed := range strings.Fields(*Cfg.ServiceSettings.AllowedUntrustedInternalConnections) { + if host == allowed { + return true + } + } + return false + } + + allowIP := func(ip net.IP) bool { + if !isReserved(ip) { + return true + } + if Cfg.ServiceSettings.AllowedUntrustedInternalConnections == nil { + return false + } + for _, allowed := range strings.Fields(*Cfg.ServiceSettings.AllowedUntrustedInternalConnections) { + if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) { + return true + } + } + return false + } + + secureHttpClient = createHttpClient(false, nil, nil) + insecureHttpClient = createHttpClient(true, nil, nil) + + secureUntrustedHttpClient = createHttpClient(false, allowHost, allowIP) + insecureUntrustedHttpClient = createHttpClient(true, allowHost, allowIP) +} + +type DialContextFunction func(ctx context.Context, network, addr string) (net.Conn, error) + +var AddressForbidden error = errors.New("address forbidden") + +func dialContextFilter(dial DialContextFunction, allowHost func(host string) bool, allowIP func(ip net.IP) bool) DialContextFunction { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + + if allowHost != nil && allowHost(host) { + return dial(ctx, network, addr) + } + + ips, err := net.LookupIP(host) + if err != nil { + return nil, err + } + + var firstErr error + for _, ip := range ips { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if allowIP == nil || !allowIP(ip) { + continue + } + + conn, err := dial(ctx, network, net.JoinHostPort(ip.String(), port)) + if err == nil { + return conn, nil + } + if firstErr == nil { + firstErr = err + } + } + if firstErr == nil { + return nil, AddressForbidden + } + return nil, firstErr + } +} + +func createHttpClient(enableInsecureConnections bool, allowHost func(host string) bool, allowIP func(ip net.IP) bool) *http.Client { + dialContext := (&net.Dialer{ + Timeout: connectTimeout, + KeepAlive: 30 * time.Second, + }).DialContext + + if allowHost != nil || allowIP != nil { + dialContext = dialContextFilter(dialContext, allowHost, allowIP) + } -func createHttpClient(enableInsecureConnections bool) *http.Client { client := &http.Client{ Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: connectTimeout, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, + Proxy: http.ProxyFromEnvironment, + DialContext: dialContext, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: connectTimeout, diff --git a/utils/httpclient_test.go b/utils/httpclient_test.go index 17353a4e7..1878b58b4 100644 --- a/utils/httpclient_test.go +++ b/utils/httpclient_test.go @@ -4,21 +4,63 @@ package utils import ( + "context" "fmt" "io/ioutil" + "net" "net/http" "net/http/httptest" - "os" + "net/url" "testing" ) +func TestHttpClient(t *testing.T) { + for _, allowInternal := range []bool{true, false} { + c := HttpClient(allowInternal) + for _, tc := range []struct { + URL string + IsInternal bool + }{ + { + URL: "https://google.com", + IsInternal: false, + }, + { + URL: "https://127.0.0.1", + IsInternal: true, + }, + } { + _, err := c.Get(tc.URL) + if !tc.IsInternal { + if err != nil { + t.Fatal("google is down?") + } + } else { + allowed := !tc.IsInternal || allowInternal + success := err == nil + switch e := err.(type) { + case *net.OpError: + success = e.Err != AddressForbidden + case *url.Error: + success = e.Err != AddressForbidden + } + if success != allowed { + t.Fatalf("failed for %v. allowed: %v, success %v", tc.URL, allowed, success) + } + } + } + } +} + func TestHttpClientWithProxy(t *testing.T) { proxy := createProxyServer() defer proxy.Close() - os.Setenv("HTTP_PROXY", proxy.URL) - client := HttpClient() - resp, err := client.Get("http://acme.com") + c := createHttpClient(true, nil, nil) + purl, _ := url.Parse(proxy.URL) + c.Transport.(*http.Transport).Proxy = http.ProxyURL(purl) + + resp, err := c.Get("http://acme.com") if err != nil { t.Fatal(err) } @@ -40,3 +82,39 @@ func createProxyServer() *httptest.Server { fmt.Fprint(w, "proxy") })) } + +func TestDialContextFilter(t *testing.T) { + for _, tc := range []struct { + Addr string + IsValid bool + }{ + { + Addr: "google.com:80", + IsValid: true, + }, + { + Addr: "8.8.8.8:53", + IsValid: true, + }, + { + Addr: "127.0.0.1:80", + }, + { + Addr: "10.0.0.1:80", + IsValid: true, + }, + } { + didDial := false + filter := dialContextFilter(func(ctx context.Context, network, addr string) (net.Conn, error) { + didDial = true + return nil, nil + }, func(host string) bool { return host == "10.0.0.1" }, func(ip net.IP) bool { return !isReserved(ip) }) + _, err := filter(context.Background(), "", tc.Addr) + switch { + case tc.IsValid == (err == AddressForbidden) || (err != nil && err != AddressForbidden): + t.Errorf("unexpected err for %v (%v)", tc.Addr, err) + case tc.IsValid != didDial: + t.Errorf("unexpected didDial for %v", tc.Addr) + } + } +} diff --git a/utils/license.go b/utils/license.go index e28a43e29..d0abc612d 100644 --- a/utils/license.go +++ b/utils/license.go @@ -16,17 +16,16 @@ import ( "os" "strconv" "strings" + "sync/atomic" l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" ) -var IsLicensed bool = false -var License *model.License = &model.License{ - Features: new(model.Features), -} -var ClientLicense map[string]string = map[string]string{"IsLicensed": "false"} +var isLicensedInt32 int32 +var licenseValue atomic.Value +var clientLicenseValue atomic.Value var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyZmShlU8Z8HdG0IWSZ8r @@ -38,6 +37,34 @@ a0v85XL6i9ote2P+fLZ3wX9EoioHzgdgB7arOxY50QRJO7OyCqpKFKv6lRWTXuSt hwIDAQAB -----END PUBLIC KEY-----`) +func init() { + SetLicense(nil) +} + +func IsLicensed() bool { + return atomic.LoadInt32(&isLicensedInt32) == 1 +} + +func SetIsLicensed(v bool) { + if v { + atomic.StoreInt32(&isLicensedInt32, 1) + } else { + atomic.StoreInt32(&isLicensedInt32, 0) + } +} + +func License() *model.License { + return licenseValue.Load().(*model.License) +} + +func SetClientLicense(m map[string]string) { + clientLicenseValue.Store(m) +} + +func ClientLicense() map[string]string { + return clientLicenseValue.Load().(map[string]string) +} + func LoadLicense(licenseBytes []byte) { if success, licenseStr := ValidateLicense(licenseBytes); success { license := model.LicenseFromJson(strings.NewReader(licenseStr)) @@ -49,23 +76,35 @@ func LoadLicense(licenseBytes []byte) { } func SetLicense(license *model.License) bool { - license.Features.SetDefaults() - if !license.IsExpired() { - License = license - IsLicensed = true - ClientLicense = getClientLicense(license) - ClientCfg = getClientConfig(Cfg) - return true - } + if license == nil { + SetIsLicensed(false) + license = &model.License{ + Features: new(model.Features), + } + license.Features.SetDefaults() + licenseValue.Store(license) - return false + SetClientLicense(map[string]string{"IsLicensed": "false"}) + + return false + } else { + license.Features.SetDefaults() + + if !license.IsExpired() { + licenseValue.Store(license) + SetIsLicensed(true) + clientLicenseValue.Store(getClientLicense(license)) + ClientCfg = getClientConfig(Cfg) + return true + } + + return false + } } func RemoveLicense() { - License = &model.License{} - IsLicensed = false - ClientLicense = getClientLicense(License) + SetLicense(nil) ClientCfg = getClientConfig(Cfg) } @@ -162,9 +201,9 @@ func GetLicenseFileLocation(fileLocation string) string { func getClientLicense(l *model.License) map[string]string { props := make(map[string]string) - props["IsLicensed"] = strconv.FormatBool(IsLicensed) + props["IsLicensed"] = strconv.FormatBool(IsLicensed()) - if IsLicensed { + if IsLicensed() { props["Id"] = l.Id props["Users"] = strconv.Itoa(*l.Features.Users) props["LDAP"] = strconv.FormatBool(*l.Features.LDAP) @@ -195,7 +234,7 @@ func getClientLicense(l *model.License) map[string]string { func GetClientLicenseEtag(useSanitized bool) string { value := "" - lic := ClientLicense + lic := ClientLicense() if useSanitized { lic = GetSanitizedClientLicense() @@ -211,11 +250,11 @@ func GetClientLicenseEtag(useSanitized bool) string { func GetSanitizedClientLicense() map[string]string { sanitizedLicense := make(map[string]string) - for k, v := range ClientLicense { + for k, v := range ClientLicense() { sanitizedLicense[k] = v } - if IsLicensed { + if IsLicensed() { delete(sanitizedLicense, "Id") delete(sanitizedLicense, "Name") delete(sanitizedLicense, "Email") diff --git a/utils/license_test.go b/utils/license_test.go index 289262f4c..1d948b02f 100644 --- a/utils/license_test.go +++ b/utils/license_test.go @@ -54,14 +54,14 @@ func TestValidateLicense(t *testing.T) { func TestClientLicenseEtag(t *testing.T) { etag1 := GetClientLicenseEtag(false) - ClientLicense["SomeFeature"] = "true" + SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "true"}) etag2 := GetClientLicenseEtag(false) if etag1 == etag2 { t.Fatal("etags should not match") } - ClientLicense["SomeFeature"] = "false" + SetClientLicense(map[string]string{"SomeFeature": "true", "IsLicensed": "false"}) etag3 := GetClientLicenseEtag(false) if etag2 == etag3 { diff --git a/utils/password.go b/utils/password.go index 6aa785df8..7c2ca538b 100644 --- a/utils/password.go +++ b/utils/password.go @@ -15,7 +15,7 @@ func IsPasswordValid(password string) *model.AppError { isError := false min := model.PASSWORD_MINIMUM_LENGTH - if IsLicensed && *License.Features.PasswordRequirements { + if IsLicensed() && *License().Features.PasswordRequirements { if len(password) < *Cfg.PasswordSettings.MinimumLength || len(password) > model.PASSWORD_MAXIMUM_LENGTH { isError = true } diff --git a/vendor/github.com/stretchr/testify/.gitignore b/vendor/github.com/stretchr/testify/.gitignore new file mode 100644 index 000000000..5aacdb7cc --- /dev/null +++ b/vendor/github.com/stretchr/testify/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe + +.DS_Store diff --git a/vendor/github.com/stretchr/testify/.travis.yml b/vendor/github.com/stretchr/testify/.travis.yml new file mode 100644 index 000000000..ffb9e0ddb --- /dev/null +++ b/vendor/github.com/stretchr/testify/.travis.yml @@ -0,0 +1,16 @@ +language: go + +sudo: false + +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - tip + +script: + - go test -v ./... diff --git a/vendor/github.com/stretchr/testify/Godeps/Godeps.json b/vendor/github.com/stretchr/testify/Godeps/Godeps.json new file mode 100644 index 000000000..df032ac31 --- /dev/null +++ b/vendor/github.com/stretchr/testify/Godeps/Godeps.json @@ -0,0 +1,23 @@ +{ + "ImportPath": "github.com/stretchr/testify", + "GoVersion": "go1.5", + "GodepVersion": "v74", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/davecgh/go-spew/spew", + "Comment": "v1.0.0-3-g6d21280", + "Rev": "6d212800a42e8ab5c146b8ace3490ee17e5225f9" + }, + { + "ImportPath": "github.com/pmezard/go-difflib/difflib", + "Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d" + }, + { + "ImportPath": "github.com/stretchr/objx", + "Rev": "cbeaeb16a013161a98496fad62933b1d21786672" + } + ] +} diff --git a/vendor/github.com/stretchr/testify/Godeps/Readme b/vendor/github.com/stretchr/testify/Godeps/Readme new file mode 100644 index 000000000..4cdaa53d5 --- /dev/null +++ b/vendor/github.com/stretchr/testify/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/vendor/github.com/stretchr/testify/LICENCE.txt b/vendor/github.com/stretchr/testify/LICENCE.txt new file mode 100644 index 000000000..473b670a7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/LICENCE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/LICENSE b/vendor/github.com/stretchr/testify/LICENSE new file mode 100644 index 000000000..473b670a7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT +OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/README.md b/vendor/github.com/stretchr/testify/README.md new file mode 100644 index 000000000..e57b1811f --- /dev/null +++ b/vendor/github.com/stretchr/testify/README.md @@ -0,0 +1,332 @@ +Testify - Thou Shalt Write Tests +================================ + +[![Build Status](https://travis-ci.org/stretchr/testify.svg)](https://travis-ci.org/stretchr/testify) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/testify)](https://goreportcard.com/report/github.com/stretchr/testify) [![GoDoc](https://godoc.org/github.com/stretchr/testify?status.svg)](https://godoc.org/github.com/stretchr/testify) + +Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend. + +Features include: + + * [Easy assertions](#assert-package) + * [Mocking](#mock-package) + * [HTTP response trapping](#http-package) + * [Testing suite interfaces and functions](#suite-package) + +Get started: + + * Install testify with [one line of code](#installation), or [update it with another](#staying-up-to-date) + * For an introduction to writing test code in Go, see http://golang.org/doc/code.html#Testing + * Check out the API Documentation http://godoc.org/github.com/stretchr/testify + * To make your testing life easier, check out our other project, [gorc](http://github.com/stretchr/gorc) + * A little about [Test-Driven Development (TDD)](http://en.wikipedia.org/wiki/Test-driven_development) + + + +[`assert`](http://godoc.org/github.com/stretchr/testify/assert "API documentation") package +------------------------------------------------------------------------------------------- + +The `assert` package provides some helpful methods that allow you to write better test code in Go. + + * Prints friendly, easy to read failure descriptions + * Allows for very readable code + * Optionally annotate each assertion with a message + +See it in action: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + + // assert equality + assert.Equal(t, 123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(t, 123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(t, object) + + // assert for not nil (good when you expect something) + if assert.NotNil(t, object) { + + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal(t, "Something", object.Value) + + } + +} +``` + + * Every assert func takes the `testing.T` object as the first argument. This is how it writes the errors out through the normal `go test` capabilities. + * Every assert func returns a bool indicating whether the assertion was successful or not, this is useful for if you want to go on making further assertions under certain conditions. + +if you assert many times, use the below: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + assert := assert.New(t) + + // assert equality + assert.Equal(123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(object) + + // assert for not nil (good when you expect something) + if assert.NotNil(object) { + + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal("Something", object.Value) + } +} +``` + +[`require`](http://godoc.org/github.com/stretchr/testify/require "API documentation") package +--------------------------------------------------------------------------------------------- + +The `require` package provides same global functions as the `assert` package, but instead of returning a boolean result they terminate current test. + +See [t.FailNow](http://golang.org/pkg/testing/#T.FailNow) for details. + + +[`http`](http://godoc.org/github.com/stretchr/testify/http "API documentation") package +--------------------------------------------------------------------------------------- + +The `http` package contains test objects useful for testing code that relies on the `net/http` package. Check out the [(deprecated) API documentation for the `http` package](http://godoc.org/github.com/stretchr/testify/http). + +We recommend you use [httptest](http://golang.org/pkg/net/http/httptest) instead. + +[`mock`](http://godoc.org/github.com/stretchr/testify/mock "API documentation") package +---------------------------------------------------------------------------------------- + +The `mock` package provides a mechanism for easily writing mock objects that can be used in place of real objects when writing test code. + +An example test function that tests a piece of code that relies on an external object `testObj`, can setup expectations (testify) and assert that they indeed happened: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/mock" +) + +/* + Test objects +*/ + +// MyMockedObject is a mocked object that implements an interface +// that describes an object that the code I am testing relies on. +type MyMockedObject struct{ + mock.Mock +} + +// DoSomething is a method on MyMockedObject that implements some interface +// and just records the activity, and returns what the Mock object tells it to. +// +// In the real object, this method would do something useful, but since this +// is a mocked object - we're just going to stub it out. +// +// NOTE: This method is not being tested here, code that uses this object is. +func (m *MyMockedObject) DoSomething(number int) (bool, error) { + + args := m.Called(number) + return args.Bool(0), args.Error(1) + +} + +/* + Actual test functions +*/ + +// TestSomething is an example of how to use our test object to +// make assertions about some target code we are testing. +func TestSomething(t *testing.T) { + + // create an instance of our test object + testObj := new(MyMockedObject) + + // setup expectations + testObj.On("DoSomething", 123).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) + +} +``` + +For more information on how to write mock code, check out the [API documentation for the `mock` package](http://godoc.org/github.com/stretchr/testify/mock). + +You can use the [mockery tool](http://github.com/vektra/mockery) to autogenerate the mock code against an interface as well, making using mocks much quicker. + +[`suite`](http://godoc.org/github.com/stretchr/testify/suite "API documentation") package +----------------------------------------------------------------------------------------- + +The `suite` package provides functionality that you might be used to from more common object oriented languages. With it, you can build a testing suite as a struct, build setup/teardown methods and testing methods on your struct, and run them with 'go test' as per normal. + +An example suite is shown below: + +```go +// Basic imports +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +For a more complete example, using all of the functionality provided by the suite package, look at our [example testing suite](https://github.com/stretchr/testify/blob/master/suite/suite_test.go) + +For more information on writing suites, check out the [API documentation for the `suite` package](http://godoc.org/github.com/stretchr/testify/suite). + +`Suite` object has assertion methods: + +```go +// Basic imports +import ( + "testing" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including assertion methods. +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + suite.Equal(suite.VariableThatShouldStartAtFive, 5) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +------ + +Installation +============ + +To install Testify, use `go get`: + + * Latest version: go get github.com/stretchr/testify + * Specific version: go get gopkg.in/stretchr/testify.v1 + +This will then make the following packages available to you: + + github.com/stretchr/testify/assert + github.com/stretchr/testify/mock + github.com/stretchr/testify/http + +Import the `testify/assert` package into your code using this template: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + + assert.True(t, true, "True is true!") + +} +``` + +------ + +Staying up to date +================== + +To update Testify to the latest version, use `go get -u github.com/stretchr/testify`. + +------ + +Version History +=============== + + * 1.0 - New package versioning strategy adopted. + +------ + +Contributing +============ + +Please feel free to submit issues, fork the repository and send pull requests! + +When submitting an issue, we ask that you please include a complete test function that demonstrates the issue. Extra credit for those using Testify to write the test code that demonstrates it. + +------ + +Licence +======= +Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell + +Please consider promoting this project if you find it useful. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/_codegen/main.go b/vendor/github.com/stretchr/testify/_codegen/main.go new file mode 100644 index 000000000..328009f84 --- /dev/null +++ b/vendor/github.com/stretchr/testify/_codegen/main.go @@ -0,0 +1,287 @@ +// This program reads all assertion functions from the assert package and +// automatically generates the corersponding requires and forwarded assertions + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/build" + "go/doc" + "go/importer" + "go/parser" + "go/token" + "go/types" + "io" + "io/ioutil" + "log" + "os" + "path" + "strings" + "text/template" + + "github.com/ernesto-jimenez/gogen/imports" +) + +var ( + pkg = flag.String("assert-path", "github.com/stretchr/testify/assert", "Path to the assert package") + outputPkg = flag.String("output-package", "", "package for the resulting code") + tmplFile = flag.String("template", "", "What file to load the function template from") + out = flag.String("out", "", "What file to write the source code to") +) + +func main() { + flag.Parse() + + scope, docs, err := parsePackageSource(*pkg) + if err != nil { + log.Fatal(err) + } + + importer, funcs, err := analyzeCode(scope, docs) + if err != nil { + log.Fatal(err) + } + + if err := generateCode(importer, funcs); err != nil { + log.Fatal(err) + } +} + +func generateCode(importer imports.Importer, funcs []testFunc) error { + buff := bytes.NewBuffer(nil) + + tmplHead, tmplFunc, err := parseTemplates() + if err != nil { + return err + } + + // Generate header + if err := tmplHead.Execute(buff, struct { + Name string + Imports map[string]string + }{ + *outputPkg, + importer.Imports(), + }); err != nil { + return err + } + + // Generate funcs + for _, fn := range funcs { + buff.Write([]byte("\n\n")) + if err := tmplFunc.Execute(buff, &fn); err != nil { + return err + } + } + + // Write file + output, err := outputFile() + if err != nil { + return err + } + defer output.Close() + _, err = io.Copy(output, buff) + return err +} + +func parseTemplates() (*template.Template, *template.Template, error) { + tmplHead, err := template.New("header").Parse(headerTemplate) + if err != nil { + return nil, nil, err + } + if *tmplFile != "" { + f, err := ioutil.ReadFile(*tmplFile) + if err != nil { + return nil, nil, err + } + funcTemplate = string(f) + } + tmpl, err := template.New("function").Parse(funcTemplate) + if err != nil { + return nil, nil, err + } + return tmplHead, tmpl, nil +} + +func outputFile() (*os.File, error) { + filename := *out + if filename == "-" || (filename == "" && *tmplFile == "") { + return os.Stdout, nil + } + if filename == "" { + filename = strings.TrimSuffix(strings.TrimSuffix(*tmplFile, ".tmpl"), ".go") + ".go" + } + return os.Create(filename) +} + +// analyzeCode takes the types scope and the docs and returns the import +// information and information about all the assertion functions. +func analyzeCode(scope *types.Scope, docs *doc.Package) (imports.Importer, []testFunc, error) { + testingT := scope.Lookup("TestingT").Type().Underlying().(*types.Interface) + + importer := imports.New(*outputPkg) + var funcs []testFunc + // Go through all the top level functions + for _, fdocs := range docs.Funcs { + // Find the function + obj := scope.Lookup(fdocs.Name) + + fn, ok := obj.(*types.Func) + if !ok { + continue + } + // Check function signatuer has at least two arguments + sig := fn.Type().(*types.Signature) + if sig.Params().Len() < 2 { + continue + } + // Check first argument is of type testingT + first, ok := sig.Params().At(0).Type().(*types.Named) + if !ok { + continue + } + firstType, ok := first.Underlying().(*types.Interface) + if !ok { + continue + } + if !types.Implements(firstType, testingT) { + continue + } + + funcs = append(funcs, testFunc{*outputPkg, fdocs, fn}) + importer.AddImportsFrom(sig.Params()) + } + return importer, funcs, nil +} + +// parsePackageSource returns the types scope and the package documentation from the pa +func parsePackageSource(pkg string) (*types.Scope, *doc.Package, error) { + pd, err := build.Import(pkg, ".", 0) + if err != nil { + return nil, nil, err + } + + fset := token.NewFileSet() + files := make(map[string]*ast.File) + fileList := make([]*ast.File, len(pd.GoFiles)) + for i, fname := range pd.GoFiles { + src, err := ioutil.ReadFile(path.Join(pd.SrcRoot, pd.ImportPath, fname)) + if err != nil { + return nil, nil, err + } + f, err := parser.ParseFile(fset, fname, src, parser.ParseComments|parser.AllErrors) + if err != nil { + return nil, nil, err + } + files[fname] = f + fileList[i] = f + } + + cfg := types.Config{ + Importer: importer.Default(), + } + info := types.Info{ + Defs: make(map[*ast.Ident]types.Object), + } + tp, err := cfg.Check(pkg, fset, fileList, &info) + if err != nil { + return nil, nil, err + } + + scope := tp.Scope() + + ap, _ := ast.NewPackage(fset, files, nil, nil) + docs := doc.New(ap, pkg, 0) + + return scope, docs, nil +} + +type testFunc struct { + CurrentPkg string + DocInfo *doc.Func + TypeInfo *types.Func +} + +func (f *testFunc) Qualifier(p *types.Package) string { + if p == nil || p.Name() == f.CurrentPkg { + return "" + } + return p.Name() +} + +func (f *testFunc) Params() string { + sig := f.TypeInfo.Type().(*types.Signature) + params := sig.Params() + p := "" + comma := "" + to := params.Len() + var i int + + if sig.Variadic() { + to-- + } + for i = 1; i < to; i++ { + param := params.At(i) + p += fmt.Sprintf("%s%s %s", comma, param.Name(), types.TypeString(param.Type(), f.Qualifier)) + comma = ", " + } + if sig.Variadic() { + param := params.At(params.Len() - 1) + p += fmt.Sprintf("%s%s ...%s", comma, param.Name(), types.TypeString(param.Type().(*types.Slice).Elem(), f.Qualifier)) + } + return p +} + +func (f *testFunc) ForwardedParams() string { + sig := f.TypeInfo.Type().(*types.Signature) + params := sig.Params() + p := "" + comma := "" + to := params.Len() + var i int + + if sig.Variadic() { + to-- + } + for i = 1; i < to; i++ { + param := params.At(i) + p += fmt.Sprintf("%s%s", comma, param.Name()) + comma = ", " + } + if sig.Variadic() { + param := params.At(params.Len() - 1) + p += fmt.Sprintf("%s%s...", comma, param.Name()) + } + return p +} + +func (f *testFunc) Comment() string { + return "// " + strings.Replace(strings.TrimSpace(f.DocInfo.Doc), "\n", "\n// ", -1) +} + +func (f *testFunc) CommentWithoutT(receiver string) string { + search := fmt.Sprintf("assert.%s(t, ", f.DocInfo.Name) + replace := fmt.Sprintf("%s.%s(", receiver, f.DocInfo.Name) + return strings.Replace(f.Comment(), search, replace, -1) +} + +var headerTemplate = `/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND +*/ + +package {{.Name}} + +import ( +{{range $path, $name := .Imports}} + {{$name}} "{{$path}}"{{end}} +) +` + +var funcTemplate = `{{.Comment}} +func (fwd *AssertionsForwarder) {{.DocInfo.Name}}({{.Params}}) bool { + return assert.{{.DocInfo.Name}}({{.ForwardedParams}}) +}` diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go new file mode 100644 index 000000000..e6a796046 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -0,0 +1,387 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND +*/ + +package assert + +import ( + + http "net/http" + url "net/url" + time "time" +) + + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool { + return Condition(a.t, comp, msgAndArgs...) +} + + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World", "But 'Hello World' does contain 'World'") +// a.Contains(["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'") +// a.Contains({"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + return Contains(a.t, s, contains, msgAndArgs...) +} + + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { + return Empty(a.t, object, msgAndArgs...) +} + + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123, "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + return Equal(a.t, expected, actual, msgAndArgs...) +} + + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool { + return EqualError(a.t, theError, errString, msgAndArgs...) +} + + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123), "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + return EqualValues(a.t, expected, actual, msgAndArgs...) +} + + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { + return Error(a.t, err, msgAndArgs...) +} + + +// Exactly asserts that two objects are equal is value and type. +// +// a.Exactly(int32(123), int64(123), "123 and 123 should NOT be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + return Exactly(a.t, expected, actual, msgAndArgs...) +} + + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool { + return Fail(a.t, failureMessage, msgAndArgs...) +} + + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool { + return FailNow(a.t, failureMessage, msgAndArgs...) +} + + +// False asserts that the specified value is false. +// +// a.False(myBool, "myBool should be false") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool { + return False(a.t, value, msgAndArgs...) +} + + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) bool { + return HTTPBodyContains(a.t, handler, method, url, values, str) +} + + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) bool { + return HTTPBodyNotContains(a.t, handler, method, url, values, str) +} + + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values) bool { + return HTTPError(a.t, handler, method, url, values) +} + + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values) bool { + return HTTPRedirect(a.t, handler, method, url, values) +} + + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values) bool { + return HTTPSuccess(a.t, handler, method, url, values) +} + + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject), "MyObject") +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + return Implements(a.t, interfaceObject, object, msgAndArgs...) +} + + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, (22 / 7.0), 0.01) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + return InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + return InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + + +// InEpsilonSlice is the same as InEpsilon, except it compares two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + return InEpsilonSlice(a.t, expected, actual, delta, msgAndArgs...) +} + + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + return IsType(a.t, expectedType, object, msgAndArgs...) +} + + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) bool { + return JSONEq(a.t, expected, actual, msgAndArgs...) +} + + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3, "The size of slice is not 3") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool { + return Len(a.t, object, length, msgAndArgs...) +} + + +// Nil asserts that the specified object is nil. +// +// a.Nil(err, "err should be nothing") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool { + return Nil(a.t, object, msgAndArgs...) +} + + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, actualObj, expectedObj) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { + return NoError(a.t, err, msgAndArgs...) +} + + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") +// a.NotContains(["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'") +// a.NotContains({"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + return NotContains(a.t, s, contains, msgAndArgs...) +} + + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool { + return NotEmpty(a.t, object, msgAndArgs...) +} + + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2, "two objects shouldn't be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + return NotEqual(a.t, expected, actual, msgAndArgs...) +} + + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err, "err should be something") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool { + return NotNil(a.t, object, msgAndArgs...) +} + + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ +// RemainCalm() +// }, "Calling RemainCalm() should NOT panic") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + return NotPanics(a.t, f, msgAndArgs...) +} + + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + return NotRegexp(a.t, rx, str, msgAndArgs...) +} + + +// NotZero asserts that i is not the zero value for its type and returns the truth. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool { + return NotZero(a.t, i, msgAndArgs...) +} + + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ +// GoCrazy() +// }, "Calling GoCrazy() should panic") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + return Panics(a.t, f, msgAndArgs...) +} + + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + return Regexp(a.t, rx, str, msgAndArgs...) +} + + +// True asserts that the specified value is true. +// +// a.True(myBool, "myBool should be true") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool { + return True(a.t, value, msgAndArgs...) +} + + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + return WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + + +// Zero asserts that i is the zero value for its type and returns the truth. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool { + return Zero(a.t, i, msgAndArgs...) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl new file mode 100644 index 000000000..99f9acfbb --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl @@ -0,0 +1,4 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool { + return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go new file mode 100644 index 000000000..b3f4e170d --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -0,0 +1,1052 @@ +package assert + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "math" + "reflect" + "regexp" + "runtime" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" +) + +func init() { + spew.Config.SortKeys = true +} + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) +} + +// Comparison a custom function that returns true on success and false on failure +type Comparison func() (success bool) + +/* + Helper functions +*/ + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + + if expected == nil || actual == nil { + return expected == actual + } + + return reflect.DeepEqual(expected, actual) + +} + +// ObjectsAreEqualValues gets whether two objects are equal, or if their +// values are equal. +func ObjectsAreEqualValues(expected, actual interface{}) bool { + if ObjectsAreEqual(expected, actual) { + return true + } + + actualType := reflect.TypeOf(actual) + if actualType == nil { + return false + } + expectedValue := reflect.ValueOf(expected) + if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { + // Attempt comparison after type conversion + return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) + } + + return false +} + +/* CallerInfo is necessary because the assert functions use the testing object +internally, causing it to print the file:line of the assert method, rather than where +the problem actually occurred in calling code.*/ + +// CallerInfo returns an array of strings containing the file and line number +// of each stack frame leading from the current test to the assert call that +// failed. +func CallerInfo() []string { + + pc := uintptr(0) + file := "" + line := 0 + ok := false + name := "" + + callers := []string{} + for i := 0; ; i++ { + pc, file, line, ok = runtime.Caller(i) + if !ok { + // The breaks below failed to terminate the loop, and we ran off the + // end of the call stack. + break + } + + // This is a huge edge case, but it will panic if this is the case, see #180 + if file == "<autogenerated>" { + break + } + + f := runtime.FuncForPC(pc) + if f == nil { + break + } + name = f.Name() + + // testing.tRunner is the standard library function that calls + // tests. Subtests are called directly by tRunner, without going through + // the Test/Benchmark/Example function that contains the t.Run calls, so + // with subtests we should break when we hit tRunner, without adding it + // to the list of callers. + if name == "testing.tRunner" { + break + } + + parts := strings.Split(file, "/") + dir := parts[len(parts)-2] + file = parts[len(parts)-1] + if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } + + // Drop the package + segments := strings.Split(name, ".") + name = segments[len(segments)-1] + if isTest(name, "Test") || + isTest(name, "Benchmark") || + isTest(name, "Example") { + break + } + } + + return callers +} + +// Stolen from the `go test` tool. +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(rune) +} + +// getWhitespaceString returns a string that is long enough to overwrite the default +// output from the go testing framework. +func getWhitespaceString() string { + + _, file, line, ok := runtime.Caller(1) + if !ok { + return "" + } + parts := strings.Split(file, "/") + file = parts[len(parts)-1] + + return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line))) + +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 0 || msgAndArgs == nil { + return "" + } + if len(msgAndArgs) == 1 { + return msgAndArgs[0].(string) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// Indents all lines of the message by appending a number of tabs to each line, in an output format compatible with Go's +// test printing (see inner comment for specifics) +func indentMessageLines(message string, tabs int) string { + outBuf := new(bytes.Buffer) + + for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { + if i != 0 { + outBuf.WriteRune('\n') + } + for ii := 0; ii < tabs; ii++ { + outBuf.WriteRune('\t') + // Bizarrely, all lines except the first need one fewer tabs prepended, so deliberately advance the counter + // by 1 prematurely. + if ii == 0 && i > 0 { + ii++ + } + } + outBuf.WriteString(scanner.Text()) + } + + return outBuf.String() +} + +type failNower interface { + FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + Fail(t, failureMessage, msgAndArgs...) + + // We cannot extend TestingT with FailNow() and + // maintain backwards compatibility, so we fallback + // to panicking when FailNow is not available in + // TestingT. + // See issue #263 + + if t, ok := t.(failNower); ok { + t.FailNow() + } else { + panic("test failed and t is missing `FailNow()`") + } + return false +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + + message := messageFromMsgAndArgs(msgAndArgs...) + + errorTrace := strings.Join(CallerInfo(), "\n\r\t\t\t") + if len(message) > 0 { + t.Errorf("\r%s\r\tError Trace:\t%s\n"+ + "\r\tError:%s\n"+ + "\r\tMessages:\t%s\n\r", + getWhitespaceString(), + errorTrace, + indentMessageLines(failureMessage, 2), + message) + } else { + t.Errorf("\r%s\r\tError Trace:\t%s\n"+ + "\r\tError:%s\n\r", + getWhitespaceString(), + errorTrace, + indentMessageLines(failureMessage, 2)) + } + + return false +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject") +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if !reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("%T must implement %v", object, interfaceType), msgAndArgs...) + } + + return true + +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + + if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { + return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + } + + return true +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123, "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + if !ObjectsAreEqual(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: %s (expected)\n"+ + " != %s (actual)%s", expected, actual, diff), msgAndArgs...) + } + + return true + +} + +// formatUnequalValues takes two values of arbitrary types and returns string +// representations appropriate to be presented to the user. +// +// If the values are not of like type, the returned strings will be prefixed +// with the type name, and the value will be enclosed in parenthesis similar +// to a type conversion in the Go grammar. +func formatUnequalValues(expected, actual interface{}) (e string, a string) { + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType && isNumericType(aType) && isNumericType(bType) { + return fmt.Sprintf("%v(%#v)", aType, expected), + fmt.Sprintf("%v(%#v)", bType, actual) + } + + return fmt.Sprintf("%#v", expected), + fmt.Sprintf("%#v", actual) +} + +func isNumericType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return true + case reflect.Float32, reflect.Float64: + return true + } + + return false +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123), "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + if !ObjectsAreEqualValues(expected, actual) { + return Fail(t, fmt.Sprintf("Not equal: %#v (expected)\n"+ + " != %#v (actual)", expected, actual), msgAndArgs...) + } + + return true + +} + +// Exactly asserts that two objects are equal is value and type. +// +// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\r\t%v != %v", aType, bType), msgAndArgs...) + } + + return Equal(t, expected, actual, msgAndArgs...) + +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err, "err should be something") +// +// Returns whether the assertion was successful (true) or not (false). +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if !isNil(object) { + return true + } + return Fail(t, "Expected value not to be nil.", msgAndArgs...) +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + kind := value.Kind() + if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { + return true + } + + return false +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err, "err should be nothing") +// +// Returns whether the assertion was successful (true) or not (false). +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if isNil(object) { + return true + } + return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) +} + +var numericZeros = []interface{}{ + int(0), + int8(0), + int16(0), + int32(0), + int64(0), + uint(0), + uint8(0), + uint16(0), + uint32(0), + uint64(0), + float32(0), + float64(0), +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + if object == nil { + return true + } else if object == "" { + return true + } else if object == false { + return true + } + + for _, v := range numericZeros { + if object == v { + return true + } + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + case reflect.Map: + fallthrough + case reflect.Slice, reflect.Chan: + { + return (objValue.Len() == 0) + } + case reflect.Struct: + switch object.(type) { + case time.Time: + return object.(time.Time).IsZero() + } + case reflect.Ptr: + { + if objValue.IsNil() { + return true + } + switch object.(type) { + case *time.Time: + return object.(*time.Time).IsZero() + default: + return false + } + } + } + return false +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +// +// Returns whether the assertion was successful (true) or not (false). +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + + pass := isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + + pass := !isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// getLen try to get length of object. +// return (false, 0) if impossible. +func getLen(x interface{}) (ok bool, length int) { + v := reflect.ValueOf(x) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + return true, v.Len() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3, "The size of slice is not 3") +// +// Returns whether the assertion was successful (true) or not (false). +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool { + ok, l := getLen(object) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...) + } + + if l != length { + return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + } + return true +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool, "myBool should be true") +// +// Returns whether the assertion was successful (true) or not (false). +func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { + + if value != true { + return Fail(t, "Should be true", msgAndArgs...) + } + + return true + +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool, "myBool should be false") +// +// Returns whether the assertion was successful (true) or not (false). +func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { + + if value != false { + return Fail(t, "Should be false", msgAndArgs...) + } + + return true + +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + if ObjectsAreEqual(expected, actual) { + return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + } + + return true + +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func includeElement(list interface{}, element interface{}) (ok, found bool) { + + listValue := reflect.ValueOf(list) + elementValue := reflect.ValueOf(element) + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if reflect.TypeOf(list).Kind() == reflect.String { + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if reflect.TypeOf(list).Kind() == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false + +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'") +// assert.Contains(t, ["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'") +// assert.Contains(t, {"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'") +// +// Returns whether the assertion was successful (true) or not (false). +func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") +// assert.NotContains(t, ["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'") +// assert.NotContains(t, {"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'") +// +// Returns whether the assertion was successful (true) or not (false). +func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if found { + return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics +// methods, and represents a simple func that takes no arguments, and returns nothing. +type PanicTestFunc func() + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (bool, interface{}) { + + didPanic := false + var message interface{} + func() { + + defer func() { + if message = recover(); message != nil { + didPanic = true + } + }() + + // call the target function + f() + + }() + + return didPanic, message + +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ +// GoCrazy() +// }, "Calling GoCrazy() should panic") +// +// Returns whether the assertion was successful (true) or not (false). +func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + + if funcDidPanic, panicValue := didPanic(f); !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ +// RemainCalm() +// }, "Calling RemainCalm() should NOT panic") +// +// Returns whether the assertion was successful (true) or not (false). +func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + + if funcDidPanic, panicValue := didPanic(f); funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should not panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") +// +// Returns whether the assertion was successful (true) or not (false). +func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + + dt := expected.Sub(actual) + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +func toFloat(x interface{}) (float64, bool) { + var xf float64 + xok := true + + switch xn := x.(type) { + case uint8: + xf = float64(xn) + case uint16: + xf = float64(xn) + case uint32: + xf = float64(xn) + case uint64: + xf = float64(xn) + case int: + xf = float64(xn) + case int8: + xf = float64(xn) + case int16: + xf = float64(xn) + case int32: + xf = float64(xn) + case int64: + xf = float64(xn) + case float32: + xf = float64(xn) + case float64: + xf = float64(xn) + default: + xok = false + } + + return xf, xok +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) +// +// Returns whether the assertion was successful (true) or not (false). +func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + + if !aok || !bok { + return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + } + + if math.IsNaN(af) { + return Fail(t, fmt.Sprintf("Actual must not be NaN"), msgAndArgs...) + } + + if math.IsNaN(bf) { + return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...) + } + + dt := af - bf + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta) + if !result { + return result + } + } + + return true +} + +func calcRelativeError(expected, actual interface{}) (float64, error) { + af, aok := toFloat(expected) + if !aok { + return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + } + if af == 0 { + return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") + } + bf, bok := toFloat(actual) + if !bok { + return 0, fmt.Errorf("expected value %q cannot be converted to float", actual) + } + + return math.Abs(af-bf) / math.Abs(af), nil +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +// +// Returns whether the assertion was successful (true) or not (false). +func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + actualEpsilon, err := calcRelativeError(expected, actual) + if err != nil { + return Fail(t, err.Error(), msgAndArgs...) + } + if actualEpsilon > epsilon { + return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ + " < %#v (actual)", actualEpsilon, epsilon), msgAndArgs...) + } + + return true +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), epsilon) + if !result { + return result + } + } + + return true +} + +/* + Errors +*/ + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, actualObj, expectedObj) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { + if err != nil { + return Fail(t, fmt.Sprintf("Received unexpected error %+v", err), msgAndArgs...) + } + + return true +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { + + if err == nil { + return Fail(t, "An error is expected but got nil.", msgAndArgs...) + } + + return true +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString, "An error was expected") +// +// Returns whether the assertion was successful (true) or not (false). +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { + + message := messageFromMsgAndArgs(msgAndArgs...) + if !NotNil(t, theError, "An error is expected but got nil. %s", message) { + return false + } + s := "An error with value \"%s\" is expected but got \"%s\". %s" + return Equal(t, errString, theError.Error(), + s, errString, theError.Error(), message) +} + +// matchRegexp return true if a specified regexp matches a string. +func matchRegexp(rx interface{}, str interface{}) bool { + + var r *regexp.Regexp + if rr, ok := rx.(*regexp.Regexp); ok { + r = rr + } else { + r = regexp.MustCompile(fmt.Sprint(rx)) + } + + return (r.FindStringIndex(fmt.Sprint(str)) != nil) + +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + + match := matchRegexp(rx, str) + + if !match { + Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) + } + + return match +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + match := matchRegexp(rx, str) + + if match { + Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...) + } + + return !match + +} + +// Zero asserts that i is the zero value for its type and returns the truth. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// NotZero asserts that i is not the zero value for its type and returns the truth. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// +// Returns whether the assertion was successful (true) or not (false). +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + var expectedJSONAsInterface, actualJSONAsInterface interface{} + + if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice or array. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array { + return "" + } + + e := spew.Sdump(expected) + a := spew.Sdump(actual) + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return "\n\nDiff:\n" + diff +} diff --git a/vendor/github.com/stretchr/testify/assert/assertions_test.go b/vendor/github.com/stretchr/testify/assert/assertions_test.go new file mode 100644 index 000000000..ac9b70172 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertions_test.go @@ -0,0 +1,1210 @@ +package assert + +import ( + "errors" + "io" + "math" + "os" + "reflect" + "regexp" + "testing" + "time" +) + +var ( + i interface{} + zeros = []interface{}{ + false, + byte(0), + complex64(0), + complex128(0), + float32(0), + float64(0), + int(0), + int8(0), + int16(0), + int32(0), + int64(0), + rune(0), + uint(0), + uint8(0), + uint16(0), + uint32(0), + uint64(0), + uintptr(0), + "", + [0]interface{}{}, + []interface{}(nil), + struct{ x int }{}, + (*interface{})(nil), + (func())(nil), + nil, + interface{}(nil), + map[interface{}]interface{}(nil), + (chan interface{})(nil), + (<-chan interface{})(nil), + (chan<- interface{})(nil), + } + nonZeros = []interface{}{ + true, + byte(1), + complex64(1), + complex128(1), + float32(1), + float64(1), + int(1), + int8(1), + int16(1), + int32(1), + int64(1), + rune(1), + uint(1), + uint8(1), + uint16(1), + uint32(1), + uint64(1), + uintptr(1), + "s", + [1]interface{}{1}, + []interface{}{}, + struct{ x int }{1}, + (*interface{})(&i), + (func())(func() {}), + interface{}(1), + map[interface{}]interface{}{}, + (chan interface{})(make(chan interface{})), + (<-chan interface{})(make(chan interface{})), + (chan<- interface{})(make(chan interface{})), + } +) + +// AssertionTesterInterface defines an interface to be used for testing assertion methods +type AssertionTesterInterface interface { + TestMethod() +} + +// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface +type AssertionTesterConformingObject struct { +} + +func (a *AssertionTesterConformingObject) TestMethod() { +} + +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +type AssertionTesterNonConformingObject struct { +} + +func TestObjectsAreEqual(t *testing.T) { + + if !ObjectsAreEqual("Hello World", "Hello World") { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(123, 123) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(123.5, 123.5) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual([]byte("Hello World"), []byte("Hello World")) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(nil, nil) { + t.Error("objectsAreEqual should return true") + } + if ObjectsAreEqual(map[int]int{5: 10}, map[int]int{10: 20}) { + t.Error("objectsAreEqual should return false") + } + if ObjectsAreEqual('x', "x") { + t.Error("objectsAreEqual should return false") + } + if ObjectsAreEqual("x", 'x') { + t.Error("objectsAreEqual should return false") + } + if ObjectsAreEqual(0, 0.1) { + t.Error("objectsAreEqual should return false") + } + if ObjectsAreEqual(0.1, 0) { + t.Error("objectsAreEqual should return false") + } + if ObjectsAreEqual(uint32(10), int32(10)) { + t.Error("objectsAreEqual should return false") + } + if !ObjectsAreEqualValues(uint32(10), int32(10)) { + t.Error("ObjectsAreEqualValues should return true") + } + if ObjectsAreEqualValues(0, nil) { + t.Fail() + } + if ObjectsAreEqualValues(nil, 0) { + t.Fail() + } + +} + +func TestImplements(t *testing.T) { + + mockT := new(testing.T) + + if !Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface") + } + +} + +func TestIsType(t *testing.T) { + + mockT := new(testing.T) + + if !IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } + if IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } + +} + +func TestEqual(t *testing.T) { + + mockT := new(testing.T) + + if !Equal(mockT, "Hello World", "Hello World") { + t.Error("Equal should return true") + } + if !Equal(mockT, 123, 123) { + t.Error("Equal should return true") + } + if !Equal(mockT, 123.5, 123.5) { + t.Error("Equal should return true") + } + if !Equal(mockT, []byte("Hello World"), []byte("Hello World")) { + t.Error("Equal should return true") + } + if !Equal(mockT, nil, nil) { + t.Error("Equal should return true") + } + if !Equal(mockT, int32(123), int32(123)) { + t.Error("Equal should return true") + } + if !Equal(mockT, uint64(123), uint64(123)) { + t.Error("Equal should return true") + } + +} + +func TestFormatUnequalValues(t *testing.T) { + expected, actual := formatUnequalValues("foo", "bar") + Equal(t, `"foo"`, expected, "value should not include type") + Equal(t, `"bar"`, actual, "value should not include type") + + expected, actual = formatUnequalValues(123, 123) + Equal(t, `123`, expected, "value should not include type") + Equal(t, `123`, actual, "value should not include type") + + expected, actual = formatUnequalValues(int64(123), int32(123)) + Equal(t, `int64(123)`, expected, "value should include type") + Equal(t, `int32(123)`, actual, "value should include type") + + type testStructType struct { + Val string + } + + expected, actual = formatUnequalValues(&testStructType{Val: "test"}, &testStructType{Val: "test"}) + Equal(t, `&assert.testStructType{Val:"test"}`, expected, "value should not include type annotation") + Equal(t, `&assert.testStructType{Val:"test"}`, actual, "value should not include type annotation") +} + +func TestNotNil(t *testing.T) { + + mockT := new(testing.T) + + if !NotNil(mockT, new(AssertionTesterConformingObject)) { + t.Error("NotNil should return true: object is not nil") + } + if NotNil(mockT, nil) { + t.Error("NotNil should return false: object is nil") + } + if NotNil(mockT, (*struct{})(nil)) { + t.Error("NotNil should return false: object is (*struct{})(nil)") + } + +} + +func TestNil(t *testing.T) { + + mockT := new(testing.T) + + if !Nil(mockT, nil) { + t.Error("Nil should return true: object is nil") + } + if !Nil(mockT, (*struct{})(nil)) { + t.Error("Nil should return true: object is (*struct{})(nil)") + } + if Nil(mockT, new(AssertionTesterConformingObject)) { + t.Error("Nil should return false: object is not nil") + } + +} + +func TestTrue(t *testing.T) { + + mockT := new(testing.T) + + if !True(mockT, true) { + t.Error("True should return true") + } + if True(mockT, false) { + t.Error("True should return false") + } + +} + +func TestFalse(t *testing.T) { + + mockT := new(testing.T) + + if !False(mockT, false) { + t.Error("False should return true") + } + if False(mockT, true) { + t.Error("False should return false") + } + +} + +func TestExactly(t *testing.T) { + + mockT := new(testing.T) + + a := float32(1) + b := float64(1) + c := float32(1) + d := float32(2) + + if Exactly(mockT, a, b) { + t.Error("Exactly should return false") + } + if Exactly(mockT, a, d) { + t.Error("Exactly should return false") + } + if !Exactly(mockT, a, c) { + t.Error("Exactly should return true") + } + + if Exactly(mockT, nil, a) { + t.Error("Exactly should return false") + } + if Exactly(mockT, a, nil) { + t.Error("Exactly should return false") + } + +} + +func TestNotEqual(t *testing.T) { + + mockT := new(testing.T) + + if !NotEqual(mockT, "Hello World", "Hello World!") { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, 123, 1234) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, 123.5, 123.55) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, []byte("Hello World"), []byte("Hello World!")) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, nil, new(AssertionTesterConformingObject)) { + t.Error("NotEqual should return true") + } + funcA := func() int { return 23 } + funcB := func() int { return 42 } + if !NotEqual(mockT, funcA, funcB) { + t.Error("NotEqual should return true") + } + + if NotEqual(mockT, "Hello World", "Hello World") { + t.Error("NotEqual should return false") + } + if NotEqual(mockT, 123, 123) { + t.Error("NotEqual should return false") + } + if NotEqual(mockT, 123.5, 123.5) { + t.Error("NotEqual should return false") + } + if NotEqual(mockT, []byte("Hello World"), []byte("Hello World")) { + t.Error("NotEqual should return false") + } + if NotEqual(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("NotEqual should return false") + } +} + +type A struct { + Name, Value string +} + +func TestContains(t *testing.T) { + + mockT := new(testing.T) + list := []string{"Foo", "Bar"} + complexList := []*A{ + {"b", "c"}, + {"d", "e"}, + {"g", "h"}, + {"j", "k"}, + } + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + + if !Contains(mockT, "Hello World", "Hello") { + t.Error("Contains should return true: \"Hello World\" contains \"Hello\"") + } + if Contains(mockT, "Hello World", "Salut") { + t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"") + } + + if !Contains(mockT, list, "Bar") { + t.Error("Contains should return true: \"[\"Foo\", \"Bar\"]\" contains \"Bar\"") + } + if Contains(mockT, list, "Salut") { + t.Error("Contains should return false: \"[\"Foo\", \"Bar\"]\" does not contain \"Salut\"") + } + if !Contains(mockT, complexList, &A{"g", "h"}) { + t.Error("Contains should return true: complexList contains {\"g\", \"h\"}") + } + if Contains(mockT, complexList, &A{"g", "e"}) { + t.Error("Contains should return false: complexList contains {\"g\", \"e\"}") + } + if Contains(mockT, complexList, &A{"g", "e"}) { + t.Error("Contains should return false: complexList contains {\"g\", \"e\"}") + } + if !Contains(mockT, simpleMap, "Foo") { + t.Error("Contains should return true: \"{\"Foo\": \"Bar\"}\" contains \"Foo\"") + } + if Contains(mockT, simpleMap, "Bar") { + t.Error("Contains should return false: \"{\"Foo\": \"Bar\"}\" does not contains \"Bar\"") + } +} + +func TestNotContains(t *testing.T) { + + mockT := new(testing.T) + list := []string{"Foo", "Bar"} + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + + if !NotContains(mockT, "Hello World", "Hello!") { + t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"") + } + if NotContains(mockT, "Hello World", "Hello") { + t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"") + } + + if !NotContains(mockT, list, "Foo!") { + t.Error("NotContains should return true: \"[\"Foo\", \"Bar\"]\" does not contain \"Foo!\"") + } + if NotContains(mockT, list, "Foo") { + t.Error("NotContains should return false: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"") + } + if NotContains(mockT, simpleMap, "Foo") { + t.Error("Contains should return true: \"{\"Foo\": \"Bar\"}\" contains \"Foo\"") + } + if !NotContains(mockT, simpleMap, "Bar") { + t.Error("Contains should return false: \"{\"Foo\": \"Bar\"}\" does not contains \"Bar\"") + } +} + +func Test_includeElement(t *testing.T) { + + list1 := []string{"Foo", "Bar"} + list2 := []int{1, 2} + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + + ok, found := includeElement("Hello World", "World") + True(t, ok) + True(t, found) + + ok, found = includeElement(list1, "Foo") + True(t, ok) + True(t, found) + + ok, found = includeElement(list1, "Bar") + True(t, ok) + True(t, found) + + ok, found = includeElement(list2, 1) + True(t, ok) + True(t, found) + + ok, found = includeElement(list2, 2) + True(t, ok) + True(t, found) + + ok, found = includeElement(list1, "Foo!") + True(t, ok) + False(t, found) + + ok, found = includeElement(list2, 3) + True(t, ok) + False(t, found) + + ok, found = includeElement(list2, "1") + True(t, ok) + False(t, found) + + ok, found = includeElement(simpleMap, "Foo") + True(t, ok) + True(t, found) + + ok, found = includeElement(simpleMap, "Bar") + True(t, ok) + False(t, found) + + ok, found = includeElement(1433, "1") + False(t, ok) + False(t, found) +} + +func TestCondition(t *testing.T) { + mockT := new(testing.T) + + if !Condition(mockT, func() bool { return true }, "Truth") { + t.Error("Condition should return true") + } + + if Condition(mockT, func() bool { return false }, "Lie") { + t.Error("Condition should return false") + } + +} + +func TestDidPanic(t *testing.T) { + + if funcDidPanic, _ := didPanic(func() { + panic("Panic!") + }); !funcDidPanic { + t.Error("didPanic should return true") + } + + if funcDidPanic, _ := didPanic(func() { + }); funcDidPanic { + t.Error("didPanic should return false") + } + +} + +func TestPanics(t *testing.T) { + + mockT := new(testing.T) + + if !Panics(mockT, func() { + panic("Panic!") + }) { + t.Error("Panics should return true") + } + + if Panics(mockT, func() { + }) { + t.Error("Panics should return false") + } + +} + +func TestNotPanics(t *testing.T) { + + mockT := new(testing.T) + + if !NotPanics(mockT, func() { + }) { + t.Error("NotPanics should return true") + } + + if NotPanics(mockT, func() { + panic("Panic!") + }) { + t.Error("NotPanics should return false") + } + +} + +func TestNoError(t *testing.T) { + + mockT := new(testing.T) + + // start with a nil error + var err error + + True(t, NoError(mockT, err), "NoError should return True for nil arg") + + // now set an error + err = errors.New("some error") + + False(t, NoError(mockT, err), "NoError with error should return False") + + // returning an empty error interface + err = func() error { + var err *customError + if err != nil { + t.Fatal("err should be nil here") + } + return err + }() + + if err == nil { // err is not nil here! + t.Errorf("Error should be nil due to empty interface", err) + } + + False(t, NoError(mockT, err), "NoError should fail with empty error interface") +} + +type customError struct{} + +func (*customError) Error() string { return "fail" } + +func TestError(t *testing.T) { + + mockT := new(testing.T) + + // start with a nil error + var err error + + False(t, Error(mockT, err), "Error should return False for nil arg") + + // now set an error + err = errors.New("some error") + + True(t, Error(mockT, err), "Error with error should return True") + + // returning an empty error interface + err = func() error { + var err *customError + if err != nil { + t.Fatal("err should be nil here") + } + return err + }() + + if err == nil { // err is not nil here! + t.Errorf("Error should be nil due to empty interface", err) + } + + True(t, Error(mockT, err), "Error should pass with empty error interface") +} + +func TestEqualError(t *testing.T) { + mockT := new(testing.T) + + // start with a nil error + var err error + False(t, EqualError(mockT, err, ""), + "EqualError should return false for nil arg") + + // now set an error + err = errors.New("some error") + False(t, EqualError(mockT, err, "Not some error"), + "EqualError should return false for different error string") + True(t, EqualError(mockT, err, "some error"), + "EqualError should return true") +} + +func Test_isEmpty(t *testing.T) { + + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + True(t, isEmpty("")) + True(t, isEmpty(nil)) + True(t, isEmpty([]string{})) + True(t, isEmpty(0)) + True(t, isEmpty(int32(0))) + True(t, isEmpty(int64(0))) + True(t, isEmpty(false)) + True(t, isEmpty(map[string]string{})) + True(t, isEmpty(new(time.Time))) + True(t, isEmpty(time.Time{})) + True(t, isEmpty(make(chan struct{}))) + False(t, isEmpty("something")) + False(t, isEmpty(errors.New("something"))) + False(t, isEmpty([]string{"something"})) + False(t, isEmpty(1)) + False(t, isEmpty(true)) + False(t, isEmpty(map[string]string{"Hello": "World"})) + False(t, isEmpty(chWithValue)) + +} + +func TestEmpty(t *testing.T) { + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + var tiP *time.Time + var tiNP time.Time + var s *string + var f *os.File + + True(t, Empty(mockT, ""), "Empty string is empty") + True(t, Empty(mockT, nil), "Nil is empty") + True(t, Empty(mockT, []string{}), "Empty string array is empty") + True(t, Empty(mockT, 0), "Zero int value is empty") + True(t, Empty(mockT, false), "False value is empty") + True(t, Empty(mockT, make(chan struct{})), "Channel without values is empty") + True(t, Empty(mockT, s), "Nil string pointer is empty") + True(t, Empty(mockT, f), "Nil os.File pointer is empty") + True(t, Empty(mockT, tiP), "Nil time.Time pointer is empty") + True(t, Empty(mockT, tiNP), "time.Time is empty") + + False(t, Empty(mockT, "something"), "Non Empty string is not empty") + False(t, Empty(mockT, errors.New("something")), "Non nil object is not empty") + False(t, Empty(mockT, []string{"something"}), "Non empty string array is not empty") + False(t, Empty(mockT, 1), "Non-zero int value is not empty") + False(t, Empty(mockT, true), "True value is not empty") + False(t, Empty(mockT, chWithValue), "Channel with values is not empty") +} + +func TestNotEmpty(t *testing.T) { + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + False(t, NotEmpty(mockT, ""), "Empty string is empty") + False(t, NotEmpty(mockT, nil), "Nil is empty") + False(t, NotEmpty(mockT, []string{}), "Empty string array is empty") + False(t, NotEmpty(mockT, 0), "Zero int value is empty") + False(t, NotEmpty(mockT, false), "False value is empty") + False(t, NotEmpty(mockT, make(chan struct{})), "Channel without values is empty") + + True(t, NotEmpty(mockT, "something"), "Non Empty string is not empty") + True(t, NotEmpty(mockT, errors.New("something")), "Non nil object is not empty") + True(t, NotEmpty(mockT, []string{"something"}), "Non empty string array is not empty") + True(t, NotEmpty(mockT, 1), "Non-zero int value is not empty") + True(t, NotEmpty(mockT, true), "True value is not empty") + True(t, NotEmpty(mockT, chWithValue), "Channel with values is not empty") +} + +func Test_getLen(t *testing.T) { + falseCases := []interface{}{ + nil, + 0, + true, + false, + 'A', + struct{}{}, + } + for _, v := range falseCases { + ok, l := getLen(v) + False(t, ok, "Expected getLen fail to get length of %#v", v) + Equal(t, 0, l, "getLen should return 0 for %#v", v) + } + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + trueCases := []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 3}, + {[...]int{1, 2, 3}, 3}, + {"ABC", 3}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3}, + {ch, 3}, + + {[]int{}, 0}, + {map[int]int{}, 0}, + {make(chan int), 0}, + + {[]int(nil), 0}, + {map[int]int(nil), 0}, + {(chan int)(nil), 0}, + } + + for _, c := range trueCases { + ok, l := getLen(c.v) + True(t, ok, "Expected getLen success to get length of %#v", c.v) + Equal(t, c.l, l) + } +} + +func TestLen(t *testing.T) { + mockT := new(testing.T) + + False(t, Len(mockT, nil, 0), "nil does not have length") + False(t, Len(mockT, 0, 0), "int does not have length") + False(t, Len(mockT, true, 0), "true does not have length") + False(t, Len(mockT, false, 0), "false does not have length") + False(t, Len(mockT, 'A', 0), "Rune does not have length") + False(t, Len(mockT, struct{}{}, 0), "Struct does not have length") + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + + cases := []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 3}, + {[...]int{1, 2, 3}, 3}, + {"ABC", 3}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3}, + {ch, 3}, + + {[]int{}, 0}, + {map[int]int{}, 0}, + {make(chan int), 0}, + + {[]int(nil), 0}, + {map[int]int(nil), 0}, + {(chan int)(nil), 0}, + } + + for _, c := range cases { + True(t, Len(mockT, c.v, c.l), "%#v have %d items", c.v, c.l) + } + + cases = []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 4}, + {[...]int{1, 2, 3}, 2}, + {"ABC", 2}, + {map[int]int{1: 2, 2: 4, 3: 6}, 4}, + {ch, 2}, + + {[]int{}, 1}, + {map[int]int{}, 1}, + {make(chan int), 1}, + + {[]int(nil), 1}, + {map[int]int(nil), 1}, + {(chan int)(nil), 1}, + } + + for _, c := range cases { + False(t, Len(mockT, c.v, c.l), "%#v have %d items", c.v, c.l) + } +} + +func TestWithinDuration(t *testing.T) { + + mockT := new(testing.T) + a := time.Now() + b := a.Add(10 * time.Second) + + True(t, WithinDuration(mockT, a, b, 10*time.Second), "A 10s difference is within a 10s time difference") + True(t, WithinDuration(mockT, b, a, 10*time.Second), "A 10s difference is within a 10s time difference") + + False(t, WithinDuration(mockT, a, b, 9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, 9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -11*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -11*time.Second), "A 10s difference is not within a 9s time difference") +} + +func TestInDelta(t *testing.T) { + mockT := new(testing.T) + + True(t, InDelta(mockT, 1.001, 1, 0.01), "|1.001 - 1| <= 0.01") + True(t, InDelta(mockT, 1, 1.001, 0.01), "|1 - 1.001| <= 0.01") + True(t, InDelta(mockT, 1, 2, 1), "|1 - 2| <= 1") + False(t, InDelta(mockT, 1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail") + False(t, InDelta(mockT, 2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail") + False(t, InDelta(mockT, "", nil, 1), "Expected non numerals to fail") + False(t, InDelta(mockT, 42, math.NaN(), 0.01), "Expected NaN for actual to fail") + False(t, InDelta(mockT, math.NaN(), 42, 0.01), "Expected NaN for expected to fail") + + cases := []struct { + a, b interface{} + delta float64 + }{ + {uint8(2), uint8(1), 1}, + {uint16(2), uint16(1), 1}, + {uint32(2), uint32(1), 1}, + {uint64(2), uint64(1), 1}, + + {int(2), int(1), 1}, + {int8(2), int8(1), 1}, + {int16(2), int16(1), 1}, + {int32(2), int32(1), 1}, + {int64(2), int64(1), 1}, + + {float32(2), float32(1), 1}, + {float64(2), float64(1), 1}, + } + + for _, tc := range cases { + True(t, InDelta(mockT, tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta) + } +} + +func TestInDeltaSlice(t *testing.T) { + mockT := new(testing.T) + + True(t, InDeltaSlice(mockT, + []float64{1.001, 0.999}, + []float64{1, 1}, + 0.1), "{1.001, 0.009} is element-wise close to {1, 1} in delta=0.1") + + True(t, InDeltaSlice(mockT, + []float64{1, 2}, + []float64{0, 3}, + 1), "{1, 2} is element-wise close to {0, 3} in delta=1") + + False(t, InDeltaSlice(mockT, + []float64{1, 2}, + []float64{0, 3}, + 0.1), "{1, 2} is not element-wise close to {0, 3} in delta=0.1") + + False(t, InDeltaSlice(mockT, "", nil, 1), "Expected non numeral slices to fail") +} + +func TestInEpsilon(t *testing.T) { + mockT := new(testing.T) + + cases := []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), uint16(2), .001}, + {2.1, 2.2, 0.1}, + {2.2, 2.1, 0.1}, + {-2.1, -2.2, 0.1}, + {-2.2, -2.1, 0.1}, + {uint64(100), uint8(101), 0.01}, + {0.1, -0.1, 2}, + {0.1, 0, 2}, + } + + for _, tc := range cases { + True(t, InEpsilon(t, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon), "test: %q", tc) + } + + cases = []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), int16(-2), .001}, + {uint64(100), uint8(102), 0.01}, + {2.1, 2.2, 0.001}, + {2.2, 2.1, 0.001}, + {2.1, -2.2, 1}, + {2.1, "bla-bla", 0}, + {0.1, -0.1, 1.99}, + {0, 0.1, 2}, // expected must be different to zero + } + + for _, tc := range cases { + False(t, InEpsilon(mockT, tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } + +} + +func TestInEpsilonSlice(t *testing.T) { + mockT := new(testing.T) + + True(t, InEpsilonSlice(mockT, + []float64{2.2, 2.0}, + []float64{2.1, 2.1}, + 0.06), "{2.2, 2.0} is element-wise close to {2.1, 2.1} in espilon=0.06") + + False(t, InEpsilonSlice(mockT, + []float64{2.2, 2.0}, + []float64{2.1, 2.1}, + 0.04), "{2.2, 2.0} is not element-wise close to {2.1, 2.1} in espilon=0.04") + + False(t, InEpsilonSlice(mockT, "", nil, 1), "Expected non numeral slices to fail") +} + +func TestRegexp(t *testing.T) { + mockT := new(testing.T) + + cases := []struct { + rx, str string + }{ + {"^start", "start of the line"}, + {"end$", "in the end"}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"}, + } + + for _, tc := range cases { + True(t, Regexp(mockT, tc.rx, tc.str)) + True(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + False(t, NotRegexp(mockT, tc.rx, tc.str)) + False(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + } + + cases = []struct { + rx, str string + }{ + {"^asdfastart", "Not the start of the line"}, + {"end$", "in the end."}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"}, + } + + for _, tc := range cases { + False(t, Regexp(mockT, tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str) + False(t, Regexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + True(t, NotRegexp(mockT, tc.rx, tc.str)) + True(t, NotRegexp(mockT, regexp.MustCompile(tc.rx), tc.str)) + } +} + +func testAutogeneratedFunction() { + defer func() { + if err := recover(); err == nil { + panic("did not panic") + } + CallerInfo() + }() + t := struct { + io.Closer + }{} + var c io.Closer + c = t + c.Close() +} + +func TestCallerInfoWithAutogeneratedFunctions(t *testing.T) { + NotPanics(t, func() { + testAutogeneratedFunction() + }) +} + +func TestZero(t *testing.T) { + mockT := new(testing.T) + + for _, test := range zeros { + True(t, Zero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + } + + for _, test := range nonZeros { + False(t, Zero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + } +} + +func TestNotZero(t *testing.T) { + mockT := new(testing.T) + + for _, test := range zeros { + False(t, NotZero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + } + + for _, test := range nonZeros { + True(t, NotZero(mockT, test, "%#v is not the %v zero value", test, reflect.TypeOf(test))) + } +} + +func TestJSONEq_EqualSONString(t *testing.T) { + mockT := new(testing.T) + True(t, JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`)) +} + +func TestJSONEq_EquivalentButNotEqual(t *testing.T) { + mockT := new(testing.T) + True(t, JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_HashOfArraysAndHashes(t *testing.T) { + mockT := new(testing.T) + True(t, JSONEq(mockT, "{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", + "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}")) +} + +func TestJSONEq_Array(t *testing.T) { + mockT := new(testing.T) + True(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`)) +} + +func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`)) +} + +func TestJSONEq_HashesNotEquivalent(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_ActualIsNotJSON(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, `{"foo": "bar"}`, "Not JSON")) +} + +func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`)) +} + +func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, "Not JSON", "Not JSON")) +} + +func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { + mockT := new(testing.T) + False(t, JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`)) +} + +func TestDiff(t *testing.T) { + expected := ` + +Diff: +--- Expected ++++ Actual +@@ -1,3 +1,3 @@ + (struct { foo string }) { +- foo: (string) (len=5) "hello" ++ foo: (string) (len=3) "bar" + } +` + actual := diff( + struct{ foo string }{"hello"}, + struct{ foo string }{"bar"}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -2,5 +2,5 @@ + (int) 1, +- (int) 2, + (int) 3, +- (int) 4 ++ (int) 5, ++ (int) 7 + } +` + actual = diff( + []int{1, 2, 3, 4}, + []int{1, 3, 5, 7}, + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -2,4 +2,4 @@ + (int) 1, +- (int) 2, +- (int) 3 ++ (int) 3, ++ (int) 5 + } +` + actual = diff( + []int{1, 2, 3, 4}[0:3], + []int{1, 3, 5, 7}[0:3], + ) + Equal(t, expected, actual) + + expected = ` + +Diff: +--- Expected ++++ Actual +@@ -1,6 +1,6 @@ + (map[string]int) (len=4) { +- (string) (len=4) "four": (int) 4, ++ (string) (len=4) "five": (int) 5, + (string) (len=3) "one": (int) 1, +- (string) (len=5) "three": (int) 3, +- (string) (len=3) "two": (int) 2 ++ (string) (len=5) "seven": (int) 7, ++ (string) (len=5) "three": (int) 3 + } +` + + actual = diff( + map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}, + map[string]int{"one": 1, "three": 3, "five": 5, "seven": 7}, + ) + Equal(t, expected, actual) +} + +func TestDiffEmptyCases(t *testing.T) { + Equal(t, "", diff(nil, nil)) + Equal(t, "", diff(struct{ foo string }{}, nil)) + Equal(t, "", diff(nil, struct{ foo string }{})) + Equal(t, "", diff(1, 2)) + Equal(t, "", diff(1, 2)) + Equal(t, "", diff([]int{1}, []bool{true})) +} + +// Ensure there are no data races +func TestDiffRace(t *testing.T) { + t.Parallel() + + expected := map[string]string{ + "a": "A", + "b": "B", + "c": "C", + } + + actual := map[string]string{ + "d": "D", + "e": "E", + "f": "F", + } + + // run diffs in parallel simulating tests with t.Parallel() + numRoutines := 10 + rChans := make([]chan string, numRoutines) + for idx := range rChans { + rChans[idx] = make(chan string) + go func(ch chan string) { + defer close(ch) + ch <- diff(expected, actual) + }(rChans[idx]) + } + + for _, ch := range rChans { + for msg := range ch { + NotZero(t, msg) // dummy assert + } + } +} + +type mockTestingT struct { +} + +func (m *mockTestingT) Errorf(format string, args ...interface{}) {} + +func TestFailNowWithPlainTestingT(t *testing.T) { + mockT := &mockTestingT{} + + Panics(t, func() { + FailNow(mockT, "failed") + }, "should panic since mockT is missing FailNow()") +} + +type mockFailNowTestingT struct { +} + +func (m *mockFailNowTestingT) Errorf(format string, args ...interface{}) {} + +func (m *mockFailNowTestingT) FailNow() {} + +func TestFailNowWithFullTestingT(t *testing.T) { + mockT := &mockFailNowTestingT{} + + NotPanics(t, func() { + FailNow(mockT, "failed") + }, "should call mockT.FailNow() rather than panicking") +} diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go new file mode 100644 index 000000000..c9dccc4d6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/doc.go @@ -0,0 +1,45 @@ +// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system. +// +// Example Usage +// +// The following is a complete example using assert in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// if you assert many times, use the format below: +// +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// assert := assert.New(t) +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(a, b, "The two words should be the same.") +// } +// +// Assertions +// +// Assertions allow you to easily write test code, and are global funcs in the `assert` package. +// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// testing framework. This allows the assertion funcs to write the failings and other details to +// the correct place. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package assert diff --git a/vendor/github.com/stretchr/testify/assert/errors.go b/vendor/github.com/stretchr/testify/assert/errors.go new file mode 100644 index 000000000..ac9dc9d1d --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/errors.go @@ -0,0 +1,10 @@ +package assert + +import ( + "errors" +) + +// AnError is an error instance useful for testing. If the code does not care +// about error specifics, and only needs to return the error for example, this +// error should be used to make the test code more readable. +var AnError = errors.New("assert.AnError general error for testing") diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions.go b/vendor/github.com/stretchr/testify/assert/forward_assertions.go new file mode 100644 index 000000000..b867e95ea --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/forward_assertions.go @@ -0,0 +1,16 @@ +package assert + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions_test.go b/vendor/github.com/stretchr/testify/assert/forward_assertions_test.go new file mode 100644 index 000000000..22e1df1d9 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/forward_assertions_test.go @@ -0,0 +1,611 @@ +package assert + +import ( + "errors" + "regexp" + "testing" + "time" +) + +func TestImplementsWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if assert.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface") + } +} + +func TestIsTypeWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } + if assert.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } + +} + +func TestEqualWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.Equal("Hello World", "Hello World") { + t.Error("Equal should return true") + } + if !assert.Equal(123, 123) { + t.Error("Equal should return true") + } + if !assert.Equal(123.5, 123.5) { + t.Error("Equal should return true") + } + if !assert.Equal([]byte("Hello World"), []byte("Hello World")) { + t.Error("Equal should return true") + } + if !assert.Equal(nil, nil) { + t.Error("Equal should return true") + } +} + +func TestEqualValuesWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.EqualValues(uint32(10), int32(10)) { + t.Error("EqualValues should return true") + } +} + +func TestNotNilWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.NotNil(new(AssertionTesterConformingObject)) { + t.Error("NotNil should return true: object is not nil") + } + if assert.NotNil(nil) { + t.Error("NotNil should return false: object is nil") + } + +} + +func TestNilWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.Nil(nil) { + t.Error("Nil should return true: object is nil") + } + if assert.Nil(new(AssertionTesterConformingObject)) { + t.Error("Nil should return false: object is not nil") + } + +} + +func TestTrueWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.True(true) { + t.Error("True should return true") + } + if assert.True(false) { + t.Error("True should return false") + } + +} + +func TestFalseWrapper(t *testing.T) { + assert := New(new(testing.T)) + + if !assert.False(false) { + t.Error("False should return true") + } + if assert.False(true) { + t.Error("False should return false") + } + +} + +func TestExactlyWrapper(t *testing.T) { + assert := New(new(testing.T)) + + a := float32(1) + b := float64(1) + c := float32(1) + d := float32(2) + + if assert.Exactly(a, b) { + t.Error("Exactly should return false") + } + if assert.Exactly(a, d) { + t.Error("Exactly should return false") + } + if !assert.Exactly(a, c) { + t.Error("Exactly should return true") + } + + if assert.Exactly(nil, a) { + t.Error("Exactly should return false") + } + if assert.Exactly(a, nil) { + t.Error("Exactly should return false") + } + +} + +func TestNotEqualWrapper(t *testing.T) { + + assert := New(new(testing.T)) + + if !assert.NotEqual("Hello World", "Hello World!") { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(123, 1234) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(123.5, 123.55) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual([]byte("Hello World"), []byte("Hello World!")) { + t.Error("NotEqual should return true") + } + if !assert.NotEqual(nil, new(AssertionTesterConformingObject)) { + t.Error("NotEqual should return true") + } +} + +func TestContainsWrapper(t *testing.T) { + + assert := New(new(testing.T)) + list := []string{"Foo", "Bar"} + + if !assert.Contains("Hello World", "Hello") { + t.Error("Contains should return true: \"Hello World\" contains \"Hello\"") + } + if assert.Contains("Hello World", "Salut") { + t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"") + } + + if !assert.Contains(list, "Foo") { + t.Error("Contains should return true: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"") + } + if assert.Contains(list, "Salut") { + t.Error("Contains should return false: \"[\"Foo\", \"Bar\"]\" does not contain \"Salut\"") + } + +} + +func TestNotContainsWrapper(t *testing.T) { + + assert := New(new(testing.T)) + list := []string{"Foo", "Bar"} + + if !assert.NotContains("Hello World", "Hello!") { + t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"") + } + if assert.NotContains("Hello World", "Hello") { + t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"") + } + + if !assert.NotContains(list, "Foo!") { + t.Error("NotContains should return true: \"[\"Foo\", \"Bar\"]\" does not contain \"Foo!\"") + } + if assert.NotContains(list, "Foo") { + t.Error("NotContains should return false: \"[\"Foo\", \"Bar\"]\" contains \"Foo\"") + } + +} + +func TestConditionWrapper(t *testing.T) { + + assert := New(new(testing.T)) + + if !assert.Condition(func() bool { return true }, "Truth") { + t.Error("Condition should return true") + } + + if assert.Condition(func() bool { return false }, "Lie") { + t.Error("Condition should return false") + } + +} + +func TestDidPanicWrapper(t *testing.T) { + + if funcDidPanic, _ := didPanic(func() { + panic("Panic!") + }); !funcDidPanic { + t.Error("didPanic should return true") + } + + if funcDidPanic, _ := didPanic(func() { + }); funcDidPanic { + t.Error("didPanic should return false") + } + +} + +func TestPanicsWrapper(t *testing.T) { + + assert := New(new(testing.T)) + + if !assert.Panics(func() { + panic("Panic!") + }) { + t.Error("Panics should return true") + } + + if assert.Panics(func() { + }) { + t.Error("Panics should return false") + } + +} + +func TestNotPanicsWrapper(t *testing.T) { + + assert := New(new(testing.T)) + + if !assert.NotPanics(func() { + }) { + t.Error("NotPanics should return true") + } + + if assert.NotPanics(func() { + panic("Panic!") + }) { + t.Error("NotPanics should return false") + } + +} + +func TestNoErrorWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + + assert.True(mockAssert.NoError(err), "NoError should return True for nil arg") + + // now set an error + err = errors.New("Some error") + + assert.False(mockAssert.NoError(err), "NoError with error should return False") + +} + +func TestErrorWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + + assert.False(mockAssert.Error(err), "Error should return False for nil arg") + + // now set an error + err = errors.New("Some error") + + assert.True(mockAssert.Error(err), "Error with error should return True") + +} + +func TestEqualErrorWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + // start with a nil error + var err error + assert.False(mockAssert.EqualError(err, ""), + "EqualError should return false for nil arg") + + // now set an error + err = errors.New("some error") + assert.False(mockAssert.EqualError(err, "Not some error"), + "EqualError should return false for different error string") + assert.True(mockAssert.EqualError(err, "some error"), + "EqualError should return true") +} + +func TestEmptyWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.True(mockAssert.Empty(""), "Empty string is empty") + assert.True(mockAssert.Empty(nil), "Nil is empty") + assert.True(mockAssert.Empty([]string{}), "Empty string array is empty") + assert.True(mockAssert.Empty(0), "Zero int value is empty") + assert.True(mockAssert.Empty(false), "False value is empty") + + assert.False(mockAssert.Empty("something"), "Non Empty string is not empty") + assert.False(mockAssert.Empty(errors.New("something")), "Non nil object is not empty") + assert.False(mockAssert.Empty([]string{"something"}), "Non empty string array is not empty") + assert.False(mockAssert.Empty(1), "Non-zero int value is not empty") + assert.False(mockAssert.Empty(true), "True value is not empty") + +} + +func TestNotEmptyWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.False(mockAssert.NotEmpty(""), "Empty string is empty") + assert.False(mockAssert.NotEmpty(nil), "Nil is empty") + assert.False(mockAssert.NotEmpty([]string{}), "Empty string array is empty") + assert.False(mockAssert.NotEmpty(0), "Zero int value is empty") + assert.False(mockAssert.NotEmpty(false), "False value is empty") + + assert.True(mockAssert.NotEmpty("something"), "Non Empty string is not empty") + assert.True(mockAssert.NotEmpty(errors.New("something")), "Non nil object is not empty") + assert.True(mockAssert.NotEmpty([]string{"something"}), "Non empty string array is not empty") + assert.True(mockAssert.NotEmpty(1), "Non-zero int value is not empty") + assert.True(mockAssert.NotEmpty(true), "True value is not empty") + +} + +func TestLenWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.False(mockAssert.Len(nil, 0), "nil does not have length") + assert.False(mockAssert.Len(0, 0), "int does not have length") + assert.False(mockAssert.Len(true, 0), "true does not have length") + assert.False(mockAssert.Len(false, 0), "false does not have length") + assert.False(mockAssert.Len('A', 0), "Rune does not have length") + assert.False(mockAssert.Len(struct{}{}, 0), "Struct does not have length") + + ch := make(chan int, 5) + ch <- 1 + ch <- 2 + ch <- 3 + + cases := []struct { + v interface{} + l int + }{ + {[]int{1, 2, 3}, 3}, + {[...]int{1, 2, 3}, 3}, + {"ABC", 3}, + {map[int]int{1: 2, 2: 4, 3: 6}, 3}, + {ch, 3}, + + {[]int{}, 0}, + {map[int]int{}, 0}, + {make(chan int), 0}, + + {[]int(nil), 0}, + {map[int]int(nil), 0}, + {(chan int)(nil), 0}, + } + + for _, c := range cases { + assert.True(mockAssert.Len(c.v, c.l), "%#v have %d items", c.v, c.l) + } +} + +func TestWithinDurationWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + a := time.Now() + b := a.Add(10 * time.Second) + + assert.True(mockAssert.WithinDuration(a, b, 10*time.Second), "A 10s difference is within a 10s time difference") + assert.True(mockAssert.WithinDuration(b, a, 10*time.Second), "A 10s difference is within a 10s time difference") + + assert.False(mockAssert.WithinDuration(a, b, 9*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, 9*time.Second), "A 10s difference is not within a 9s time difference") + + assert.False(mockAssert.WithinDuration(a, b, -9*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, -9*time.Second), "A 10s difference is not within a 9s time difference") + + assert.False(mockAssert.WithinDuration(a, b, -11*time.Second), "A 10s difference is not within a 9s time difference") + assert.False(mockAssert.WithinDuration(b, a, -11*time.Second), "A 10s difference is not within a 9s time difference") +} + +func TestInDeltaWrapper(t *testing.T) { + assert := New(new(testing.T)) + + True(t, assert.InDelta(1.001, 1, 0.01), "|1.001 - 1| <= 0.01") + True(t, assert.InDelta(1, 1.001, 0.01), "|1 - 1.001| <= 0.01") + True(t, assert.InDelta(1, 2, 1), "|1 - 2| <= 1") + False(t, assert.InDelta(1, 2, 0.5), "Expected |1 - 2| <= 0.5 to fail") + False(t, assert.InDelta(2, 1, 0.5), "Expected |2 - 1| <= 0.5 to fail") + False(t, assert.InDelta("", nil, 1), "Expected non numerals to fail") + + cases := []struct { + a, b interface{} + delta float64 + }{ + {uint8(2), uint8(1), 1}, + {uint16(2), uint16(1), 1}, + {uint32(2), uint32(1), 1}, + {uint64(2), uint64(1), 1}, + + {int(2), int(1), 1}, + {int8(2), int8(1), 1}, + {int16(2), int16(1), 1}, + {int32(2), int32(1), 1}, + {int64(2), int64(1), 1}, + + {float32(2), float32(1), 1}, + {float64(2), float64(1), 1}, + } + + for _, tc := range cases { + True(t, assert.InDelta(tc.a, tc.b, tc.delta), "Expected |%V - %V| <= %v", tc.a, tc.b, tc.delta) + } +} + +func TestInEpsilonWrapper(t *testing.T) { + assert := New(new(testing.T)) + + cases := []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), uint16(2), .001}, + {2.1, 2.2, 0.1}, + {2.2, 2.1, 0.1}, + {-2.1, -2.2, 0.1}, + {-2.2, -2.1, 0.1}, + {uint64(100), uint8(101), 0.01}, + {0.1, -0.1, 2}, + } + + for _, tc := range cases { + True(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } + + cases = []struct { + a, b interface{} + epsilon float64 + }{ + {uint8(2), int16(-2), .001}, + {uint64(100), uint8(102), 0.01}, + {2.1, 2.2, 0.001}, + {2.2, 2.1, 0.001}, + {2.1, -2.2, 1}, + {2.1, "bla-bla", 0}, + {0.1, -0.1, 1.99}, + } + + for _, tc := range cases { + False(t, assert.InEpsilon(tc.a, tc.b, tc.epsilon, "Expected %V and %V to have a relative difference of %v", tc.a, tc.b, tc.epsilon)) + } +} + +func TestRegexpWrapper(t *testing.T) { + + assert := New(new(testing.T)) + + cases := []struct { + rx, str string + }{ + {"^start", "start of the line"}, + {"end$", "in the end"}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12.34"}, + } + + for _, tc := range cases { + True(t, assert.Regexp(tc.rx, tc.str)) + True(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str)) + False(t, assert.NotRegexp(tc.rx, tc.str)) + False(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str)) + } + + cases = []struct { + rx, str string + }{ + {"^asdfastart", "Not the start of the line"}, + {"end$", "in the end."}, + {"[0-9]{3}[.-]?[0-9]{2}[.-]?[0-9]{2}", "My phone number is 650.12a.34"}, + } + + for _, tc := range cases { + False(t, assert.Regexp(tc.rx, tc.str), "Expected \"%s\" to not match \"%s\"", tc.rx, tc.str) + False(t, assert.Regexp(regexp.MustCompile(tc.rx), tc.str)) + True(t, assert.NotRegexp(tc.rx, tc.str)) + True(t, assert.NotRegexp(regexp.MustCompile(tc.rx), tc.str)) + } +} + +func TestZeroWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + for _, test := range zeros { + assert.True(mockAssert.Zero(test), "Zero should return true for %v", test) + } + + for _, test := range nonZeros { + assert.False(mockAssert.Zero(test), "Zero should return false for %v", test) + } +} + +func TestNotZeroWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + for _, test := range zeros { + assert.False(mockAssert.NotZero(test), "Zero should return true for %v", test) + } + + for _, test := range nonZeros { + assert.True(mockAssert.NotZero(test), "Zero should return false for %v", test) + } +} + +func TestJSONEqWrapper_EqualSONString(t *testing.T) { + assert := New(new(testing.T)) + if !assert.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { + assert := New(new(testing.T)) + if !assert.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { + assert := New(new(testing.T)) + if !assert.JSONEq("{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", + "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") { + t.Error("JSONEq should return true") + } +} + +func TestJSONEqWrapper_Array(t *testing.T) { + assert := New(new(testing.T)) + if !assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) { + t.Error("JSONEq should return true") + } + +} + +func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq(`{"foo": "bar"}`, "Not JSON") { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq("Not JSON", "Not JSON") { + t.Error("JSONEq should return false") + } +} + +func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + assert := New(new(testing.T)) + if assert.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) { + t.Error("JSONEq should return false") + } +} diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions.go b/vendor/github.com/stretchr/testify/assert/http_assertions.go new file mode 100644 index 000000000..fa7ab89b1 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/http_assertions.go @@ -0,0 +1,106 @@ +package assert + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" +) + +// httpCode is a helper that returns HTTP code of the response. It returns -1 +// if building a new request fails. +func httpCode(handler http.HandlerFunc, method, url string, values url.Values) int { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) + if err != nil { + return -1 + } + handler(w, req) + return w.Code +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { + code := httpCode(handler, method, url, values) + if code == -1 { + return false + } + return code >= http.StatusOK && code <= http.StatusPartialContent +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { + code := httpCode(handler, method, url, values) + if code == -1 { + return false + } + return code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values) bool { + code := httpCode(handler, method, url, values) + if code == -1 { + return false + } + return code >= http.StatusBadRequest +} + +// HTTPBody is a helper that returns HTTP body of the response. It returns +// empty string if building a new request fails. +func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) + if err != nil { + return "" + } + handler(w, req) + return w.Body.String() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if !contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return contains +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}) bool { + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return !contains +} diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions_test.go b/vendor/github.com/stretchr/testify/assert/http_assertions_test.go new file mode 100644 index 000000000..684c2d5d1 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/http_assertions_test.go @@ -0,0 +1,86 @@ +package assert + +import ( + "fmt" + "net/http" + "net/url" + "testing" +) + +func httpOK(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func httpRedirect(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTemporaryRedirect) +} + +func httpError(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) +} + +func TestHTTPStatuses(t *testing.T) { + assert := New(t) + mockT := new(testing.T) + + assert.Equal(HTTPSuccess(mockT, httpOK, "GET", "/", nil), true) + assert.Equal(HTTPSuccess(mockT, httpRedirect, "GET", "/", nil), false) + assert.Equal(HTTPSuccess(mockT, httpError, "GET", "/", nil), false) + + assert.Equal(HTTPRedirect(mockT, httpOK, "GET", "/", nil), false) + assert.Equal(HTTPRedirect(mockT, httpRedirect, "GET", "/", nil), true) + assert.Equal(HTTPRedirect(mockT, httpError, "GET", "/", nil), false) + + assert.Equal(HTTPError(mockT, httpOK, "GET", "/", nil), false) + assert.Equal(HTTPError(mockT, httpRedirect, "GET", "/", nil), false) + assert.Equal(HTTPError(mockT, httpError, "GET", "/", nil), true) +} + +func TestHTTPStatusesWrapper(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.Equal(mockAssert.HTTPSuccess(httpOK, "GET", "/", nil), true) + assert.Equal(mockAssert.HTTPSuccess(httpRedirect, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPSuccess(httpError, "GET", "/", nil), false) + + assert.Equal(mockAssert.HTTPRedirect(httpOK, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPRedirect(httpRedirect, "GET", "/", nil), true) + assert.Equal(mockAssert.HTTPRedirect(httpError, "GET", "/", nil), false) + + assert.Equal(mockAssert.HTTPError(httpOK, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPError(httpRedirect, "GET", "/", nil), false) + assert.Equal(mockAssert.HTTPError(httpError, "GET", "/", nil), true) +} + +func httpHelloName(w http.ResponseWriter, r *http.Request) { + name := r.FormValue("name") + w.Write([]byte(fmt.Sprintf("Hello, %s!", name))) +} + +func TestHttpBody(t *testing.T) { + assert := New(t) + mockT := new(testing.T) + + assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.True(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.False(HTTPBodyContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + + assert.False(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.False(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.True(HTTPBodyNotContains(mockT, httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) +} + +func TestHttpBodyWrappers(t *testing.T) { + assert := New(t) + mockAssert := New(new(testing.T)) + + assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.True(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.False(mockAssert.HTTPBodyContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + + assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "Hello, World!")) + assert.False(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "World")) + assert.True(mockAssert.HTTPBodyNotContains(httpHelloName, "GET", "/", url.Values{"name": []string{"World"}}, "world")) + +} diff --git a/vendor/github.com/stretchr/testify/doc.go b/vendor/github.com/stretchr/testify/doc.go new file mode 100644 index 000000000..377d5cc56 --- /dev/null +++ b/vendor/github.com/stretchr/testify/doc.go @@ -0,0 +1,22 @@ +// Package testify is a set of packages that provide many tools for testifying that your code will behave as you intend. +// +// testify contains the following packages: +// +// The assert package provides a comprehensive set of assertion functions that tie in to the Go testing system. +// +// The http package contains tools to make it easier to test http activity using the Go testing system. +// +// The mock package provides a system by which it is possible to mock your objects and verify calls are happening as expected. +// +// The suite package provides a basic structure for using structs as testing suites, and methods on those structs as tests. It includes setup/teardown functionality in the way of interfaces. +package testify + +// blank imports help docs. +import ( + // assert package + _ "github.com/stretchr/testify/assert" + // http package + _ "github.com/stretchr/testify/http" + // mock package + _ "github.com/stretchr/testify/mock" +) diff --git a/vendor/github.com/stretchr/testify/http/doc.go b/vendor/github.com/stretchr/testify/http/doc.go new file mode 100644 index 000000000..695167c6d --- /dev/null +++ b/vendor/github.com/stretchr/testify/http/doc.go @@ -0,0 +1,2 @@ +// Package http DEPRECATED USE net/http/httptest +package http diff --git a/vendor/github.com/stretchr/testify/http/test_response_writer.go b/vendor/github.com/stretchr/testify/http/test_response_writer.go new file mode 100644 index 000000000..5c3f813fa --- /dev/null +++ b/vendor/github.com/stretchr/testify/http/test_response_writer.go @@ -0,0 +1,49 @@ +package http + +import ( + "net/http" +) + +// TestResponseWriter DEPRECATED: We recommend you use http://golang.org/pkg/net/http/httptest instead. +type TestResponseWriter struct { + + // StatusCode is the last int written by the call to WriteHeader(int) + StatusCode int + + // Output is a string containing the written bytes using the Write([]byte) func. + Output string + + // header is the internal storage of the http.Header object + header http.Header +} + +// Header DEPRECATED: We recommend you use http://golang.org/pkg/net/http/httptest instead. +func (rw *TestResponseWriter) Header() http.Header { + + if rw.header == nil { + rw.header = make(http.Header) + } + + return rw.header +} + +// Write DEPRECATED: We recommend you use http://golang.org/pkg/net/http/httptest instead. +func (rw *TestResponseWriter) Write(bytes []byte) (int, error) { + + // assume 200 success if no header has been set + if rw.StatusCode == 0 { + rw.WriteHeader(200) + } + + // add these bytes to the output string + rw.Output = rw.Output + string(bytes) + + // return normal values + return 0, nil + +} + +// WriteHeader DEPRECATED: We recommend you use http://golang.org/pkg/net/http/httptest instead. +func (rw *TestResponseWriter) WriteHeader(i int) { + rw.StatusCode = i +} diff --git a/vendor/github.com/stretchr/testify/http/test_round_tripper.go b/vendor/github.com/stretchr/testify/http/test_round_tripper.go new file mode 100644 index 000000000..b1e32f1d8 --- /dev/null +++ b/vendor/github.com/stretchr/testify/http/test_round_tripper.go @@ -0,0 +1,17 @@ +package http + +import ( + "github.com/stretchr/testify/mock" + "net/http" +) + +// TestRoundTripper DEPRECATED USE net/http/httptest +type TestRoundTripper struct { + mock.Mock +} + +// RoundTrip DEPRECATED USE net/http/httptest +func (t *TestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + args := t.Called(req) + return args.Get(0).(*http.Response), args.Error(1) +} diff --git a/vendor/github.com/stretchr/testify/mock/doc.go b/vendor/github.com/stretchr/testify/mock/doc.go new file mode 100644 index 000000000..7324128ef --- /dev/null +++ b/vendor/github.com/stretchr/testify/mock/doc.go @@ -0,0 +1,44 @@ +// Package mock provides a system by which it is possible to mock your objects +// and verify calls are happening as expected. +// +// Example Usage +// +// The mock package provides an object, Mock, that tracks activity on another object. It is usually +// embedded into a test object as shown below: +// +// type MyTestObject struct { +// // add a Mock object instance +// mock.Mock +// +// // other fields go here as normal +// } +// +// When implementing the methods of an interface, you wire your functions up +// to call the Mock.Called(args...) method, and return the appropriate values. +// +// For example, to mock a method that saves the name and age of a person and returns +// the year of their birth or an error, you might write this: +// +// func (o *MyTestObject) SavePersonDetails(firstname, lastname string, age int) (int, error) { +// args := o.Called(firstname, lastname, age) +// return args.Int(0), args.Error(1) +// } +// +// The Int, Error and Bool methods are examples of strongly typed getters that take the argument +// index position. Given this argument list: +// +// (12, true, "Something") +// +// You could read them out strongly typed like this: +// +// args.Int(0) +// args.Bool(1) +// args.String(2) +// +// For objects of your own type, use the generic Arguments.Get(index) method and make a type assertion: +// +// return args.Get(0).(*MyObject), args.Get(1).(*AnotherObjectOfMine) +// +// This may cause a panic if the object you are getting is nil (the type assertion will fail), in those +// cases you should check for nil first. +package mock diff --git a/vendor/github.com/stretchr/testify/mock/mock.go b/vendor/github.com/stretchr/testify/mock/mock.go new file mode 100644 index 000000000..20d7b8b1f --- /dev/null +++ b/vendor/github.com/stretchr/testify/mock/mock.go @@ -0,0 +1,763 @@ +package mock + +import ( + "fmt" + "reflect" + "regexp" + "runtime" + "strings" + "sync" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" + "github.com/stretchr/objx" + "github.com/stretchr/testify/assert" +) + +func inin() { + spew.Config.SortKeys = true +} + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Logf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + FailNow() +} + +/* + Call +*/ + +// Call represents a method call and is used for setting expectations, +// as well as recording activity. +type Call struct { + Parent *Mock + + // The name of the method that was or will be called. + Method string + + // Holds the arguments of the method. + Arguments Arguments + + // Holds the arguments that should be returned when + // this method is called. + ReturnArguments Arguments + + // The number of times to return the return arguments when setting + // expectations. 0 means to always return the value. + Repeatability int + + // Amount of times this call has been called + totalCalls int + + // Holds a channel that will be used to block the Return until it either + // receives a message or is closed. nil means it returns immediately. + WaitFor <-chan time.Time + + // Holds a handler used to manipulate arguments content that are passed by + // reference. It's useful when mocking methods such as unmarshalers or + // decoders. + RunFn func(Arguments) +} + +func newCall(parent *Mock, methodName string, methodArguments ...interface{}) *Call { + return &Call{ + Parent: parent, + Method: methodName, + Arguments: methodArguments, + ReturnArguments: make([]interface{}, 0), + Repeatability: 0, + WaitFor: nil, + RunFn: nil, + } +} + +func (c *Call) lock() { + c.Parent.mutex.Lock() +} + +func (c *Call) unlock() { + c.Parent.mutex.Unlock() +} + +// Return specifies the return arguments for the expectation. +// +// Mock.On("DoSomething").Return(errors.New("failed")) +func (c *Call) Return(returnArguments ...interface{}) *Call { + c.lock() + defer c.unlock() + + c.ReturnArguments = returnArguments + + return c +} + +// Once indicates that that the mock should only return the value once. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Once() +func (c *Call) Once() *Call { + return c.Times(1) +} + +// Twice indicates that that the mock should only return the value twice. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Twice() +func (c *Call) Twice() *Call { + return c.Times(2) +} + +// Times indicates that that the mock should only return the indicated number +// of times. +// +// Mock.On("MyMethod", arg1, arg2).Return(returnArg1, returnArg2).Times(5) +func (c *Call) Times(i int) *Call { + c.lock() + defer c.unlock() + c.Repeatability = i + return c +} + +// WaitUntil sets the channel that will block the mock's return until its closed +// or a message is received. +// +// Mock.On("MyMethod", arg1, arg2).WaitUntil(time.After(time.Second)) +func (c *Call) WaitUntil(w <-chan time.Time) *Call { + c.lock() + defer c.unlock() + c.WaitFor = w + return c +} + +// After sets how long to block until the call returns +// +// Mock.On("MyMethod", arg1, arg2).After(time.Second) +func (c *Call) After(d time.Duration) *Call { + return c.WaitUntil(time.After(d)) +} + +// Run sets a handler to be called before returning. It can be used when +// mocking a method such as unmarshalers that takes a pointer to a struct and +// sets properties in such struct +// +// Mock.On("Unmarshal", AnythingOfType("*map[string]interface{}").Return().Run(func(args Arguments) { +// arg := args.Get(0).(*map[string]interface{}) +// arg["foo"] = "bar" +// }) +func (c *Call) Run(fn func(Arguments)) *Call { + c.lock() + defer c.unlock() + c.RunFn = fn + return c +} + +// On chains a new expectation description onto the mocked interface. This +// allows syntax like. +// +// Mock. +// On("MyMethod", 1).Return(nil). +// On("MyOtherMethod", 'a', 'b', 'c').Return(errors.New("Some Error")) +func (c *Call) On(methodName string, arguments ...interface{}) *Call { + return c.Parent.On(methodName, arguments...) +} + +// Mock is the workhorse used to track activity on another object. +// For an example of its usage, refer to the "Example Usage" section at the top +// of this document. +type Mock struct { + // Represents the calls that are expected of + // an object. + ExpectedCalls []*Call + + // Holds the calls that were made to this mocked object. + Calls []Call + + // TestData holds any data that might be useful for testing. Testify ignores + // this data completely allowing you to do whatever you like with it. + testData objx.Map + + mutex sync.Mutex +} + +// TestData holds any data that might be useful for testing. Testify ignores +// this data completely allowing you to do whatever you like with it. +func (m *Mock) TestData() objx.Map { + + if m.testData == nil { + m.testData = make(objx.Map) + } + + return m.testData +} + +/* + Setting expectations +*/ + +// On starts a description of an expectation of the specified method +// being called. +// +// Mock.On("MyMethod", arg1, arg2) +func (m *Mock) On(methodName string, arguments ...interface{}) *Call { + for _, arg := range arguments { + if v := reflect.ValueOf(arg); v.Kind() == reflect.Func { + panic(fmt.Sprintf("cannot use Func in expectations. Use mock.AnythingOfType(\"%T\")", arg)) + } + } + + m.mutex.Lock() + defer m.mutex.Unlock() + c := newCall(m, methodName, arguments...) + m.ExpectedCalls = append(m.ExpectedCalls, c) + return c +} + +// /* +// Recording and responding to activity +// */ + +func (m *Mock) findExpectedCall(method string, arguments ...interface{}) (int, *Call) { + m.mutex.Lock() + defer m.mutex.Unlock() + for i, call := range m.ExpectedCalls { + if call.Method == method && call.Repeatability > -1 { + + _, diffCount := call.Arguments.Diff(arguments) + if diffCount == 0 { + return i, call + } + + } + } + return -1, nil +} + +func (m *Mock) findClosestCall(method string, arguments ...interface{}) (bool, *Call) { + diffCount := 0 + var closestCall *Call + + for _, call := range m.expectedCalls() { + if call.Method == method { + + _, tempDiffCount := call.Arguments.Diff(arguments) + if tempDiffCount < diffCount || diffCount == 0 { + diffCount = tempDiffCount + closestCall = call + } + + } + } + + if closestCall == nil { + return false, nil + } + + return true, closestCall +} + +func callString(method string, arguments Arguments, includeArgumentValues bool) string { + + var argValsString string + if includeArgumentValues { + var argVals []string + for argIndex, arg := range arguments { + argVals = append(argVals, fmt.Sprintf("%d: %#v", argIndex, arg)) + } + argValsString = fmt.Sprintf("\n\t\t%s", strings.Join(argVals, "\n\t\t")) + } + + return fmt.Sprintf("%s(%s)%s", method, arguments.String(), argValsString) +} + +// Called tells the mock object that a method has been called, and gets an array +// of arguments to return. Panics if the call is unexpected (i.e. not preceded by +// appropriate .On .Return() calls) +// If Call.WaitFor is set, blocks until the channel is closed or receives a message. +func (m *Mock) Called(arguments ...interface{}) Arguments { + // get the calling function's name + pc, _, _, ok := runtime.Caller(1) + if !ok { + panic("Couldn't get the caller information") + } + functionPath := runtime.FuncForPC(pc).Name() + //Next four lines are required to use GCCGO function naming conventions. + //For Ex: github_com_docker_libkv_store_mock.WatchTree.pN39_github_com_docker_libkv_store_mock.Mock + //uses inteface information unlike golang github.com/docker/libkv/store/mock.(*Mock).WatchTree + //With GCCGO we need to remove interface information starting from pN<dd>. + re := regexp.MustCompile("\\.pN\\d+_") + if re.MatchString(functionPath) { + functionPath = re.Split(functionPath, -1)[0] + } + parts := strings.Split(functionPath, ".") + functionName := parts[len(parts)-1] + + found, call := m.findExpectedCall(functionName, arguments...) + + if found < 0 { + // we have to fail here - because we don't know what to do + // as the return arguments. This is because: + // + // a) this is a totally unexpected call to this method, + // b) the arguments are not what was expected, or + // c) the developer has forgotten to add an accompanying On...Return pair. + + closestFound, closestCall := m.findClosestCall(functionName, arguments...) + + if closestFound { + panic(fmt.Sprintf("\n\nmock: Unexpected Method Call\n-----------------------------\n\n%s\n\nThe closest call I have is: \n\n%s\n\n%s\n", callString(functionName, arguments, true), callString(functionName, closestCall.Arguments, true), diffArguments(arguments, closestCall.Arguments))) + } else { + panic(fmt.Sprintf("\nassert: mock: I don't know what to return because the method call was unexpected.\n\tEither do Mock.On(\"%s\").Return(...) first, or remove the %s() call.\n\tThis method was unexpected:\n\t\t%s\n\tat: %s", functionName, functionName, callString(functionName, arguments, true), assert.CallerInfo())) + } + } else { + m.mutex.Lock() + switch { + case call.Repeatability == 1: + call.Repeatability = -1 + call.totalCalls++ + + case call.Repeatability > 1: + call.Repeatability-- + call.totalCalls++ + + case call.Repeatability == 0: + call.totalCalls++ + } + m.mutex.Unlock() + } + + // add the call + m.mutex.Lock() + m.Calls = append(m.Calls, *newCall(m, functionName, arguments...)) + m.mutex.Unlock() + + // block if specified + if call.WaitFor != nil { + <-call.WaitFor + } + + if call.RunFn != nil { + call.RunFn(arguments) + } + + return call.ReturnArguments +} + +/* + Assertions +*/ + +type assertExpectationser interface { + AssertExpectations(TestingT) bool +} + +// AssertExpectationsForObjects asserts that everything specified with On and Return +// of the specified objects was in fact called as expected. +// +// Calls may have occurred in any order. +func AssertExpectationsForObjects(t TestingT, testObjects ...interface{}) bool { + for _, obj := range testObjects { + if m, ok := obj.(Mock); ok { + t.Logf("Deprecated mock.AssertExpectationsForObjects(myMock.Mock) use mock.AssertExpectationsForObjects(myMock)") + obj = &m + } + m := obj.(assertExpectationser) + if !m.AssertExpectations(t) { + return false + } + } + return true +} + +// AssertExpectations asserts that everything specified with On and Return was +// in fact called as expected. Calls may have occurred in any order. +func (m *Mock) AssertExpectations(t TestingT) bool { + var somethingMissing bool + var failedExpectations int + + // iterate through each expectation + expectedCalls := m.expectedCalls() + for _, expectedCall := range expectedCalls { + if !m.methodWasCalled(expectedCall.Method, expectedCall.Arguments) && expectedCall.totalCalls == 0 { + somethingMissing = true + failedExpectations++ + t.Logf("\u274C\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String()) + } else { + m.mutex.Lock() + if expectedCall.Repeatability > 0 { + somethingMissing = true + failedExpectations++ + } else { + t.Logf("\u2705\t%s(%s)", expectedCall.Method, expectedCall.Arguments.String()) + } + m.mutex.Unlock() + } + } + + if somethingMissing { + t.Errorf("FAIL: %d out of %d expectation(s) were met.\n\tThe code you are testing needs to make %d more call(s).\n\tat: %s", len(expectedCalls)-failedExpectations, len(expectedCalls), failedExpectations, assert.CallerInfo()) + } + + return !somethingMissing +} + +// AssertNumberOfCalls asserts that the method was called expectedCalls times. +func (m *Mock) AssertNumberOfCalls(t TestingT, methodName string, expectedCalls int) bool { + var actualCalls int + for _, call := range m.calls() { + if call.Method == methodName { + actualCalls++ + } + } + return assert.Equal(t, expectedCalls, actualCalls, fmt.Sprintf("Expected number of calls (%d) does not match the actual number of calls (%d).", expectedCalls, actualCalls)) +} + +// AssertCalled asserts that the method was called. +// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method. +func (m *Mock) AssertCalled(t TestingT, methodName string, arguments ...interface{}) bool { + if !assert.True(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method should have been called with %d argument(s), but was not.", methodName, len(arguments))) { + t.Logf("%v", m.expectedCalls()) + return false + } + return true +} + +// AssertNotCalled asserts that the method was not called. +// It can produce a false result when an argument is a pointer type and the underlying value changed after calling the mocked method. +func (m *Mock) AssertNotCalled(t TestingT, methodName string, arguments ...interface{}) bool { + if !assert.False(t, m.methodWasCalled(methodName, arguments), fmt.Sprintf("The \"%s\" method was called with %d argument(s), but should NOT have been.", methodName, len(arguments))) { + t.Logf("%v", m.expectedCalls()) + return false + } + return true +} + +func (m *Mock) methodWasCalled(methodName string, expected []interface{}) bool { + for _, call := range m.calls() { + if call.Method == methodName { + + _, differences := Arguments(expected).Diff(call.Arguments) + + if differences == 0 { + // found the expected call + return true + } + + } + } + // we didn't find the expected call + return false +} + +func (m *Mock) expectedCalls() []*Call { + m.mutex.Lock() + defer m.mutex.Unlock() + return append([]*Call{}, m.ExpectedCalls...) +} + +func (m *Mock) calls() []Call { + m.mutex.Lock() + defer m.mutex.Unlock() + return append([]Call{}, m.Calls...) +} + +/* + Arguments +*/ + +// Arguments holds an array of method arguments or return values. +type Arguments []interface{} + +const ( + // Anything is used in Diff and Assert when the argument being tested + // shouldn't be taken into consideration. + Anything string = "mock.Anything" +) + +// AnythingOfTypeArgument is a string that contains the type of an argument +// for use when type checking. Used in Diff and Assert. +type AnythingOfTypeArgument string + +// AnythingOfType returns an AnythingOfTypeArgument object containing the +// name of the type to check for. Used in Diff and Assert. +// +// For example: +// Assert(t, AnythingOfType("string"), AnythingOfType("int")) +func AnythingOfType(t string) AnythingOfTypeArgument { + return AnythingOfTypeArgument(t) +} + +// argumentMatcher performs custom argument matching, returning whether or +// not the argument is matched by the expectation fixture function. +type argumentMatcher struct { + // fn is a function which accepts one argument, and returns a bool. + fn reflect.Value +} + +func (f argumentMatcher) Matches(argument interface{}) bool { + expectType := f.fn.Type().In(0) + + if reflect.TypeOf(argument).AssignableTo(expectType) { + result := f.fn.Call([]reflect.Value{reflect.ValueOf(argument)}) + return result[0].Bool() + } + return false +} + +func (f argumentMatcher) String() string { + return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name()) +} + +// MatchedBy can be used to match a mock call based on only certain properties +// from a complex struct or some calculation. It takes a function that will be +// evaluated with the called argument and will return true when there's a match +// and false otherwise. +// +// Example: +// m.On("Do", MatchedBy(func(req *http.Request) bool { return req.Host == "example.com" })) +// +// |fn|, must be a function accepting a single argument (of the expected type) +// which returns a bool. If |fn| doesn't match the required signature, +// MathedBy() panics. +func MatchedBy(fn interface{}) argumentMatcher { + fnType := reflect.TypeOf(fn) + + if fnType.Kind() != reflect.Func { + panic(fmt.Sprintf("assert: arguments: %s is not a func", fn)) + } + if fnType.NumIn() != 1 { + panic(fmt.Sprintf("assert: arguments: %s does not take exactly one argument", fn)) + } + if fnType.NumOut() != 1 || fnType.Out(0).Kind() != reflect.Bool { + panic(fmt.Sprintf("assert: arguments: %s does not return a bool", fn)) + } + + return argumentMatcher{fn: reflect.ValueOf(fn)} +} + +// Get Returns the argument at the specified index. +func (args Arguments) Get(index int) interface{} { + if index+1 > len(args) { + panic(fmt.Sprintf("assert: arguments: Cannot call Get(%d) because there are %d argument(s).", index, len(args))) + } + return args[index] +} + +// Is gets whether the objects match the arguments specified. +func (args Arguments) Is(objects ...interface{}) bool { + for i, obj := range args { + if obj != objects[i] { + return false + } + } + return true +} + +// Diff gets a string describing the differences between the arguments +// and the specified objects. +// +// Returns the diff string and number of differences found. +func (args Arguments) Diff(objects []interface{}) (string, int) { + + var output = "\n" + var differences int + + var maxArgCount = len(args) + if len(objects) > maxArgCount { + maxArgCount = len(objects) + } + + for i := 0; i < maxArgCount; i++ { + var actual, expected interface{} + + if len(objects) <= i { + actual = "(Missing)" + } else { + actual = objects[i] + } + + if len(args) <= i { + expected = "(Missing)" + } else { + expected = args[i] + } + + if matcher, ok := expected.(argumentMatcher); ok { + if matcher.Matches(actual) { + output = fmt.Sprintf("%s\t%d: \u2705 %s matched by %s\n", output, i, actual, matcher) + } else { + differences++ + output = fmt.Sprintf("%s\t%d: \u2705 %s not matched by %s\n", output, i, actual, matcher) + } + } else if reflect.TypeOf(expected) == reflect.TypeOf((*AnythingOfTypeArgument)(nil)).Elem() { + + // type checking + if reflect.TypeOf(actual).Name() != string(expected.(AnythingOfTypeArgument)) && reflect.TypeOf(actual).String() != string(expected.(AnythingOfTypeArgument)) { + // not match + differences++ + output = fmt.Sprintf("%s\t%d: \u274C type %s != type %s - %s\n", output, i, expected, reflect.TypeOf(actual).Name(), actual) + } + + } else { + + // normal checking + + if assert.ObjectsAreEqual(expected, Anything) || assert.ObjectsAreEqual(actual, Anything) || assert.ObjectsAreEqual(actual, expected) { + // match + output = fmt.Sprintf("%s\t%d: \u2705 %s == %s\n", output, i, actual, expected) + } else { + // not match + differences++ + output = fmt.Sprintf("%s\t%d: \u274C %s != %s\n", output, i, actual, expected) + } + } + + } + + if differences == 0 { + return "No differences.", differences + } + + return output, differences + +} + +// Assert compares the arguments with the specified objects and fails if +// they do not exactly match. +func (args Arguments) Assert(t TestingT, objects ...interface{}) bool { + + // get the differences + diff, diffCount := args.Diff(objects) + + if diffCount == 0 { + return true + } + + // there are differences... report them... + t.Logf(diff) + t.Errorf("%sArguments do not match.", assert.CallerInfo()) + + return false + +} + +// String gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +// +// If no index is provided, String() returns a complete string representation +// of the arguments. +func (args Arguments) String(indexOrNil ...int) string { + + if len(indexOrNil) == 0 { + // normal String() method - return a string representation of the args + var argsStr []string + for _, arg := range args { + argsStr = append(argsStr, fmt.Sprintf("%s", reflect.TypeOf(arg))) + } + return strings.Join(argsStr, ",") + } else if len(indexOrNil) == 1 { + // Index has been specified - get the argument at that index + var index = indexOrNil[0] + var s string + var ok bool + if s, ok = args.Get(index).(string); !ok { + panic(fmt.Sprintf("assert: arguments: String(%d) failed because object wasn't correct type: %s", index, args.Get(index))) + } + return s + } + + panic(fmt.Sprintf("assert: arguments: Wrong number of arguments passed to String. Must be 0 or 1, not %d", len(indexOrNil))) + +} + +// Int gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Int(index int) int { + var s int + var ok bool + if s, ok = args.Get(index).(int); !ok { + panic(fmt.Sprintf("assert: arguments: Int(%d) failed because object wasn't correct type: %v", index, args.Get(index))) + } + return s +} + +// Error gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Error(index int) error { + obj := args.Get(index) + var s error + var ok bool + if obj == nil { + return nil + } + if s, ok = obj.(error); !ok { + panic(fmt.Sprintf("assert: arguments: Error(%d) failed because object wasn't correct type: %v", index, args.Get(index))) + } + return s +} + +// Bool gets the argument at the specified index. Panics if there is no argument, or +// if the argument is of the wrong type. +func (args Arguments) Bool(index int) bool { + var s bool + var ok bool + if s, ok = args.Get(index).(bool); !ok { + panic(fmt.Sprintf("assert: arguments: Bool(%d) failed because object wasn't correct type: %v", index, args.Get(index))) + } + return s +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +func diffArguments(expected Arguments, actual Arguments) string { + for x := range expected { + if diffString := diff(expected[x], actual[x]); diffString != "" { + return fmt.Sprintf("Difference found in argument %v:\n\n%s", x, diffString) + } + } + + return "" +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice or array. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array { + return "" + } + + e := spew.Sdump(expected) + a := spew.Sdump(actual) + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return diff +} diff --git a/vendor/github.com/stretchr/testify/mock/mock_test.go b/vendor/github.com/stretchr/testify/mock/mock_test.go new file mode 100644 index 000000000..8cb4615db --- /dev/null +++ b/vendor/github.com/stretchr/testify/mock/mock_test.go @@ -0,0 +1,1132 @@ +package mock + +import ( + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +/* + Test objects +*/ + +// ExampleInterface represents an example interface. +type ExampleInterface interface { + TheExampleMethod(a, b, c int) (int, error) +} + +// TestExampleImplementation is a test implementation of ExampleInterface +type TestExampleImplementation struct { + Mock +} + +func (i *TestExampleImplementation) TheExampleMethod(a, b, c int) (int, error) { + args := i.Called(a, b, c) + return args.Int(0), errors.New("Whoops") +} + +func (i *TestExampleImplementation) TheExampleMethod2(yesorno bool) { + i.Called(yesorno) +} + +type ExampleType struct { + ran bool +} + +func (i *TestExampleImplementation) TheExampleMethod3(et *ExampleType) error { + args := i.Called(et) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodFunc(fn func(string) error) error { + args := i.Called(fn) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodVariadic(a ...int) error { + args := i.Called(a) + return args.Error(0) +} + +func (i *TestExampleImplementation) TheExampleMethodVariadicInterface(a ...interface{}) error { + args := i.Called(a) + return args.Error(0) +} + +type ExampleFuncType func(string) error + +func (i *TestExampleImplementation) TheExampleMethodFuncType(fn ExampleFuncType) error { + args := i.Called(fn) + return args.Error(0) +} + +/* + Mock +*/ + +func Test_Mock_TestData(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + if assert.NotNil(t, mockedService.TestData()) { + + mockedService.TestData().Set("something", 123) + assert.Equal(t, 123, mockedService.TestData().Get("something").Data()) + } +} + +func Test_Mock_On(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod") + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethod", c.Method) +} + +func Test_Mock_Chained_On(t *testing.T) { + // make a test impl object + var mockedService = new(TestExampleImplementation) + + mockedService. + On("TheExampleMethod", 1, 2, 3). + Return(0). + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Return(nil) + + expectedCalls := []*Call{ + &Call{ + Parent: &mockedService.Mock, + Method: "TheExampleMethod", + Arguments: []interface{}{1, 2, 3}, + ReturnArguments: []interface{}{0}, + }, + &Call{ + Parent: &mockedService.Mock, + Method: "TheExampleMethod3", + Arguments: []interface{}{AnythingOfType("*mock.ExampleType")}, + ReturnArguments: []interface{}{nil}, + }, + } + assert.Equal(t, expectedCalls, mockedService.ExpectedCalls) +} + +func Test_Mock_On_WithArgs(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod", 1, 2, 3, 4) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethod", c.Method) + assert.Equal(t, Arguments{1, 2, 3, 4}, c.Arguments) +} + +func Test_Mock_On_WithFuncArg(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodFunc", AnythingOfType("func(string) error")). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, "TheExampleMethodFunc", c.Method) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, AnythingOfType("func(string) error"), c.Arguments[0]) + + fn := func(string) error { return nil } + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodFunc(fn) + }) +} + +func Test_Mock_On_WithIntArgMatcher(t *testing.T) { + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod", + MatchedBy(func(a int) bool { + return a == 1 + }), MatchedBy(func(b int) bool { + return b == 2 + }), MatchedBy(func(c int) bool { + return c == 3 + })).Return(0, nil) + + assert.Panics(t, func() { + mockedService.TheExampleMethod(1, 2, 4) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethod(2, 2, 3) + }) + assert.NotPanics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) +} + +func Test_Mock_On_WithPtrArgMatcher(t *testing.T) { + var mockedService TestExampleImplementation + + mockedService.On("TheExampleMethod3", + MatchedBy(func(a *ExampleType) bool { return a.ran == true }), + ).Return(nil) + + mockedService.On("TheExampleMethod3", + MatchedBy(func(a *ExampleType) bool { return a.ran == false }), + ).Return(errors.New("error")) + + assert.Equal(t, mockedService.TheExampleMethod3(&ExampleType{true}), nil) + assert.EqualError(t, mockedService.TheExampleMethod3(&ExampleType{false}), "error") +} + +func Test_Mock_On_WithFuncArgMatcher(t *testing.T) { + var mockedService TestExampleImplementation + + fixture1, fixture2 := errors.New("fixture1"), errors.New("fixture2") + + mockedService.On("TheExampleMethodFunc", + MatchedBy(func(a func(string) error) bool { return a("string") == fixture1 }), + ).Return(errors.New("fixture1")) + + mockedService.On("TheExampleMethodFunc", + MatchedBy(func(a func(string) error) bool { return a("string") == fixture2 }), + ).Return(errors.New("fixture2")) + + assert.EqualError(t, mockedService.TheExampleMethodFunc( + func(string) error { return fixture1 }), "fixture1") + assert.EqualError(t, mockedService.TheExampleMethodFunc( + func(string) error { return fixture2 }), "fixture2") +} + +func Test_Mock_On_WithVariadicFunc(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodVariadic", []int{1, 2, 3}). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, []int{1, 2, 3}, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadic(1, 2, 3) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadic(1, 2) + }) + +} + +func Test_Mock_On_WithVariadicFuncWithInterface(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethodVariadicInterface", []interface{}{1, 2, 3}). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, []interface{}{1, 2, 3}, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2, 3) + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2) + }) + +} + +func Test_Mock_On_WithVariadicFuncWithEmptyInterfaceArray(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + var expected []interface{} + c := mockedService. + On("TheExampleMethodVariadicInterface", expected). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, expected, c.Arguments[0]) + + assert.NotPanics(t, func() { + mockedService.TheExampleMethodVariadicInterface() + }) + assert.Panics(t, func() { + mockedService.TheExampleMethodVariadicInterface(1, 2) + }) + +} + +func Test_Mock_On_WithFuncPanics(t *testing.T) { + // make a test impl object + var mockedService = new(TestExampleImplementation) + + assert.Panics(t, func() { + mockedService.On("TheExampleMethodFunc", func(string) error { return nil }) + }) +} + +func Test_Mock_On_WithFuncTypeArg(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethodFuncType", AnythingOfType("mock.ExampleFuncType")). + Return(nil) + + assert.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + assert.Equal(t, 1, len(c.Arguments)) + assert.Equal(t, AnythingOfType("mock.ExampleFuncType"), c.Arguments[0]) + + fn := func(string) error { return nil } + assert.NotPanics(t, func() { + mockedService.TheExampleMethodFuncType(fn) + }) +} + +func Test_Mock_Return(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_WaitUntil(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + ch := time.After(time.Second) + + c := mockedService.Mock. + On("TheExampleMethod", "A", "B", true). + WaitUntil(ch). + Return(1, "two", true) + + // assert that the call was created + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.Equal(t, ch, call.WaitFor) +} + +func Test_Mock_Return_After(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.Mock. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + After(time.Second) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + +} + +func Test_Mock_Return_Run(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + fn := func(args Arguments) { + arg := args.Get(0).(*ExampleType) + arg.ran = true + } + + c := mockedService.Mock. + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Return(nil). + Run(fn) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod3", call.Method) + assert.Equal(t, AnythingOfType("*mock.ExampleType"), call.Arguments[0]) + assert.Equal(t, nil, call.ReturnArguments[0]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + assert.NotNil(t, call.Run) + + et := ExampleType{} + assert.Equal(t, false, et.ran) + mockedService.TheExampleMethod3(&et) + assert.Equal(t, true, et.ran) +} + +func Test_Mock_Return_Run_Out_Of_Order(t *testing.T) { + // make a test impl object + var mockedService = new(TestExampleImplementation) + f := func(args Arguments) { + arg := args.Get(0).(*ExampleType) + arg.ran = true + } + + c := mockedService.Mock. + On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")). + Run(f). + Return(nil) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.Mock.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod3", call.Method) + assert.Equal(t, AnythingOfType("*mock.ExampleType"), call.Arguments[0]) + assert.Equal(t, nil, call.ReturnArguments[0]) + assert.Equal(t, 0, call.Repeatability) + assert.NotEqual(t, nil, call.WaitFor) + assert.NotNil(t, call.Run) +} + +func Test_Mock_Return_Once(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService.On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Once() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 1, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Twice(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Twice() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 2, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Times(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return(1, "two", true). + Times(5) + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 1, call.ReturnArguments[0]) + assert.Equal(t, "two", call.ReturnArguments[1]) + assert.Equal(t, true, call.ReturnArguments[2]) + assert.Equal(t, 5, call.Repeatability) + assert.Nil(t, call.WaitFor) +} + +func Test_Mock_Return_Nothing(t *testing.T) { + + // make a test impl object + var mockedService = new(TestExampleImplementation) + + c := mockedService. + On("TheExampleMethod", "A", "B", true). + Return() + + require.Equal(t, []*Call{c}, mockedService.ExpectedCalls) + + call := mockedService.ExpectedCalls[0] + + assert.Equal(t, "TheExampleMethod", call.Method) + assert.Equal(t, "A", call.Arguments[0]) + assert.Equal(t, "B", call.Arguments[1]) + assert.Equal(t, true, call.Arguments[2]) + assert.Equal(t, 0, len(call.ReturnArguments)) +} + +func Test_Mock_findExpectedCall(t *testing.T) { + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two") + m.On("Two", 3).Return("three") + + f, c := m.findExpectedCall("Two", 3) + + if assert.Equal(t, 2, f) { + if assert.NotNil(t, c) { + assert.Equal(t, "Two", c.Method) + assert.Equal(t, 3, c.Arguments[0]) + assert.Equal(t, "three", c.ReturnArguments[0]) + } + } + +} + +func Test_Mock_findExpectedCall_For_Unknown_Method(t *testing.T) { + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two") + m.On("Two", 3).Return("three") + + f, _ := m.findExpectedCall("Two") + + assert.Equal(t, -1, f) + +} + +func Test_Mock_findExpectedCall_Respects_Repeatability(t *testing.T) { + + m := new(Mock) + m.On("One", 1).Return("one") + m.On("Two", 2).Return("two").Once() + m.On("Two", 3).Return("three").Twice() + m.On("Two", 3).Return("three").Times(8) + + f, c := m.findExpectedCall("Two", 3) + + if assert.Equal(t, 2, f) { + if assert.NotNil(t, c) { + assert.Equal(t, "Two", c.Method) + assert.Equal(t, 3, c.Arguments[0]) + assert.Equal(t, "three", c.ReturnArguments[0]) + } + } + +} + +func Test_callString(t *testing.T) { + + assert.Equal(t, `Method(int,bool,string)`, callString("Method", []interface{}{1, true, "something"}, false)) + +} + +func Test_Mock_Called(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_Called", 1, 2, 3).Return(5, "6", true) + + returnArguments := mockedService.Called(1, 2, 3) + + if assert.Equal(t, 1, len(mockedService.Calls)) { + assert.Equal(t, "Test_Mock_Called", mockedService.Calls[0].Method) + assert.Equal(t, 1, mockedService.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[0].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments)) { + assert.Equal(t, 5, returnArguments[0]) + assert.Equal(t, "6", returnArguments[1]) + assert.Equal(t, true, returnArguments[2]) + } + +} + +func asyncCall(m *Mock, ch chan Arguments) { + ch <- m.Called(1, 2, 3) +} + +func Test_Mock_Called_blocks(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.Mock.On("asyncCall", 1, 2, 3).Return(5, "6", true).After(2 * time.Millisecond) + + ch := make(chan Arguments) + + go asyncCall(&mockedService.Mock, ch) + + select { + case <-ch: + t.Fatal("should have waited") + case <-time.After(1 * time.Millisecond): + } + + returnArguments := <-ch + + if assert.Equal(t, 1, len(mockedService.Mock.Calls)) { + assert.Equal(t, "asyncCall", mockedService.Mock.Calls[0].Method) + assert.Equal(t, 1, mockedService.Mock.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Mock.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Mock.Calls[0].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments)) { + assert.Equal(t, 5, returnArguments[0]) + assert.Equal(t, "6", returnArguments[1]) + assert.Equal(t, true, returnArguments[2]) + } + +} + +func Test_Mock_Called_For_Bounded_Repeatability(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService. + On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3). + Return(5, "6", true). + Once() + mockedService. + On("Test_Mock_Called_For_Bounded_Repeatability", 1, 2, 3). + Return(-1, "hi", false) + + returnArguments1 := mockedService.Called(1, 2, 3) + returnArguments2 := mockedService.Called(1, 2, 3) + + if assert.Equal(t, 2, len(mockedService.Calls)) { + assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[0].Method) + assert.Equal(t, 1, mockedService.Calls[0].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[0].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[0].Arguments[2]) + + assert.Equal(t, "Test_Mock_Called_For_Bounded_Repeatability", mockedService.Calls[1].Method) + assert.Equal(t, 1, mockedService.Calls[1].Arguments[0]) + assert.Equal(t, 2, mockedService.Calls[1].Arguments[1]) + assert.Equal(t, 3, mockedService.Calls[1].Arguments[2]) + } + + if assert.Equal(t, 3, len(returnArguments1)) { + assert.Equal(t, 5, returnArguments1[0]) + assert.Equal(t, "6", returnArguments1[1]) + assert.Equal(t, true, returnArguments1[2]) + } + + if assert.Equal(t, 3, len(returnArguments2)) { + assert.Equal(t, -1, returnArguments2[0]) + assert.Equal(t, "hi", returnArguments2[1]) + assert.Equal(t, false, returnArguments2[2]) + } + +} + +func Test_Mock_Called_For_SetTime_Expectation(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethod", 1, 2, 3).Return(5, "6", true).Times(4) + + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + mockedService.TheExampleMethod(1, 2, 3) + assert.Panics(t, func() { + mockedService.TheExampleMethod(1, 2, 3) + }) + +} + +func Test_Mock_Called_Unexpected(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + // make sure it panics if no expectation was made + assert.Panics(t, func() { + mockedService.Called(1, 2, 3) + }, "Calling unexpected method should panic") + +} + +func Test_AssertExpectationsForObjects_Helper(t *testing.T) { + + var mockedService1 = new(TestExampleImplementation) + var mockedService2 = new(TestExampleImplementation) + var mockedService3 = new(TestExampleImplementation) + + mockedService1.On("Test_AssertExpectationsForObjects_Helper", 1).Return() + mockedService2.On("Test_AssertExpectationsForObjects_Helper", 2).Return() + mockedService3.On("Test_AssertExpectationsForObjects_Helper", 3).Return() + + mockedService1.Called(1) + mockedService2.Called(2) + mockedService3.Called(3) + + assert.True(t, AssertExpectationsForObjects(t, mockedService1.Mock, mockedService2.Mock, mockedService3.Mock)) + assert.True(t, AssertExpectationsForObjects(t, mockedService1, mockedService2, mockedService3)) + +} + +func Test_AssertExpectationsForObjects_Helper_Failed(t *testing.T) { + + var mockedService1 = new(TestExampleImplementation) + var mockedService2 = new(TestExampleImplementation) + var mockedService3 = new(TestExampleImplementation) + + mockedService1.On("Test_AssertExpectationsForObjects_Helper_Failed", 1).Return() + mockedService2.On("Test_AssertExpectationsForObjects_Helper_Failed", 2).Return() + mockedService3.On("Test_AssertExpectationsForObjects_Helper_Failed", 3).Return() + + mockedService1.Called(1) + mockedService3.Called(3) + + tt := new(testing.T) + assert.False(t, AssertExpectationsForObjects(tt, mockedService1.Mock, mockedService2.Mock, mockedService3.Mock)) + assert.False(t, AssertExpectationsForObjects(tt, mockedService1, mockedService2, mockedService3)) + +} + +func Test_Mock_AssertExpectations(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations", 1, 2, 3).Return(5, 6, 7) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_Placeholder_NoArgs(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertExpectations_Placeholder_NoArgs").Return(7, 6, 5) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called() + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_Placeholder(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_Placeholder", 1, 2, 3).Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertExpectations_Placeholder", 3, 2, 1).Return(7, 6, 5) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.False(t, mockedService.AssertExpectations(tt)) + + // make call to the second expectation + mockedService.Called(3, 2, 1) + + // now assert expectations again + assert.True(t, mockedService.AssertExpectations(tt)) +} + +func Test_Mock_AssertExpectations_With_Pointers(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{1}).Return(1) + mockedService.On("Test_Mock_AssertExpectations_With_Pointers", &struct{ Foo int }{2}).Return(2) + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + s := struct{ Foo int }{1} + // make the calls now + mockedService.Called(&s) + s.Foo = 2 + mockedService.Called(&s) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectationsCustomType(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("TheExampleMethod3", AnythingOfType("*mock.ExampleType")).Return(nil).Once() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.TheExampleMethod3(&ExampleType{}) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_AssertExpectations_With_Repeatability(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertExpectations_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Twice() + + tt := new(testing.T) + assert.False(t, mockedService.AssertExpectations(tt)) + + // make the call now + mockedService.Called(1, 2, 3) + + assert.False(t, mockedService.AssertExpectations(tt)) + + mockedService.Called(1, 2, 3) + + // now assert expectations + assert.True(t, mockedService.AssertExpectations(tt)) + +} + +func Test_Mock_TwoCallsWithDifferentArguments(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 1, 2, 3).Return(5, 6, 7) + mockedService.On("Test_Mock_TwoCallsWithDifferentArguments", 4, 5, 6).Return(5, 6, 7) + + args1 := mockedService.Called(1, 2, 3) + assert.Equal(t, 5, args1.Int(0)) + assert.Equal(t, 6, args1.Int(1)) + assert.Equal(t, 7, args1.Int(2)) + + args2 := mockedService.Called(4, 5, 6) + assert.Equal(t, 5, args2.Int(0)) + assert.Equal(t, 6, args2.Int(1)) + assert.Equal(t, 7, args2.Int(2)) + +} + +func Test_Mock_AssertNumberOfCalls(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertNumberOfCalls", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 1)) + + mockedService.Called(1, 2, 3) + assert.True(t, mockedService.AssertNumberOfCalls(t, "Test_Mock_AssertNumberOfCalls", 2)) + +} + +func Test_Mock_AssertCalled(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled", 1, 2, 3)) + +} + +func Test_Mock_AssertCalled_WithAnythingOfTypeArgument(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService. + On("Test_Mock_AssertCalled_WithAnythingOfTypeArgument", Anything, Anything, Anything). + Return() + + mockedService.Called(1, "two", []uint8("three")) + + assert.True(t, mockedService.AssertCalled(t, "Test_Mock_AssertCalled_WithAnythingOfTypeArgument", AnythingOfType("int"), AnythingOfType("string"), AnythingOfType("[]uint8"))) + +} + +func Test_Mock_AssertCalled_WithArguments(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled_WithArguments", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + tt := new(testing.T) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 1, 2, 3)) + assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments", 2, 3, 4)) + +} + +func Test_Mock_AssertCalled_WithArguments_With_Repeatability(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3).Return(5, 6, 7).Once() + mockedService.On("Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4).Return(5, 6, 7).Once() + + mockedService.Called(1, 2, 3) + mockedService.Called(2, 3, 4) + + tt := new(testing.T) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 1, 2, 3)) + assert.True(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 2, 3, 4)) + assert.False(t, mockedService.AssertCalled(tt, "Test_Mock_AssertCalled_WithArguments_With_Repeatability", 3, 4, 5)) + +} + +func Test_Mock_AssertNotCalled(t *testing.T) { + + var mockedService = new(TestExampleImplementation) + + mockedService.On("Test_Mock_AssertNotCalled", 1, 2, 3).Return(5, 6, 7) + + mockedService.Called(1, 2, 3) + + assert.True(t, mockedService.AssertNotCalled(t, "Test_Mock_NotCalled")) + +} + +/* + Arguments helper methods +*/ +func Test_Arguments_Get(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.Equal(t, "string", args.Get(0).(string)) + assert.Equal(t, 123, args.Get(1).(int)) + assert.Equal(t, true, args.Get(2).(bool)) + +} + +func Test_Arguments_Is(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.True(t, args.Is("string", 123, true)) + assert.False(t, args.Is("wrong", 456, false)) + +} + +func Test_Arguments_Diff(t *testing.T) { + + var args = Arguments([]interface{}{"Hello World", 123, true}) + var diff string + var count int + diff, count = args.Diff([]interface{}{"Hello World", 456, "false"}) + + assert.Equal(t, 2, count) + assert.Contains(t, diff, `%!s(int=456) != %!s(int=123)`) + assert.Contains(t, diff, `false != %!s(bool=true)`) + +} + +func Test_Arguments_Diff_DifferentNumberOfArgs(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + var diff string + var count int + diff, count = args.Diff([]interface{}{"string", 456, "false", "extra"}) + + assert.Equal(t, 3, count) + assert.Contains(t, diff, `extra != (Missing)`) + +} + +func Test_Arguments_Diff_WithAnythingArgument(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + var count int + _, count = args.Diff([]interface{}{"string", Anything, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingArgument_InActualToo(t *testing.T) { + + var args = Arguments([]interface{}{"string", Anything, true}) + var count int + _, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingOfTypeArgument(t *testing.T) { + + var args = Arguments([]interface{}{"string", AnythingOfType("int"), true}) + var count int + _, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 0, count) + +} + +func Test_Arguments_Diff_WithAnythingOfTypeArgument_Failing(t *testing.T) { + + var args = Arguments([]interface{}{"string", AnythingOfType("string"), true}) + var count int + var diff string + diff, count = args.Diff([]interface{}{"string", 123, true}) + + assert.Equal(t, 1, count) + assert.Contains(t, diff, `string != type int - %!s(int=123)`) + +} + +func Test_Arguments_Diff_WithArgMatcher(t *testing.T) { + matchFn := func(a int) bool { + return a == 123 + } + var args = Arguments([]interface{}{"string", MatchedBy(matchFn), true}) + + diff, count := args.Diff([]interface{}{"string", 124, true}) + assert.Equal(t, 1, count) + assert.Contains(t, diff, `%!s(int=124) not matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", false, true}) + assert.Equal(t, 1, count) + assert.Contains(t, diff, `%!s(bool=false) not matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", 123, false}) + assert.Contains(t, diff, `%!s(int=123) matched by func(int) bool`) + + diff, count = args.Diff([]interface{}{"string", 123, true}) + assert.Equal(t, 0, count) + assert.Contains(t, diff, `No differences.`) +} + +func Test_Arguments_Assert(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + + assert.True(t, args.Assert(t, "string", 123, true)) + +} + +func Test_Arguments_String_Representation(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, `string,int,bool`, args.String()) + +} + +func Test_Arguments_String(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, "string", args.String(0)) + +} + +func Test_Arguments_Error(t *testing.T) { + + var err = errors.New("An Error") + var args = Arguments([]interface{}{"string", 123, true, err}) + assert.Equal(t, err, args.Error(3)) + +} + +func Test_Arguments_Error_Nil(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true, nil}) + assert.Equal(t, nil, args.Error(3)) + +} + +func Test_Arguments_Int(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, 123, args.Int(1)) + +} + +func Test_Arguments_Bool(t *testing.T) { + + var args = Arguments([]interface{}{"string", 123, true}) + assert.Equal(t, true, args.Bool(2)) + +} diff --git a/vendor/github.com/stretchr/testify/package_test.go b/vendor/github.com/stretchr/testify/package_test.go new file mode 100644 index 000000000..7ac5d6d8e --- /dev/null +++ b/vendor/github.com/stretchr/testify/package_test.go @@ -0,0 +1,12 @@ +package testify + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestImports(t *testing.T) { + if assert.Equal(t, 1, 1) != true { + t.Error("Something is wrong.") + } +} diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go new file mode 100644 index 000000000..169de3922 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/doc.go @@ -0,0 +1,28 @@ +// Package require implements the same assertions as the `assert` package but +// stops test execution when a test fails. +// +// Example Usage +// +// The following is a complete example using require in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/require" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// require.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// Assertions +// +// The `require` package have same global functions as in the `assert` package, +// but instead of returning a boolean result they call `t.FailNow()`. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package require diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements.go b/vendor/github.com/stretchr/testify/require/forward_requirements.go new file mode 100644 index 000000000..d3c2ab9bc --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/forward_requirements.go @@ -0,0 +1,16 @@ +package require + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate go run ../_codegen/main.go -output-package=require -template=require_forward.go.tmpl diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements_test.go b/vendor/github.com/stretchr/testify/require/forward_requirements_test.go new file mode 100644 index 000000000..b120ae3b8 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/forward_requirements_test.go @@ -0,0 +1,385 @@ +package require + +import ( + "errors" + "testing" + "time" +) + +func TestImplementsWrapper(t *testing.T) { + require := New(t) + + require.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Implements((*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestIsTypeWrapper(t *testing.T) { + require := New(t) + require.IsType(new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.IsType(new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualWrapper(t *testing.T) { + require := New(t) + require.Equal(1, 1) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Equal(1, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEqualWrapper(t *testing.T) { + require := New(t) + require.NotEqual(1, 2) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotEqual(2, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestExactlyWrapper(t *testing.T) { + require := New(t) + + a := float32(1) + b := float32(1) + c := float64(1) + + require.Exactly(a, b) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Exactly(a, c) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotNilWrapper(t *testing.T) { + require := New(t) + require.NotNil(t, new(AssertionTesterConformingObject)) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotNil(nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNilWrapper(t *testing.T) { + require := New(t) + require.Nil(nil) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Nil(new(AssertionTesterConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestTrueWrapper(t *testing.T) { + require := New(t) + require.True(true) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.True(false) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestFalseWrapper(t *testing.T) { + require := New(t) + require.False(false) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.False(true) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestContainsWrapper(t *testing.T) { + require := New(t) + require.Contains("Hello World", "Hello") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Contains("Hello World", "Salut") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotContainsWrapper(t *testing.T) { + require := New(t) + require.NotContains("Hello World", "Hello!") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotContains("Hello World", "Hello") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestPanicsWrapper(t *testing.T) { + require := New(t) + require.Panics(func() { + panic("Panic!") + }) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Panics(func() {}) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotPanicsWrapper(t *testing.T) { + require := New(t) + require.NotPanics(func() {}) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotPanics(func() { + panic("Panic!") + }) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNoErrorWrapper(t *testing.T) { + require := New(t) + require.NoError(nil) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NoError(errors.New("some error")) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestErrorWrapper(t *testing.T) { + require := New(t) + require.Error(errors.New("some error")) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Error(nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualErrorWrapper(t *testing.T) { + require := New(t) + require.EqualError(errors.New("some error"), "some error") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.EqualError(errors.New("some error"), "Not some error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEmptyWrapper(t *testing.T) { + require := New(t) + require.Empty("") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Empty("x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEmptyWrapper(t *testing.T) { + require := New(t) + require.NotEmpty("x") + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotEmpty("") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestWithinDurationWrapper(t *testing.T) { + require := New(t) + a := time.Now() + b := a.Add(10 * time.Second) + + require.WithinDuration(a, b, 15*time.Second) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.WithinDuration(a, b, 5*time.Second) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestInDeltaWrapper(t *testing.T) { + require := New(t) + require.InDelta(1.001, 1, 0.01) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.InDelta(1, 2, 0.5) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestZeroWrapper(t *testing.T) { + require := New(t) + require.Zero(0) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.Zero(1) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotZeroWrapper(t *testing.T) { + require := New(t) + require.NotZero(1) + + mockT := new(MockT) + mockRequire := New(mockT) + mockRequire.NotZero(0) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_EqualSONString(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_EquivalentButNotEqual(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_HashOfArraysAndHashes(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq("{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", + "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_Array(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEqWrapper_HashAndArrayNotEquivalent(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_HashesNotEquivalent(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ActualIsNotJSON(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`{"foo": "bar"}`, "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ExpectedIsNotJSON(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq("Not JSON", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ExpectedAndActualNotJSON(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq("Not JSON", "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEqWrapper_ArraysOfDifferentOrder(t *testing.T) { + mockT := new(MockT) + mockRequire := New(mockT) + + mockRequire.JSONEq(`["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go new file mode 100644 index 000000000..1bcfcb0d9 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -0,0 +1,464 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND +*/ + +package require + +import ( + + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) { + if !assert.Condition(t, comp, msgAndArgs...) { + t.FailNow() + } +} + + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'") +// assert.Contains(t, ["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'") +// assert.Contains(t, {"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'") +// +// Returns whether the assertion was successful (true) or not (false). +func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if !assert.Contains(t, s, contains, msgAndArgs...) { + t.FailNow() + } +} + + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +// +// Returns whether the assertion was successful (true) or not (false). +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if !assert.Empty(t, object, msgAndArgs...) { + t.FailNow() + } +} + + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123, "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if !assert.Equal(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { + if !assert.EqualError(t, theError, errString, msgAndArgs...) { + t.FailNow() + } +} + + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123), "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if !assert.EqualValues(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func Error(t TestingT, err error, msgAndArgs ...interface{}) { + if !assert.Error(t, err, msgAndArgs...) { + t.FailNow() + } +} + + +// Exactly asserts that two objects are equal is value and type. +// +// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if !assert.Exactly(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if !assert.Fail(t, failureMessage, msgAndArgs...) { + t.FailNow() + } +} + + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if !assert.FailNow(t, failureMessage, msgAndArgs...) { + t.FailNow() + } +} + + +// False asserts that the specified value is false. +// +// assert.False(t, myBool, "myBool should be false") +// +// Returns whether the assertion was successful (true) or not (false). +func False(t TestingT, value bool, msgAndArgs ...interface{}) { + if !assert.False(t, value, msgAndArgs...) { + t.FailNow() + } +} + + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) { + if !assert.HTTPBodyContains(t, handler, method, url, values, str) { + t.FailNow() + } +} + + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) { + if !assert.HTTPBodyNotContains(t, handler, method, url, values, str) { + t.FailNow() + } +} + + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) { + if !assert.HTTPError(t, handler, method, url, values) { + t.FailNow() + } +} + + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) { + if !assert.HTTPRedirect(t, handler, method, url, values) { + t.FailNow() + } +} + + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) { + if !assert.HTTPSuccess(t, handler, method, url, values) { + t.FailNow() + } +} + + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject") +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if !assert.Implements(t, interfaceObject, object, msgAndArgs...) { + t.FailNow() + } +} + + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) +// +// Returns whether the assertion was successful (true) or not (false). +func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if !assert.InDelta(t, expected, actual, delta, msgAndArgs...) { + t.FailNow() + } +} + + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if !assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) { + t.FailNow() + } +} + + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +// +// Returns whether the assertion was successful (true) or not (false). +func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if !assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) { + t.FailNow() + } +} + + +// InEpsilonSlice is the same as InEpsilon, except it compares two slices. +func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if !assert.InEpsilonSlice(t, expected, actual, delta, msgAndArgs...) { + t.FailNow() + } +} + + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if !assert.IsType(t, expectedType, object, msgAndArgs...) { + t.FailNow() + } +} + + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// +// Returns whether the assertion was successful (true) or not (false). +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if !assert.JSONEq(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3, "The size of slice is not 3") +// +// Returns whether the assertion was successful (true) or not (false). +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { + if !assert.Len(t, object, length, msgAndArgs...) { + t.FailNow() + } +} + + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err, "err should be nothing") +// +// Returns whether the assertion was successful (true) or not (false). +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if !assert.Nil(t, object, msgAndArgs...) { + t.FailNow() + } +} + + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, actualObj, expectedObj) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NoError(t TestingT, err error, msgAndArgs ...interface{}) { + if !assert.NoError(t, err, msgAndArgs...) { + t.FailNow() + } +} + + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") +// assert.NotContains(t, ["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'") +// assert.NotContains(t, {"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'") +// +// Returns whether the assertion was successful (true) or not (false). +func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if !assert.NotContains(t, s, contains, msgAndArgs...) { + t.FailNow() + } +} + + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if !assert.NotEmpty(t, object, msgAndArgs...) { + t.FailNow() + } +} + + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if !assert.NotEqual(t, expected, actual, msgAndArgs...) { + t.FailNow() + } +} + + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err, "err should be something") +// +// Returns whether the assertion was successful (true) or not (false). +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if !assert.NotNil(t, object, msgAndArgs...) { + t.FailNow() + } +} + + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ +// RemainCalm() +// }, "Calling RemainCalm() should NOT panic") +// +// Returns whether the assertion was successful (true) or not (false). +func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if !assert.NotPanics(t, f, msgAndArgs...) { + t.FailNow() + } +} + + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if !assert.NotRegexp(t, rx, str, msgAndArgs...) { + t.FailNow() + } +} + + +// NotZero asserts that i is not the zero value for its type and returns the truth. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if !assert.NotZero(t, i, msgAndArgs...) { + t.FailNow() + } +} + + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ +// GoCrazy() +// }, "Calling GoCrazy() should panic") +// +// Returns whether the assertion was successful (true) or not (false). +func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if !assert.Panics(t, f, msgAndArgs...) { + t.FailNow() + } +} + + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if !assert.Regexp(t, rx, str, msgAndArgs...) { + t.FailNow() + } +} + + +// True asserts that the specified value is true. +// +// assert.True(t, myBool, "myBool should be true") +// +// Returns whether the assertion was successful (true) or not (false). +func True(t TestingT, value bool, msgAndArgs ...interface{}) { + if !assert.True(t, value, msgAndArgs...) { + t.FailNow() + } +} + + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") +// +// Returns whether the assertion was successful (true) or not (false). +func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if !assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) { + t.FailNow() + } +} + + +// Zero asserts that i is the zero value for its type and returns the truth. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if !assert.Zero(t, i, msgAndArgs...) { + t.FailNow() + } +} diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl new file mode 100644 index 000000000..ab1b1e9fd --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go.tmpl @@ -0,0 +1,6 @@ +{{.Comment}} +func {{.DocInfo.Name}}(t TestingT, {{.Params}}) { + if !assert.{{.DocInfo.Name}}(t, {{.ForwardedParams}}) { + t.FailNow() + } +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go new file mode 100644 index 000000000..58324f105 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -0,0 +1,388 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND +*/ + +package require + +import ( + + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) { + Condition(a.t, comp, msgAndArgs...) +} + + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World", "But 'Hello World' does contain 'World'") +// a.Contains(["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'") +// a.Contains({"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + Contains(a.t, s, contains, msgAndArgs...) +} + + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { + Empty(a.t, object, msgAndArgs...) +} + + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123, "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + Equal(a.t, expected, actual, msgAndArgs...) +} + + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) { + EqualError(a.t, theError, errString, msgAndArgs...) +} + + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123), "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + EqualValues(a.t, expected, actual, msgAndArgs...) +} + + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) { + Error(a.t, err, msgAndArgs...) +} + + +// Exactly asserts that two objects are equal is value and type. +// +// a.Exactly(int32(123), int64(123), "123 and 123 should NOT be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + Exactly(a.t, expected, actual, msgAndArgs...) +} + + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) { + Fail(a.t, failureMessage, msgAndArgs...) +} + + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) { + FailNow(a.t, failureMessage, msgAndArgs...) +} + + +// False asserts that the specified value is false. +// +// a.False(myBool, "myBool should be false") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) { + False(a.t, value, msgAndArgs...) +} + + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) { + HTTPBodyContains(a.t, handler, method, url, values, str) +} + + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) { + HTTPBodyNotContains(a.t, handler, method, url, values, str) +} + + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values) { + HTTPError(a.t, handler, method, url, values) +} + + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values) { + HTTPRedirect(a.t, handler, method, url, values) +} + + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values) { + HTTPSuccess(a.t, handler, method, url, values) +} + + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject), "MyObject") +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + Implements(a.t, interfaceObject, object, msgAndArgs...) +} + + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, (22 / 7.0), 0.01) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + + +// InEpsilonSlice is the same as InEpsilon, except it compares two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + InEpsilonSlice(a.t, expected, actual, delta, msgAndArgs...) +} + + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + IsType(a.t, expectedType, object, msgAndArgs...) +} + + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) { + JSONEq(a.t, expected, actual, msgAndArgs...) +} + + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3, "The size of slice is not 3") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) { + Len(a.t, object, length, msgAndArgs...) +} + + +// Nil asserts that the specified object is nil. +// +// a.Nil(err, "err should be nothing") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) { + Nil(a.t, object, msgAndArgs...) +} + + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, actualObj, expectedObj) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { + NoError(a.t, err, msgAndArgs...) +} + + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") +// a.NotContains(["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'") +// a.NotContains({"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + NotContains(a.t, s, contains, msgAndArgs...) +} + + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { + NotEmpty(a.t, object, msgAndArgs...) +} + + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2, "two objects shouldn't be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + NotEqual(a.t, expected, actual, msgAndArgs...) +} + + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err, "err should be something") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) { + NotNil(a.t, object, msgAndArgs...) +} + + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ +// RemainCalm() +// }, "Calling RemainCalm() should NOT panic") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + NotPanics(a.t, f, msgAndArgs...) +} + + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + NotRegexp(a.t, rx, str, msgAndArgs...) +} + + +// NotZero asserts that i is not the zero value for its type and returns the truth. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) { + NotZero(a.t, i, msgAndArgs...) +} + + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ +// GoCrazy() +// }, "Calling GoCrazy() should panic") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + Panics(a.t, f, msgAndArgs...) +} + + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + Regexp(a.t, rx, str, msgAndArgs...) +} + + +// True asserts that the specified value is true. +// +// a.True(myBool, "myBool should be true") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) { + True(a.t, value, msgAndArgs...) +} + + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + + +// Zero asserts that i is the zero value for its type and returns the truth. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) { + Zero(a.t, i, msgAndArgs...) +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl new file mode 100644 index 000000000..b93569e0a --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl @@ -0,0 +1,4 @@ +{{.CommentWithoutT "a"}} +func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) { + {{.DocInfo.Name}}(a.t, {{.ForwardedParams}}) +} diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go new file mode 100644 index 000000000..41147562d --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -0,0 +1,9 @@ +package require + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) + FailNow() +} + +//go:generate go run ../_codegen/main.go -output-package=require -template=require.go.tmpl diff --git a/vendor/github.com/stretchr/testify/require/requirements_test.go b/vendor/github.com/stretchr/testify/require/requirements_test.go new file mode 100644 index 000000000..d2ccc99c9 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/requirements_test.go @@ -0,0 +1,369 @@ +package require + +import ( + "errors" + "testing" + "time" +) + +// AssertionTesterInterface defines an interface to be used for testing assertion methods +type AssertionTesterInterface interface { + TestMethod() +} + +// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface +type AssertionTesterConformingObject struct { +} + +func (a *AssertionTesterConformingObject) TestMethod() { +} + +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +type AssertionTesterNonConformingObject struct { +} + +type MockT struct { + Failed bool +} + +func (t *MockT) FailNow() { + t.Failed = true +} + +func (t *MockT) Errorf(format string, args ...interface{}) { + _, _ = format, args +} + +func TestImplements(t *testing.T) { + + Implements(t, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestIsType(t *testing.T) { + + IsType(t, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) + + mockT := new(MockT) + IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqual(t *testing.T) { + + Equal(t, 1, 1) + + mockT := new(MockT) + Equal(mockT, 1, 2) + if !mockT.Failed { + t.Error("Check should fail") + } + +} + +func TestNotEqual(t *testing.T) { + + NotEqual(t, 1, 2) + mockT := new(MockT) + NotEqual(mockT, 2, 2) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestExactly(t *testing.T) { + + a := float32(1) + b := float32(1) + c := float64(1) + + Exactly(t, a, b) + + mockT := new(MockT) + Exactly(mockT, a, c) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotNil(t *testing.T) { + + NotNil(t, new(AssertionTesterConformingObject)) + + mockT := new(MockT) + NotNil(mockT, nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNil(t *testing.T) { + + Nil(t, nil) + + mockT := new(MockT) + Nil(mockT, new(AssertionTesterConformingObject)) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestTrue(t *testing.T) { + + True(t, true) + + mockT := new(MockT) + True(mockT, false) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestFalse(t *testing.T) { + + False(t, false) + + mockT := new(MockT) + False(mockT, true) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestContains(t *testing.T) { + + Contains(t, "Hello World", "Hello") + + mockT := new(MockT) + Contains(mockT, "Hello World", "Salut") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotContains(t *testing.T) { + + NotContains(t, "Hello World", "Hello!") + + mockT := new(MockT) + NotContains(mockT, "Hello World", "Hello") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestPanics(t *testing.T) { + + Panics(t, func() { + panic("Panic!") + }) + + mockT := new(MockT) + Panics(mockT, func() {}) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotPanics(t *testing.T) { + + NotPanics(t, func() {}) + + mockT := new(MockT) + NotPanics(mockT, func() { + panic("Panic!") + }) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNoError(t *testing.T) { + + NoError(t, nil) + + mockT := new(MockT) + NoError(mockT, errors.New("some error")) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestError(t *testing.T) { + + Error(t, errors.New("some error")) + + mockT := new(MockT) + Error(mockT, nil) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEqualError(t *testing.T) { + + EqualError(t, errors.New("some error"), "some error") + + mockT := new(MockT) + EqualError(mockT, errors.New("some error"), "Not some error") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestEmpty(t *testing.T) { + + Empty(t, "") + + mockT := new(MockT) + Empty(mockT, "x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotEmpty(t *testing.T) { + + NotEmpty(t, "x") + + mockT := new(MockT) + NotEmpty(mockT, "") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestWithinDuration(t *testing.T) { + + a := time.Now() + b := a.Add(10 * time.Second) + + WithinDuration(t, a, b, 15*time.Second) + + mockT := new(MockT) + WithinDuration(mockT, a, b, 5*time.Second) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestInDelta(t *testing.T) { + + InDelta(t, 1.001, 1, 0.01) + + mockT := new(MockT) + InDelta(mockT, 1, 2, 0.5) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestZero(t *testing.T) { + + Zero(t, "") + + mockT := new(MockT) + Zero(mockT, "x") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestNotZero(t *testing.T) { + + NotZero(t, "x") + + mockT := new(MockT) + NotZero(mockT, "") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_EqualSONString(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"hello": "world", "foo": "bar"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_EquivalentButNotEqual(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_HashOfArraysAndHashes(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, "{\r\n\t\"numeric\": 1.5,\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]],\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\"\r\n}", + "{\r\n\t\"numeric\": 1.5,\r\n\t\"hash\": {\"nested\": \"hash\", \"nested_slice\": [\"this\", \"is\", \"nested\"]},\r\n\t\"string\": \"foo\",\r\n\t\"array\": [{\"foo\": \"bar\"}, 1, \"string\", [\"nested\", \"array\", 5.5]]\r\n}") + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_Array(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `["foo", {"nested": "hash", "hello": "world"}]`) + if mockT.Failed { + t.Error("Check should pass") + } +} + +func TestJSONEq_HashAndArrayNotEquivalent(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `{"foo": "bar", {"nested": "hash", "hello": "world"}}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_HashesNotEquivalent(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `{"foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ActualIsNotJSON(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `{"foo": "bar"}`, "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ExpectedIsNotJSON(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, "Not JSON", `{"foo": "bar", "hello": "world"}`) + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ExpectedAndActualNotJSON(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, "Not JSON", "Not JSON") + if !mockT.Failed { + t.Error("Check should fail") + } +} + +func TestJSONEq_ArraysOfDifferentOrder(t *testing.T) { + mockT := new(MockT) + JSONEq(mockT, `["foo", {"hello": "world", "nested": "hash"}]`, `[{ "hello": "world", "nested": "hash"}, "foo"]`) + if !mockT.Failed { + t.Error("Check should fail") + } +} diff --git a/vendor/github.com/stretchr/testify/suite/doc.go b/vendor/github.com/stretchr/testify/suite/doc.go new file mode 100644 index 000000000..f91a245d3 --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/doc.go @@ -0,0 +1,65 @@ +// Package suite contains logic for creating testing suite structs +// and running the methods on those structs as tests. The most useful +// piece of this package is that you can create setup/teardown methods +// on your testing suites, which will run before/after the whole suite +// or individual tests (depending on which interface(s) you +// implement). +// +// A testing suite is usually built by first extending the built-in +// suite functionality from suite.Suite in testify. Alternatively, +// you could reproduce that logic on your own if you wanted (you +// just need to implement the TestingSuite interface from +// suite/interfaces.go). +// +// After that, you can implement any of the interfaces in +// suite/interfaces.go to add setup/teardown functionality to your +// suite, and add any methods that start with "Test" to add tests. +// Methods that do not match any suite interfaces and do not begin +// with "Test" will not be run by testify, and can safely be used as +// helper methods. +// +// Once you've built your testing suite, you need to run the suite +// (using suite.Run from testify) inside any function that matches the +// identity that "go test" is already looking for (i.e. +// func(*testing.T)). +// +// Regular expression to select test suites specified command-line +// argument "-run". Regular expression to select the methods +// of test suites specified command-line argument "-m". +// Suite object has assertion methods. +// +// A crude example: +// // Basic imports +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/suite" +// ) +// +// // Define the suite, and absorb the built-in basic suite +// // functionality from testify - including a T() method which +// // returns the current testing context +// type ExampleTestSuite struct { +// suite.Suite +// VariableThatShouldStartAtFive int +// } +// +// // Make sure that VariableThatShouldStartAtFive is set to five +// // before each test +// func (suite *ExampleTestSuite) SetupTest() { +// suite.VariableThatShouldStartAtFive = 5 +// } +// +// // All methods that begin with "Test" are run as tests within a +// // suite. +// func (suite *ExampleTestSuite) TestExample() { +// assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +// suite.Equal(5, suite.VariableThatShouldStartAtFive) +// } +// +// // In order for 'go test' to run this suite, we need to create +// // a normal test function and pass our suite to suite.Run +// func TestExampleTestSuite(t *testing.T) { +// suite.Run(t, new(ExampleTestSuite)) +// } +package suite diff --git a/vendor/github.com/stretchr/testify/suite/interfaces.go b/vendor/github.com/stretchr/testify/suite/interfaces.go new file mode 100644 index 000000000..20969472c --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/interfaces.go @@ -0,0 +1,34 @@ +package suite + +import "testing" + +// TestingSuite can store and return the current *testing.T context +// generated by 'go test'. +type TestingSuite interface { + T() *testing.T + SetT(*testing.T) +} + +// SetupAllSuite has a SetupSuite method, which will run before the +// tests in the suite are run. +type SetupAllSuite interface { + SetupSuite() +} + +// SetupTestSuite has a SetupTest method, which will run before each +// test in the suite. +type SetupTestSuite interface { + SetupTest() +} + +// TearDownAllSuite has a TearDownSuite method, which will run after +// all the tests in the suite have been run. +type TearDownAllSuite interface { + TearDownSuite() +} + +// TearDownTestSuite has a TearDownTest method, which will run after +// each test in the suite. +type TearDownTestSuite interface { + TearDownTest() +} diff --git a/vendor/github.com/stretchr/testify/suite/suite.go b/vendor/github.com/stretchr/testify/suite/suite.go new file mode 100644 index 000000000..db7413000 --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/suite.go @@ -0,0 +1,115 @@ +package suite + +import ( + "flag" + "fmt" + "os" + "reflect" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") + +// Suite is a basic testing suite with methods for storing and +// retrieving the current *testing.T context. +type Suite struct { + *assert.Assertions + require *require.Assertions + t *testing.T +} + +// T retrieves the current *testing.T context. +func (suite *Suite) T() *testing.T { + return suite.t +} + +// SetT sets the current *testing.T context. +func (suite *Suite) SetT(t *testing.T) { + suite.t = t + suite.Assertions = assert.New(t) + suite.require = require.New(t) +} + +// Require returns a require context for suite. +func (suite *Suite) Require() *require.Assertions { + if suite.require == nil { + suite.require = require.New(suite.T()) + } + return suite.require +} + +// Assert returns an assert context for suite. Normally, you can call +// `suite.NoError(expected, actual)`, but for situations where the embedded +// methods are overridden (for example, you might want to override +// assert.Assertions with require.Assertions), this method is provided so you +// can call `suite.Assert().NoError()`. +func (suite *Suite) Assert() *assert.Assertions { + if suite.Assertions == nil { + suite.Assertions = assert.New(suite.T()) + } + return suite.Assertions +} + +// Run takes a testing suite and runs all of the tests attached +// to it. +func Run(t *testing.T, suite TestingSuite) { + suite.SetT(t) + + if setupAllSuite, ok := suite.(SetupAllSuite); ok { + setupAllSuite.SetupSuite() + } + defer func() { + if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok { + tearDownAllSuite.TearDownSuite() + } + }() + + methodFinder := reflect.TypeOf(suite) + tests := []testing.InternalTest{} + for index := 0; index < methodFinder.NumMethod(); index++ { + method := methodFinder.Method(index) + ok, err := methodFilter(method.Name) + if err != nil { + fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) + os.Exit(1) + } + if ok { + test := testing.InternalTest{ + Name: method.Name, + F: func(t *testing.T) { + parentT := suite.T() + suite.SetT(t) + if setupTestSuite, ok := suite.(SetupTestSuite); ok { + setupTestSuite.SetupTest() + } + defer func() { + if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok { + tearDownTestSuite.TearDownTest() + } + suite.SetT(parentT) + }() + method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) + }, + } + tests = append(tests, test) + } + } + + if !testing.RunTests(func(_, _ string) (bool, error) { return true, nil }, + tests) { + t.Fail() + } +} + +// Filtering method according to set regular expression +// specified command-line argument -m +func methodFilter(name string) (bool, error) { + if ok, _ := regexp.MatchString("^Test", name); !ok { + return false, nil + } + return regexp.MatchString(*matchMethod, name) +} diff --git a/vendor/github.com/stretchr/testify/suite/suite_test.go b/vendor/github.com/stretchr/testify/suite/suite_test.go new file mode 100644 index 000000000..c7c4e88f7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/suite_test.go @@ -0,0 +1,239 @@ +package suite + +import ( + "errors" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// SuiteRequireTwice is intended to test the usage of suite.Require in two +// different tests +type SuiteRequireTwice struct{ Suite } + +// TestSuiteRequireTwice checks for regressions of issue #149 where +// suite.requirements was not initialised in suite.SetT() +// A regression would result on these tests panicking rather than failing. +func TestSuiteRequireTwice(t *testing.T) { + ok := testing.RunTests( + func(_, _ string) (bool, error) { return true, nil }, + []testing.InternalTest{{ + Name: "TestSuiteRequireTwice", + F: func(t *testing.T) { + suite := new(SuiteRequireTwice) + Run(t, suite) + }, + }}, + ) + assert.Equal(t, false, ok) +} + +func (s *SuiteRequireTwice) TestRequireOne() { + r := s.Require() + r.Equal(1, 2) +} + +func (s *SuiteRequireTwice) TestRequireTwo() { + r := s.Require() + r.Equal(1, 2) +} + +// This suite is intended to store values to make sure that only +// testing-suite-related methods are run. It's also a fully +// functional example of a testing suite, using setup/teardown methods +// and a helper method that is ignored by testify. To make this look +// more like a real world example, all tests in the suite perform some +// type of assertion. +type SuiteTester struct { + // Include our basic suite logic. + Suite + + // Keep counts of how many times each method is run. + SetupSuiteRunCount int + TearDownSuiteRunCount int + SetupTestRunCount int + TearDownTestRunCount int + TestOneRunCount int + TestTwoRunCount int + NonTestMethodRunCount int +} + +type SuiteSkipTester struct { + // Include our basic suite logic. + Suite + + // Keep counts of how many times each method is run. + SetupSuiteRunCount int + TearDownSuiteRunCount int +} + +// The SetupSuite method will be run by testify once, at the very +// start of the testing suite, before any tests are run. +func (suite *SuiteTester) SetupSuite() { + suite.SetupSuiteRunCount++ +} + +func (suite *SuiteSkipTester) SetupSuite() { + suite.SetupSuiteRunCount++ + suite.T().Skip() +} + +// The TearDownSuite method will be run by testify once, at the very +// end of the testing suite, after all tests have been run. +func (suite *SuiteTester) TearDownSuite() { + suite.TearDownSuiteRunCount++ +} + +func (suite *SuiteSkipTester) TearDownSuite() { + suite.TearDownSuiteRunCount++ +} + +// The SetupTest method will be run before every test in the suite. +func (suite *SuiteTester) SetupTest() { + suite.SetupTestRunCount++ +} + +// The TearDownTest method will be run after every test in the suite. +func (suite *SuiteTester) TearDownTest() { + suite.TearDownTestRunCount++ +} + +// Every method in a testing suite that begins with "Test" will be run +// as a test. TestOne is an example of a test. For the purposes of +// this example, we've included assertions in the tests, since most +// tests will issue assertions. +func (suite *SuiteTester) TestOne() { + beforeCount := suite.TestOneRunCount + suite.TestOneRunCount++ + assert.Equal(suite.T(), suite.TestOneRunCount, beforeCount+1) + suite.Equal(suite.TestOneRunCount, beforeCount+1) +} + +// TestTwo is another example of a test. +func (suite *SuiteTester) TestTwo() { + beforeCount := suite.TestTwoRunCount + suite.TestTwoRunCount++ + assert.NotEqual(suite.T(), suite.TestTwoRunCount, beforeCount) + suite.NotEqual(suite.TestTwoRunCount, beforeCount) +} + +func (suite *SuiteTester) TestSkip() { + suite.T().Skip() +} + +// NonTestMethod does not begin with "Test", so it will not be run by +// testify as a test in the suite. This is useful for creating helper +// methods for your tests. +func (suite *SuiteTester) NonTestMethod() { + suite.NonTestMethodRunCount++ +} + +// TestRunSuite will be run by the 'go test' command, so within it, we +// can run our suite using the Run(*testing.T, TestingSuite) function. +func TestRunSuite(t *testing.T) { + suiteTester := new(SuiteTester) + Run(t, suiteTester) + + // Normally, the test would end here. The following are simply + // some assertions to ensure that the Run function is working as + // intended - they are not part of the example. + + // The suite was only run once, so the SetupSuite and TearDownSuite + // methods should have each been run only once. + assert.Equal(t, suiteTester.SetupSuiteRunCount, 1) + assert.Equal(t, suiteTester.TearDownSuiteRunCount, 1) + + // There are three test methods (TestOne, TestTwo, and TestSkip), so + // the SetupTest and TearDownTest methods (which should be run once for + // each test) should have been run three times. + assert.Equal(t, suiteTester.SetupTestRunCount, 3) + assert.Equal(t, suiteTester.TearDownTestRunCount, 3) + + // Each test should have been run once. + assert.Equal(t, suiteTester.TestOneRunCount, 1) + assert.Equal(t, suiteTester.TestTwoRunCount, 1) + + // Methods that don't match the test method identifier shouldn't + // have been run at all. + assert.Equal(t, suiteTester.NonTestMethodRunCount, 0) + + suiteSkipTester := new(SuiteSkipTester) + Run(t, suiteSkipTester) + + // The suite was only run once, so the SetupSuite and TearDownSuite + // methods should have each been run only once, even though SetupSuite + // called Skip() + assert.Equal(t, suiteSkipTester.SetupSuiteRunCount, 1) + assert.Equal(t, suiteSkipTester.TearDownSuiteRunCount, 1) + +} + +func TestSuiteGetters(t *testing.T) { + suite := new(SuiteTester) + suite.SetT(t) + assert.NotNil(t, suite.Assert()) + assert.Equal(t, suite.Assertions, suite.Assert()) + assert.NotNil(t, suite.Require()) + assert.Equal(t, suite.require, suite.Require()) +} + +type SuiteLoggingTester struct { + Suite +} + +func (s *SuiteLoggingTester) TestLoggingPass() { + s.T().Log("TESTLOGPASS") +} + +func (s *SuiteLoggingTester) TestLoggingFail() { + s.T().Log("TESTLOGFAIL") + assert.NotNil(s.T(), nil) // expected to fail +} + +type StdoutCapture struct { + oldStdout *os.File + readPipe *os.File +} + +func (sc *StdoutCapture) StartCapture() { + sc.oldStdout = os.Stdout + sc.readPipe, os.Stdout, _ = os.Pipe() +} + +func (sc *StdoutCapture) StopCapture() (string, error) { + if sc.oldStdout == nil || sc.readPipe == nil { + return "", errors.New("StartCapture not called before StopCapture") + } + os.Stdout.Close() + os.Stdout = sc.oldStdout + bytes, err := ioutil.ReadAll(sc.readPipe) + if err != nil { + return "", err + } + return string(bytes), nil +} + +func TestSuiteLogging(t *testing.T) { + testT := testing.T{} + + suiteLoggingTester := new(SuiteLoggingTester) + + capture := StdoutCapture{} + capture.StartCapture() + Run(&testT, suiteLoggingTester) + output, err := capture.StopCapture() + + assert.Nil(t, err, "Got an error trying to capture stdout!") + + // Failed tests' output is always printed + assert.Contains(t, output, "TESTLOGFAIL") + + if testing.Verbose() { + // In verbose mode, output from successful tests is also printed + assert.Contains(t, output, "TESTLOGPASS") + } else { + assert.NotContains(t, output, "TESTLOGPASS") + } +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/LICENSE new file mode 100644 index 000000000..bb6733231 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2013 Dave Collins <dave@davec.name> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypass.go new file mode 100644 index 000000000..d42a0bc4a --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -0,0 +1,152 @@ +// Copyright (c) 2015 Dave Collins <dave@davec.name> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build !js,!appengine,!safe,!disableunsafe + +package spew + +import ( + "reflect" + "unsafe" +) + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = false + + // ptrSize is the size of a pointer on the current arch. + ptrSize = unsafe.Sizeof((*byte)(nil)) +) + +var ( + // offsetPtr, offsetScalar, and offsetFlag are the offsets for the + // internal reflect.Value fields. These values are valid before golang + // commit ecccf07e7f9d which changed the format. The are also valid + // after commit 82f48826c6c7 which changed the format again to mirror + // the original format. Code in the init function updates these offsets + // as necessary. + offsetPtr = uintptr(ptrSize) + offsetScalar = uintptr(0) + offsetFlag = uintptr(ptrSize * 2) + + // flagKindWidth and flagKindShift indicate various bits that the + // reflect package uses internally to track kind information. + // + // flagRO indicates whether or not the value field of a reflect.Value is + // read-only. + // + // flagIndir indicates whether the value field of a reflect.Value is + // the actual data or a pointer to the data. + // + // These values are valid before golang commit 90a7c3c86944 which + // changed their positions. Code in the init function updates these + // flags as necessary. + flagKindWidth = uintptr(5) + flagKindShift = uintptr(flagKindWidth - 1) + flagRO = uintptr(1 << 0) + flagIndir = uintptr(1 << 1) +) + +func init() { + // Older versions of reflect.Value stored small integers directly in the + // ptr field (which is named val in the older versions). Versions + // between commits ecccf07e7f9d and 82f48826c6c7 added a new field named + // scalar for this purpose which unfortunately came before the flag + // field, so the offset of the flag field is different for those + // versions. + // + // This code constructs a new reflect.Value from a known small integer + // and checks if the size of the reflect.Value struct indicates it has + // the scalar field. When it does, the offsets are updated accordingly. + vv := reflect.ValueOf(0xf00) + if unsafe.Sizeof(vv) == (ptrSize * 4) { + offsetScalar = ptrSize * 2 + offsetFlag = ptrSize * 3 + } + + // Commit 90a7c3c86944 changed the flag positions such that the low + // order bits are the kind. This code extracts the kind from the flags + // field and ensures it's the correct type. When it's not, the flag + // order has been changed to the newer format, so the flags are updated + // accordingly. + upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag) + upfv := *(*uintptr)(upf) + flagKindMask := uintptr((1<<flagKindWidth - 1) << flagKindShift) + if (upfv&flagKindMask)>>flagKindShift != uintptr(reflect.Int) { + flagKindShift = 0 + flagRO = 1 << 5 + flagIndir = 1 << 6 + + // Commit adf9b30e5594 modified the flags to separate the + // flagRO flag into two bits which specifies whether or not the + // field is embedded. This causes flagIndir to move over a bit + // and means that flagRO is the combination of either of the + // original flagRO bit and the new bit. + // + // This code detects the change by extracting what used to be + // the indirect bit to ensure it's set. When it's not, the flag + // order has been changed to the newer format, so the flags are + // updated accordingly. + if upfv&flagIndir == 0 { + flagRO = 3 << 5 + flagIndir = 1 << 7 + } + } +} + +// unsafeReflectValue converts the passed reflect.Value into a one that bypasses +// the typical safety restrictions preventing access to unaddressable and +// unexported data. It works by digging the raw pointer to the underlying +// value out of the protected value and generating a new unprotected (unsafe) +// reflect.Value to it. +// +// This allows us to check for implementations of the Stringer and error +// interfaces to be used for pretty printing ordinarily unaddressable and +// inaccessible values such as unexported struct fields. +func unsafeReflectValue(v reflect.Value) (rv reflect.Value) { + indirects := 1 + vt := v.Type() + upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr) + rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag)) + if rvf&flagIndir != 0 { + vt = reflect.PtrTo(v.Type()) + indirects++ + } else if offsetScalar != 0 { + // The value is in the scalar field when it's not one of the + // reference types. + switch vt.Kind() { + case reflect.Uintptr: + case reflect.Chan: + case reflect.Func: + case reflect.Map: + case reflect.Ptr: + case reflect.UnsafePointer: + default: + upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + + offsetScalar) + } + } + + pv := reflect.NewAt(vt, upv) + rv = pv + for i := 0; i < indirects; i++ { + rv = rv.Elem() + } + return rv +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go new file mode 100644 index 000000000..e47a4e795 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015 Dave Collins <dave@davec.name> +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is running on Google App Engine, compiled by GopherJS, or +// "-tags safe" is added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build js appengine safe disableunsafe + +package spew + +import "reflect" + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = true +) + +// unsafeReflectValue typically converts the passed reflect.Value into a one +// that bypasses the typical safety restrictions preventing access to +// unaddressable and unexported data. However, doing this relies on access to +// the unsafe package. This is a stub version which simply returns the passed +// reflect.Value when the unsafe package is not available. +func unsafeReflectValue(v reflect.Value) reflect.Value { + return v +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/common.go new file mode 100644 index 000000000..14f02dc15 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/common.go @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "reflect" + "sort" + "strconv" +) + +// Some constants in the form of bytes to avoid string overhead. This mirrors +// the technique used in the fmt package. +var ( + panicBytes = []byte("(PANIC=") + plusBytes = []byte("+") + iBytes = []byte("i") + trueBytes = []byte("true") + falseBytes = []byte("false") + interfaceBytes = []byte("(interface {})") + commaNewlineBytes = []byte(",\n") + newlineBytes = []byte("\n") + openBraceBytes = []byte("{") + openBraceNewlineBytes = []byte("{\n") + closeBraceBytes = []byte("}") + asteriskBytes = []byte("*") + colonBytes = []byte(":") + colonSpaceBytes = []byte(": ") + openParenBytes = []byte("(") + closeParenBytes = []byte(")") + spaceBytes = []byte(" ") + pointerChainBytes = []byte("->") + nilAngleBytes = []byte("<nil>") + maxNewlineBytes = []byte("<max depth reached>\n") + maxShortBytes = []byte("<max>") + circularBytes = []byte("<already shown>") + circularShortBytes = []byte("<shown>") + invalidAngleBytes = []byte("<invalid>") + openBracketBytes = []byte("[") + closeBracketBytes = []byte("]") + percentBytes = []byte("%") + precisionBytes = []byte(".") + openAngleBytes = []byte("<") + closeAngleBytes = []byte(">") + openMapBytes = []byte("map[") + closeMapBytes = []byte("]") + lenEqualsBytes = []byte("len=") + capEqualsBytes = []byte("cap=") +) + +// hexDigits is used to map a decimal value to a hex digit. +var hexDigits = "0123456789abcdef" + +// catchPanic handles any panics that might occur during the handleMethods +// calls. +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + w.Write(panicBytes) + fmt.Fprintf(w, "%v", err) + w.Write(closeParenBytes) + } +} + +// handleMethods attempts to call the Error and String methods on the underlying +// type the passed reflect.Value represents and outputes the result to Writer w. +// +// It handles panics in any called methods by catching and displaying the error +// as the formatted value. +func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { + // We need an interface to check if the type implements the error or + // Stringer interface. However, the reflect package won't give us an + // interface on certain things like unexported struct fields in order + // to enforce visibility rules. We use unsafe, when it's available, + // to bypass these restrictions since this package does not mutate the + // values. + if !v.CanInterface() { + if UnsafeDisabled { + return false + } + + v = unsafeReflectValue(v) + } + + // Choose whether or not to do error and Stringer interface lookups against + // the base type or a pointer to the base type depending on settings. + // Technically calling one of these methods with a pointer receiver can + // mutate the value, however, types which choose to satisify an error or + // Stringer interface with a pointer receiver should not be mutating their + // state inside these interface methods. + if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { + v = unsafeReflectValue(v) + } + if v.CanAddr() { + v = v.Addr() + } + + // Is it an error or Stringer? + switch iface := v.Interface().(type) { + case error: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.Error())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + + w.Write([]byte(iface.Error())) + return true + + case fmt.Stringer: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.String())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + w.Write([]byte(iface.String())) + return true + } + return false +} + +// printBool outputs a boolean value as true or false to Writer w. +func printBool(w io.Writer, val bool) { + if val { + w.Write(trueBytes) + } else { + w.Write(falseBytes) + } +} + +// printInt outputs a signed integer value to Writer w. +func printInt(w io.Writer, val int64, base int) { + w.Write([]byte(strconv.FormatInt(val, base))) +} + +// printUint outputs an unsigned integer value to Writer w. +func printUint(w io.Writer, val uint64, base int) { + w.Write([]byte(strconv.FormatUint(val, base))) +} + +// printFloat outputs a floating point value using the specified precision, +// which is expected to be 32 or 64bit, to Writer w. +func printFloat(w io.Writer, val float64, precision int) { + w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) +} + +// printComplex outputs a complex value using the specified float precision +// for the real and imaginary parts to Writer w. +func printComplex(w io.Writer, c complex128, floatPrecision int) { + r := real(c) + w.Write(openParenBytes) + w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) + i := imag(c) + if i >= 0 { + w.Write(plusBytes) + } + w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) + w.Write(iBytes) + w.Write(closeParenBytes) +} + +// printHexPtr outputs a uintptr formatted as hexidecimal with a leading '0x' +// prefix to Writer w. +func printHexPtr(w io.Writer, p uintptr) { + // Null pointer. + num := uint64(p) + if num == 0 { + w.Write(nilAngleBytes) + return + } + + // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix + buf := make([]byte, 18) + + // It's simpler to construct the hex string right to left. + base := uint64(16) + i := len(buf) - 1 + for num >= base { + buf[i] = hexDigits[num%base] + num /= base + i-- + } + buf[i] = hexDigits[num] + + // Add '0x' prefix. + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + + // Strip unused leading bytes. + buf = buf[i:] + w.Write(buf) +} + +// valuesSorter implements sort.Interface to allow a slice of reflect.Value +// elements to be sorted. +type valuesSorter struct { + values []reflect.Value + strings []string // either nil or same len and values + cs *ConfigState +} + +// newValuesSorter initializes a valuesSorter instance, which holds a set of +// surrogate keys on which the data should be sorted. It uses flags in +// ConfigState to decide if and how to populate those surrogate keys. +func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { + vs := &valuesSorter{values: values, cs: cs} + if canSortSimply(vs.values[0].Kind()) { + return vs + } + if !cs.DisableMethods { + vs.strings = make([]string, len(values)) + for i := range vs.values { + b := bytes.Buffer{} + if !handleMethods(cs, &b, vs.values[i]) { + vs.strings = nil + break + } + vs.strings[i] = b.String() + } + } + if vs.strings == nil && cs.SpewKeys { + vs.strings = make([]string, len(values)) + for i := range vs.values { + vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) + } + } + return vs +} + +// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted +// directly, or whether it should be considered for sorting by surrogate keys +// (if the ConfigState allows it). +func canSortSimply(kind reflect.Kind) bool { + // This switch parallels valueSortLess, except for the default case. + switch kind { + case reflect.Bool: + return true + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return true + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Uintptr: + return true + case reflect.Array: + return true + } + return false +} + +// Len returns the number of values in the slice. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Len() int { + return len(s.values) +} + +// Swap swaps the values at the passed indices. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] + if s.strings != nil { + s.strings[i], s.strings[j] = s.strings[j], s.strings[i] + } +} + +// valueSortLess returns whether the first value should sort before the second +// value. It is used by valueSorter.Less as part of the sort.Interface +// implementation. +func valueSortLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Bool: + return !a.Bool() && b.Bool() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return a.Int() < b.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + case reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Array: + // Compare the contents of both arrays. + l := a.Len() + for i := 0; i < l; i++ { + av := a.Index(i) + bv := b.Index(i) + if av.Interface() == bv.Interface() { + continue + } + return valueSortLess(av, bv) + } + } + return a.String() < b.String() +} + +// Less returns whether the value at index i should sort before the +// value at index j. It is part of the sort.Interface implementation. +func (s *valuesSorter) Less(i, j int) bool { + if s.strings == nil { + return valueSortLess(s.values[i], s.values[j]) + } + return s.strings[i] < s.strings[j] +} + +// sortValues is a sort function that handles both native types and any type that +// can be converted to error or Stringer. Other inputs are sorted according to +// their Value.String() value to ensure display stability. +func sortValues(values []reflect.Value, cs *ConfigState) { + if len(values) == 0 { + return + } + sort.Sort(newValuesSorter(values, cs)) +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/config.go new file mode 100644 index 000000000..555282723 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/config.go @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// ConfigState houses the configuration options used by spew to format and +// display values. There is a global instance, Config, that is used to control +// all top-level Formatter and Dump functionality. Each ConfigState instance +// provides methods equivalent to the top-level functions. +// +// The zero value for ConfigState provides no indentation. You would typically +// want to set it to a space or a tab. +// +// Alternatively, you can use NewDefaultConfig to get a ConfigState instance +// with default settings. See the documentation of NewDefaultConfig for default +// values. +type ConfigState struct { + // Indent specifies the string to use for each indentation level. The + // global config instance that all top-level functions use set this to a + // single space by default. If you would like more indentation, you might + // set this to a tab with "\t" or perhaps two spaces with " ". + Indent string + + // MaxDepth controls the maximum number of levels to descend into nested + // data structures. The default, 0, means there is no limit. + // + // NOTE: Circular data structures are properly detected, so it is not + // necessary to set this value unless you specifically want to limit deeply + // nested data structures. + MaxDepth int + + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + + // DisablePointerMethods specifies whether or not to check for and invoke + // error and Stringer interfaces on types which only accept a pointer + // receiver when the current type is not a pointer. + // + // NOTE: This might be an unsafe action since calling one of these methods + // with a pointer receiver could technically mutate the value, however, + // in practice, types which choose to satisify an error or Stringer + // interface with a pointer receiver should not be mutating their state + // inside these interface methods. As a result, this option relies on + // access to the unsafe package, so it will not have any effect when + // running in environments without access to the unsafe package such as + // Google App Engine or with the "safe" build tag specified. + DisablePointerMethods bool + + // ContinueOnMethod specifies whether or not recursion should continue once + // a custom error or Stringer interface is invoked. The default, false, + // means it will print the results of invoking the custom error or Stringer + // interface and return immediately instead of continuing to recurse into + // the internals of the data type. + // + // NOTE: This flag does not have any effect if method invocation is disabled + // via the DisableMethods or DisablePointerMethods options. + ContinueOnMethod bool + + // SortKeys specifies map keys should be sorted before being printed. Use + // this to have a more deterministic, diffable output. Note that only + // native types (bool, int, uint, floats, uintptr and string) and types + // that support the error or Stringer interfaces (if methods are + // enabled) are supported, with other types sorted according to the + // reflect.Value.String() output which guarantees display stability. + SortKeys bool + + // SpewKeys specifies that, as a last resort attempt, map keys should + // be spewed to strings and sorted by those strings. This is only + // considered if SortKeys is true. + SpewKeys bool +} + +// Config is the active configuration of the top-level functions. +// The configuration can be changed by modifying the contents of spew.Config. +var Config = ConfigState{Indent: " "} + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the formatted string as a value that satisfies error. See NewFormatter +// for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, c.convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, c.convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, c.convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a Formatter interface returned by c.NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Print(a ...interface{}) (n int, err error) { + return fmt.Print(c.convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, c.convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Println(a ...interface{}) (n int, err error) { + return fmt.Println(c.convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprint(a ...interface{}) string { + return fmt.Sprint(c.convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, c.convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a Formatter interface returned by c.NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.convertArgs(a)...) +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +c.Printf, c.Println, or c.Printf. +*/ +func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(c, v) +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { + fdump(c, w, a...) +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by modifying the public members +of c. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func (c *ConfigState) Dump(a ...interface{}) { + fdump(c, os.Stdout, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func (c *ConfigState) Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(c, &buf, a...) + return buf.String() +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a spew Formatter interface using +// the ConfigState associated with s. +func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = newFormatter(c, arg) + } + return formatters +} + +// NewDefaultConfig returns a ConfigState with the following default settings. +// +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false +func NewDefaultConfig() *ConfigState { + return &ConfigState{Indent: " "} +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/doc.go new file mode 100644 index 000000000..5be0c4060 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/doc.go @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package spew implements a deep pretty printer for Go data structures to aid in +debugging. + +A quick overview of the additional features spew provides over the built-in +printing facilities for Go data types are as follows: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) + +There are two different approaches spew allows for dumping Go data structures: + + * Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + * A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt + +Quick Start + +This section demonstrates how to quickly get started with spew. See the +sections below for further details on formatting and configuration options. + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) + spew.Fdump(someWriter, myVar1, myVar2, ...) + str := spew.Sdump(myVar1, myVar2, ...) + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with +%v (most compact), %+v (adds pointer addresses), %#v (adds types), or +%#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available +via the spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +The following configuration options are available: + * Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + * MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + * DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + * DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + * ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + * SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + * SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +Dump Usage + +Simply call spew.Dump with a list of variables you want to dump: + + spew.Dump(myVar1, myVar2, ...) + +You may also call spew.Fdump if you would prefer to output to an arbitrary +io.Writer. For example, to dump to standard error: + + spew.Fdump(os.Stderr, myVar1, myVar2, ...) + +A third option is to call spew.Sdump to get the formatted output as a string: + + str := spew.Sdump(myVar1, myVar2, ...) + +Sample Dump Output + +See the Dump example for details on the setup of the types and variables being +shown here. + + (main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) <nil> + }), + ExportedField: (map[interface {}]interface {}) (len=1) { + (string) (len=3) "one": (bool) true + } + } + +Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C +command as shown. + ([]uint8) (len=32 cap=32) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| + } + +Custom Formatter + +Spew provides a custom formatter that implements the fmt.Formatter interface +so that it integrates cleanly with standard fmt package printing functions. The +formatter is useful for inline printing of smaller data types similar to the +standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Custom Formatter Usage + +The simplest way to make use of the spew custom formatter is to call one of the +convenience functions such as spew.Printf, spew.Println, or spew.Printf. The +functions have syntax you are most likely already familiar with: + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Println(myVar, myVar2) + spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +See the Index for the full list convenience functions. + +Sample Formatter Output + +Double pointer to a uint8: + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 + +Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*><shown>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>} + +See the Printf example for details on the setup of variables being shown +here. + +Errors + +Since it is possible for custom Stringer/error interfaces to panic, spew +detects them and handles them internally by printing the panic information +inline with the output. Since spew is intended to provide deep pretty printing +capabilities on structures, it intentionally does not return any errors. +*/ +package spew diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/dump.go new file mode 100644 index 000000000..a0ff95e27 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +var ( + // uint8Type is a reflect.Type representing a uint8. It is used to + // convert cgo types to uint8 slices for hexdumping. + uint8Type = reflect.TypeOf(uint8(0)) + + // cCharRE is a regular expression that matches a cgo char. + // It is used to detect character arrays to hexdump them. + cCharRE = regexp.MustCompile("^.*\\._Ctype_char$") + + // cUnsignedCharRE is a regular expression that matches a cgo unsigned + // char. It is used to detect unsigned character arrays to hexdump + // them. + cUnsignedCharRE = regexp.MustCompile("^.*\\._Ctype_unsignedchar$") + + // cUint8tCharRE is a regular expression that matches a cgo uint8_t. + // It is used to detect uint8_t arrays to hexdump them. + cUint8tCharRE = regexp.MustCompile("^.*\\._Ctype_uint8_t$") +) + +// dumpState contains information about the state of a dump operation. +type dumpState struct { + w io.Writer + depth int + pointers map[uintptr]int + ignoreNextType bool + ignoreNextIndent bool + cs *ConfigState +} + +// indent performs indentation according to the depth level and cs.Indent +// option. +func (d *dumpState) indent() { + if d.ignoreNextIndent { + d.ignoreNextIndent = false + return + } + d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) +} + +// unpackValue returns values inside of non-nil interfaces when possible. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v +} + +// dumpPtr handles formatting of pointers by indirecting them as necessary. +func (d *dumpState) dumpPtr(v reflect.Value) { + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range d.pointers { + if depth >= d.depth { + delete(d.pointers, k) + } + } + + // Keep list of all dereferenced pointers to show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by dereferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := d.pointers[addr]; ok && pd < d.depth { + cycleFound = true + indirects-- + break + } + d.pointers[addr] = d.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type information. + d.w.Write(openParenBytes) + d.w.Write(bytes.Repeat(asteriskBytes, indirects)) + d.w.Write([]byte(ve.Type().String())) + d.w.Write(closeParenBytes) + + // Display pointer information. + if len(pointerChain) > 0 { + d.w.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + d.w.Write(pointerChainBytes) + } + printHexPtr(d.w, addr) + } + d.w.Write(closeParenBytes) + } + + // Display dereferenced value. + d.w.Write(openParenBytes) + switch { + case nilFound == true: + d.w.Write(nilAngleBytes) + + case cycleFound == true: + d.w.Write(circularBytes) + + default: + d.ignoreNextType = true + d.dump(ve) + } + d.w.Write(closeParenBytes) +} + +// dumpSlice handles formatting of arrays and slices. Byte (uint8 under +// reflection) arrays and slices are dumped in hexdump -C fashion. +func (d *dumpState) dumpSlice(v reflect.Value) { + // Determine whether this type should be hex dumped or not. Also, + // for types which should be hexdumped, try to use the underlying data + // first, then fall back to trying to convert them to a uint8 slice. + var buf []uint8 + doConvert := false + doHexDump := false + numEntries := v.Len() + if numEntries > 0 { + vt := v.Index(0).Type() + vts := vt.String() + switch { + // C types that need to be converted. + case cCharRE.MatchString(vts): + fallthrough + case cUnsignedCharRE.MatchString(vts): + fallthrough + case cUint8tCharRE.MatchString(vts): + doConvert = true + + // Try to use existing uint8 slices and fall back to converting + // and copying if that fails. + case vt.Kind() == reflect.Uint8: + // We need an addressable interface to convert the type + // to a byte slice. However, the reflect package won't + // give us an interface on certain things like + // unexported struct fields in order to enforce + // visibility rules. We use unsafe, when available, to + // bypass these restrictions since this package does not + // mutate the values. + vs := v + if !vs.CanInterface() || !vs.CanAddr() { + vs = unsafeReflectValue(vs) + } + if !UnsafeDisabled { + vs = vs.Slice(0, numEntries) + + // Use the existing uint8 slice if it can be + // type asserted. + iface := vs.Interface() + if slice, ok := iface.([]uint8); ok { + buf = slice + doHexDump = true + break + } + } + + // The underlying data needs to be converted if it can't + // be type asserted to a uint8 slice. + doConvert = true + } + + // Copy and convert the underlying type if needed. + if doConvert && vt.ConvertibleTo(uint8Type) { + // Convert and copy each element into a uint8 byte + // slice. + buf = make([]uint8, numEntries) + for i := 0; i < numEntries; i++ { + vv := v.Index(i) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) + } + doHexDump = true + } + } + + // Hexdump the entire slice as needed. + if doHexDump { + indent := strings.Repeat(d.cs.Indent, d.depth) + str := indent + hex.Dump(buf) + str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.TrimRight(str, d.cs.Indent) + d.w.Write([]byte(str)) + return + } + + // Recursively call dump for each item. + for i := 0; i < numEntries; i++ { + d.dump(d.unpackValue(v.Index(i))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } +} + +// dump is the main workhorse for dumping a value. It uses the passed reflect +// value to figure out what kind of object we are dealing with and formats it +// appropriately. It is a recursive function, however circular data structures +// are detected and handled properly. +func (d *dumpState) dump(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + d.w.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + d.indent() + d.dumpPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !d.ignoreNextType { + d.indent() + d.w.Write(openParenBytes) + d.w.Write([]byte(v.Type().String())) + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + d.ignoreNextType = false + + // Display length and capacity if the built-in len and cap functions + // work with the value's kind and the len/cap itself is non-zero. + valueLen, valueCap := 0, 0 + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.Chan: + valueLen, valueCap = v.Len(), v.Cap() + case reflect.Map, reflect.String: + valueLen = v.Len() + } + if valueLen != 0 || valueCap != 0 { + d.w.Write(openParenBytes) + if valueLen != 0 { + d.w.Write(lenEqualsBytes) + printInt(d.w, int64(valueLen), 10) + } + if valueCap != 0 { + if valueLen != 0 { + d.w.Write(spaceBytes) + } + d.w.Write(capEqualsBytes) + printInt(d.w, int64(valueCap), 10) + } + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + + // Call Stringer/error interfaces if they exist and the handle methods flag + // is enabled + if !d.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(d.cs, d.w, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(d.w, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(d.w, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(d.w, v.Uint(), 10) + + case reflect.Float32: + printFloat(d.w, v.Float(), 32) + + case reflect.Float64: + printFloat(d.w, v.Float(), 64) + + case reflect.Complex64: + printComplex(d.w, v.Complex(), 32) + + case reflect.Complex128: + printComplex(d.w, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + d.dumpSlice(v) + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.String: + d.w.Write([]byte(strconv.Quote(v.String()))) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + d.w.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + numEntries := v.Len() + keys := v.MapKeys() + if d.cs.SortKeys { + sortValues(keys, d.cs) + } + for i, key := range keys { + d.dump(d.unpackValue(key)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.MapIndex(key))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Struct: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + vt := v.Type() + numFields := v.NumField() + for i := 0; i < numFields; i++ { + d.indent() + vtf := vt.Field(i) + d.w.Write([]byte(vtf.Name)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.Field(i))) + if i < (numFields - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(d.w, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(d.w, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it in case any new + // types are added. + default: + if v.CanInterface() { + fmt.Fprintf(d.w, "%v", v.Interface()) + } else { + fmt.Fprintf(d.w, "%v", v.String()) + } + } +} + +// fdump is a helper function to consolidate the logic from the various public +// methods which take varying writers and config states. +func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { + for _, arg := range a { + if arg == nil { + w.Write(interfaceBytes) + w.Write(spaceBytes) + w.Write(nilAngleBytes) + w.Write(newlineBytes) + continue + } + + d := dumpState{w: w, cs: cs} + d.pointers = make(map[uintptr]int) + d.dump(reflect.ValueOf(arg)) + d.w.Write(newlineBytes) + } +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func Fdump(w io.Writer, a ...interface{}) { + fdump(&Config, w, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(&Config, &buf, a...) + return buf.String() +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by an exported package global, +spew.Config. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func Dump(a ...interface{}) { + fdump(&Config, os.Stdout, a...) +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/format.go new file mode 100644 index 000000000..ecf3b80e2 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/format.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" +) + +// supportedFlags is a list of all the character flags supported by fmt package. +const supportedFlags = "0-+# " + +// formatState implements the fmt.Formatter interface and contains information +// about the state of a formatting operation. The NewFormatter function can +// be used to get a new Formatter which can be used directly as arguments +// in standard fmt package printing calls. +type formatState struct { + value interface{} + fs fmt.State + depth int + pointers map[uintptr]int + ignoreNextType bool + cs *ConfigState +} + +// buildDefaultFormat recreates the original format string without precision +// and width information to pass in to fmt.Sprintf in the case of an +// unrecognized type. Unless new types are added to the language, this +// function won't ever be called. +func (f *formatState) buildDefaultFormat() (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + buf.WriteRune('v') + + format = buf.String() + return format +} + +// constructOrigFormat recreates the original format string including precision +// and width information to pass along to the standard fmt package. This allows +// automatic deferral of all format strings this package doesn't support. +func (f *formatState) constructOrigFormat(verb rune) (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + if width, ok := f.fs.Width(); ok { + buf.WriteString(strconv.Itoa(width)) + } + + if precision, ok := f.fs.Precision(); ok { + buf.Write(precisionBytes) + buf.WriteString(strconv.Itoa(precision)) + } + + buf.WriteRune(verb) + + format = buf.String() + return format +} + +// unpackValue returns values inside of non-nil interfaces when possible and +// ensures that types for values which have been unpacked from an interface +// are displayed when the show types flag is also set. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (f *formatState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface { + f.ignoreNextType = false + if !v.IsNil() { + v = v.Elem() + } + } + return v +} + +// formatPtr handles formatting of pointers by indirecting them as necessary. +func (f *formatState) formatPtr(v reflect.Value) { + // Display nil if top level pointer is nil. + showTypes := f.fs.Flag('#') + if v.IsNil() && (!showTypes || f.ignoreNextType) { + f.fs.Write(nilAngleBytes) + return + } + + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range f.pointers { + if depth >= f.depth { + delete(f.pointers, k) + } + } + + // Keep list of all dereferenced pointers to possibly show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by derferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := f.pointers[addr]; ok && pd < f.depth { + cycleFound = true + indirects-- + break + } + f.pointers[addr] = f.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type or indirection level depending on flags. + if showTypes && !f.ignoreNextType { + f.fs.Write(openParenBytes) + f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) + f.fs.Write([]byte(ve.Type().String())) + f.fs.Write(closeParenBytes) + } else { + if nilFound || cycleFound { + indirects += strings.Count(ve.Type().String(), "*") + } + f.fs.Write(openAngleBytes) + f.fs.Write([]byte(strings.Repeat("*", indirects))) + f.fs.Write(closeAngleBytes) + } + + // Display pointer information depending on flags. + if f.fs.Flag('+') && (len(pointerChain) > 0) { + f.fs.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + f.fs.Write(pointerChainBytes) + } + printHexPtr(f.fs, addr) + } + f.fs.Write(closeParenBytes) + } + + // Display dereferenced value. + switch { + case nilFound == true: + f.fs.Write(nilAngleBytes) + + case cycleFound == true: + f.fs.Write(circularShortBytes) + + default: + f.ignoreNextType = true + f.format(ve) + } +} + +// format is the main workhorse for providing the Formatter interface. It +// uses the passed reflect value to figure out what kind of object we are +// dealing with and formats it appropriately. It is a recursive function, +// however circular data structures are detected and handled properly. +func (f *formatState) format(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + f.fs.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + f.formatPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !f.ignoreNextType && f.fs.Flag('#') { + f.fs.Write(openParenBytes) + f.fs.Write([]byte(v.Type().String())) + f.fs.Write(closeParenBytes) + } + f.ignoreNextType = false + + // Call Stringer/error interfaces if they exist and the handle methods + // flag is enabled. + if !f.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(f.cs, f.fs, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(f.fs, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(f.fs, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(f.fs, v.Uint(), 10) + + case reflect.Float32: + printFloat(f.fs, v.Float(), 32) + + case reflect.Float64: + printFloat(f.fs, v.Float(), 64) + + case reflect.Complex64: + printComplex(f.fs, v.Complex(), 32) + + case reflect.Complex128: + printComplex(f.fs, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + f.fs.Write(openBracketBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + numEntries := v.Len() + for i := 0; i < numEntries; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(v.Index(i))) + } + } + f.depth-- + f.fs.Write(closeBracketBytes) + + case reflect.String: + f.fs.Write([]byte(v.String())) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + f.fs.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + + f.fs.Write(openMapBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + keys := v.MapKeys() + if f.cs.SortKeys { + sortValues(keys, f.cs) + } + for i, key := range keys { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(key)) + f.fs.Write(colonBytes) + f.ignoreNextType = true + f.format(f.unpackValue(v.MapIndex(key))) + } + } + f.depth-- + f.fs.Write(closeMapBytes) + + case reflect.Struct: + numFields := v.NumField() + f.fs.Write(openBraceBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + vt := v.Type() + for i := 0; i < numFields; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + vtf := vt.Field(i) + if f.fs.Flag('+') || f.fs.Flag('#') { + f.fs.Write([]byte(vtf.Name)) + f.fs.Write(colonBytes) + } + f.format(f.unpackValue(v.Field(i))) + } + } + f.depth-- + f.fs.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(f.fs, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(f.fs, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it if any get added. + default: + format := f.buildDefaultFormat() + if v.CanInterface() { + fmt.Fprintf(f.fs, format, v.Interface()) + } else { + fmt.Fprintf(f.fs, format, v.String()) + } + } +} + +// Format satisfies the fmt.Formatter interface. See NewFormatter for usage +// details. +func (f *formatState) Format(fs fmt.State, verb rune) { + f.fs = fs + + // Use standard formatting for verbs that are not v. + if verb != 'v' { + format := f.constructOrigFormat(verb) + fmt.Fprintf(fs, format, f.value) + return + } + + if f.value == nil { + if fs.Flag('#') { + fs.Write(interfaceBytes) + } + fs.Write(nilAngleBytes) + return + } + + f.format(reflect.ValueOf(f.value)) +} + +// newFormatter is a helper function to consolidate the logic from the various +// public methods which take varying config states. +func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { + fs := &formatState{value: v, cs: cs} + fs.pointers = make(map[uintptr]int) + return fs +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +Printf, Println, or Fprintf. +*/ +func NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(&Config, v) +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/spew.go new file mode 100644 index 000000000..d8233f542 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/davecgh/go-spew/spew/spew.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2013 Dave Collins <dave@davec.name> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "fmt" + "io" +) + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the formatted string as a value that satisfies error. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a default Formatter interface returned by NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprint(a ...interface{}) string { + return fmt.Sprint(convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(convertArgs(a)...) +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a default spew Formatter interface. +func convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = NewFormatter(arg) + } + return formatters +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/LICENSE b/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/LICENSE new file mode 100644 index 000000000..c67dad612 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib/difflib.go new file mode 100644 index 000000000..64cc40fe1 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/pmezard/go-difflib/difflib/difflib.go @@ -0,0 +1,758 @@ +// Package difflib is a partial port of Python difflib module. +// +// It provides tools to compare sequences of strings and generate textual diffs. +// +// The following class and functions have been ported: +// +// - SequenceMatcher +// +// - unified_diff +// +// - context_diff +// +// Getting unified diffs was the main goal of the port. Keep in mind this code +// is mostly suitable to output text differences in a human friendly way, there +// are no guarantees generated diffs are consumable by patch(1). +package difflib + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func calculateRatio(matches, length int) float64 { + if length > 0 { + return 2.0 * float64(matches) / float64(length) + } + return 1.0 +} + +type Match struct { + A int + B int + Size int +} + +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "<P>" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" <wink>. +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +func NewMatcherWithJunk(a, b []string, autoJunk bool, + isJunk func(string) bool) *SequenceMatcher { + + m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk} + m.SetSeqs(a, b) + return &m +} + +// Set two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// Set the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// Set the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s, _ := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s, _ := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s, _ := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// and for all (i',j',k') meeting those conditions, +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// Return list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// Return list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// Isolate change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} + +// Return a measure of the sequences' similarity (float in [0,1]). +// +// Where T is the total number of elements in both sequences, and +// M is the number of matches, this is 2.0*M / T. +// Note that this is 1 if the sequences are identical, and 0 if +// they have nothing in common. +// +// .Ratio() is expensive to compute if you haven't already computed +// .GetMatchingBlocks() or .GetOpCodes(), in which case you may +// want to try .QuickRatio() or .RealQuickRation() first to get an +// upper bound. +func (m *SequenceMatcher) Ratio() float64 { + matches := 0 + for _, m := range m.GetMatchingBlocks() { + matches += m.Size + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() relatively quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute. +func (m *SequenceMatcher) QuickRatio() float64 { + // viewing a and b as multisets, set matches to the cardinality + // of their intersection; this counts the number of matches + // without regard to order, so is clearly an upper bound + if m.fullBCount == nil { + m.fullBCount = map[string]int{} + for _, s := range m.b { + m.fullBCount[s] = m.fullBCount[s] + 1 + } + } + + // avail[x] is the number of times x appears in 'b' less the + // number of times we've seen it in 'a' so far ... kinda + avail := map[string]int{} + matches := 0 + for _, s := range m.a { + n, ok := avail[s] + if !ok { + n = m.fullBCount[s] + } + avail[s] = n - 1 + if n > 0 { + matches += 1 + } + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() very quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute than either .Ratio() or .QuickRatio(). +func (m *SequenceMatcher) RealQuickRatio() float64 { + la, lb := len(m.a), len(m.b) + return calculateRatio(min(la, lb), la+lb) +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +// Unified diff parameters +type UnifiedDiff struct { + A []string // First sequence lines + FromFile string // First file name + FromDate string // First file time + B []string // Second sequence lines + ToFile string // Second file name + ToDate string // Second file time + Eol string // Headers end of line, defaults to LF + Context int // Number of context lines +} + +// Compare two sequences of lines; generate the delta as a unified diff. +// +// Unified diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by 'n' which +// defaults to three. +// +// By default, the diff control lines (those with ---, +++, or @@) are +// created with a trailing newline. This is helpful so that inputs +// created from file.readlines() result in diffs that are suitable for +// file.writelines() since both the inputs and outputs have trailing +// newlines. +// +// For inputs that do not have trailing newlines, set the lineterm +// argument to "" so that the output will be uniformly newline free. +// +// The unidiff format normally has a header for filenames and modification +// times. Any or all of these may be specified using strings for +// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. +// The modification times are normally expressed in the ISO 8601 format. +func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + w := func(format string, args ...interface{}) error { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + return err + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + err := w("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + if err != nil { + return err + } + err = w("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + if err != nil { + return err + } + } + first, last := g[0], g[len(g)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + if err := w("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + return err + } + for _, c := range g { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + if c.Tag == 'e' { + for _, line := range diff.A[i1:i2] { + if err := w(" " + line); err != nil { + return err + } + } + continue + } + if c.Tag == 'r' || c.Tag == 'd' { + for _, line := range diff.A[i1:i2] { + if err := w("-" + line); err != nil { + return err + } + } + } + if c.Tag == 'r' || c.Tag == 'i' { + for _, line := range diff.B[j1:j2] { + if err := w("+" + line); err != nil { + return err + } + } + } + } + } + return nil +} + +// Like WriteUnifiedDiff but returns the diff a string. +func GetUnifiedDiffString(diff UnifiedDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteUnifiedDiff(w, diff) + return string(w.Bytes()), err +} + +// Convert range to the "ed" format. +func formatRangeContext(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + if length <= 1 { + return fmt.Sprintf("%d", beginning) + } + return fmt.Sprintf("%d,%d", beginning, beginning+length-1) +} + +type ContextDiff UnifiedDiff + +// Compare two sequences of lines; generate the delta as a context diff. +// +// Context diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by diff.Context +// which defaults to three. +// +// By default, the diff control lines (those with *** or ---) are +// created with a trailing newline. +// +// For inputs that do not have trailing newlines, set the diff.Eol +// argument to "" so that the output will be uniformly newline free. +// +// The context diff format normally has a header for filenames and +// modification times. Any or all of these may be specified using +// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate. +// The modification times are normally expressed in the ISO 8601 format. +// If not specified, the strings default to blanks. +func WriteContextDiff(writer io.Writer, diff ContextDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + var diffErr error + w := func(format string, args ...interface{}) { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + if diffErr == nil && err != nil { + diffErr = err + } + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + prefix := map[byte]string{ + 'i': "+ ", + 'd': "- ", + 'r': "! ", + 'e': " ", + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + w("*** %s%s%s", diff.FromFile, fromDate, diff.Eol) + w("--- %s%s%s", diff.ToFile, toDate, diff.Eol) + } + + first, last := g[0], g[len(g)-1] + w("***************" + diff.Eol) + + range1 := formatRangeContext(first.I1, last.I2) + w("*** %s ****%s", range1, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'd' { + for _, cc := range g { + if cc.Tag == 'i' { + continue + } + for _, line := range diff.A[cc.I1:cc.I2] { + w(prefix[cc.Tag] + line) + } + } + break + } + } + + range2 := formatRangeContext(first.J1, last.J2) + w("--- %s ----%s", range2, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'i' { + for _, cc := range g { + if cc.Tag == 'd' { + continue + } + for _, line := range diff.B[cc.J1:cc.J2] { + w(prefix[cc.Tag] + line) + } + } + break + } + } + } + return diffErr +} + +// Like WriteContextDiff but returns the diff a string. +func GetContextDiffString(diff ContextDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteContextDiff(w, diff) + return string(w.Bytes()), err +} + +// Split a string on "\n" while preserving them. The output can be used +// as input for UnifiedDiff and ContextDiff structures. +func SplitLines(s string) []string { + lines := strings.SplitAfter(s, "\n") + lines[len(lines)-1] += "\n" + return lines +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/.gitignore b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/.gitignore new file mode 100644 index 000000000..00268614f --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/LICENSE.md b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/LICENSE.md new file mode 100644 index 000000000..219994581 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/LICENSE.md @@ -0,0 +1,23 @@ +objx - by Mat Ryer and Tyler Bunnell + +The MIT License (MIT) + +Copyright (c) 2014 Stretchr, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/README.md b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/README.md new file mode 100644 index 000000000..4aa180687 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/README.md @@ -0,0 +1,3 @@ +# objx + + * Jump into the [API Documentation](http://godoc.org/github.com/stretchr/objx) diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/accessors.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/accessors.go new file mode 100644 index 000000000..721bcac79 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/accessors.go @@ -0,0 +1,179 @@ +package objx + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// arrayAccesRegexString is the regex used to extract the array number +// from the access path +const arrayAccesRegexString = `^(.+)\[([0-9]+)\]$` + +// arrayAccesRegex is the compiled arrayAccesRegexString +var arrayAccesRegex = regexp.MustCompile(arrayAccesRegexString) + +// Get gets the value using the specified selector and +// returns it inside a new Obj object. +// +// If it cannot find the value, Get will return a nil +// value inside an instance of Obj. +// +// Get can only operate directly on map[string]interface{} and []interface. +// +// Example +// +// To access the title of the third chapter of the second book, do: +// +// o.Get("books[1].chapters[2].title") +func (m Map) Get(selector string) *Value { + rawObj := access(m, selector, nil, false, false) + return &Value{data: rawObj} +} + +// Set sets the value using the specified selector and +// returns the object on which Set was called. +// +// Set can only operate directly on map[string]interface{} and []interface +// +// Example +// +// To set the title of the third chapter of the second book, do: +// +// o.Set("books[1].chapters[2].title","Time to Go") +func (m Map) Set(selector string, value interface{}) Map { + access(m, selector, value, true, false) + return m +} + +// access accesses the object using the selector and performs the +// appropriate action. +func access(current, selector, value interface{}, isSet, panics bool) interface{} { + + switch selector.(type) { + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + + if array, ok := current.([]interface{}); ok { + index := intFromInterface(selector) + + if index >= len(array) { + if panics { + panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array))) + } + return nil + } + + return array[index] + } + + return nil + + case string: + + selStr := selector.(string) + selSegs := strings.SplitN(selStr, PathSeparator, 2) + thisSel := selSegs[0] + index := -1 + var err error + + // https://github.com/stretchr/objx/issues/12 + if strings.Contains(thisSel, "[") { + + arrayMatches := arrayAccesRegex.FindStringSubmatch(thisSel) + + if len(arrayMatches) > 0 { + + // Get the key into the map + thisSel = arrayMatches[1] + + // Get the index into the array at the key + index, err = strconv.Atoi(arrayMatches[2]) + + if err != nil { + // This should never happen. If it does, something has gone + // seriously wrong. Panic. + panic("objx: Array index is not an integer. Must use array[int].") + } + + } + } + + if curMap, ok := current.(Map); ok { + current = map[string]interface{}(curMap) + } + + // get the object in question + switch current.(type) { + case map[string]interface{}: + curMSI := current.(map[string]interface{}) + if len(selSegs) <= 1 && isSet { + curMSI[thisSel] = value + return nil + } else { + current = curMSI[thisSel] + } + default: + current = nil + } + + if current == nil && panics { + panic(fmt.Sprintf("objx: '%v' invalid on object.", selector)) + } + + // do we need to access the item of an array? + if index > -1 { + if array, ok := current.([]interface{}); ok { + if index < len(array) { + current = array[index] + } else { + if panics { + panic(fmt.Sprintf("objx: Index %d is out of range. Slice only contains %d items.", index, len(array))) + } + current = nil + } + } + } + + if len(selSegs) > 1 { + current = access(current, selSegs[1], value, isSet, panics) + } + + } + + return current + +} + +// intFromInterface converts an interface object to the largest +// representation of an unsigned integer using a type switch and +// assertions +func intFromInterface(selector interface{}) int { + var value int + switch selector.(type) { + case int: + value = selector.(int) + case int8: + value = int(selector.(int8)) + case int16: + value = int(selector.(int16)) + case int32: + value = int(selector.(int32)) + case int64: + value = int(selector.(int64)) + case uint: + value = int(selector.(uint)) + case uint8: + value = int(selector.(uint8)) + case uint16: + value = int(selector.(uint16)) + case uint32: + value = int(selector.(uint32)) + case uint64: + value = int(selector.(uint64)) + default: + panic("objx: array access argument is not an integer type (this should never happen)") + } + + return value +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/array-access.txt b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/array-access.txt new file mode 100644 index 000000000..306023475 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/array-access.txt @@ -0,0 +1,14 @@ + case []{1}: + a := object.([]{1}) + if isSet { + a[index] = value.({1}) + } else { + if index >= len(a) { + if panics { + panic(fmt.Sprintf("objx: Index %d is out of range because the []{1} only contains %d items.", index, len(a))) + } + return nil + } else { + return a[index] + } + } diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/index.html b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/index.html new file mode 100644 index 000000000..379ffc3c0 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/index.html @@ -0,0 +1,86 @@ +<html> + <head> + <title> + Codegen + </title> + <style> + body { + width: 800px; + margin: auto; + } + textarea { + width: 100%; + min-height: 100px; + font-family: Courier; + } + </style> + </head> + <body> + + <h2> + Template + </h2> + <p> + Use <code>{x}</code> as a placeholder for each argument. + </p> + <textarea id="template"></textarea> + + <h2> + Arguments (comma separated) + </h2> + <p> + One block per line + </p> + <textarea id="args"></textarea> + + <h2> + Output + </h2> + <input id="go" type="button" value="Generate code" /> + + <textarea id="output"></textarea> + + <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> + <script> + + $(function(){ + + $("#go").click(function(){ + + var output = "" + var template = $("#template").val() + var args = $("#args").val() + + // collect the args + var argLines = args.split("\n") + for (var line in argLines) { + + var argLine = argLines[line]; + var thisTemp = template + + // get individual args + var args = argLine.split(",") + + for (var argI in args) { + var argText = args[argI]; + var argPlaceholder = "{" + argI + "}"; + + while (thisTemp.indexOf(argPlaceholder) > -1) { + thisTemp = thisTemp.replace(argPlaceholder, argText); + } + + } + + output += thisTemp + + } + + $("#output").val(output); + + }); + + }); + + </script> + </body> +</html> diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/template.txt b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/template.txt new file mode 100644 index 000000000..b396900b8 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/template.txt @@ -0,0 +1,286 @@ +/* + {4} ({1} and []{1}) + -------------------------------------------------- +*/ + +// {4} gets the value as a {1}, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) {4}(optionalDefault ...{1}) {1} { + if s, ok := v.data.({1}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return {3} +} + +// Must{4} gets the value as a {1}. +// +// Panics if the object is not a {1}. +func (v *Value) Must{4}() {1} { + return v.data.({1}) +} + +// {4}Slice gets the value as a []{1}, returns the optionalDefault +// value or nil if the value is not a []{1}. +func (v *Value) {4}Slice(optionalDefault ...[]{1}) []{1} { + if s, ok := v.data.([]{1}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// Must{4}Slice gets the value as a []{1}. +// +// Panics if the object is not a []{1}. +func (v *Value) Must{4}Slice() []{1} { + return v.data.([]{1}) +} + +// Is{4} gets whether the object contained is a {1} or not. +func (v *Value) Is{4}() bool { + _, ok := v.data.({1}) + return ok +} + +// Is{4}Slice gets whether the object contained is a []{1} or not. +func (v *Value) Is{4}Slice() bool { + _, ok := v.data.([]{1}) + return ok +} + +// Each{4} calls the specified callback for each object +// in the []{1}. +// +// Panics if the object is the wrong type. +func (v *Value) Each{4}(callback func(int, {1}) bool) *Value { + + for index, val := range v.Must{4}Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// Where{4} uses the specified decider function to select items +// from the []{1}. The object contained in the result will contain +// only the selected items. +func (v *Value) Where{4}(decider func(int, {1}) bool) *Value { + + var selected []{1} + + v.Each{4}(func(index int, val {1}) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data:selected} + +} + +// Group{4} uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]{1}. +func (v *Value) Group{4}(grouper func(int, {1}) string) *Value { + + groups := make(map[string][]{1}) + + v.Each{4}(func(index int, val {1}) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]{1}, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data:groups} + +} + +// Replace{4} uses the specified function to replace each {1}s +// by iterating each item. The data in the returned result will be a +// []{1} containing the replaced items. +func (v *Value) Replace{4}(replacer func(int, {1}) {1}) *Value { + + arr := v.Must{4}Slice() + replaced := make([]{1}, len(arr)) + + v.Each{4}(func(index int, val {1}) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data:replaced} + +} + +// Collect{4} uses the specified collector function to collect a value +// for each of the {1}s in the slice. The data returned will be a +// []interface{}. +func (v *Value) Collect{4}(collector func(int, {1}) interface{}) *Value { + + arr := v.Must{4}Slice() + collected := make([]interface{}, len(arr)) + + v.Each{4}(func(index int, val {1}) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data:collected} +} + +// ************************************************************ +// TESTS +// ************************************************************ + +func Test{4}(t *testing.T) { + + val := {1}( {2} ) + m := map[string]interface{}{"value": val, "nothing": nil} + assert.Equal(t, val, New(m).Get("value").{4}()) + assert.Equal(t, val, New(m).Get("value").Must{4}()) + assert.Equal(t, {1}({3}), New(m).Get("nothing").{4}()) + assert.Equal(t, val, New(m).Get("nothing").{4}({2})) + + assert.Panics(t, func() { + New(m).Get("age").Must{4}() + }) + +} + +func Test{4}Slice(t *testing.T) { + + val := {1}( {2} ) + m := map[string]interface{}{"value": []{1}{ val }, "nothing": nil} + assert.Equal(t, val, New(m).Get("value").{4}Slice()[0]) + assert.Equal(t, val, New(m).Get("value").Must{4}Slice()[0]) + assert.Equal(t, []{1}(nil), New(m).Get("nothing").{4}Slice()) + assert.Equal(t, val, New(m).Get("nothing").{4}Slice( []{1}{ {1}({2}) } )[0]) + + assert.Panics(t, func() { + New(m).Get("nothing").Must{4}Slice() + }) + +} + +func TestIs{4}(t *testing.T) { + + var v *Value + + v = &Value{data: {1}({2})} + assert.True(t, v.Is{4}()) + + v = &Value{data: []{1}{ {1}({2}) }} + assert.True(t, v.Is{4}Slice()) + +} + +func TestEach{4}(t *testing.T) { + + v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }} + count := 0 + replacedVals := make([]{1}, 0) + assert.Equal(t, v, v.Each{4}(func(i int, val {1}) bool { + + count++ + replacedVals = append(replacedVals, val) + + // abort early + if i == 2 { + return false + } + + return true + + })) + + assert.Equal(t, count, 3) + assert.Equal(t, replacedVals[0], v.Must{4}Slice()[0]) + assert.Equal(t, replacedVals[1], v.Must{4}Slice()[1]) + assert.Equal(t, replacedVals[2], v.Must{4}Slice()[2]) + +} + +func TestWhere{4}(t *testing.T) { + + v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }} + + selected := v.Where{4}(func(i int, val {1}) bool { + return i%2==0 + }).Must{4}Slice() + + assert.Equal(t, 3, len(selected)) + +} + +func TestGroup{4}(t *testing.T) { + + v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }} + + grouped := v.Group{4}(func(i int, val {1}) string { + return fmt.Sprintf("%v", i%2==0) + }).data.(map[string][]{1}) + + assert.Equal(t, 2, len(grouped)) + assert.Equal(t, 3, len(grouped["true"])) + assert.Equal(t, 3, len(grouped["false"])) + +} + +func TestReplace{4}(t *testing.T) { + + v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }} + + rawArr := v.Must{4}Slice() + + replaced := v.Replace{4}(func(index int, val {1}) {1} { + if index < len(rawArr)-1 { + return rawArr[index+1] + } + return rawArr[0] + }) + + replacedArr := replaced.Must{4}Slice() + if assert.Equal(t, 6, len(replacedArr)) { + assert.Equal(t, replacedArr[0], rawArr[1]) + assert.Equal(t, replacedArr[1], rawArr[2]) + assert.Equal(t, replacedArr[2], rawArr[3]) + assert.Equal(t, replacedArr[3], rawArr[4]) + assert.Equal(t, replacedArr[4], rawArr[5]) + assert.Equal(t, replacedArr[5], rawArr[0]) + } + +} + +func TestCollect{4}(t *testing.T) { + + v := &Value{data: []{1}{ {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}), {1}({2}) }} + + collected := v.Collect{4}(func(index int, val {1}) interface{} { + return index + }) + + collectedArr := collected.MustInterSlice() + if assert.Equal(t, 6, len(collectedArr)) { + assert.Equal(t, collectedArr[0], 0) + assert.Equal(t, collectedArr[1], 1) + assert.Equal(t, collectedArr[2], 2) + assert.Equal(t, collectedArr[3], 3) + assert.Equal(t, collectedArr[4], 4) + assert.Equal(t, collectedArr[5], 5) + } + +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/types_list.txt b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/types_list.txt new file mode 100644 index 000000000..069d43d8e --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/codegen/types_list.txt @@ -0,0 +1,20 @@ +Interface,interface{},"something",nil,Inter +Map,map[string]interface{},map[string]interface{}{"name":"Tyler"},nil,MSI +ObjxMap,(Map),New(1),New(nil),ObjxMap +Bool,bool,true,false,Bool +String,string,"hello","",Str +Int,int,1,0,Int +Int8,int8,1,0,Int8 +Int16,int16,1,0,Int16 +Int32,int32,1,0,Int32 +Int64,int64,1,0,Int64 +Uint,uint,1,0,Uint +Uint8,uint8,1,0,Uint8 +Uint16,uint16,1,0,Uint16 +Uint32,uint32,1,0,Uint32 +Uint64,uint64,1,0,Uint64 +Uintptr,uintptr,1,0,Uintptr +Float32,float32,1,0,Float32 +Float64,float64,1,0,Float64 +Complex64,complex64,1,0,Complex64 +Complex128,complex128,1,0,Complex128 diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/constants.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/constants.go new file mode 100644 index 000000000..f9eb42a25 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/constants.go @@ -0,0 +1,13 @@ +package objx + +const ( + // PathSeparator is the character used to separate the elements + // of the keypath. + // + // For example, `location.address.city` + PathSeparator string = "." + + // SignatureSeparator is the character that is used to + // separate the Base64 string from the security signature. + SignatureSeparator = "_" +) diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/conversions.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/conversions.go new file mode 100644 index 000000000..9cdfa9f9f --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/conversions.go @@ -0,0 +1,117 @@ +package objx + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" +) + +// JSON converts the contained object to a JSON string +// representation +func (m Map) JSON() (string, error) { + + result, err := json.Marshal(m) + + if err != nil { + err = errors.New("objx: JSON encode failed with: " + err.Error()) + } + + return string(result), err + +} + +// MustJSON converts the contained object to a JSON string +// representation and panics if there is an error +func (m Map) MustJSON() string { + result, err := m.JSON() + if err != nil { + panic(err.Error()) + } + return result +} + +// Base64 converts the contained object to a Base64 string +// representation of the JSON string representation +func (m Map) Base64() (string, error) { + + var buf bytes.Buffer + + jsonData, err := m.JSON() + if err != nil { + return "", err + } + + encoder := base64.NewEncoder(base64.StdEncoding, &buf) + encoder.Write([]byte(jsonData)) + encoder.Close() + + return buf.String(), nil + +} + +// MustBase64 converts the contained object to a Base64 string +// representation of the JSON string representation and panics +// if there is an error +func (m Map) MustBase64() string { + result, err := m.Base64() + if err != nil { + panic(err.Error()) + } + return result +} + +// SignedBase64 converts the contained object to a Base64 string +// representation of the JSON string representation and signs it +// using the provided key. +func (m Map) SignedBase64(key string) (string, error) { + + base64, err := m.Base64() + if err != nil { + return "", err + } + + sig := HashWithKey(base64, key) + + return base64 + SignatureSeparator + sig, nil + +} + +// MustSignedBase64 converts the contained object to a Base64 string +// representation of the JSON string representation and signs it +// using the provided key and panics if there is an error +func (m Map) MustSignedBase64(key string) string { + result, err := m.SignedBase64(key) + if err != nil { + panic(err.Error()) + } + return result +} + +/* + URL Query + ------------------------------------------------ +*/ + +// URLValues creates a url.Values object from an Obj. This +// function requires that the wrapped object be a map[string]interface{} +func (m Map) URLValues() url.Values { + + vals := make(url.Values) + + for k, v := range m { + //TODO: can this be done without sprintf? + vals.Set(k, fmt.Sprintf("%v", v)) + } + + return vals +} + +// URLQuery gets an encoded URL query representing the given +// Obj. This function requires that the wrapped object be a +// map[string]interface{} +func (m Map) URLQuery() (string, error) { + return m.URLValues().Encode(), nil +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/doc.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/doc.go new file mode 100644 index 000000000..47bf85e46 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/doc.go @@ -0,0 +1,72 @@ +// objx - Go package for dealing with maps, slices, JSON and other data. +// +// Overview +// +// Objx provides the `objx.Map` type, which is a `map[string]interface{}` that exposes +// a powerful `Get` method (among others) that allows you to easily and quickly get +// access to data within the map, without having to worry too much about type assertions, +// missing data, default values etc. +// +// Pattern +// +// Objx uses a preditable pattern to make access data from within `map[string]interface{}'s +// easy. +// +// Call one of the `objx.` functions to create your `objx.Map` to get going: +// +// m, err := objx.FromJSON(json) +// +// NOTE: Any methods or functions with the `Must` prefix will panic if something goes wrong, +// the rest will be optimistic and try to figure things out without panicking. +// +// Use `Get` to access the value you're interested in. You can use dot and array +// notation too: +// +// m.Get("places[0].latlng") +// +// Once you have saught the `Value` you're interested in, you can use the `Is*` methods +// to determine its type. +// +// if m.Get("code").IsStr() { /* ... */ } +// +// Or you can just assume the type, and use one of the strong type methods to +// extract the real value: +// +// m.Get("code").Int() +// +// If there's no value there (or if it's the wrong type) then a default value +// will be returned, or you can be explicit about the default value. +// +// Get("code").Int(-1) +// +// If you're dealing with a slice of data as a value, Objx provides many useful +// methods for iterating, manipulating and selecting that data. You can find out more +// by exploring the index below. +// +// Reading data +// +// A simple example of how to use Objx: +// +// // use MustFromJSON to make an objx.Map from some JSON +// m := objx.MustFromJSON(`{"name": "Mat", "age": 30}`) +// +// // get the details +// name := m.Get("name").Str() +// age := m.Get("age").Int() +// +// // get their nickname (or use their name if they +// // don't have one) +// nickname := m.Get("nickname").Str(name) +// +// Ranging +// +// Since `objx.Map` is a `map[string]interface{}` you can treat it as such. For +// example, to `range` the data, do what you would expect: +// +// m := objx.MustFromJSON(json) +// for key, value := range m { +// +// /* ... do your magic ... */ +// +// } +package objx diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/map.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/map.go new file mode 100644 index 000000000..eb6ed8e28 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/map.go @@ -0,0 +1,222 @@ +package objx + +import ( + "encoding/base64" + "encoding/json" + "errors" + "io/ioutil" + "net/url" + "strings" +) + +// MSIConvertable is an interface that defines methods for converting your +// custom types to a map[string]interface{} representation. +type MSIConvertable interface { + // MSI gets a map[string]interface{} (msi) representing the + // object. + MSI() map[string]interface{} +} + +// Map provides extended functionality for working with +// untyped data, in particular map[string]interface (msi). +type Map map[string]interface{} + +// Value returns the internal value instance +func (m Map) Value() *Value { + return &Value{data: m} +} + +// Nil represents a nil Map. +var Nil Map = New(nil) + +// New creates a new Map containing the map[string]interface{} in the data argument. +// If the data argument is not a map[string]interface, New attempts to call the +// MSI() method on the MSIConvertable interface to create one. +func New(data interface{}) Map { + if _, ok := data.(map[string]interface{}); !ok { + if converter, ok := data.(MSIConvertable); ok { + data = converter.MSI() + } else { + return nil + } + } + return Map(data.(map[string]interface{})) +} + +// MSI creates a map[string]interface{} and puts it inside a new Map. +// +// The arguments follow a key, value pattern. +// +// Panics +// +// Panics if any key arugment is non-string or if there are an odd number of arguments. +// +// Example +// +// To easily create Maps: +// +// m := objx.MSI("name", "Mat", "age", 29, "subobj", objx.MSI("active", true)) +// +// // creates an Map equivalent to +// m := objx.New(map[string]interface{}{"name": "Mat", "age": 29, "subobj": map[string]interface{}{"active": true}}) +func MSI(keyAndValuePairs ...interface{}) Map { + + newMap := make(map[string]interface{}) + keyAndValuePairsLen := len(keyAndValuePairs) + + if keyAndValuePairsLen%2 != 0 { + panic("objx: MSI must have an even number of arguments following the 'key, value' pattern.") + } + + for i := 0; i < keyAndValuePairsLen; i = i + 2 { + + key := keyAndValuePairs[i] + value := keyAndValuePairs[i+1] + + // make sure the key is a string + keyString, keyStringOK := key.(string) + if !keyStringOK { + panic("objx: MSI must follow 'string, interface{}' pattern. " + keyString + " is not a valid key.") + } + + newMap[keyString] = value + + } + + return New(newMap) +} + +// ****** Conversion Constructors + +// MustFromJSON creates a new Map containing the data specified in the +// jsonString. +// +// Panics if the JSON is invalid. +func MustFromJSON(jsonString string) Map { + o, err := FromJSON(jsonString) + + if err != nil { + panic("objx: MustFromJSON failed with error: " + err.Error()) + } + + return o +} + +// FromJSON creates a new Map containing the data specified in the +// jsonString. +// +// Returns an error if the JSON is invalid. +func FromJSON(jsonString string) (Map, error) { + + var data interface{} + err := json.Unmarshal([]byte(jsonString), &data) + + if err != nil { + return Nil, err + } + + return New(data), nil + +} + +// FromBase64 creates a new Obj containing the data specified +// in the Base64 string. +// +// The string is an encoded JSON string returned by Base64 +func FromBase64(base64String string) (Map, error) { + + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(base64String)) + + decoded, err := ioutil.ReadAll(decoder) + if err != nil { + return nil, err + } + + return FromJSON(string(decoded)) +} + +// MustFromBase64 creates a new Obj containing the data specified +// in the Base64 string and panics if there is an error. +// +// The string is an encoded JSON string returned by Base64 +func MustFromBase64(base64String string) Map { + + result, err := FromBase64(base64String) + + if err != nil { + panic("objx: MustFromBase64 failed with error: " + err.Error()) + } + + return result +} + +// FromSignedBase64 creates a new Obj containing the data specified +// in the Base64 string. +// +// The string is an encoded JSON string returned by SignedBase64 +func FromSignedBase64(base64String, key string) (Map, error) { + parts := strings.Split(base64String, SignatureSeparator) + if len(parts) != 2 { + return nil, errors.New("objx: Signed base64 string is malformed.") + } + + sig := HashWithKey(parts[0], key) + if parts[1] != sig { + return nil, errors.New("objx: Signature for base64 data does not match.") + } + + return FromBase64(parts[0]) +} + +// MustFromSignedBase64 creates a new Obj containing the data specified +// in the Base64 string and panics if there is an error. +// +// The string is an encoded JSON string returned by Base64 +func MustFromSignedBase64(base64String, key string) Map { + + result, err := FromSignedBase64(base64String, key) + + if err != nil { + panic("objx: MustFromSignedBase64 failed with error: " + err.Error()) + } + + return result +} + +// FromURLQuery generates a new Obj by parsing the specified +// query. +// +// For queries with multiple values, the first value is selected. +func FromURLQuery(query string) (Map, error) { + + vals, err := url.ParseQuery(query) + + if err != nil { + return nil, err + } + + m := make(map[string]interface{}) + for k, vals := range vals { + m[k] = vals[0] + } + + return New(m), nil +} + +// MustFromURLQuery generates a new Obj by parsing the specified +// query. +// +// For queries with multiple values, the first value is selected. +// +// Panics if it encounters an error +func MustFromURLQuery(query string) Map { + + o, err := FromURLQuery(query) + + if err != nil { + panic("objx: MustFromURLQuery failed with error: " + err.Error()) + } + + return o + +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/mutations.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/mutations.go new file mode 100644 index 000000000..b35c86392 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/mutations.go @@ -0,0 +1,81 @@ +package objx + +// Exclude returns a new Map with the keys in the specified []string +// excluded. +func (d Map) Exclude(exclude []string) Map { + + excluded := make(Map) + for k, v := range d { + var shouldInclude bool = true + for _, toExclude := range exclude { + if k == toExclude { + shouldInclude = false + break + } + } + if shouldInclude { + excluded[k] = v + } + } + + return excluded +} + +// Copy creates a shallow copy of the Obj. +func (m Map) Copy() Map { + copied := make(map[string]interface{}) + for k, v := range m { + copied[k] = v + } + return New(copied) +} + +// Merge blends the specified map with a copy of this map and returns the result. +// +// Keys that appear in both will be selected from the specified map. +// This method requires that the wrapped object be a map[string]interface{} +func (m Map) Merge(merge Map) Map { + return m.Copy().MergeHere(merge) +} + +// Merge blends the specified map with this map and returns the current map. +// +// Keys that appear in both will be selected from the specified map. The original map +// will be modified. This method requires that +// the wrapped object be a map[string]interface{} +func (m Map) MergeHere(merge Map) Map { + + for k, v := range merge { + m[k] = v + } + + return m + +} + +// Transform builds a new Obj giving the transformer a chance +// to change the keys and values as it goes. This method requires that +// the wrapped object be a map[string]interface{} +func (m Map) Transform(transformer func(key string, value interface{}) (string, interface{})) Map { + newMap := make(map[string]interface{}) + for k, v := range m { + modifiedKey, modifiedVal := transformer(k, v) + newMap[modifiedKey] = modifiedVal + } + return New(newMap) +} + +// TransformKeys builds a new map using the specified key mapping. +// +// Unspecified keys will be unaltered. +// This method requires that the wrapped object be a map[string]interface{} +func (m Map) TransformKeys(mapping map[string]string) Map { + return m.Transform(func(key string, value interface{}) (string, interface{}) { + + if newKey, ok := mapping[key]; ok { + return newKey, value + } + + return key, value + }) +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/security.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/security.go new file mode 100644 index 000000000..fdd6be9cf --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/security.go @@ -0,0 +1,14 @@ +package objx + +import ( + "crypto/sha1" + "encoding/hex" +) + +// HashWithKey hashes the specified string using the security +// key. +func HashWithKey(data, key string) string { + hash := sha1.New() + hash.Write([]byte(data + ":" + key)) + return hex.EncodeToString(hash.Sum(nil)) +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/tests.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/tests.go new file mode 100644 index 000000000..d9e0b479a --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/tests.go @@ -0,0 +1,17 @@ +package objx + +// Has gets whether there is something at the specified selector +// or not. +// +// If m is nil, Has will always return false. +func (m Map) Has(selector string) bool { + if m == nil { + return false + } + return !m.Get(selector).IsNil() +} + +// IsNil gets whether the data is nil or not. +func (v *Value) IsNil() bool { + return v == nil || v.data == nil +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/type_specific_codegen.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/type_specific_codegen.go new file mode 100644 index 000000000..f3ecb29b9 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/type_specific_codegen.go @@ -0,0 +1,2881 @@ +package objx + +/* + Inter (interface{} and []interface{}) + -------------------------------------------------- +*/ + +// Inter gets the value as a interface{}, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Inter(optionalDefault ...interface{}) interface{} { + if s, ok := v.data.(interface{}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInter gets the value as a interface{}. +// +// Panics if the object is not a interface{}. +func (v *Value) MustInter() interface{} { + return v.data.(interface{}) +} + +// InterSlice gets the value as a []interface{}, returns the optionalDefault +// value or nil if the value is not a []interface{}. +func (v *Value) InterSlice(optionalDefault ...[]interface{}) []interface{} { + if s, ok := v.data.([]interface{}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInterSlice gets the value as a []interface{}. +// +// Panics if the object is not a []interface{}. +func (v *Value) MustInterSlice() []interface{} { + return v.data.([]interface{}) +} + +// IsInter gets whether the object contained is a interface{} or not. +func (v *Value) IsInter() bool { + _, ok := v.data.(interface{}) + return ok +} + +// IsInterSlice gets whether the object contained is a []interface{} or not. +func (v *Value) IsInterSlice() bool { + _, ok := v.data.([]interface{}) + return ok +} + +// EachInter calls the specified callback for each object +// in the []interface{}. +// +// Panics if the object is the wrong type. +func (v *Value) EachInter(callback func(int, interface{}) bool) *Value { + + for index, val := range v.MustInterSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInter uses the specified decider function to select items +// from the []interface{}. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInter(decider func(int, interface{}) bool) *Value { + + var selected []interface{} + + v.EachInter(func(index int, val interface{}) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInter uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]interface{}. +func (v *Value) GroupInter(grouper func(int, interface{}) string) *Value { + + groups := make(map[string][]interface{}) + + v.EachInter(func(index int, val interface{}) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]interface{}, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInter uses the specified function to replace each interface{}s +// by iterating each item. The data in the returned result will be a +// []interface{} containing the replaced items. +func (v *Value) ReplaceInter(replacer func(int, interface{}) interface{}) *Value { + + arr := v.MustInterSlice() + replaced := make([]interface{}, len(arr)) + + v.EachInter(func(index int, val interface{}) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInter uses the specified collector function to collect a value +// for each of the interface{}s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInter(collector func(int, interface{}) interface{}) *Value { + + arr := v.MustInterSlice() + collected := make([]interface{}, len(arr)) + + v.EachInter(func(index int, val interface{}) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + MSI (map[string]interface{} and []map[string]interface{}) + -------------------------------------------------- +*/ + +// MSI gets the value as a map[string]interface{}, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) MSI(optionalDefault ...map[string]interface{}) map[string]interface{} { + if s, ok := v.data.(map[string]interface{}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustMSI gets the value as a map[string]interface{}. +// +// Panics if the object is not a map[string]interface{}. +func (v *Value) MustMSI() map[string]interface{} { + return v.data.(map[string]interface{}) +} + +// MSISlice gets the value as a []map[string]interface{}, returns the optionalDefault +// value or nil if the value is not a []map[string]interface{}. +func (v *Value) MSISlice(optionalDefault ...[]map[string]interface{}) []map[string]interface{} { + if s, ok := v.data.([]map[string]interface{}); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustMSISlice gets the value as a []map[string]interface{}. +// +// Panics if the object is not a []map[string]interface{}. +func (v *Value) MustMSISlice() []map[string]interface{} { + return v.data.([]map[string]interface{}) +} + +// IsMSI gets whether the object contained is a map[string]interface{} or not. +func (v *Value) IsMSI() bool { + _, ok := v.data.(map[string]interface{}) + return ok +} + +// IsMSISlice gets whether the object contained is a []map[string]interface{} or not. +func (v *Value) IsMSISlice() bool { + _, ok := v.data.([]map[string]interface{}) + return ok +} + +// EachMSI calls the specified callback for each object +// in the []map[string]interface{}. +// +// Panics if the object is the wrong type. +func (v *Value) EachMSI(callback func(int, map[string]interface{}) bool) *Value { + + for index, val := range v.MustMSISlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereMSI uses the specified decider function to select items +// from the []map[string]interface{}. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereMSI(decider func(int, map[string]interface{}) bool) *Value { + + var selected []map[string]interface{} + + v.EachMSI(func(index int, val map[string]interface{}) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupMSI uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]map[string]interface{}. +func (v *Value) GroupMSI(grouper func(int, map[string]interface{}) string) *Value { + + groups := make(map[string][]map[string]interface{}) + + v.EachMSI(func(index int, val map[string]interface{}) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]map[string]interface{}, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceMSI uses the specified function to replace each map[string]interface{}s +// by iterating each item. The data in the returned result will be a +// []map[string]interface{} containing the replaced items. +func (v *Value) ReplaceMSI(replacer func(int, map[string]interface{}) map[string]interface{}) *Value { + + arr := v.MustMSISlice() + replaced := make([]map[string]interface{}, len(arr)) + + v.EachMSI(func(index int, val map[string]interface{}) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectMSI uses the specified collector function to collect a value +// for each of the map[string]interface{}s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectMSI(collector func(int, map[string]interface{}) interface{}) *Value { + + arr := v.MustMSISlice() + collected := make([]interface{}, len(arr)) + + v.EachMSI(func(index int, val map[string]interface{}) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + ObjxMap ((Map) and [](Map)) + -------------------------------------------------- +*/ + +// ObjxMap gets the value as a (Map), returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) ObjxMap(optionalDefault ...(Map)) Map { + if s, ok := v.data.((Map)); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return New(nil) +} + +// MustObjxMap gets the value as a (Map). +// +// Panics if the object is not a (Map). +func (v *Value) MustObjxMap() Map { + return v.data.((Map)) +} + +// ObjxMapSlice gets the value as a [](Map), returns the optionalDefault +// value or nil if the value is not a [](Map). +func (v *Value) ObjxMapSlice(optionalDefault ...[](Map)) [](Map) { + if s, ok := v.data.([](Map)); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustObjxMapSlice gets the value as a [](Map). +// +// Panics if the object is not a [](Map). +func (v *Value) MustObjxMapSlice() [](Map) { + return v.data.([](Map)) +} + +// IsObjxMap gets whether the object contained is a (Map) or not. +func (v *Value) IsObjxMap() bool { + _, ok := v.data.((Map)) + return ok +} + +// IsObjxMapSlice gets whether the object contained is a [](Map) or not. +func (v *Value) IsObjxMapSlice() bool { + _, ok := v.data.([](Map)) + return ok +} + +// EachObjxMap calls the specified callback for each object +// in the [](Map). +// +// Panics if the object is the wrong type. +func (v *Value) EachObjxMap(callback func(int, Map) bool) *Value { + + for index, val := range v.MustObjxMapSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereObjxMap uses the specified decider function to select items +// from the [](Map). The object contained in the result will contain +// only the selected items. +func (v *Value) WhereObjxMap(decider func(int, Map) bool) *Value { + + var selected [](Map) + + v.EachObjxMap(func(index int, val Map) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupObjxMap uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][](Map). +func (v *Value) GroupObjxMap(grouper func(int, Map) string) *Value { + + groups := make(map[string][](Map)) + + v.EachObjxMap(func(index int, val Map) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([](Map), 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceObjxMap uses the specified function to replace each (Map)s +// by iterating each item. The data in the returned result will be a +// [](Map) containing the replaced items. +func (v *Value) ReplaceObjxMap(replacer func(int, Map) Map) *Value { + + arr := v.MustObjxMapSlice() + replaced := make([](Map), len(arr)) + + v.EachObjxMap(func(index int, val Map) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectObjxMap uses the specified collector function to collect a value +// for each of the (Map)s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectObjxMap(collector func(int, Map) interface{}) *Value { + + arr := v.MustObjxMapSlice() + collected := make([]interface{}, len(arr)) + + v.EachObjxMap(func(index int, val Map) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Bool (bool and []bool) + -------------------------------------------------- +*/ + +// Bool gets the value as a bool, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Bool(optionalDefault ...bool) bool { + if s, ok := v.data.(bool); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return false +} + +// MustBool gets the value as a bool. +// +// Panics if the object is not a bool. +func (v *Value) MustBool() bool { + return v.data.(bool) +} + +// BoolSlice gets the value as a []bool, returns the optionalDefault +// value or nil if the value is not a []bool. +func (v *Value) BoolSlice(optionalDefault ...[]bool) []bool { + if s, ok := v.data.([]bool); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustBoolSlice gets the value as a []bool. +// +// Panics if the object is not a []bool. +func (v *Value) MustBoolSlice() []bool { + return v.data.([]bool) +} + +// IsBool gets whether the object contained is a bool or not. +func (v *Value) IsBool() bool { + _, ok := v.data.(bool) + return ok +} + +// IsBoolSlice gets whether the object contained is a []bool or not. +func (v *Value) IsBoolSlice() bool { + _, ok := v.data.([]bool) + return ok +} + +// EachBool calls the specified callback for each object +// in the []bool. +// +// Panics if the object is the wrong type. +func (v *Value) EachBool(callback func(int, bool) bool) *Value { + + for index, val := range v.MustBoolSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereBool uses the specified decider function to select items +// from the []bool. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereBool(decider func(int, bool) bool) *Value { + + var selected []bool + + v.EachBool(func(index int, val bool) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupBool uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]bool. +func (v *Value) GroupBool(grouper func(int, bool) string) *Value { + + groups := make(map[string][]bool) + + v.EachBool(func(index int, val bool) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]bool, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceBool uses the specified function to replace each bools +// by iterating each item. The data in the returned result will be a +// []bool containing the replaced items. +func (v *Value) ReplaceBool(replacer func(int, bool) bool) *Value { + + arr := v.MustBoolSlice() + replaced := make([]bool, len(arr)) + + v.EachBool(func(index int, val bool) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectBool uses the specified collector function to collect a value +// for each of the bools in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectBool(collector func(int, bool) interface{}) *Value { + + arr := v.MustBoolSlice() + collected := make([]interface{}, len(arr)) + + v.EachBool(func(index int, val bool) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Str (string and []string) + -------------------------------------------------- +*/ + +// Str gets the value as a string, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Str(optionalDefault ...string) string { + if s, ok := v.data.(string); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return "" +} + +// MustStr gets the value as a string. +// +// Panics if the object is not a string. +func (v *Value) MustStr() string { + return v.data.(string) +} + +// StrSlice gets the value as a []string, returns the optionalDefault +// value or nil if the value is not a []string. +func (v *Value) StrSlice(optionalDefault ...[]string) []string { + if s, ok := v.data.([]string); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustStrSlice gets the value as a []string. +// +// Panics if the object is not a []string. +func (v *Value) MustStrSlice() []string { + return v.data.([]string) +} + +// IsStr gets whether the object contained is a string or not. +func (v *Value) IsStr() bool { + _, ok := v.data.(string) + return ok +} + +// IsStrSlice gets whether the object contained is a []string or not. +func (v *Value) IsStrSlice() bool { + _, ok := v.data.([]string) + return ok +} + +// EachStr calls the specified callback for each object +// in the []string. +// +// Panics if the object is the wrong type. +func (v *Value) EachStr(callback func(int, string) bool) *Value { + + for index, val := range v.MustStrSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereStr uses the specified decider function to select items +// from the []string. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereStr(decider func(int, string) bool) *Value { + + var selected []string + + v.EachStr(func(index int, val string) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupStr uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]string. +func (v *Value) GroupStr(grouper func(int, string) string) *Value { + + groups := make(map[string][]string) + + v.EachStr(func(index int, val string) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]string, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceStr uses the specified function to replace each strings +// by iterating each item. The data in the returned result will be a +// []string containing the replaced items. +func (v *Value) ReplaceStr(replacer func(int, string) string) *Value { + + arr := v.MustStrSlice() + replaced := make([]string, len(arr)) + + v.EachStr(func(index int, val string) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectStr uses the specified collector function to collect a value +// for each of the strings in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectStr(collector func(int, string) interface{}) *Value { + + arr := v.MustStrSlice() + collected := make([]interface{}, len(arr)) + + v.EachStr(func(index int, val string) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Int (int and []int) + -------------------------------------------------- +*/ + +// Int gets the value as a int, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Int(optionalDefault ...int) int { + if s, ok := v.data.(int); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustInt gets the value as a int. +// +// Panics if the object is not a int. +func (v *Value) MustInt() int { + return v.data.(int) +} + +// IntSlice gets the value as a []int, returns the optionalDefault +// value or nil if the value is not a []int. +func (v *Value) IntSlice(optionalDefault ...[]int) []int { + if s, ok := v.data.([]int); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustIntSlice gets the value as a []int. +// +// Panics if the object is not a []int. +func (v *Value) MustIntSlice() []int { + return v.data.([]int) +} + +// IsInt gets whether the object contained is a int or not. +func (v *Value) IsInt() bool { + _, ok := v.data.(int) + return ok +} + +// IsIntSlice gets whether the object contained is a []int or not. +func (v *Value) IsIntSlice() bool { + _, ok := v.data.([]int) + return ok +} + +// EachInt calls the specified callback for each object +// in the []int. +// +// Panics if the object is the wrong type. +func (v *Value) EachInt(callback func(int, int) bool) *Value { + + for index, val := range v.MustIntSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInt uses the specified decider function to select items +// from the []int. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInt(decider func(int, int) bool) *Value { + + var selected []int + + v.EachInt(func(index int, val int) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInt uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]int. +func (v *Value) GroupInt(grouper func(int, int) string) *Value { + + groups := make(map[string][]int) + + v.EachInt(func(index int, val int) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]int, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInt uses the specified function to replace each ints +// by iterating each item. The data in the returned result will be a +// []int containing the replaced items. +func (v *Value) ReplaceInt(replacer func(int, int) int) *Value { + + arr := v.MustIntSlice() + replaced := make([]int, len(arr)) + + v.EachInt(func(index int, val int) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInt uses the specified collector function to collect a value +// for each of the ints in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInt(collector func(int, int) interface{}) *Value { + + arr := v.MustIntSlice() + collected := make([]interface{}, len(arr)) + + v.EachInt(func(index int, val int) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Int8 (int8 and []int8) + -------------------------------------------------- +*/ + +// Int8 gets the value as a int8, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Int8(optionalDefault ...int8) int8 { + if s, ok := v.data.(int8); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustInt8 gets the value as a int8. +// +// Panics if the object is not a int8. +func (v *Value) MustInt8() int8 { + return v.data.(int8) +} + +// Int8Slice gets the value as a []int8, returns the optionalDefault +// value or nil if the value is not a []int8. +func (v *Value) Int8Slice(optionalDefault ...[]int8) []int8 { + if s, ok := v.data.([]int8); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInt8Slice gets the value as a []int8. +// +// Panics if the object is not a []int8. +func (v *Value) MustInt8Slice() []int8 { + return v.data.([]int8) +} + +// IsInt8 gets whether the object contained is a int8 or not. +func (v *Value) IsInt8() bool { + _, ok := v.data.(int8) + return ok +} + +// IsInt8Slice gets whether the object contained is a []int8 or not. +func (v *Value) IsInt8Slice() bool { + _, ok := v.data.([]int8) + return ok +} + +// EachInt8 calls the specified callback for each object +// in the []int8. +// +// Panics if the object is the wrong type. +func (v *Value) EachInt8(callback func(int, int8) bool) *Value { + + for index, val := range v.MustInt8Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInt8 uses the specified decider function to select items +// from the []int8. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInt8(decider func(int, int8) bool) *Value { + + var selected []int8 + + v.EachInt8(func(index int, val int8) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInt8 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]int8. +func (v *Value) GroupInt8(grouper func(int, int8) string) *Value { + + groups := make(map[string][]int8) + + v.EachInt8(func(index int, val int8) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]int8, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInt8 uses the specified function to replace each int8s +// by iterating each item. The data in the returned result will be a +// []int8 containing the replaced items. +func (v *Value) ReplaceInt8(replacer func(int, int8) int8) *Value { + + arr := v.MustInt8Slice() + replaced := make([]int8, len(arr)) + + v.EachInt8(func(index int, val int8) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInt8 uses the specified collector function to collect a value +// for each of the int8s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInt8(collector func(int, int8) interface{}) *Value { + + arr := v.MustInt8Slice() + collected := make([]interface{}, len(arr)) + + v.EachInt8(func(index int, val int8) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Int16 (int16 and []int16) + -------------------------------------------------- +*/ + +// Int16 gets the value as a int16, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Int16(optionalDefault ...int16) int16 { + if s, ok := v.data.(int16); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustInt16 gets the value as a int16. +// +// Panics if the object is not a int16. +func (v *Value) MustInt16() int16 { + return v.data.(int16) +} + +// Int16Slice gets the value as a []int16, returns the optionalDefault +// value or nil if the value is not a []int16. +func (v *Value) Int16Slice(optionalDefault ...[]int16) []int16 { + if s, ok := v.data.([]int16); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInt16Slice gets the value as a []int16. +// +// Panics if the object is not a []int16. +func (v *Value) MustInt16Slice() []int16 { + return v.data.([]int16) +} + +// IsInt16 gets whether the object contained is a int16 or not. +func (v *Value) IsInt16() bool { + _, ok := v.data.(int16) + return ok +} + +// IsInt16Slice gets whether the object contained is a []int16 or not. +func (v *Value) IsInt16Slice() bool { + _, ok := v.data.([]int16) + return ok +} + +// EachInt16 calls the specified callback for each object +// in the []int16. +// +// Panics if the object is the wrong type. +func (v *Value) EachInt16(callback func(int, int16) bool) *Value { + + for index, val := range v.MustInt16Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInt16 uses the specified decider function to select items +// from the []int16. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInt16(decider func(int, int16) bool) *Value { + + var selected []int16 + + v.EachInt16(func(index int, val int16) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInt16 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]int16. +func (v *Value) GroupInt16(grouper func(int, int16) string) *Value { + + groups := make(map[string][]int16) + + v.EachInt16(func(index int, val int16) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]int16, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInt16 uses the specified function to replace each int16s +// by iterating each item. The data in the returned result will be a +// []int16 containing the replaced items. +func (v *Value) ReplaceInt16(replacer func(int, int16) int16) *Value { + + arr := v.MustInt16Slice() + replaced := make([]int16, len(arr)) + + v.EachInt16(func(index int, val int16) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInt16 uses the specified collector function to collect a value +// for each of the int16s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInt16(collector func(int, int16) interface{}) *Value { + + arr := v.MustInt16Slice() + collected := make([]interface{}, len(arr)) + + v.EachInt16(func(index int, val int16) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Int32 (int32 and []int32) + -------------------------------------------------- +*/ + +// Int32 gets the value as a int32, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Int32(optionalDefault ...int32) int32 { + if s, ok := v.data.(int32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustInt32 gets the value as a int32. +// +// Panics if the object is not a int32. +func (v *Value) MustInt32() int32 { + return v.data.(int32) +} + +// Int32Slice gets the value as a []int32, returns the optionalDefault +// value or nil if the value is not a []int32. +func (v *Value) Int32Slice(optionalDefault ...[]int32) []int32 { + if s, ok := v.data.([]int32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInt32Slice gets the value as a []int32. +// +// Panics if the object is not a []int32. +func (v *Value) MustInt32Slice() []int32 { + return v.data.([]int32) +} + +// IsInt32 gets whether the object contained is a int32 or not. +func (v *Value) IsInt32() bool { + _, ok := v.data.(int32) + return ok +} + +// IsInt32Slice gets whether the object contained is a []int32 or not. +func (v *Value) IsInt32Slice() bool { + _, ok := v.data.([]int32) + return ok +} + +// EachInt32 calls the specified callback for each object +// in the []int32. +// +// Panics if the object is the wrong type. +func (v *Value) EachInt32(callback func(int, int32) bool) *Value { + + for index, val := range v.MustInt32Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInt32 uses the specified decider function to select items +// from the []int32. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInt32(decider func(int, int32) bool) *Value { + + var selected []int32 + + v.EachInt32(func(index int, val int32) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInt32 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]int32. +func (v *Value) GroupInt32(grouper func(int, int32) string) *Value { + + groups := make(map[string][]int32) + + v.EachInt32(func(index int, val int32) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]int32, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInt32 uses the specified function to replace each int32s +// by iterating each item. The data in the returned result will be a +// []int32 containing the replaced items. +func (v *Value) ReplaceInt32(replacer func(int, int32) int32) *Value { + + arr := v.MustInt32Slice() + replaced := make([]int32, len(arr)) + + v.EachInt32(func(index int, val int32) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInt32 uses the specified collector function to collect a value +// for each of the int32s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInt32(collector func(int, int32) interface{}) *Value { + + arr := v.MustInt32Slice() + collected := make([]interface{}, len(arr)) + + v.EachInt32(func(index int, val int32) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Int64 (int64 and []int64) + -------------------------------------------------- +*/ + +// Int64 gets the value as a int64, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Int64(optionalDefault ...int64) int64 { + if s, ok := v.data.(int64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustInt64 gets the value as a int64. +// +// Panics if the object is not a int64. +func (v *Value) MustInt64() int64 { + return v.data.(int64) +} + +// Int64Slice gets the value as a []int64, returns the optionalDefault +// value or nil if the value is not a []int64. +func (v *Value) Int64Slice(optionalDefault ...[]int64) []int64 { + if s, ok := v.data.([]int64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustInt64Slice gets the value as a []int64. +// +// Panics if the object is not a []int64. +func (v *Value) MustInt64Slice() []int64 { + return v.data.([]int64) +} + +// IsInt64 gets whether the object contained is a int64 or not. +func (v *Value) IsInt64() bool { + _, ok := v.data.(int64) + return ok +} + +// IsInt64Slice gets whether the object contained is a []int64 or not. +func (v *Value) IsInt64Slice() bool { + _, ok := v.data.([]int64) + return ok +} + +// EachInt64 calls the specified callback for each object +// in the []int64. +// +// Panics if the object is the wrong type. +func (v *Value) EachInt64(callback func(int, int64) bool) *Value { + + for index, val := range v.MustInt64Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereInt64 uses the specified decider function to select items +// from the []int64. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereInt64(decider func(int, int64) bool) *Value { + + var selected []int64 + + v.EachInt64(func(index int, val int64) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupInt64 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]int64. +func (v *Value) GroupInt64(grouper func(int, int64) string) *Value { + + groups := make(map[string][]int64) + + v.EachInt64(func(index int, val int64) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]int64, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceInt64 uses the specified function to replace each int64s +// by iterating each item. The data in the returned result will be a +// []int64 containing the replaced items. +func (v *Value) ReplaceInt64(replacer func(int, int64) int64) *Value { + + arr := v.MustInt64Slice() + replaced := make([]int64, len(arr)) + + v.EachInt64(func(index int, val int64) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectInt64 uses the specified collector function to collect a value +// for each of the int64s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectInt64(collector func(int, int64) interface{}) *Value { + + arr := v.MustInt64Slice() + collected := make([]interface{}, len(arr)) + + v.EachInt64(func(index int, val int64) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uint (uint and []uint) + -------------------------------------------------- +*/ + +// Uint gets the value as a uint, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uint(optionalDefault ...uint) uint { + if s, ok := v.data.(uint); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUint gets the value as a uint. +// +// Panics if the object is not a uint. +func (v *Value) MustUint() uint { + return v.data.(uint) +} + +// UintSlice gets the value as a []uint, returns the optionalDefault +// value or nil if the value is not a []uint. +func (v *Value) UintSlice(optionalDefault ...[]uint) []uint { + if s, ok := v.data.([]uint); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUintSlice gets the value as a []uint. +// +// Panics if the object is not a []uint. +func (v *Value) MustUintSlice() []uint { + return v.data.([]uint) +} + +// IsUint gets whether the object contained is a uint or not. +func (v *Value) IsUint() bool { + _, ok := v.data.(uint) + return ok +} + +// IsUintSlice gets whether the object contained is a []uint or not. +func (v *Value) IsUintSlice() bool { + _, ok := v.data.([]uint) + return ok +} + +// EachUint calls the specified callback for each object +// in the []uint. +// +// Panics if the object is the wrong type. +func (v *Value) EachUint(callback func(int, uint) bool) *Value { + + for index, val := range v.MustUintSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUint uses the specified decider function to select items +// from the []uint. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUint(decider func(int, uint) bool) *Value { + + var selected []uint + + v.EachUint(func(index int, val uint) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUint uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uint. +func (v *Value) GroupUint(grouper func(int, uint) string) *Value { + + groups := make(map[string][]uint) + + v.EachUint(func(index int, val uint) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uint, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUint uses the specified function to replace each uints +// by iterating each item. The data in the returned result will be a +// []uint containing the replaced items. +func (v *Value) ReplaceUint(replacer func(int, uint) uint) *Value { + + arr := v.MustUintSlice() + replaced := make([]uint, len(arr)) + + v.EachUint(func(index int, val uint) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUint uses the specified collector function to collect a value +// for each of the uints in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUint(collector func(int, uint) interface{}) *Value { + + arr := v.MustUintSlice() + collected := make([]interface{}, len(arr)) + + v.EachUint(func(index int, val uint) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uint8 (uint8 and []uint8) + -------------------------------------------------- +*/ + +// Uint8 gets the value as a uint8, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uint8(optionalDefault ...uint8) uint8 { + if s, ok := v.data.(uint8); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUint8 gets the value as a uint8. +// +// Panics if the object is not a uint8. +func (v *Value) MustUint8() uint8 { + return v.data.(uint8) +} + +// Uint8Slice gets the value as a []uint8, returns the optionalDefault +// value or nil if the value is not a []uint8. +func (v *Value) Uint8Slice(optionalDefault ...[]uint8) []uint8 { + if s, ok := v.data.([]uint8); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUint8Slice gets the value as a []uint8. +// +// Panics if the object is not a []uint8. +func (v *Value) MustUint8Slice() []uint8 { + return v.data.([]uint8) +} + +// IsUint8 gets whether the object contained is a uint8 or not. +func (v *Value) IsUint8() bool { + _, ok := v.data.(uint8) + return ok +} + +// IsUint8Slice gets whether the object contained is a []uint8 or not. +func (v *Value) IsUint8Slice() bool { + _, ok := v.data.([]uint8) + return ok +} + +// EachUint8 calls the specified callback for each object +// in the []uint8. +// +// Panics if the object is the wrong type. +func (v *Value) EachUint8(callback func(int, uint8) bool) *Value { + + for index, val := range v.MustUint8Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUint8 uses the specified decider function to select items +// from the []uint8. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUint8(decider func(int, uint8) bool) *Value { + + var selected []uint8 + + v.EachUint8(func(index int, val uint8) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUint8 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uint8. +func (v *Value) GroupUint8(grouper func(int, uint8) string) *Value { + + groups := make(map[string][]uint8) + + v.EachUint8(func(index int, val uint8) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uint8, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUint8 uses the specified function to replace each uint8s +// by iterating each item. The data in the returned result will be a +// []uint8 containing the replaced items. +func (v *Value) ReplaceUint8(replacer func(int, uint8) uint8) *Value { + + arr := v.MustUint8Slice() + replaced := make([]uint8, len(arr)) + + v.EachUint8(func(index int, val uint8) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUint8 uses the specified collector function to collect a value +// for each of the uint8s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUint8(collector func(int, uint8) interface{}) *Value { + + arr := v.MustUint8Slice() + collected := make([]interface{}, len(arr)) + + v.EachUint8(func(index int, val uint8) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uint16 (uint16 and []uint16) + -------------------------------------------------- +*/ + +// Uint16 gets the value as a uint16, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uint16(optionalDefault ...uint16) uint16 { + if s, ok := v.data.(uint16); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUint16 gets the value as a uint16. +// +// Panics if the object is not a uint16. +func (v *Value) MustUint16() uint16 { + return v.data.(uint16) +} + +// Uint16Slice gets the value as a []uint16, returns the optionalDefault +// value or nil if the value is not a []uint16. +func (v *Value) Uint16Slice(optionalDefault ...[]uint16) []uint16 { + if s, ok := v.data.([]uint16); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUint16Slice gets the value as a []uint16. +// +// Panics if the object is not a []uint16. +func (v *Value) MustUint16Slice() []uint16 { + return v.data.([]uint16) +} + +// IsUint16 gets whether the object contained is a uint16 or not. +func (v *Value) IsUint16() bool { + _, ok := v.data.(uint16) + return ok +} + +// IsUint16Slice gets whether the object contained is a []uint16 or not. +func (v *Value) IsUint16Slice() bool { + _, ok := v.data.([]uint16) + return ok +} + +// EachUint16 calls the specified callback for each object +// in the []uint16. +// +// Panics if the object is the wrong type. +func (v *Value) EachUint16(callback func(int, uint16) bool) *Value { + + for index, val := range v.MustUint16Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUint16 uses the specified decider function to select items +// from the []uint16. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUint16(decider func(int, uint16) bool) *Value { + + var selected []uint16 + + v.EachUint16(func(index int, val uint16) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUint16 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uint16. +func (v *Value) GroupUint16(grouper func(int, uint16) string) *Value { + + groups := make(map[string][]uint16) + + v.EachUint16(func(index int, val uint16) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uint16, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUint16 uses the specified function to replace each uint16s +// by iterating each item. The data in the returned result will be a +// []uint16 containing the replaced items. +func (v *Value) ReplaceUint16(replacer func(int, uint16) uint16) *Value { + + arr := v.MustUint16Slice() + replaced := make([]uint16, len(arr)) + + v.EachUint16(func(index int, val uint16) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUint16 uses the specified collector function to collect a value +// for each of the uint16s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUint16(collector func(int, uint16) interface{}) *Value { + + arr := v.MustUint16Slice() + collected := make([]interface{}, len(arr)) + + v.EachUint16(func(index int, val uint16) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uint32 (uint32 and []uint32) + -------------------------------------------------- +*/ + +// Uint32 gets the value as a uint32, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uint32(optionalDefault ...uint32) uint32 { + if s, ok := v.data.(uint32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUint32 gets the value as a uint32. +// +// Panics if the object is not a uint32. +func (v *Value) MustUint32() uint32 { + return v.data.(uint32) +} + +// Uint32Slice gets the value as a []uint32, returns the optionalDefault +// value or nil if the value is not a []uint32. +func (v *Value) Uint32Slice(optionalDefault ...[]uint32) []uint32 { + if s, ok := v.data.([]uint32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUint32Slice gets the value as a []uint32. +// +// Panics if the object is not a []uint32. +func (v *Value) MustUint32Slice() []uint32 { + return v.data.([]uint32) +} + +// IsUint32 gets whether the object contained is a uint32 or not. +func (v *Value) IsUint32() bool { + _, ok := v.data.(uint32) + return ok +} + +// IsUint32Slice gets whether the object contained is a []uint32 or not. +func (v *Value) IsUint32Slice() bool { + _, ok := v.data.([]uint32) + return ok +} + +// EachUint32 calls the specified callback for each object +// in the []uint32. +// +// Panics if the object is the wrong type. +func (v *Value) EachUint32(callback func(int, uint32) bool) *Value { + + for index, val := range v.MustUint32Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUint32 uses the specified decider function to select items +// from the []uint32. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUint32(decider func(int, uint32) bool) *Value { + + var selected []uint32 + + v.EachUint32(func(index int, val uint32) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUint32 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uint32. +func (v *Value) GroupUint32(grouper func(int, uint32) string) *Value { + + groups := make(map[string][]uint32) + + v.EachUint32(func(index int, val uint32) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uint32, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUint32 uses the specified function to replace each uint32s +// by iterating each item. The data in the returned result will be a +// []uint32 containing the replaced items. +func (v *Value) ReplaceUint32(replacer func(int, uint32) uint32) *Value { + + arr := v.MustUint32Slice() + replaced := make([]uint32, len(arr)) + + v.EachUint32(func(index int, val uint32) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUint32 uses the specified collector function to collect a value +// for each of the uint32s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUint32(collector func(int, uint32) interface{}) *Value { + + arr := v.MustUint32Slice() + collected := make([]interface{}, len(arr)) + + v.EachUint32(func(index int, val uint32) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uint64 (uint64 and []uint64) + -------------------------------------------------- +*/ + +// Uint64 gets the value as a uint64, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uint64(optionalDefault ...uint64) uint64 { + if s, ok := v.data.(uint64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUint64 gets the value as a uint64. +// +// Panics if the object is not a uint64. +func (v *Value) MustUint64() uint64 { + return v.data.(uint64) +} + +// Uint64Slice gets the value as a []uint64, returns the optionalDefault +// value or nil if the value is not a []uint64. +func (v *Value) Uint64Slice(optionalDefault ...[]uint64) []uint64 { + if s, ok := v.data.([]uint64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUint64Slice gets the value as a []uint64. +// +// Panics if the object is not a []uint64. +func (v *Value) MustUint64Slice() []uint64 { + return v.data.([]uint64) +} + +// IsUint64 gets whether the object contained is a uint64 or not. +func (v *Value) IsUint64() bool { + _, ok := v.data.(uint64) + return ok +} + +// IsUint64Slice gets whether the object contained is a []uint64 or not. +func (v *Value) IsUint64Slice() bool { + _, ok := v.data.([]uint64) + return ok +} + +// EachUint64 calls the specified callback for each object +// in the []uint64. +// +// Panics if the object is the wrong type. +func (v *Value) EachUint64(callback func(int, uint64) bool) *Value { + + for index, val := range v.MustUint64Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUint64 uses the specified decider function to select items +// from the []uint64. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUint64(decider func(int, uint64) bool) *Value { + + var selected []uint64 + + v.EachUint64(func(index int, val uint64) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUint64 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uint64. +func (v *Value) GroupUint64(grouper func(int, uint64) string) *Value { + + groups := make(map[string][]uint64) + + v.EachUint64(func(index int, val uint64) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uint64, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUint64 uses the specified function to replace each uint64s +// by iterating each item. The data in the returned result will be a +// []uint64 containing the replaced items. +func (v *Value) ReplaceUint64(replacer func(int, uint64) uint64) *Value { + + arr := v.MustUint64Slice() + replaced := make([]uint64, len(arr)) + + v.EachUint64(func(index int, val uint64) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUint64 uses the specified collector function to collect a value +// for each of the uint64s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUint64(collector func(int, uint64) interface{}) *Value { + + arr := v.MustUint64Slice() + collected := make([]interface{}, len(arr)) + + v.EachUint64(func(index int, val uint64) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Uintptr (uintptr and []uintptr) + -------------------------------------------------- +*/ + +// Uintptr gets the value as a uintptr, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Uintptr(optionalDefault ...uintptr) uintptr { + if s, ok := v.data.(uintptr); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustUintptr gets the value as a uintptr. +// +// Panics if the object is not a uintptr. +func (v *Value) MustUintptr() uintptr { + return v.data.(uintptr) +} + +// UintptrSlice gets the value as a []uintptr, returns the optionalDefault +// value or nil if the value is not a []uintptr. +func (v *Value) UintptrSlice(optionalDefault ...[]uintptr) []uintptr { + if s, ok := v.data.([]uintptr); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustUintptrSlice gets the value as a []uintptr. +// +// Panics if the object is not a []uintptr. +func (v *Value) MustUintptrSlice() []uintptr { + return v.data.([]uintptr) +} + +// IsUintptr gets whether the object contained is a uintptr or not. +func (v *Value) IsUintptr() bool { + _, ok := v.data.(uintptr) + return ok +} + +// IsUintptrSlice gets whether the object contained is a []uintptr or not. +func (v *Value) IsUintptrSlice() bool { + _, ok := v.data.([]uintptr) + return ok +} + +// EachUintptr calls the specified callback for each object +// in the []uintptr. +// +// Panics if the object is the wrong type. +func (v *Value) EachUintptr(callback func(int, uintptr) bool) *Value { + + for index, val := range v.MustUintptrSlice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereUintptr uses the specified decider function to select items +// from the []uintptr. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereUintptr(decider func(int, uintptr) bool) *Value { + + var selected []uintptr + + v.EachUintptr(func(index int, val uintptr) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupUintptr uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]uintptr. +func (v *Value) GroupUintptr(grouper func(int, uintptr) string) *Value { + + groups := make(map[string][]uintptr) + + v.EachUintptr(func(index int, val uintptr) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]uintptr, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceUintptr uses the specified function to replace each uintptrs +// by iterating each item. The data in the returned result will be a +// []uintptr containing the replaced items. +func (v *Value) ReplaceUintptr(replacer func(int, uintptr) uintptr) *Value { + + arr := v.MustUintptrSlice() + replaced := make([]uintptr, len(arr)) + + v.EachUintptr(func(index int, val uintptr) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectUintptr uses the specified collector function to collect a value +// for each of the uintptrs in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectUintptr(collector func(int, uintptr) interface{}) *Value { + + arr := v.MustUintptrSlice() + collected := make([]interface{}, len(arr)) + + v.EachUintptr(func(index int, val uintptr) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Float32 (float32 and []float32) + -------------------------------------------------- +*/ + +// Float32 gets the value as a float32, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Float32(optionalDefault ...float32) float32 { + if s, ok := v.data.(float32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustFloat32 gets the value as a float32. +// +// Panics if the object is not a float32. +func (v *Value) MustFloat32() float32 { + return v.data.(float32) +} + +// Float32Slice gets the value as a []float32, returns the optionalDefault +// value or nil if the value is not a []float32. +func (v *Value) Float32Slice(optionalDefault ...[]float32) []float32 { + if s, ok := v.data.([]float32); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustFloat32Slice gets the value as a []float32. +// +// Panics if the object is not a []float32. +func (v *Value) MustFloat32Slice() []float32 { + return v.data.([]float32) +} + +// IsFloat32 gets whether the object contained is a float32 or not. +func (v *Value) IsFloat32() bool { + _, ok := v.data.(float32) + return ok +} + +// IsFloat32Slice gets whether the object contained is a []float32 or not. +func (v *Value) IsFloat32Slice() bool { + _, ok := v.data.([]float32) + return ok +} + +// EachFloat32 calls the specified callback for each object +// in the []float32. +// +// Panics if the object is the wrong type. +func (v *Value) EachFloat32(callback func(int, float32) bool) *Value { + + for index, val := range v.MustFloat32Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereFloat32 uses the specified decider function to select items +// from the []float32. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereFloat32(decider func(int, float32) bool) *Value { + + var selected []float32 + + v.EachFloat32(func(index int, val float32) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupFloat32 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]float32. +func (v *Value) GroupFloat32(grouper func(int, float32) string) *Value { + + groups := make(map[string][]float32) + + v.EachFloat32(func(index int, val float32) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]float32, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceFloat32 uses the specified function to replace each float32s +// by iterating each item. The data in the returned result will be a +// []float32 containing the replaced items. +func (v *Value) ReplaceFloat32(replacer func(int, float32) float32) *Value { + + arr := v.MustFloat32Slice() + replaced := make([]float32, len(arr)) + + v.EachFloat32(func(index int, val float32) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectFloat32 uses the specified collector function to collect a value +// for each of the float32s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectFloat32(collector func(int, float32) interface{}) *Value { + + arr := v.MustFloat32Slice() + collected := make([]interface{}, len(arr)) + + v.EachFloat32(func(index int, val float32) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Float64 (float64 and []float64) + -------------------------------------------------- +*/ + +// Float64 gets the value as a float64, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Float64(optionalDefault ...float64) float64 { + if s, ok := v.data.(float64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustFloat64 gets the value as a float64. +// +// Panics if the object is not a float64. +func (v *Value) MustFloat64() float64 { + return v.data.(float64) +} + +// Float64Slice gets the value as a []float64, returns the optionalDefault +// value or nil if the value is not a []float64. +func (v *Value) Float64Slice(optionalDefault ...[]float64) []float64 { + if s, ok := v.data.([]float64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustFloat64Slice gets the value as a []float64. +// +// Panics if the object is not a []float64. +func (v *Value) MustFloat64Slice() []float64 { + return v.data.([]float64) +} + +// IsFloat64 gets whether the object contained is a float64 or not. +func (v *Value) IsFloat64() bool { + _, ok := v.data.(float64) + return ok +} + +// IsFloat64Slice gets whether the object contained is a []float64 or not. +func (v *Value) IsFloat64Slice() bool { + _, ok := v.data.([]float64) + return ok +} + +// EachFloat64 calls the specified callback for each object +// in the []float64. +// +// Panics if the object is the wrong type. +func (v *Value) EachFloat64(callback func(int, float64) bool) *Value { + + for index, val := range v.MustFloat64Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereFloat64 uses the specified decider function to select items +// from the []float64. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereFloat64(decider func(int, float64) bool) *Value { + + var selected []float64 + + v.EachFloat64(func(index int, val float64) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupFloat64 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]float64. +func (v *Value) GroupFloat64(grouper func(int, float64) string) *Value { + + groups := make(map[string][]float64) + + v.EachFloat64(func(index int, val float64) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]float64, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceFloat64 uses the specified function to replace each float64s +// by iterating each item. The data in the returned result will be a +// []float64 containing the replaced items. +func (v *Value) ReplaceFloat64(replacer func(int, float64) float64) *Value { + + arr := v.MustFloat64Slice() + replaced := make([]float64, len(arr)) + + v.EachFloat64(func(index int, val float64) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectFloat64 uses the specified collector function to collect a value +// for each of the float64s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectFloat64(collector func(int, float64) interface{}) *Value { + + arr := v.MustFloat64Slice() + collected := make([]interface{}, len(arr)) + + v.EachFloat64(func(index int, val float64) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Complex64 (complex64 and []complex64) + -------------------------------------------------- +*/ + +// Complex64 gets the value as a complex64, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Complex64(optionalDefault ...complex64) complex64 { + if s, ok := v.data.(complex64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustComplex64 gets the value as a complex64. +// +// Panics if the object is not a complex64. +func (v *Value) MustComplex64() complex64 { + return v.data.(complex64) +} + +// Complex64Slice gets the value as a []complex64, returns the optionalDefault +// value or nil if the value is not a []complex64. +func (v *Value) Complex64Slice(optionalDefault ...[]complex64) []complex64 { + if s, ok := v.data.([]complex64); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustComplex64Slice gets the value as a []complex64. +// +// Panics if the object is not a []complex64. +func (v *Value) MustComplex64Slice() []complex64 { + return v.data.([]complex64) +} + +// IsComplex64 gets whether the object contained is a complex64 or not. +func (v *Value) IsComplex64() bool { + _, ok := v.data.(complex64) + return ok +} + +// IsComplex64Slice gets whether the object contained is a []complex64 or not. +func (v *Value) IsComplex64Slice() bool { + _, ok := v.data.([]complex64) + return ok +} + +// EachComplex64 calls the specified callback for each object +// in the []complex64. +// +// Panics if the object is the wrong type. +func (v *Value) EachComplex64(callback func(int, complex64) bool) *Value { + + for index, val := range v.MustComplex64Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereComplex64 uses the specified decider function to select items +// from the []complex64. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereComplex64(decider func(int, complex64) bool) *Value { + + var selected []complex64 + + v.EachComplex64(func(index int, val complex64) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupComplex64 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]complex64. +func (v *Value) GroupComplex64(grouper func(int, complex64) string) *Value { + + groups := make(map[string][]complex64) + + v.EachComplex64(func(index int, val complex64) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]complex64, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceComplex64 uses the specified function to replace each complex64s +// by iterating each item. The data in the returned result will be a +// []complex64 containing the replaced items. +func (v *Value) ReplaceComplex64(replacer func(int, complex64) complex64) *Value { + + arr := v.MustComplex64Slice() + replaced := make([]complex64, len(arr)) + + v.EachComplex64(func(index int, val complex64) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectComplex64 uses the specified collector function to collect a value +// for each of the complex64s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectComplex64(collector func(int, complex64) interface{}) *Value { + + arr := v.MustComplex64Slice() + collected := make([]interface{}, len(arr)) + + v.EachComplex64(func(index int, val complex64) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} + +/* + Complex128 (complex128 and []complex128) + -------------------------------------------------- +*/ + +// Complex128 gets the value as a complex128, returns the optionalDefault +// value or a system default object if the value is the wrong type. +func (v *Value) Complex128(optionalDefault ...complex128) complex128 { + if s, ok := v.data.(complex128); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return 0 +} + +// MustComplex128 gets the value as a complex128. +// +// Panics if the object is not a complex128. +func (v *Value) MustComplex128() complex128 { + return v.data.(complex128) +} + +// Complex128Slice gets the value as a []complex128, returns the optionalDefault +// value or nil if the value is not a []complex128. +func (v *Value) Complex128Slice(optionalDefault ...[]complex128) []complex128 { + if s, ok := v.data.([]complex128); ok { + return s + } + if len(optionalDefault) == 1 { + return optionalDefault[0] + } + return nil +} + +// MustComplex128Slice gets the value as a []complex128. +// +// Panics if the object is not a []complex128. +func (v *Value) MustComplex128Slice() []complex128 { + return v.data.([]complex128) +} + +// IsComplex128 gets whether the object contained is a complex128 or not. +func (v *Value) IsComplex128() bool { + _, ok := v.data.(complex128) + return ok +} + +// IsComplex128Slice gets whether the object contained is a []complex128 or not. +func (v *Value) IsComplex128Slice() bool { + _, ok := v.data.([]complex128) + return ok +} + +// EachComplex128 calls the specified callback for each object +// in the []complex128. +// +// Panics if the object is the wrong type. +func (v *Value) EachComplex128(callback func(int, complex128) bool) *Value { + + for index, val := range v.MustComplex128Slice() { + carryon := callback(index, val) + if carryon == false { + break + } + } + + return v + +} + +// WhereComplex128 uses the specified decider function to select items +// from the []complex128. The object contained in the result will contain +// only the selected items. +func (v *Value) WhereComplex128(decider func(int, complex128) bool) *Value { + + var selected []complex128 + + v.EachComplex128(func(index int, val complex128) bool { + shouldSelect := decider(index, val) + if shouldSelect == false { + selected = append(selected, val) + } + return true + }) + + return &Value{data: selected} + +} + +// GroupComplex128 uses the specified grouper function to group the items +// keyed by the return of the grouper. The object contained in the +// result will contain a map[string][]complex128. +func (v *Value) GroupComplex128(grouper func(int, complex128) string) *Value { + + groups := make(map[string][]complex128) + + v.EachComplex128(func(index int, val complex128) bool { + group := grouper(index, val) + if _, ok := groups[group]; !ok { + groups[group] = make([]complex128, 0) + } + groups[group] = append(groups[group], val) + return true + }) + + return &Value{data: groups} + +} + +// ReplaceComplex128 uses the specified function to replace each complex128s +// by iterating each item. The data in the returned result will be a +// []complex128 containing the replaced items. +func (v *Value) ReplaceComplex128(replacer func(int, complex128) complex128) *Value { + + arr := v.MustComplex128Slice() + replaced := make([]complex128, len(arr)) + + v.EachComplex128(func(index int, val complex128) bool { + replaced[index] = replacer(index, val) + return true + }) + + return &Value{data: replaced} + +} + +// CollectComplex128 uses the specified collector function to collect a value +// for each of the complex128s in the slice. The data returned will be a +// []interface{}. +func (v *Value) CollectComplex128(collector func(int, complex128) interface{}) *Value { + + arr := v.MustComplex128Slice() + collected := make([]interface{}, len(arr)) + + v.EachComplex128(func(index int, val complex128) bool { + collected[index] = collector(index, val) + return true + }) + + return &Value{data: collected} +} diff --git a/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/value.go b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/value.go new file mode 100644 index 000000000..7aaef06b1 --- /dev/null +++ b/vendor/github.com/stretchr/testify/vendor/github.com/stretchr/objx/value.go @@ -0,0 +1,13 @@ +package objx + +// Value provides methods for extracting interface{} data in various +// types. +type Value struct { + // data contains the raw data being managed by this Value + data interface{} +} + +// Data returns the raw data contained by this Value +func (v *Value) Data() interface{} { + return v.data +} diff --git a/webapp/components/admin_console/custom_integrations_settings.jsx b/webapp/components/admin_console/custom_integrations_settings.jsx index 3b5c51171..e6bcb4b32 100644 --- a/webapp/components/admin_console/custom_integrations_settings.jsx +++ b/webapp/components/admin_console/custom_integrations_settings.jsx @@ -185,7 +185,7 @@ export default class WebhookSettings extends AdminSettings { helpText={ <FormattedHTMLMessage id='admin.service.userAccessTokensDescription' - defaultMessage='When true, users can create <a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">user access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create user access tokens, go to the <strong>System Console > Users</strong> page.' + defaultMessage='When true, users can create <a href="https://about.mattermost.com/default-user-access-tokens" target="_blank">user access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create personal access tokens or to search users by token ID, go to the <strong>System Console > Users</strong> page.' /> } value={this.state.enableUserAccessTokens} diff --git a/webapp/components/admin_console/developer_settings.jsx b/webapp/components/admin_console/developer_settings.jsx index c7ffd18d5..6a8f49dbd 100644 --- a/webapp/components/admin_console/developer_settings.jsx +++ b/webapp/components/admin_console/developer_settings.jsx @@ -3,10 +3,13 @@ import React from 'react'; +import * as Utils from 'utils/utils.jsx'; + import AdminSettings from './admin_settings.jsx'; import BooleanSetting from './boolean_setting.jsx'; -import {FormattedMessage} from 'react-intl'; +import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; import SettingsGroup from './settings_group.jsx'; +import TextSetting from './text_setting.jsx'; export default class DeveloperSettings extends AdminSettings { constructor(props) { @@ -20,6 +23,7 @@ export default class DeveloperSettings extends AdminSettings { getConfigFromState(config) { config.ServiceSettings.EnableTesting = this.state.enableTesting; config.ServiceSettings.EnableDeveloper = this.state.enableDeveloper; + config.ServiceSettings.AllowedUntrustedInternalConnections = this.state.allowedUntrustedInternalConnections; return config; } @@ -27,7 +31,8 @@ export default class DeveloperSettings extends AdminSettings { getStateFromConfig(config) { return { enableTesting: config.ServiceSettings.EnableTesting, - enableDeveloper: config.ServiceSettings.EnableDeveloper + enableDeveloper: config.ServiceSettings.EnableDeveloper, + allowedUntrustedInternalConnections: config.ServiceSettings.AllowedUntrustedInternalConnections }; } @@ -77,6 +82,24 @@ export default class DeveloperSettings extends AdminSettings { value={this.state.enableDeveloper} onChange={this.handleChange} /> + <TextSetting + id='allowedUntrustedInternalConnections' + label={ + <FormattedMessage + id='admin.service.internalConnectionsTitle' + defaultMessage='Allow untrusted internal connections to: ' + /> + } + placeholder={Utils.localizeMessage('admin.service.internalConnectionsEx', 'webhooks.internal.example.com 127.0.0.1 10.0.16.0/28')} + helpText={ + <FormattedHTMLMessage + id='admin.service.internalConnectionsDesc' + defaultMessage='In testing environments, such as when developing integrations locally on a development machine, use this setting to specify domains, IP addresses, or CIDR notations to allow internal connections. <b>Not recommended for use in production</b>, since this can allow a user to extract confidential data from your server or internal network.<br /><br />By default, user-supplied URLs such as those used for Open Graph metadata, webhooks, or slash commands will not be allowed to connect to reserved IP addresses including loopback or link-local addresses used for internal networks. Push notification, OAuth 2.0 and WebRTC server URLs are trusted and not affected by this setting.' + /> + } + value={this.state.allowedUntrustedInternalConnections} + onChange={this.handleChange} + /> </SettingsGroup> ); } diff --git a/webapp/components/admin_console/server_logs/logs.jsx b/webapp/components/admin_console/server_logs/logs.jsx index b60a66ce6..b0d8b38ac 100644 --- a/webapp/components/admin_console/server_logs/logs.jsx +++ b/webapp/components/admin_console/server_logs/logs.jsx @@ -96,7 +96,7 @@ export default class Logs extends React.PureComponent { <div className='banner__content'> <FormattedMessage id='admin.logs.bannerDesc' - defaultMessage='To look up users by User ID, go to Reporting > Users and paste the ID into the search filter.' + defaultMessage='To look up users by User ID or Token ID, go to Reporting > Users and paste the ID into the search filter.' /> </div> </div> diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx index 42e66fd3a..f89c18745 100644 --- a/webapp/components/channel_header.jsx +++ b/webapp/components/channel_header.jsx @@ -286,10 +286,9 @@ export default class ChannelHeader extends React.Component { </Popover> ); let channelTitle = channel.display_name; - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); - const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); const isDirect = (this.state.channel.type === Constants.DM_CHANNEL); const isGroup = (this.state.channel.type === Constants.GM_CHANNEL); let webrtc; @@ -533,7 +532,7 @@ export default class ChannelHeader extends React.Component { /> ); - if (ChannelUtils.canManageMembers(channel, isSystemAdmin, isTeamAdmin, isChannelAdmin)) { + if (ChannelUtils.canManageMembers(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { dropdownContents.push( <li key='add_members' @@ -594,26 +593,7 @@ export default class ChannelHeader extends React.Component { } } - const deleteOption = ( - <li - key='delete_channel' - role='presentation' - > - <ToggleModalButton - id='channelDelete' - role='menuitem' - dialogType={DeleteChannelModal} - dialogProps={{channel}} - > - <FormattedMessage - id='channel_header.delete' - defaultMessage='Delete Channel' - /> - </ToggleModalButton> - </li> - ); - - if (ChannelUtils.showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { + if (ChannelUtils.showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { dropdownContents.push( <li key='divider-2' @@ -679,8 +659,25 @@ export default class ChannelHeader extends React.Component { ); } - if (ChannelUtils.showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin, this.state.userCount)) { - dropdownContents.push(deleteOption); + if (ChannelUtils.showDeleteOptionForCurrentUser(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { + dropdownContents.push( + <li + key='delete_channel' + role='presentation' + > + <ToggleModalButton + id='channelDelete' + role='menuitem' + dialogType={DeleteChannelModal} + dialogProps={{channel}} + > + <FormattedMessage + id='channel_header.delete' + defaultMessage='Delete Channel' + /> + </ToggleModalButton> + </li> + ); } const canLeave = channel.type === Constants.PRIVATE_CHANNEL ? this.state.userCount > 1 : true; diff --git a/webapp/components/channel_members_dropdown/channel_members_dropdown.jsx b/webapp/components/channel_members_dropdown/channel_members_dropdown.jsx index f8ff3a4f6..fbad6cde1 100644 --- a/webapp/components/channel_members_dropdown/channel_members_dropdown.jsx +++ b/webapp/components/channel_members_dropdown/channel_members_dropdown.jsx @@ -95,7 +95,7 @@ export default class ChannelMembersDropdown extends React.Component { // Checks if the current user has the power to remove this member from the channel. canRemoveMember() { - return canManageMembers(this.props.channel, UserStore.isSystemAdminForCurrentUser(), TeamStore.isTeamAdminForCurrentTeam(), ChannelStore.isChannelAdminForCurrentChannel()); + return canManageMembers(this.props.channel, ChannelStore.isChannelAdminForCurrentChannel(), TeamStore.isTeamAdminForCurrentTeam(), UserStore.isSystemAdminForCurrentUser()); } render() { diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx index d361f6b7c..f991b7599 100644 --- a/webapp/components/channel_members_modal.jsx +++ b/webapp/components/channel_members_modal.jsx @@ -38,7 +38,7 @@ export default class ChannelMembersModal extends React.Component { const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); let addMembersButton = null; - if (canManageMembers(this.state.channel, isSystemAdmin, isTeamAdmin, isChannelAdmin) && this.state.channel.name !== Constants.DEFAULT_CHANNEL) { + if (canManageMembers(this.state.channel, isChannelAdmin, isTeamAdmin, isSystemAdmin) && this.state.channel.name !== Constants.DEFAULT_CHANNEL) { addMembersButton = ( <a id='showInviteModal' diff --git a/webapp/components/more_channels/more_channels.jsx b/webapp/components/more_channels/more_channels.jsx index 45833ead2..ecc7aecf8 100644 --- a/webapp/components/more_channels/more_channels.jsx +++ b/webapp/components/more_channels/more_channels.jsx @@ -165,10 +165,10 @@ export default class MoreChannels extends React.Component { </p> ); - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); - if (!showCreateOption(Constants.OPEN_CHANNEL, isAdmin, isSystemAdmin)) { + if (!showCreateOption(Constants.OPEN_CHANNEL, isTeamAdmin, isSystemAdmin)) { createNewChannelButton = null; createChannelHelpText = null; } diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index 0217dc15c..25c458523 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -287,7 +287,6 @@ export default class Navbar extends React.Component { }; createDropdown(channel, channelTitle, isSystemAdmin, isTeamAdmin, isChannelAdmin, isDirect, isGroup, popoverContent) { - const isAdmin = isSystemAdmin || isTeamAdmin; const infoIcon = Constants.INFO_ICON_SVG; if (channel) { @@ -434,7 +433,7 @@ export default class Navbar extends React.Component { </li> ); - if (ChannelUtils.canManageMembers(channel, isSystemAdmin, isTeamAdmin, isChannelAdmin)) { + if (ChannelUtils.canManageMembers(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { manageMembersOption = ( <li key='manage_members' @@ -492,7 +491,7 @@ export default class Navbar extends React.Component { </li> ); - if (ChannelUtils.showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { + if (ChannelUtils.showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { setChannelHeaderOption = ( <li role='presentation'> <a @@ -539,7 +538,7 @@ export default class Navbar extends React.Component { ); } - if (ChannelUtils.showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin, this.state.userCount)) { + if (ChannelUtils.showDeleteOptionForCurrentUser(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { deleteChannelOption = ( <li role='presentation'> <ToggleModalButton diff --git a/webapp/components/new_channel_modal/new_channel_modal.jsx b/webapp/components/new_channel_modal/new_channel_modal.jsx index 60c0271a7..721defe08 100644 --- a/webapp/components/new_channel_modal/new_channel_modal.jsx +++ b/webapp/components/new_channel_modal/new_channel_modal.jsx @@ -192,13 +192,11 @@ export default class NewChannelModal extends React.PureComponent { </a> ); - const isAdmin = this.props.isTeamAdmin || this.props.isSystemAdmin; - - if (!ChannelUtils.showCreateOption(Constants.OPEN_CHANNEL, isAdmin, this.props.isSystemAdmin)) { + if (!ChannelUtils.showCreateOption(Constants.OPEN_CHANNEL, this.props.isTeamAdmin, this.props.isSystemAdmin)) { createPublicChannelLink = null; } - if (!ChannelUtils.showCreateOption(Constants.PRIVATE_CHANNEL, isAdmin, this.props.isSystemAdmin)) { + if (!ChannelUtils.showCreateOption(Constants.PRIVATE_CHANNEL, this.props.isTeamAdmin, this.props.isSystemAdmin)) { createPrivateChannelLink = null; } diff --git a/webapp/components/popover_list_members/popover_list_members.jsx b/webapp/components/popover_list_members/popover_list_members.jsx index e706a1cec..eca0fa5c2 100644 --- a/webapp/components/popover_list_members/popover_list_members.jsx +++ b/webapp/components/popover_list_members/popover_list_members.jsx @@ -52,7 +52,7 @@ export default class PopoverListMembers extends React.Component { } componentDidUpdate() { - $('.member-list__popover .popover-content').perfectScrollbar(); + $('.member-list__popover .popover-content .more-modal__body').perfectScrollbar(); } handleShowDirectChannel(teammate, e) { @@ -86,6 +86,7 @@ export default class PopoverListMembers extends React.Component { } render() { + let popoverButton; const popoverHtml = []; const members = this.props.members; const teamMembers = UserStore.getProfilesUsernameMap(); @@ -124,8 +125,8 @@ export default class PopoverListMembers extends React.Component { > <ProfilePicture src={Client4.getProfilePictureUrl(m.id, m.last_picture_update)} - width='32' - height='32' + width='40' + height='40' /> <div className='more-modal__details'> <div @@ -156,7 +157,7 @@ export default class PopoverListMembers extends React.Component { /> ); - const manageMembers = canManageMembers(this.props.channel, isSystemAdmin, isTeamAdmin, isChannelAdmin); + const manageMembers = canManageMembers(this.props.channel, isChannelAdmin, isTeamAdmin, isSystemAdmin); const isDefaultChannel = ChannelStore.isDefault(this.props.channel); if ((manageMembers === false && isDefaultChannel === false) || isDefaultChannel) { @@ -168,13 +169,13 @@ export default class PopoverListMembers extends React.Component { ); } - popoverHtml.push( + popoverButton = ( <div - className='more-modal__row more-modal__row--button' + className='more-modal__button' key={'popover-member-more'} > <button - className='btn btn-primary' + className='btn btn-link' onClick={this.showMembersModal} > {membersName} @@ -193,7 +194,7 @@ export default class PopoverListMembers extends React.Component { const title = ( <FormattedMessage id='members_popover.title' - defaultMessage='Members' + defaultMessage='Channel Members' /> ); @@ -275,14 +276,12 @@ export default class PopoverListMembers extends React.Component { id='member-list-popover' > <div className='more-modal__header'> - <span - className='icon icon__members' - dangerouslySetInnerHTML={{__html: membersIcon}} - aria-hidden='true' - /> {title} </div> - <div className='more-modal__list'>{popoverHtml}</div> + <div className='more-modal__body'> + <div className='more-modal__list'>{popoverHtml}</div> + </div> + {popoverButton} </Popover> </Overlay> {channelMembersModal} diff --git a/webapp/components/post_view/post_info/post_info.jsx b/webapp/components/post_view/post_info/post_info.jsx index 5f3701761..21492b92f 100644 --- a/webapp/components/post_view/post_info/post_info.jsx +++ b/webapp/components/post_view/post_info/post_info.jsx @@ -11,6 +11,7 @@ import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; import {emitEmojiPosted} from 'actions/post_actions.jsx'; import Constants from 'utils/constants.jsx'; +import {Posts} from 'mattermost-redux/constants'; import React from 'react'; import PropTypes from 'prop-types'; @@ -215,7 +216,7 @@ export default class PostInfo extends React.PureComponent { } let visibleMessage; - if (isEphemeral && !this.props.compactDisplay) { + if (isEphemeral && !this.props.compactDisplay && post.state !== Posts.POST_DELETED) { visibleMessage = ( <span className='post__visibility'> <FormattedMessage diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx index 31afa7b6d..a4db89835 100644 --- a/webapp/components/rhs_comment.jsx +++ b/webapp/components/rhs_comment.jsx @@ -11,7 +11,7 @@ import FailedPostOptions from 'components/post_view/failed_post_options'; import DotMenu from 'components/dot_menu'; import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx'; -import {addReaction} from 'actions/post_actions.jsx'; +import {addReaction, emitEmojiPosted} from 'actions/post_actions.jsx'; import TeamStore from 'stores/team_store.jsx'; @@ -166,6 +166,7 @@ export default class RhsComment extends React.Component { this.setState({showEmojiPicker: false}); const emojiName = emoji.name || emoji.aliases[0]; addReaction(this.props.post.channel_id, this.props.post.id, emojiName); + emitEmojiPosted(emojiName); this.handleDropdownOpened(false); } diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx index 620a87e94..ed88fff05 100644 --- a/webapp/components/rhs_root_post.jsx +++ b/webapp/components/rhs_root_post.jsx @@ -15,7 +15,7 @@ import ChannelStore from 'stores/channel_store.jsx'; import UserStore from 'stores/user_store.jsx'; import TeamStore from 'stores/team_store.jsx'; -import {addReaction} from 'actions/post_actions.jsx'; +import {addReaction, emitEmojiPosted} from 'actions/post_actions.jsx'; import * as Utils from 'utils/utils.jsx'; import * as PostUtils from 'utils/post_utils.jsx'; @@ -159,6 +159,7 @@ export default class RhsRootPost extends React.Component { this.setState({showEmojiPicker: false}); const emojiName = emoji.name || emoji.aliases[0]; addReaction(this.props.post.channel_id, this.props.post.id, emojiName); + emitEmojiPosted(emojiName); this.handleDropdownOpened(false); } diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx index 581fe0536..11ceaac73 100644 --- a/webapp/components/search_bar.jsx +++ b/webapp/components/search_bar.jsx @@ -15,7 +15,9 @@ import {getFlaggedPosts, performSearch} from 'actions/post_actions.jsx'; import {FormattedMessage, FormattedHTMLMessage} from 'react-intl'; -var ActionTypes = Constants.ActionTypes; +const ActionTypes = Constants.ActionTypes; +const KeyCodes = Constants.KeyCodes; + import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap'; import PropTypes from 'prop-types'; @@ -110,8 +112,11 @@ export default class SearchBar extends React.Component { }); } - handleKeyDown() { - // This is just to prevent a JS error + handleKeyDown(e) { + if (e.which === KeyCodes.ESCAPE) { + e.stopPropagation(); + e.preventDefault(); + } } handleChange(e) { diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx index 9b4625ea5..31d6825c8 100644 --- a/webapp/components/sidebar.jsx +++ b/webapp/components/sidebar.jsx @@ -714,7 +714,7 @@ export default class Sidebar extends React.Component { /> ); - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); let createPublicChannelIcon = ( @@ -753,11 +753,11 @@ export default class Sidebar extends React.Component { </OverlayTrigger> ); - if (!ChannelUtils.showCreateOption(Constants.OPEN_CHANNEL, isAdmin, isSystemAdmin)) { + if (!ChannelUtils.showCreateOption(Constants.OPEN_CHANNEL, isTeamAdmin, isSystemAdmin)) { createPublicChannelIcon = null; } - if (!ChannelUtils.showCreateOption(Constants.PRIVATE_CHANNEL, isAdmin, isSystemAdmin)) { + if (!ChannelUtils.showCreateOption(Constants.PRIVATE_CHANNEL, isTeamAdmin, isSystemAdmin)) { createPrivateChannelIcon = null; } diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index 2dfd2b5c5..8f50a23ef 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -323,6 +323,7 @@ export default class SuggestionBox extends React.Component { e.preventDefault(); } else if (e.which === KeyCodes.ESCAPE) { GlobalActions.emitClearSuggestions(this.suggestionId); + e.preventDefault(); e.stopPropagation(); } else if (this.props.onKeyDown) { this.props.onKeyDown(e); diff --git a/webapp/components/user_settings/manage_languages.jsx b/webapp/components/user_settings/manage_languages.jsx index 2cb38068b..f83733c12 100644 --- a/webapp/components/user_settings/manage_languages.jsx +++ b/webapp/components/user_settings/manage_languages.jsx @@ -16,18 +16,14 @@ export default class ManageLanguage extends React.Component { constructor(props) { super(props); - this.setupInitialState = this.setupInitialState.bind(this); this.setLanguage = this.setLanguage.bind(this); this.changeLanguage = this.changeLanguage.bind(this); this.submitUser = this.submitUser.bind(this); - this.state = this.setupInitialState(props); - } - setupInitialState(props) { - var user = props.user; - return { - locale: user.locale + this.state = { + locale: props.locale }; } + setLanguage(e) { this.setState({locale: e.target.value}); } @@ -134,5 +130,6 @@ export default class ManageLanguage extends React.Component { ManageLanguage.propTypes = { user: PropTypes.object.isRequired, + locale: PropTypes.string.isRequired, updateSection: PropTypes.func.isRequired }; diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx index aef82f229..74a939994 100644 --- a/webapp/components/user_settings/user_settings_display.jsx +++ b/webapp/components/user_settings/user_settings_display.jsx @@ -555,14 +555,15 @@ export default class UserSettingsDisplay extends React.Component { ); } - const userLocale = this.props.user.locale; + let userLocale = this.props.user.locale; if (this.props.activeSection === 'languages') { if (!I18n.isLanguageAvailable(userLocale)) { - this.props.user.locale = global.window.mm_config.DefaultClientLocale; + userLocale = global.window.mm_config.DefaultClientLocale; } languagesSection = ( <ManageLanguages user={this.props.user} + locale={userLocale} updateSection={(e) => { this.updateSection(''); e.preventDefault(); diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json index dd0237769..1bea0e0f5 100755 --- a/webapp/i18n/en.json +++ b/webapp/i18n/en.json @@ -587,7 +587,7 @@ "admin.log.locationPlaceholder": "Enter your file location", "admin.log.locationTitle": "File Log Directory:", "admin.log.logSettings": "Log Settings", - "admin.logs.bannerDesc": "To look up users by User ID, go to Reporting > Users and paste the ID into the search filter.", + "admin.logs.bannerDesc": "To look up users by User ID or Token ID, go to Reporting > Users and paste the ID into the search filter.", "admin.logs.reload": "Reload", "admin.logs.title": "Server Logs", "admin.manage_roles.additionalRoles": "Select additional permissions for the account. <a href=\"https://about.mattermost.com/default-permissions\" target=\"_blank\">Read more about roles and permissions</a>.", @@ -805,6 +805,9 @@ "admin.service.iconTitle": "Enable integrations to override profile picture icons:", "admin.service.insecureTlsDesc": "When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.", "admin.service.insecureTlsTitle": "Enable Insecure Outgoing Connections: ", + "admin.service.internalConnectionsDesc": "In testing environments, such as when developing integrations locally on a development machine, use this setting to specify domains, IP addresses, or CIDR notations to allow internal connections. <b>Not recommended for use in production</b>, since this can allow a user to extract confidential data from your server or internal network.<br /><br />By default, user-supplied URLs such as those used for Open Graph metadata, webhooks, or slash commands will not be allowed to connect to reserved IP addresses including loopback or link-local addresses used for internal networks. Push notification, OAuth 2.0 and WebRTC server URLs are trusted and not affected by this setting.", + "admin.service.internalConnectionsTitle": "Allow untrusted internal connections to: ", + "admin.service.internalConnectionsEx": "webhooks.internal.example.com 127.0.0.1 10.0.16.0/28", "admin.service.integrationAdmin": "Restrict managing integrations to Admins:", "admin.service.integrationAdminDesc": "When true, webhooks and slash commands can only be created, edited and viewed by Team and System Admins, and OAuth 2.0 applications by System Admins. Integrations are available to all users after they have been created by the Admin.", "admin.service.letsEncryptCertificateCacheFile": "Let's Encrypt Certificate Cache File:", @@ -841,7 +844,7 @@ "admin.service.useLetsEncrypt": "Use Let's Encrypt:", "admin.service.useLetsEncryptDescription": "Enable the automatic retreval of certificates from the Let's Encrypt. The certificate will be retrieved when a client attempts to connect from a new domain. This will work with multiple domains.", "admin.service.userAccessTokensDescLabel": "Name: ", - "admin.service.userAccessTokensDescription": "When true, users can create <a href=\"https://about.mattermost.com/default-user-access-tokens\" target=\"_blank\">personal access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create personal access tokens, go to the <strong>System Console > Users</strong> page.", + "admin.service.userAccessTokensDescription": "When true, users can create <a href=\"https://about.mattermost.com/default-user-access-tokens\" target=\"_blank\">personal access tokens</a> for integrations in <strong>Account Settings > Security</strong>. They can be used to authenticate against the API and give full access to the account.<br/><br/>To manage who can create personal access tokens or to search users by token ID, go to the <strong>System Console > Users</strong> page.", "admin.service.userAccessTokensIdLabel": "Token ID: ", "admin.service.userAccessTokensTitle": "Enable Personal Access Tokens: ", "admin.service.webSessionDays": "Session length AD/LDAP and email (days):", @@ -1761,7 +1764,7 @@ "member_list.noUsersAdd": "No users to add.", "members_popover.manageMembers": "Manage Members", "members_popover.msg": "Message", - "members_popover.title": "Members", + "members_popover.title": "Channel Members", "members_popover.viewMembers": "View Members", "mfa.confirm.complete": "<strong>Set up complete!</strong>", "mfa.confirm.okay": "Okay", diff --git a/webapp/package.json b/webapp/package.json index a3dd87b58..04a73d58a 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,7 +23,7 @@ "jquery": "3.2.1", "key-mirror": "1.0.1", "localforage": "1.5.0", - "marked": "mattermost/marked#9f0c8b96fd5efedb99f5fc45cca9ac7684b5aa69", + "marked": "mattermost/marked#5194fc037b35036910c6542b04bb471fe56b27a9", "match-at": "0.1.0", "mattermost-redux": "mattermost/mattermost-redux#webapp-4.1", "object-assign": "4.1.1", diff --git a/webapp/sass/components/_modal.scss b/webapp/sass/components/_modal.scss index b98ec85c9..5ecc0d43e 100644 --- a/webapp/sass/components/_modal.scss +++ b/webapp/sass/components/_modal.scss @@ -546,6 +546,15 @@ } +.more-modal__button { + .btn { + font-size: .9em; + font-weight: 600; + height: 39px; + width: 100%; + } +} + .more-modal__list { display: flex; flex-direction: column; @@ -565,10 +574,6 @@ .more-modal__row { padding: 5px 10px; } - - .more-modal__name { - font-weight: normal; - } } .more-modal__image { diff --git a/webapp/sass/components/_popover.scss b/webapp/sass/components/_popover.scss index 90573c7ec..be0403292 100644 --- a/webapp/sass/components/_popover.scss +++ b/webapp/sass/components/_popover.scss @@ -2,8 +2,9 @@ @import 'compass/css3/transition'; .popover { - @include border-radius($border-rad); + @include border-radius($border-rad * 2); color: lighten($black, 25%); + font-family: inherit; padding: 0; &.bottom, @@ -11,12 +12,18 @@ &.top, &.left { > .arrow { + display: none; + &:after { border-color: transparent; } } } + &.bottom { + margin-top: 10px; + } + .popover-title { @include border-radius(0); background: alpha-color($black, .05); @@ -219,10 +226,19 @@ } .member-list__popover { + margin-left: -95px; max-width: initial; + .more-modal__body { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + max-height: 275px; + overflow: auto; + position: relative; + } + .more-modal__header { - padding: 14px 0; + padding: 14px 20px; .icon { @include opacity(.8); @@ -232,26 +248,36 @@ } span { - font-weight: 500; + font-weight: 600; + } + } + + .more-modal__button { + .btn { + height: 42px; } } .more-modal__list { .more-modal__row { - border-bottom: none; - border-top: 1px solid; + border: none; cursor: pointer; - height: 45px; + height: 50px; + margin: 1px 0; overflow: hidden; - padding: 5px 19px 5px 17px; + padding: 6px 19px 0 20px; &:hover { border-left: 3px solid transparent; - padding-left: 14px; + padding-left: 17px; .more-modal__actions { @include opacity(1); } + + .more-modal__name { + font-weight: 600; + } } &.more-modal__row--button { @@ -271,15 +297,18 @@ } .more-modal__name { - line-height: 33px; + font-size: 13.5px; + font-weight: normal; } .more-modal__image { margin: 0; + position: relative; + top: -1px; } .more-modal__details { - line-height: 32px; + line-height: 39px; padding-left: 30px; } @@ -287,11 +316,16 @@ @include opacity(0); line-height: 42px; margin: 0; + + .icon { + position: relative; + top: 2px; + } } } .popover-content { - max-height: 350px; + max-height: 380px; padding: 0; position: relative; width: 260px; diff --git a/webapp/tests/components/post_view/post_info/__snapshots__/post_info.test.jsx.snap b/webapp/tests/components/post_view/post_info/__snapshots__/post_info.test.jsx.snap new file mode 100644 index 000000000..427467d68 --- /dev/null +++ b/webapp/tests/components/post_view/post_info/__snapshots__/post_info.test.jsx.snap @@ -0,0 +1,488 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/post_view/PostInfo should match snapshot 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={false} + isFlagged={false} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__reply" + > + <Connect(DotMenu) + commentCount={0} + handleCommentClick={[Function]} + handleDropdownOpened={[Function]} + idCount={-1} + idPrefix="center" + isFlagged={false} + post={ + Object { + "channel_id": "g6139tbospd18cmxroesdk3kkc", + "create_at": 1502715365009, + "delete_at": 0, + "edit_at": 1502715372443, + "hashtags": "", + "id": "e584uzbwwpny9kengqayx5ayzw", + "is_pinned": false, + "message": "post message", + "original_id": "", + "parent_id": "", + "pending_post_id": "", + "props": Object {}, + "root_id": "", + "type": "", + "update_at": 1502715372443, + "user_id": "b4pfxi8sn78y8yq7phzxxfor7h", + } + } + /> + <span> + <EmojiPickerOverlay + container={[Function]} + onEmojiClick={[Function]} + onHide={[Function]} + rightOffset={7} + show={false} + spaceRequiredAbove={422} + spaceRequiredBelow={436} + target={[Function]} + /> + <a + className="reacticon__container" + href="#" + onClick={[Function]} + > + <i + className="fa fa-smile-o" + /> + </a> + </span> + <CommentIcon + commentCount={0} + handleCommentClick={[Function]} + id="g6139tbospd18cmxroesdk3kkc_e584uzbwwpny9kengqayx5ayzw" + idCount={-1} + idPrefix="commentIcon" + searchStyle="" + /> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, compact display 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={false} + isFlagged={false} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__reply" + > + <Connect(DotMenu) + commentCount={0} + handleCommentClick={[Function]} + handleDropdownOpened={[Function]} + idCount={-1} + idPrefix="center" + isFlagged={false} + post={ + Object { + "channel_id": "g6139tbospd18cmxroesdk3kkc", + "create_at": 1502715365009, + "delete_at": 0, + "edit_at": 1502715372443, + "hashtags": "", + "id": "e584uzbwwpny9kengqayx5ayzw", + "is_pinned": false, + "message": "post message", + "original_id": "", + "parent_id": "", + "pending_post_id": "", + "props": Object {}, + "root_id": "", + "type": "", + "update_at": 1502715372443, + "user_id": "b4pfxi8sn78y8yq7phzxxfor7h", + } + } + /> + <span> + <EmojiPickerOverlay + container={[Function]} + onEmojiClick={[Function]} + onHide={[Function]} + rightOffset={7} + show={false} + spaceRequiredAbove={422} + spaceRequiredBelow={436} + target={[Function]} + /> + <a + className="reacticon__container" + href="#" + onClick={[Function]} + > + <i + className="fa fa-smile-o" + /> + </a> + </span> + <CommentIcon + commentCount={0} + handleCommentClick={[Function]} + id="g6139tbospd18cmxroesdk3kkc_e584uzbwwpny9kengqayx5ayzw" + idCount={-1} + idPrefix="commentIcon" + searchStyle="" + /> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, ephemeral deleted post 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={true} + isFlagged={true} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__remove" + > + <a + className="post__remove theme" + href="#" + onClick={[Function]} + type="button" + > + × + </a> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, ephemeral post 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={true} + isFlagged={true} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + <span + className="post__visibility" + > + <FormattedMessage + defaultMessage="(Only visible to you)" + id="post_info.message.visible" + values={Object {}} + /> + </span> + </div> + <div + className="col col__remove" + > + <a + className="post__remove theme" + href="#" + onClick={[Function]} + type="button" + > + × + </a> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, flagged post 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={false} + isFlagged={true} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__reply" + > + <Connect(DotMenu) + commentCount={0} + handleCommentClick={[Function]} + handleDropdownOpened={[Function]} + idCount={-1} + idPrefix="center" + isFlagged={true} + post={ + Object { + "channel_id": "g6139tbospd18cmxroesdk3kkc", + "create_at": 1502715365009, + "delete_at": 0, + "edit_at": 1502715372443, + "hashtags": "", + "id": "e584uzbwwpny9kengqayx5ayzw", + "is_pinned": false, + "message": "post message", + "original_id": "", + "parent_id": "", + "pending_post_id": "", + "props": Object {}, + "root_id": "", + "type": "", + "update_at": 1502715372443, + "user_id": "b4pfxi8sn78y8yq7phzxxfor7h", + } + } + /> + <span> + <EmojiPickerOverlay + container={[Function]} + onEmojiClick={[Function]} + onHide={[Function]} + rightOffset={7} + show={false} + spaceRequiredAbove={422} + spaceRequiredBelow={436} + target={[Function]} + /> + <a + className="reacticon__container" + href="#" + onClick={[Function]} + > + <i + className="fa fa-smile-o" + /> + </a> + </span> + <CommentIcon + commentCount={0} + handleCommentClick={[Function]} + id="g6139tbospd18cmxroesdk3kkc_e584uzbwwpny9kengqayx5ayzw" + idCount={-1} + idPrefix="commentIcon" + searchStyle="" + /> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, military time 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={true} + /> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={false} + isFlagged={false} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__reply" + > + <Connect(DotMenu) + commentCount={0} + handleCommentClick={[Function]} + handleDropdownOpened={[Function]} + idCount={-1} + idPrefix="center" + isFlagged={false} + post={ + Object { + "channel_id": "g6139tbospd18cmxroesdk3kkc", + "create_at": 1502715365009, + "delete_at": 0, + "edit_at": 1502715372443, + "hashtags": "", + "id": "e584uzbwwpny9kengqayx5ayzw", + "is_pinned": false, + "message": "post message", + "original_id": "", + "parent_id": "", + "pending_post_id": "", + "props": Object {}, + "root_id": "", + "type": "", + "update_at": 1502715372443, + "user_id": "b4pfxi8sn78y8yq7phzxxfor7h", + } + } + /> + <CommentIcon + commentCount={0} + handleCommentClick={[Function]} + id="g6139tbospd18cmxroesdk3kkc_e584uzbwwpny9kengqayx5ayzw" + idCount={-1} + idPrefix="commentIcon" + searchStyle="" + /> + </div> +</div> +`; + +exports[`components/post_view/PostInfo should match snapshot, pinned post 1`] = ` +<div + className="post__header--info" +> + <div + className="col" + > + <PostTime + eventTime={1502715365009} + postId="e584uzbwwpny9kengqayx5ayzw" + useMilitaryTime={false} + /> + <span + className="post__pinned-badge" + > + <FormattedMessage + defaultMessage="Pinned" + id="post_info.pinned" + values={Object {}} + /> + </span> + <PostFlagIcon + idCount={-1} + idPrefix="centerPostFlag" + isEphemeral={false} + isFlagged={true} + postId="e584uzbwwpny9kengqayx5ayzw" + /> + </div> + <div + className="col col__reply" + > + <Connect(DotMenu) + commentCount={0} + handleCommentClick={[Function]} + handleDropdownOpened={[Function]} + idCount={-1} + idPrefix="center" + isFlagged={true} + post={ + Object { + "channel_id": "g6139tbospd18cmxroesdk3kkc", + "create_at": 1502715365009, + "delete_at": 0, + "edit_at": 1502715372443, + "hashtags": "", + "id": "e584uzbwwpny9kengqayx5ayzw", + "is_pinned": true, + "message": "post message", + "original_id": "", + "parent_id": "", + "pending_post_id": "", + "props": Object {}, + "root_id": "", + "type": "", + "update_at": 1502715372443, + "user_id": "b4pfxi8sn78y8yq7phzxxfor7h", + } + } + /> + <span> + <EmojiPickerOverlay + container={[Function]} + onEmojiClick={[Function]} + onHide={[Function]} + rightOffset={7} + show={false} + spaceRequiredAbove={422} + spaceRequiredBelow={436} + target={[Function]} + /> + <a + className="reacticon__container" + href="#" + onClick={[Function]} + > + <i + className="fa fa-smile-o" + /> + </a> + </span> + <CommentIcon + commentCount={0} + handleCommentClick={[Function]} + id="g6139tbospd18cmxroesdk3kkc_e584uzbwwpny9kengqayx5ayzw" + idCount={-1} + idPrefix="commentIcon" + searchStyle="" + /> + </div> +</div> +`; diff --git a/webapp/tests/components/post_view/post_info/post_info.test.jsx b/webapp/tests/components/post_view/post_info/post_info.test.jsx new file mode 100644 index 000000000..d254226eb --- /dev/null +++ b/webapp/tests/components/post_view/post_info/post_info.test.jsx @@ -0,0 +1,225 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import Constants from 'utils/constants.jsx'; +import PostInfo from 'components/post_view/post_info/post_info.jsx'; +import {Posts} from 'mattermost-redux/constants'; + +const post = { + channel_id: 'g6139tbospd18cmxroesdk3kkc', + create_at: 1502715365009, + delete_at: 0, + edit_at: 1502715372443, + hashtags: '', + id: 'e584uzbwwpny9kengqayx5ayzw', + is_pinned: false, + message: 'post message', + original_id: '', + parent_id: '', + pending_post_id: '', + props: {}, + root_id: '', + type: '', + update_at: 1502715372443, + user_id: 'b4pfxi8sn78y8yq7phzxxfor7h' +}; + +describe('components/post_view/PostInfo', () => { + afterEach(() => { + global.window.mm_config = null; + global.window.EnableEmojiPicker = null; + }); + + test('should match snapshot', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={false} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, compact display', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={false} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, military time', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'false'; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={true} + isFlagged={false} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, flagged post', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={true} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, pinned post', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + post.is_pinned = true; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={true} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, ephemeral post', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + post.is_pinned = false; + post.type = Constants.PostTypes.EPHEMERAL; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={true} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('should match snapshot, ephemeral deleted post', () => { + function emptyFunction() {} //eslint-disable-line no-empty-function + + global.window.mm_config = {}; + global.window.mm_config.EnableEmojiPicker = 'true'; + + post.type = Constants.PostTypes.EPHEMERAL; + post.state = Posts.POST_DELETED; + + const wrapper = shallow( + <PostInfo + post={post} + handleCommentClick={emptyFunction} + handleDropdownOpened={emptyFunction} + compactDisplay={false} + lastPostCount={0} + replyCount={0} + getPostList={emptyFunction} + useMilitaryTime={false} + isFlagged={true} + actions={{ + removePost: emptyFunction, + addReaction: emptyFunction + }} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); +});
\ No newline at end of file diff --git a/webapp/tests/utils/channel_utils.test.jsx b/webapp/tests/utils/channel_utils.test.jsx new file mode 100644 index 000000000..74a9339e2 --- /dev/null +++ b/webapp/tests/utils/channel_utils.test.jsx @@ -0,0 +1,782 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from 'utils/channel_utils.jsx'; +import Constants from 'utils/constants.jsx'; + +describe('Channel Utils', () => { + describe('showDeleteOption', () => { + test('all users can delete channels on unlicensed instances', () => { + global.window.mm_license = {IsLicensed: 'false'}; + expect(Utils.showDeleteOptionForCurrentUser(null, true, true, true)). + toEqual(true); + }); + + test('users cannot delete default channels', () => { + global.window.mm_license = {IsLicensed: 'true'}; + const channel = {name: Constants.DEFAULT_CHANNEL}; + expect(Utils.showDeleteOptionForCurrentUser(channel, true, true, true)). + toEqual(false); + }); + + test('system admins can delete private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('system admins can delete private channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('system admins can delete public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('system admins can delete public channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can delete private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('system admins or team admins can delete private channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can delete public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('system admins or team admins can delete public channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can delete private channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, true, false)). + toEqual(true); + }); + + test('system admins or team admins can delete public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can delete public channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, true, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can delete private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, true, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can delete public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can delete private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can delete public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can delete private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can delete public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('channel, team, and system admins can delete private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(false); + }); + + test('any member can delete public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelDeletion: Constants.PERMISSIONS_ALL}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(true); + }); + + test('any member can delete private channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelDeletion: Constants.PERMISSIONS_ALL}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showDeleteOptionForCurrentUser(channel, false, false, false)). + toEqual(true); + }); + }); + + describe('showManagementOptions', () => { + test('all users can manage channel options on unlicensed instances', () => { + global.window.mm_license = {IsLicensed: 'false'}; + expect(Utils.showManagementOptions(null, true, true, true)). + toEqual(true); + }); + + test('system admins can manage channel options in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('system admins can manage channel options in private channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('system admins can manage channel options in public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('system admins can manage channel options in public channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can manage channel options in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('system admins or team admins can manage channel options in private channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can manage channel options in public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('system admins or team admins can manage channel options in public channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can manage channel options in private channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, true, false)). + toEqual(true); + }); + + test('system admins or team admins can manage channel options in public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in public channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, true, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, true, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel options in public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('channel, team, and system admins can manage channel options in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(false); + }); + + test('any member can manage channel options in public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelManagement: Constants.PERMISSIONS_ALL}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(true); + }); + + test('any member can manage channel options in private channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManagement: Constants.PERMISSIONS_ALL}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.showManagementOptions(channel, false, false, false)). + toEqual(true); + }); + }); + + describe('showCreateOption', () => { + test('all users can create new channels on unlicensed instances', () => { + global.window.mm_license = {IsLicensed: 'false'}; + expect(Utils.showCreateOption(null, true, true)). + toEqual(true); + }); + + test('system admins can create new private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, true)). + toEqual(true); + }); + + test('system admins can create new private channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, false)). + toEqual(false); + }); + + test('system admins can create new public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, true)). + toEqual(true); + }); + + test('system admins can create new public channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, false)). + toEqual(false); + }); + + test('system admins or team admins can create new private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, true)). + toEqual(true); + }); + + test('system admins or team admins can create new private channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, false)). + toEqual(false); + }); + + test('system admins or team admins can create new public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, true)). + toEqual(true); + }); + + test('system admins or team admins can create new public channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, false)). + toEqual(false); + }); + + test('system admins or team admins can create new private channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, true, false)). + toEqual(true); + }); + + test('system admins or team admins can create new public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_TEAM_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can create new public channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can create new public channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can create new private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can create new public channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can create new private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, true)). + toEqual(true); + }); + + test('any member can create new public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPublicChannelCreation: Constants.PERMISSIONS_ALL}; + + expect(Utils.showCreateOption(Constants.OPEN_CHANNEL, false, false)). + toEqual(true); + }); + + test('any member can create new private channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelCreation: Constants.PERMISSIONS_ALL}; + + expect(Utils.showCreateOption(Constants.PRIVATE_CHANNEL, false, false)). + toEqual(true); + }); + }); + + describe('canManageMembers', () => { + test('all users can manage channel members on unlicensed instances', () => { + global.window.mm_license = {IsLicensed: 'false'}; + expect(Utils.canManageMembers(null, true, true, true)). + toEqual(true); + }); + + test('system admins can manage channel members in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, true)). + toEqual(true); + }); + + test('system admins can manage channel members in private channels, user is not system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_SYSTEM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can manage channel members in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, true)). + toEqual(true); + }); + + test('system admins or team admins can manage channel members in private channels, user is not system admin or team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, false)). + toEqual(false); + }); + + test('system admins or team admins can manage channel members in private channels, user is team admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_TEAM_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel members in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, true, false, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel members in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, true, false)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel members in private channels, user is system admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, true)). + toEqual(true); + }); + + test('channel, team, and system admins can manage channel members in private channels, user is channel admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_CHANNEL_ADMIN}; + + const channel = { + name: 'fakeChannelName', + type: Constants.PRIVATE_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, false)). + toEqual(false); + }); + + test('any member can manage channel members in public channels, user is not admin test', () => { + global.window.mm_license = {IsLicensed: 'true'}; + global.window.mm_config = {RestrictPrivateChannelManageMembers: Constants.PERMISSIONS_ALL}; + + const channel = { + name: 'fakeChannelName', + type: Constants.OPEN_CHANNEL + }; + expect(Utils.canManageMembers(channel, false, false, false)). + toEqual(true); + }); + }); +});
\ No newline at end of file diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx index baf6c4fb1..f85b0cf87 100644 --- a/webapp/utils/channel_intro_messages.jsx +++ b/webapp/utils/channel_intro_messages.jsx @@ -160,12 +160,12 @@ export function createOffTopicIntroMessage(channel, centeredIntro) { /> ); - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); - const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); let setHeaderButton = createSetHeaderButton(channel); - if (!showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { + if (!showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { setHeaderButton = null; } @@ -199,20 +199,20 @@ export function createDefaultIntroMessage(channel, centeredIntro) { </a> ); - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); - const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); if (global.window.mm_license.IsLicensed === 'true') { if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { inviteModalLink = null; - } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { inviteModalLink = null; } } let setHeaderButton = createSetHeaderButton(channel); - if (!showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { + if (!showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { setHeaderButton = null; } @@ -321,12 +321,12 @@ export function createStandardIntroMessage(channel, centeredIntro) { ); } - const isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser(); - const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); const isChannelAdmin = ChannelStore.isChannelAdminForCurrentChannel(); + const isTeamAdmin = TeamStore.isTeamAdminForCurrentTeam(); + const isSystemAdmin = UserStore.isSystemAdminForCurrentUser(); let setHeaderButton = createSetHeaderButton(channel); - if (!showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin)) { + if (!showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin)) { setHeaderButton = null; } diff --git a/webapp/utils/channel_utils.jsx b/webapp/utils/channel_utils.jsx index c29cea386..d9887ff89 100644 --- a/webapp/utils/channel_utils.jsx +++ b/webapp/utils/channel_utils.jsx @@ -138,7 +138,7 @@ export function getChannelDisplayName(channel) { return channel.display_name; } -export function showCreateOption(channelType, isAdmin, isSystemAdmin) { +export function showCreateOption(channelType, isTeamAdmin, isSystemAdmin) { if (global.window.mm_license.IsLicensed !== 'true') { return true; } @@ -146,13 +146,13 @@ export function showCreateOption(channelType, isAdmin, isSystemAdmin) { if (channelType === Constants.OPEN_CHANNEL) { if (global.window.mm_config.RestrictPublicChannelCreation === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; - } else if (global.window.mm_config.RestrictPublicChannelCreation === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictPublicChannelCreation === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } } else if (channelType === Constants.PRIVATE_CHANNEL) { if (global.window.mm_config.RestrictPrivateChannelCreation === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; - } else if (global.window.mm_config.RestrictPrivateChannelCreation === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + } else if (global.window.mm_config.RestrictPrivateChannelCreation === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } } @@ -160,29 +160,30 @@ export function showCreateOption(channelType, isAdmin, isSystemAdmin) { return true; } -export function showManagementOptions(channel, isAdmin, isSystemAdmin, isChannelAdmin) { +export function showManagementOptions(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin) { if (global.window.mm_license.IsLicensed !== 'true') { + // policies are only enforced in enterprise editions return true; } if (channel.type === Constants.OPEN_CHANNEL) { - if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_CHANNEL_ADMIN && !(isChannelAdmin || isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isAdmin) { + if (global.window.mm_config.RestrictPublicChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } } else if (channel.type === Constants.PRIVATE_CHANNEL) { - if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_CHANNEL_ADMIN && !(isChannelAdmin || isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManagement === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } } @@ -190,36 +191,35 @@ export function showManagementOptions(channel, isAdmin, isSystemAdmin, isChannel return true; } -export function showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin, userCount) { +export function showDeleteOptionForCurrentUser(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin) { if (global.window.mm_license.IsLicensed !== 'true') { + // policies are only enforced in enterprise editions return true; } if (ChannelStore.isDefault(channel)) { + // can't delete default channels, no matter who you are return false; } if (channel.type === Constants.OPEN_CHANNEL) { - if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_CHANNEL_ADMIN && !(isChannelAdmin || isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isAdmin) { + if (global.window.mm_config.RestrictPublicChannelDeletion === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } } else if (channel.type === Constants.PRIVATE_CHANNEL) { - if (userCount === 1) { - return true; - } - if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_CHANNEL_ADMIN && !(isChannelAdmin || isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) { + if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isAdmin) { + if (global.window.mm_config.RestrictPrivateChannelDeletion === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } } @@ -227,19 +227,19 @@ export function showDeleteOption(channel, isAdmin, isSystemAdmin, isChannelAdmin return true; } -export function canManageMembers(channel, isSystemAdmin, isTeamAdmin, isChannelAdmin) { +export function canManageMembers(channel, isChannelAdmin, isTeamAdmin, isSystemAdmin) { if (global.window.mm_license.IsLicensed !== 'true') { return true; } if (channel.type === Constants.PRIVATE_CHANNEL) { - if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_CHANNEL_ADMIN && !(isChannelAdmin || isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_TEAM_ADMIN && !isTeamAdmin && !isSystemAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_TEAM_ADMIN && !(isTeamAdmin || isSystemAdmin)) { return false; } - if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_CHANNEL_ADMIN && !isChannelAdmin && !isTeamAdmin && !isSystemAdmin) { + if (global.window.mm_config.RestrictPrivateChannelManageMembers === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) { return false; } } diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx index 60151fc8e..8f7a72809 100644 --- a/webapp/utils/constants.jsx +++ b/webapp/utils/constants.jsx @@ -492,7 +492,7 @@ export const Constants = { MENTIONS_ICON_SVG: "<svg width='20px' height='20px' viewBox='0 0 20 20' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-1057.000000, -31.000000)' fill='inherit'> <g> <g transform='translate(1049.000000, 22.000000)'> <path d='M17.4296875,15.8867188 C15.9882812,15.8867188 15.1210938,17.0351562 15.1210938,18.96875 C15.1210938,20.8789062 15.9882812,22.0507812 17.4179688,22.0507812 C18.8945312,22.0507812 19.84375,20.8554688 19.84375,18.96875 C19.84375,17.0820312 18.90625,15.8867188 17.4296875,15.8867188 Z M17.8398438,9.125 C23.3242188,9.125 27.25,12.59375 27.25,17.7734375 C27.25,21.5117188 25.5625,23.9609375 22.7734375,23.9609375 C21.4023438,23.9609375 20.265625,23.1992188 20.078125,22.0390625 L19.9609375,22.0390625 C19.46875,23.2226562 18.4140625,23.8789062 17.0429688,23.8789062 C14.6054687,23.8789062 12.9648438,21.8867188 12.9648438,18.9101562 C12.9648438,16.0625 14.6171875,14.09375 16.9960938,14.09375 C18.25,14.09375 19.3632812,14.7382812 19.8085938,15.7460938 L19.9375,15.7460938 L19.9375,14.328125 L21.9179688,14.328125 L21.9179688,20.984375 C21.9179688,21.7578125 22.328125,22.2851562 23.171875,22.2851562 C24.4726562,22.2851562 25.421875,20.6679688 25.421875,17.8320312 C25.421875,13.5664062 22.2929688,10.7421875 17.7109375,10.7421875 C13.1640625,10.7421875 9.90625,14.140625 9.90625,18.96875 C9.90625,24.1367188 13.3515625,27.0429688 18.109375,27.0429688 C19.5625,27.0429688 21.0507812,26.84375 21.7773438,26.5390625 L21.7773438,28.15625 C20.78125,28.484375 19.4570312,28.671875 18.0273438,28.671875 C12.2382812,28.671875 8.078125,25.109375 8.078125,18.8984375 C8.078125,13.0625 12.0859375,9.125 17.8398438,9.125 Z'></path> </g> </g> </g></g></svg>", MENU_ICON_SVG: "<svg width='16px' height='10px' viewBox='0 0 16 10' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-26.000000, -24.000000)' fill-rule='nonzero' fill='inherit'> <g transform='translate(26.000000, 24.000000)'> <path d='M1.5,0 L0.5,0 C0.224,0 0,0.224 0,0.5 L0,1.5 C0,1.776 0.224,2 0.5,2 L1.5,2 C1.776,2 2,1.776 2,1.5 L2,0.5 C2,0.224 1.776,0 1.5,0 Z'></path> <path d='M15.5,0 L3.5,0 C3.224,0 3,0.224 3,0.5 L3,1.5 C3,1.776 3.224,2 3.5,2 L15.5,2 C15.776,2 16,1.776 16,1.5 L16,0.5 C16,0.224 15.776,0 15.5,0 Z'></path> <path d='M1.5,4 L0.5,4 C0.224,4 0,4.224 0,4.5 L0,5.5 C0,5.776 0.224,6 0.5,6 L1.5,6 C1.776,6 2,5.776 2,5.5 L2,4.5 C2,4.224 1.776,4 1.5,4 Z'></path> <path d='M3.5,6 L11.5,6 C11.776,6 12,5.776 12,5.5 L12,4.5 C12,4.224 11.776,4 11.5,4 L3.5,4 C3.224,4 3,4.224 3,4.5 L3,5.5 C3,5.776 3.224,6 3.5,6 Z'></path> <path d='M1.5,8 L0.5,8 C0.224,8 0,8.224 0,8.5 L0,9.5 C0,9.776 0.224,10 0.5,10 L1.5,10 C1.776,10 2,9.776 2,9.5 L2,8.5 C2,8.224 1.776,8 1.5,8 Z'></path> <path d='M13.5,8 L3.5,8 C3.224,8 3,8.224 3,8.5 L3,9.5 C3,9.776 3.224,10 3.5,10 L13.5,10 C13.776,10 14,9.776 14,9.5 L14,8.5 C14,8.224 13.776,8 13.5,8 Z'></path> </g> </g> </g> </svg>", INFO_ICON_SVG: "<svg width='22px' height='22px' viewBox='0 0 22 22' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-388.000000, -18.000000)' fill='inherit'> <g> <g transform='translate(381.000000, 11.000000)'> <g transform='translate(7.000000, 7.000000)'> <path d='M11,22 C4.92486775,22 0,17.0751322 0,11 C0,4.92486775 4.92486775,0 11,0 C17.0751322,0 22,4.92486775 22,11 C22,17.0751322 17.0751322,22 11,22 Z M11,20.7924685 C16.408231,20.7924685 20.7924685,16.408231 20.7924685,11 C20.7924685,5.59176898 16.408231,1.20753149 11,1.20753149 C5.59176898,1.20753149 1.20753149,5.59176898 1.20753149,11 C1.20753149,16.408231 5.59176898,20.7924685 11,20.7924685 Z M10.1572266,16.0625 L10.1572266,8.69335938 L11.3466797,8.69335938 L11.3466797,16.0625 L10.1572266,16.0625 Z M10.7519531,7.50390625 C10.3417969,7.50390625 10,7.16210938 10,6.75195312 C10,6.33496094 10.3417969,6 10.7519531,6 C11.1689453,6 11.5039062,6.33496094 11.5039062,6.75195312 C11.5039062,7.16210938 11.1689453,7.50390625 10.7519531,7.50390625 Z'></path> </g> </g> </g> </g> </g> </svg>", - MESSAGE_ICON_SVG: "<svg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-202.000000, -176.000000)' fill-rule='nonzero' fill='inherit'> <g> <g transform='translate(202.000000, 176.000000)'> <path d='M15,0 C15.553,0 16,0.496 16,1.048 L16,8.048 C16,8.601 15.553,9 15,9 L14,9 L14,7.048 L14,3.048 C14,2.496 13.553,2 13,2 L5,2 L3,2 L3,1.048 C3,0.496 3.447,0 4,0 L15,0 Z M12,3 C12.55,3 13,3.508 13,4.07 L13,12.048 C13,12.61 12.55,13 12,13 L11,13 L11,15.376 C11,15.94 10.596,16.035 10.098,15.791 L4.72,13 L1,13 C0.45,13 0,12.61 0,12.048 L0,4.07 C0,3.508 0.45,3 1,3 L12,3 Z'></path> </g> </g> </g> </g> </svg>", + MESSAGE_ICON_SVG: "<svg width='18px' height='16px' viewBox='0 0 18 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-200.000000, -174.000000)' fill='inherit'> <g transform='translate(200.000000, 174.000000)'> <path d='M7.2546625,1.42801356 C10.458475,1.42801356 12.999475,3.24528136 12.999475,5.52023729 C12.9895,8.04188475 10.6062625,9.89326102 7.40245,9.89326102 C7.40245,9.89326102 6.9134125,9.91229831 6.4115125,9.83747119 L5.82535,9.79622373 L5.15335,10.3586169 C4.997425,10.5397356 4.3199125,11.1095322 3.736375,11.4794373 C4.0915375,10.4598847 4.07605,10.1370441 4.07605,10.1370441 L4.1251375,9.49004068 L3.55315,9.19549153 C2.0986375,8.44616271 1.4444875,6.88616271 1.4444875,5.52023729 C1.4444875,3.24528136 4.05085,1.42801356 7.2546625,1.42801356 M7.2546625,0.370386441 C3.465475,0.370386441 0.3944875,2.65829831 0.3944875,5.52023729 C0.3944875,7.3028678 1.2623125,9.20342373 3.0751375,10.1370441 C3.0751375,10.1478847 3.07225,10.1560814 3.07225,10.1679797 C3.07225,10.9426915 2.43175,12.0048136 2.1794875,12.4429356 L2.1805375,12.4429356 C2.1605875,12.4902644 2.148775,12.5420881 2.148775,12.5973492 C2.148775,12.8141627 2.322025,12.9881424 2.5375375,12.9881424 C2.5693,12.9881424 2.6210125,12.9815322 2.6393875,12.9815322 C2.6446375,12.9815322 2.6467375,12.9815322 2.6462125,12.9831186 C3.986275,12.762339 5.9642125,11.2435864 6.2576875,10.8837288 C6.5585125,10.928678 6.761425,10.9358169 7.0136875,10.9358169 C7.120525,10.9358169 7.2347125,10.9342305 7.3696375,10.9342305 C11.1583,10.9342305 14.094625,8.75446102 14.049475,5.52023729 C14.049475,2.65829831 11.0435875,0.370386441 7.2546625,0.370386441'></path> <path d='M17.2055125,9.79172881 C17.2055125,8.35811525 16.6498,7.26532203 15.2624875,6.4451322 C15.228625,6.82614237 15.120475,7.23517966 15.0031375,7.59477288 C15.8998375,8.21903729 16.1555125,8.85995932 16.1555125,9.79172881 C16.1555125,10.9323797 15.62815,11.7597085 14.40175,12.3919051 L13.879375,12.653139 C13.879375,12.653139 13.9337125,14.0082237 14.0140375,14.3511593 C12.9895,13.5946915 12.6374875,12.9630237 12.6374875,12.9630237 L12.08545,13.0486915 C11.86705,13.0809492 11.276425,13.0812136 11.276425,13.0812136 C9.85,13.0812136 8.7929125,12.7388068 7.8909625,12.0278169 C8.135875,12.0124814 6.42805,12.0132746 6.3899875,12.0468542 C7.4326375,13.3297559 9.1373125,14.1388407 11.276425,14.1388407 C11.3927125,14.1388407 11.49115,14.1398983 11.58355,14.1398983 C11.801425,14.1398983 11.9773,14.1338169 12.237175,14.095478 C12.491275,14.4058915 13.914025,15.7728746 15.0724375,15.9629831 C15.0719125,15.9619254 15.073225,15.9619254 15.078475,15.9619254 C15.0939625,15.9619254 15.13885,15.967478 15.16615,15.967478 C15.3522625,15.967478 15.5024125,15.8167661 15.5024125,15.6293017 C15.5024125,15.5817085 15.49165,15.5367593 15.47485,15.4960407 L15.4759,15.4960407 C15.258025,15.117939 14.9159875,14.0129831 14.9159875,13.3435051 C14.9159875,13.3331932 14.9128375,13.3260542 14.9128375,13.3168 C16.4797,12.5095661 17.2055125,11.3321627 17.2055125,9.79172881'></path> </g> </g> </g> </svg>", SWITCH_CHANNEL_ICON_SVG: "<svg width='24px' height='24px' viewBox='0 0 24 24' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-32.000000, -982.000000)' fill-rule='nonzero' fill='inherit'> <g> <g transform='translate(0.000000, 961.000000)'> <g transform='translate(32.000000, 21.000000)'> <path d='M21.5,0.5 L2.5,0.5 C1.121,0.5 0,1.622 0,3 L0,21 C0,22.378 1.121,23.5 2.5,23.5 L21.5,23.5 C22.879,23.5 24,22.378 24,21 L24,3 C24,1.622 22.879,0.5 21.5,0.5 Z M2.5,1.5 L21.5,1.5 C22.327,1.5 23,2.173 23,3 L23,4.5 L1,4.5 L1,3 C1,2.173 1.673,1.5 2.5,1.5 Z M21.5,22.5 L2.5,22.5 C1.673,22.5 1,21.827 1,21 L1,5.5 L23,5.5 L23,21 C23,21.827 22.327,22.5 21.5,22.5 Z'></path> <circle cx='2.5' cy='3' r='1'></circle> <circle cx='4.5' cy='3' r='1'></circle> <circle cx='6.5' cy='3' r='1'></circle> <path d='M19.146,16.94 C19.673,16.263 20,15.423 20,14.5 C20,12.294 18.206,10.5 16,10.5 C13.794,10.5 12,12.294 12,14.5 C12,16.706 13.794,18.5 16,18.5 C16.922,18.5 17.762,18.174 18.439,17.647 L21.146,20.354 C21.244,20.451 21.372,20.5 21.5,20.5 C21.628,20.5 21.756,20.451 21.853,20.354 C22.048,20.159 22.048,19.842 21.853,19.647 L19.146,16.94 Z M13,14.5 C13,12.846 14.346,11.5 16,11.5 C17.654,11.5 19,12.846 19,14.5 C19,16.154 17.654,17.5 16,17.5 C14.346,17.5 13,16.154 13,14.5 Z'></path> <path d='M2.5,8.5 L7.5,8.5 C7.776,8.5 8,8.276 8,8 C8,7.724 7.776,7.5 7.5,7.5 L2.5,7.5 C2.224,7.5 2,7.724 2,8 C2,8.276 2.224,8.5 2.5,8.5 Z'></path> <path d='M10.5,8.5 L13.5,8.5 C13.776,8.5 14,8.276 14,8 C14,7.724 13.776,7.5 13.5,7.5 L10.5,7.5 C10.224,7.5 10,7.724 10,8 C10,8.276 10.224,8.5 10.5,8.5 Z'></path> <path d='M21.5,7.5 L16.5,7.5 C16.224,7.5 16,7.724 16,8 C16,8.276 16.224,8.5 16.5,8.5 L21.5,8.5 C21.776,8.5 22,8.276 22,8 C22,7.724 21.776,7.5 21.5,7.5 Z'></path> <path d='M2.5,11.5 L5.5,11.5 C5.776,11.5 6,11.276 6,11 C6,10.724 5.776,10.5 5.5,10.5 L2.5,10.5 C2.224,10.5 2,10.724 2,11 C2,11.276 2.224,11.5 2.5,11.5 Z'></path> <path d='M8.5,11.5 L12.5,11.5 C12.776,11.5 13,11.276 13,11 C13,10.724 12.776,10.5 12.5,10.5 L8.5,10.5 C8.224,10.5 8,10.724 8,11 C8,11.276 8.224,11.5 8.5,11.5 Z'></path> <path d='M10.5,13.5 L7.5,13.5 C7.224,13.5 7,13.724 7,14 C7,14.276 7.224,14.5 7.5,14.5 L10.5,14.5 C10.776,14.5 11,14.276 11,14 C11,13.724 10.776,13.5 10.5,13.5 Z'></path> <path d='M2.5,14.5 L4.5,14.5 C4.776,14.5 5,14.276 5,14 C5,13.724 4.776,13.5 4.5,13.5 L2.5,13.5 C2.224,13.5 2,13.724 2,14 C2,14.276 2.224,14.5 2.5,14.5 Z'></path> <path d='M2.5,17.5 L8.5,17.5 C8.776,17.5 9,17.276 9,17 C9,16.724 8.776,16.5 8.5,16.5 L2.5,16.5 C2.224,16.5 2,16.724 2,17 C2,17.276 2.224,17.5 2.5,17.5 Z'></path> <path d='M10.5,19.5 L2.5,19.5 C2.224,19.5 2,19.724 2,20 C2,20.276 2.224,20.5 2.5,20.5 L10.5,20.5 C10.776,20.5 11,20.276 11,20 C11,19.724 10.776,19.5 10.5,19.5 Z'></path> </g> </g> </g> </g> </g> </svg>", GLOBE_ICON_SVG: "<svg width='13px' height='13px' viewBox='0 0 14 14' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-115.000000, -147.000000)' fill-rule='nonzero' fill='inherit'> <g transform='translate(95.000000, 0.000000)'> <g transform='translate(20.000000, 113.000000)'> <g transform='translate(0.000000, 34.000000)'> <path d='M10.409,0.893375 C9.40275,0.329875 8.24075,0.00875 7,0 C3.13075,0 0,3.140375 0,7 C0,10.521875 2.594375,13.42775 5.97625,13.93 C6.314875,13.974625 6.6535,14 7,14 C8.24075,14 9.40275,13.678875 10.409,13.1145 C12.551875,11.918375 14,9.6285 14,7 C13.99125,4.3715 12.551875,2.090375 10.409,0.893375 Z M11.554375,4.375 L9.431625,4.375 C9.302125,3.5 9.10875,2.736125 8.86725,2.085125 C10.003875,2.519125 10.9515,3.5 11.554375,4.375 Z M6.941375,1.73775 C6.960625,1.736875 6.979875,1.73425 7,1.73425 C7.020125,1.73425 7.039375,1.736875 7.058625,1.73775 C7.340375,2.172625 7.697375,3.5 7.92225,4.375 L6.07775,4.375 C6.302625,3.5 6.659625,2.172625 6.941375,1.73775 Z M1.81475,7.875 C1.7675,7.875 1.73425,7.29925 1.73425,7 C1.73425,6.70075 1.764875,6.125 1.813,6.125 L4.396875,6.125 C4.384625,6.125 4.375,6.7025 4.375,7 C4.375,7.2975 4.384625,7.875 4.396875,7.875 L1.81475,7.875 Z M4.354875,11.54475 C4.346125,11.54475 4.337375,11.54475 4.337375,11.536 C3.548125,11.07575 2.893625,10.5 2.436875,9.625 L4.568375,9.625 C4.697875,10.5 4.890375,11.262125 5.131875,11.91225 C4.8615,11.81075 4.599875,11.692625 4.354875,11.54475 Z M4.568375,4.375 L2.443875,4.375 C3.045875,3.5 3.994375,2.517375 5.131875,2.083375 C4.89125,2.734375 4.697875,3.5 4.568375,4.375 Z M7.0595,12.26225 C7.039375,12.26225 7.020125,12.26575 7,12.26575 C6.979875,12.26575 6.960625,12.26225 6.9405,12.26225 C6.65875,11.8265 6.302625,11.375 6.07775,9.625 L7.921375,9.625 C7.697375,11.375 7.34125,11.8265 7.0595,12.26225 Z M8.11125,7.875 L5.88875,7.875 C5.873875,7.875 5.8625,7.30625 5.8625,7 C5.8625,6.69375 5.873875,6.125 5.88875,6.125 L8.11125,6.125 C8.126125,6.125 8.1375,6.69375 8.1375,7 C8.1375,7.30625 8.126125,7.875 8.11125,7.875 Z M10.409,11.0075 C10.13075,11.242 9.828,11.45025 9.506875,11.631375 C9.30125,11.74075 9.086875,11.839625 8.8655,11.923625 C9.107,11.270875 9.30125,10.5 9.431625,9.625 L11.557875,9.625 C11.25425,10.5 10.8675,10.618125 10.409,11.0075 Z M9.603125,7.875 C9.615375,7.875 9.625,7.2975 9.625,7 C9.625,6.7025 9.615375,6.125 9.603125,6.125 L12.186125,6.125 C12.235125,6.125 12.26575,6.70075 12.26575,7 C12.26575,7.29925 12.233375,7.875 12.18525,7.875 L9.603125,7.875 Z'></path> </g> </g> </g> </g> </g> </svg>", LOCK_ICON_SVG: "<svg width='12px' height='13px' viewBox='0 0 13 15' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'> <g stroke='none' stroke-width='1' fill='inherit' fill-rule='evenodd'> <g transform='translate(-116.000000, -175.000000)' fill-rule='nonzero' fill='inherit'> <g transform='translate(95.000000, 0.000000)'> <g transform='translate(20.000000, 113.000000)'> <g transform='translate(1.000000, 62.000000)'> <path d='M12.0714286,6.5 L11.1428571,6.5 L11.1428571,4.64285714 C11.1428571,2.07814286 9.06471429,0 6.5,0 C3.93528571,0 1.85714286,2.07814286 1.85714286,4.64285714 L1.85714286,6.5 L0.928571429,6.5 C0.415071429,6.5 0,7.00792857 0,7.52142857 L0,13.9285714 C0,14.4420714 0.415071429,14.8571429 0.928571429,14.8571429 L12.0714286,14.8571429 C12.5849286,14.8571429 13,14.4420714 13,13.9285714 L13,7.52142857 C13,7.00792857 12.5849286,6.5 12.0714286,6.5 Z M6.5,1.85714286 C8.03585714,1.85714286 9.28571429,3.107 9.28571429,4.64285714 L9.28571429,6.5 L8.35714286,6.5 L4.64285714,6.5 L3.71428571,6.5 L3.71428571,4.64285714 C3.71428571,3.107 4.96414286,1.85714286 6.5,1.85714286 Z'></path> </g> </g> </g> </g> </g> </svg>", diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx index 8abad70e0..97f56fc4c 100644 --- a/webapp/utils/utils.jsx +++ b/webapp/utils/utils.jsx @@ -610,7 +610,7 @@ export function applyTheme(theme) { if (theme.centerChannelColor) { changeCss('.app__body .mentions__name .status.status--group, .app__body .multi-select__note', 'background:' + changeOpacity(theme.centerChannelColor, 0.12)); - changeCss('.app__body .alert.alert-transparent, .app__body .channel-header .channel-header__icon, .app__body .search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.12)); + changeCss('.app__body .member-list__popover .more-modal__body, .app__body .alert.alert-transparent, .app__body .channel-header .channel-header__icon, .app__body .search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.12)); changeCss('.app__body .post-list__arrows, .app__body .post .flag-icon__container', 'fill:' + changeOpacity(theme.centerChannelColor, 0.3)); changeCss('@media(min-width: 768px){.app__body .search__icon svg', 'stroke:' + changeOpacity(theme.centerChannelColor, 0.4)); changeCss('.app__body .channel-header__icon svg', 'fill:' + changeOpacity(theme.centerChannelColor, 0.4)); @@ -625,16 +625,16 @@ export function applyTheme(theme) { changeCss('.app__body .mentions--top, .app__body .suggestion-list', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px'); changeCss('.app__body .mentions--top, .app__body .suggestion-list', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px'); changeCss('.app__body .mentions--top, .app__body .suggestion-list', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.2) + ' 1px -3px 12px'); - changeCss('.app__body .dropdown-menu, .app__body .popover ', 'box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px'); - changeCss('.app__body .dropdown-menu, .app__body .popover ', '-webkit-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px'); - changeCss('.app__body .dropdown-menu, .app__body .popover ', '-moz-box-shadow:' + changeOpacity(theme.centerChannelColor, 0.1) + ' 0px 6px 12px'); + changeCss('.app__body .dropdown-menu, .app__body .popover ', 'box-shadow: 0 17px 50px 0 ' + changeOpacity(theme.centerChannelColor, 0.1) + ', 0 12px 15px 0 ' + changeOpacity(theme.centerChannelColor, 0.1)); + changeCss('.app__body .dropdown-menu, .app__body .popover ', '-moz-box-shadow: 0 17px 50px 0 ' + changeOpacity(theme.centerChannelColor, 0.1) + ', 0 12px 15px 0 ' + changeOpacity(theme.centerChannelColor, 0.1)); + changeCss('.app__body .dropdown-menu, .app__body .popover ', '-webkit-box-shadow: 0 17px 50px 0 ' + changeOpacity(theme.centerChannelColor, 0.1) + ', 0 12px 15px 0 ' + changeOpacity(theme.centerChannelColor, 0.1)); changeCss('.app__body .post__body hr, .app__body .loading-screen .loading__content .round, .app__body .tutorial__circles .circle', 'background:' + theme.centerChannelColor); changeCss('.app__body .channel-header .heading', 'color:' + theme.centerChannelColor); changeCss('.app__body .markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07)); changeCss('.app__body .channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8)); changeCss('.app__body .channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.4)); changeCss('.app__body .channel-header .pinned-posts-button svg', 'fill:' + changeOpacity(theme.centerChannelColor, 0.6)); - changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header, .app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); + changeCss('.app__body .custom-textarea, .app__body .custom-textarea:focus, .app__body .file-preview, .app__body .post-image__details, .app__body .sidebar--right .sidebar-right__body, .app__body .markdown__table th, .app__body .markdown__table td, .app__body .suggestion-list__content, .app__body .modal .modal-content, .app__body .modal .settings-modal .settings-table .settings-content .divider-light, .app__body .webhooks__container, .app__body .dropdown-menu, .app__body .modal .modal-header', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); changeCss('.app__body .popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25)); changeCss('.app__body .search-help-popover .search-autocomplete__divider span, .app__body .suggestion-list__divider > span', 'color:' + changeOpacity(theme.centerChannelColor, 0.7)); changeCss('.app__body .popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25)); @@ -652,7 +652,7 @@ export function applyTheme(theme) { changeCss('@media(min-width: 768px){.app__body .post-list__table .post-list__content .dropdown-menu a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.1)); changeCss('.app__body .form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3)); changeCss('.app__body .attachment .attachment__content', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3)); - changeCss('.app__body .input-group-addon, .app__body .channel-intro .channel-intro__content, .app__body .webhooks__container, .app__body .member-list__popover .more-modal__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.05)); + changeCss('.app__body .input-group-addon, .app__body .channel-intro .channel-intro__content, .app__body .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05)); changeCss('.app__body .date-separator .separator__text', 'color:' + theme.centerChannelColor); changeCss('.app__body .date-separator .separator__hr, .app__body .modal-footer, .app__body .modal .custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2)); changeCss('.app__body .search-item-container, .app__body .post-right__container .post.post--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.1)); @@ -697,6 +697,7 @@ export function applyTheme(theme) { changeCss('.app__body .emoji-picker__item-wrapper:hover', 'background-color:' + changeOpacity(theme.centerChannelColor, 0.8)); changeCss('.app__body .emojisprite:hover', 'background-color:' + changeOpacity(theme.centerChannelColor, 0.8)); changeCss('.app__body .icon__postcontent_picker:hover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8)); + changeCss('.app__body .popover', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07)); } if (theme.newMessageSeparator) { @@ -706,7 +707,8 @@ export function applyTheme(theme) { if (theme.linkColor) { changeCss('.app__body .channel-header__links > a.active, .app__body a, .app__body a:focus, .app__body a:hover, .app__body .btn, .app__body .btn:focus, .app__body .btn:hover', 'color:' + theme.linkColor); - changeCss('.app__body .attachment .attachment__container, .app__body .member-list__popover .more-modal__list .more-modal__row:hover', 'border-left-color:' + changeOpacity(theme.linkColor, 0.5)); + changeCss('.app__body .attachment .attachment__container', 'border-left-color:' + changeOpacity(theme.linkColor, 0.5)); + changeCss('.app__body .member-list__popover .more-modal__list .more-modal__row:hover', 'background:' + changeOpacity(theme.linkColor, 0.08)); changeCss('.app__body .channel-header__links .icon:hover, .app__body .channel-header__links > a.active .icon, .app__body .post .flag-icon__container.visible, .app__body .post .reacticon__container, .app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor); changeCss('@media(min-width: 768px){.app__body .search__form.focused .search__icon svg', 'stroke:' + theme.linkColor); changeCss('.app__body .channel-header__links .icon:hover, .app__body .post .flag-icon__container.visible, .app__body .post .comment-icon__container, .app__body .post .post__reply', 'fill:' + theme.linkColor); @@ -715,7 +717,7 @@ export function applyTheme(theme) { changeCss('.app__body .member-list__popover .more-modal__actions svg, .app__body .channel-header .channel-header__icon:hover svg, .app__body .channel-header .channel-header__icon.active svg', 'fill:' + theme.linkColor); changeCss('.app__body .post-reaction.post-reaction--current-user', 'background:' + changeOpacity(theme.linkColor, 0.1)); changeCss('.app__body .post-reaction.post-reaction--current-user', 'border-color:' + changeOpacity(theme.linkColor, 0.4)); - changeCss('.app__body .channel-header .channel-header__icon:hover, .app__body .channel-header .channel-header__icon.active, .app__body .search-bar__container .search__form.focused', 'border-color:' + theme.linkColor); + changeCss('.app__body .member-list__popover .more-modal__list .more-modal__row:hover, .app__body .channel-header .channel-header__icon:hover, .app__body .channel-header .channel-header__icon.active, .app__body .search-bar__container .search__form.focused', 'border-color:' + theme.linkColor); changeCss('.app__body .post-reaction.post-reaction--current-user', 'color:' + theme.linkColor); } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1fadfcc05..ebafd7de2 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4986,9 +4986,9 @@ map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" -marked@mattermost/marked#9f0c8b96fd5efedb99f5fc45cca9ac7684b5aa69: +marked@mattermost/marked#5194fc037b35036910c6542b04bb471fe56b27a9: version "0.3.5" - resolved "https://codeload.github.com/mattermost/marked/tar.gz/9f0c8b96fd5efedb99f5fc45cca9ac7684b5aa69" + resolved "https://codeload.github.com/mattermost/marked/tar.gz/5194fc037b35036910c6542b04bb471fe56b27a9" match-at@0.1.0: version "0.1.0" |