From cb62e69997840dc6b75112daf8c804b89f7f523b Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Fri, 13 Nov 2020 09:52:38 +0100 Subject: [PATCH] Chore: Convert API tests to standard Go lib (#29009) * Chore: Convert tests to standard Go lib Signed-off-by: Arve Knudsen Co-authored-by: Will Browne --- pkg/api/admin_users_test.go | 573 +++++----- pkg/api/alerting_test.go | 140 ++- pkg/api/annotations.go | 2 - pkg/api/annotations_test.go | 248 +++-- pkg/api/common_test.go | 26 +- pkg/api/dashboard.go | 4 +- pkg/api/dashboard_permission_test.go | 207 ++-- pkg/api/dashboard_snapshot.go | 2 +- pkg/api/dashboard_snapshot_test.go | 349 +++--- pkg/api/dashboard_test.go | 1300 +++++++++++----------- pkg/api/datasources_test.go | 89 +- pkg/api/folder_permission_test.go | 356 +++--- pkg/api/folder_test.go | 221 ++-- pkg/api/frontend_logging_test.go | 2 +- pkg/api/ldap_debug.go | 1 - pkg/api/ldap_debug_test.go | 225 ++-- pkg/api/login.go | 8 +- pkg/api/login_test.go | 36 +- pkg/api/pluginproxy/ds_auth_provider.go | 2 +- pkg/api/pluginproxy/ds_proxy_test.go | 1026 ++++++++--------- pkg/api/team_test.go | 82 +- pkg/api/user_test.go | 254 +++-- pkg/api/user_token_test.go | 226 ++-- pkg/services/licensing/oss.go | 2 +- pkg/services/rendering/rendering.go | 2 +- pkg/services/rendering/rendering_test.go | 8 +- pkg/setting/setting.go | 14 +- 27 files changed, 2816 insertions(+), 2589 deletions(-) diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 8f5dd648c5b..a45afadc18f 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "testing" "github.com/grafana/grafana/pkg/api/dtos" @@ -8,288 +9,356 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( - TestLogin = "test@example.com" - TestPassword = "password" + testLogin = "test@example.com" + testPassword = "password" nonExistingOrgID = 1000 ) -func TestAdminApiEndpoint(t *testing.T) { - role := models.ROLE_ADMIN - Convey("Given a server admin attempts to remove themself as an admin", t, func() { +func TestAdminAPIEndpoint(t *testing.T) { + const role = models.ROLE_ADMIN + + t.Run("Given a server admin attempts to remove themself as an admin", func(t *testing.T) { updateCmd := dtos.AdminUpdateUserPermissionsForm{ IsGrafanaAdmin: false, } - bus.AddHandler("test", func(cmd *models.UpdateUserPermissionsCommand) error { - return models.ErrLastGrafanaAdmin - }) + putAdminScenario(t, "When calling PUT on", "/api/admin/users/1/permissions", + "/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.UpdateUserPermissionsCommand) error { + return models.ErrLastGrafanaAdmin + }) - putAdminScenario("When calling PUT on", "/api/admin/users/1/permissions", "/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 400) - }) + sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() + assert.Equal(t, 400, sc.resp.Code) + }) }) - Convey("When a server admin attempts to logout himself from all devices", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: TestUserID} - return nil - }) + t.Run("When a server admin attempts to logout himself from all devices", func(t *testing.T) { + adminLogoutUserScenario(t, "Should not be allowed when calling POST on", + "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + cmd.Result = &models.User{Id: testUserID} + return nil + }) - adminLogoutUserScenario("Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 400) - }) + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 400, sc.resp.Code) + }) }) - Convey("When a server admin attempts to logout a non-existing user from all devices", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - userId = cmd.Id - return models.ErrUserNotFound - }) + t.Run("When a server admin attempts to logout a non-existing user from all devices", func(t *testing.T) { + adminLogoutUserScenario(t, "Should return not found when calling POST on", "/api/admin/users/200/logout", + "/api/admin/users/:id/logout", func(sc *scenarioContext) { + userID := int64(0) - adminLogoutUserScenario("Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - So(userId, ShouldEqual, 200) - }) + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + userID = cmd.Id + return models.ErrUserNotFound + }) + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 404, sc.resp.Code) + assert.Equal(t, int64(200), userID) + }) }) - Convey("When a server admin attempts to revoke an auth token for a non-existing user", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - userId = cmd.Id - return models.ErrUserNotFound - }) - + t.Run("When a server admin attempts to revoke an auth token for a non-existing user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - adminRevokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - So(userId, ShouldEqual, 200) - }) - }) + adminRevokeUserAuthTokenScenario(t, "Should return not found when calling POST on", + "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + userID = cmd.Id + return models.ErrUserNotFound + }) - Convey("When a server admin gets auth tokens for a non-existing user", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - userId = cmd.Id - return models.ErrUserNotFound - }) - - adminGetUserAuthTokensScenario("Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) { - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - So(userId, ShouldEqual, 200) - }) - }) - - Convey("When a server admin attempts to enable/disable a nonexistent user", t, func() { - var userId int64 - isDisabled := false - bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { - return models.ErrUserNotFound - }) - - bus.AddHandler("test", func(cmd *models.DisableUserCommand) error { - userId = cmd.UserId - isDisabled = cmd.IsDisabled - return models.ErrUserNotFound - }) - - adminDisableUserScenario("Should return user not found on a POST request", "enable", "/api/admin/users/42/enable", "/api/admin/users/:id/enable", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 404) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - So(respJSON.Get("message").MustString(), ShouldEqual, "user not found") - - So(userId, ShouldEqual, 42) - So(isDisabled, ShouldEqual, false) - }) - - adminDisableUserScenario("Should return user not found on a POST request", "disable", "/api/admin/users/42/disable", "/api/admin/users/:id/disable", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 404) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - So(respJSON.Get("message").MustString(), ShouldEqual, "user not found") - - So(userId, ShouldEqual, 42) - So(isDisabled, ShouldEqual, true) - }) - }) - - Convey("When a server admin attempts to disable/enable external user", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { - userId = cmd.UserId - return nil - }) - - adminDisableUserScenario("Should return Could not disable external user error", "disable", "/api/admin/users/42/disable", "/api/admin/users/:id/disable", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 500) - - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("message").MustString(), ShouldEqual, "Could not disable external user") - - So(userId, ShouldEqual, 42) - }) - - adminDisableUserScenario("Should return Could not enable external user error", "enable", "/api/admin/users/42/enable", "/api/admin/users/:id/enable", func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 500) - - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("message").MustString(), ShouldEqual, "Could not enable external user") - - So(userId, ShouldEqual, 42) - }) - }) - - Convey("When a server admin attempts to delete a nonexistent user", t, func() { - var userId int64 - bus.AddHandler("test", func(cmd *models.DeleteUserCommand) error { - userId = cmd.UserId - return models.ErrUserNotFound - }) - - adminDeleteUserScenario("Should return user not found error", "/api/admin/users/42", "/api/admin/users/:id", func(sc *scenarioContext) { - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - - So(sc.resp.Code, ShouldEqual, 404) - - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("message").MustString(), ShouldEqual, "user not found") - - So(userId, ShouldEqual, 42) - }) - }) - - Convey("When a server admin attempts to create a user", t, func() { - var userLogin string - var orgId int64 - - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - userLogin = cmd.Login - orgId = cmd.OrgId - - if orgId == nonExistingOrgID { - return models.ErrOrgNotFound - } - - cmd.Result = models.User{Id: TestUserID} - return nil - }) - - Convey("Without an organization", func() { - createCmd := dtos.AdminCreateUserForm{ - Login: TestLogin, - Password: TestPassword, - } - - adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 404, sc.resp.Code) + assert.Equal(t, int64(200), userID) + }) + }) + + t.Run("When a server admin gets auth tokens for a non-existing user", func(t *testing.T) { + adminGetUserAuthTokensScenario(t, "Should return not found when calling GET on", + "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + userID = cmd.Id + return models.ErrUserNotFound + }) + + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + assert.Equal(t, 404, sc.resp.Code) + assert.Equal(t, int64(200), userID) + }) + }) + + t.Run("When a server admin attempts to enable/disable a nonexistent user", func(t *testing.T) { + adminDisableUserScenario(t, "Should return user not found on a POST request", "enable", + "/api/admin/users/42/enable", "/api/admin/users/:id/enable", func(sc *scenarioContext) { + var userID int64 + isDisabled := false + bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { + return models.ErrUserNotFound + }) + + bus.AddHandler("test", func(cmd *models.DisableUserCommand) error { + userID = cmd.UserId + isDisabled = cmd.IsDisabled + return models.ErrUserNotFound + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + + assert.Equal(t, 404, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.Equal(t, "user not found", respJSON.Get("message").MustString()) + + assert.Equal(t, int64(42), userID) + assert.Equal(t, false, isDisabled) + }) + + adminDisableUserScenario(t, "Should return user not found on a POST request", "disable", + "/api/admin/users/42/disable", "/api/admin/users/:id/disable", func(sc *scenarioContext) { + var userID int64 + isDisabled := false + bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { + return models.ErrUserNotFound + }) + + bus.AddHandler("test", func(cmd *models.DisableUserCommand) error { + userID = cmd.UserId + isDisabled = cmd.IsDisabled + return models.ErrUserNotFound + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + + assert.Equal(t, 404, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.Equal(t, "user not found", respJSON.Get("message").MustString()) + + assert.Equal(t, int64(42), userID) + assert.Equal(t, true, isDisabled) + }) + }) + + t.Run("When a server admin attempts to disable/enable external user", func(t *testing.T) { + adminDisableUserScenario(t, "Should return Could not disable external user error", "disable", + "/api/admin/users/42/disable", "/api/admin/users/:id/disable", func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { + userID = cmd.UserId + return nil + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 500, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID) - So(respJSON.Get("message").MustString(), ShouldEqual, "User created") + require.NoError(t, err) + assert.Equal(t, "Could not disable external user", respJSON.Get("message").MustString()) - // test that userLogin and orgId were transmitted correctly to the handler - So(userLogin, ShouldEqual, TestLogin) - So(orgId, ShouldEqual, 0) + assert.Equal(t, int64(42), userID) + }) + + adminDisableUserScenario(t, "Should return Could not enable external user error", "enable", + "/api/admin/users/42/enable", "/api/admin/users/:id/enable", func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetAuthInfoQuery) error { + userID = cmd.UserId + return nil + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 500, sc.resp.Code) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, "Could not enable external user", respJSON.Get("message").MustString()) + + assert.Equal(t, int64(42), userID) + }) + }) + + t.Run("When a server admin attempts to delete a nonexistent user", func(t *testing.T) { + adminDeleteUserScenario(t, "Should return user not found error", "/api/admin/users/42", + "/api/admin/users/:id", func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.DeleteUserCommand) error { + userID = cmd.UserId + return models.ErrUserNotFound + }) + + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + + assert.Equal(t, 404, sc.resp.Code) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, "user not found", respJSON.Get("message").MustString()) + + assert.Equal(t, int64(42), userID) + }) + }) + + t.Run("When a server admin attempts to create a user", func(t *testing.T) { + t.Run("Without an organization", func(t *testing.T) { + createCmd := dtos.AdminCreateUserForm{ + Login: testLogin, + Password: testPassword, + } + + adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + bus.ClearBusHandlers() + + var userLogin string + var orgID int64 + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + userLogin = cmd.Login + orgID = cmd.OrgId + + if orgID == nonExistingOrgID { + return models.ErrOrgNotFound + } + + cmd.Result = models.User{Id: testUserID} + return nil + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, testUserID, respJSON.Get("id").MustInt64()) + assert.Equal(t, "User created", respJSON.Get("message").MustString()) + + // Verify that userLogin and orgID were transmitted correctly to the handler + assert.Equal(t, testLogin, userLogin) + assert.Equal(t, int64(0), orgID) }) }) - Convey("With an organization", func() { + t.Run("With an organization", func(t *testing.T) { createCmd := dtos.AdminCreateUserForm{ - Login: TestLogin, - Password: TestPassword, - OrgId: TestOrgID, + Login: testLogin, + Password: testPassword, + OrgId: testOrgID, } - adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + bus.ClearBusHandlers() + + var userLogin string + var orgID int64 + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + userLogin = cmd.Login + orgID = cmd.OrgId + + if orgID == nonExistingOrgID { + return models.ErrOrgNotFound + } + + cmd.Result = models.User{Id: testUserID} + return nil + }) + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID) - So(respJSON.Get("message").MustString(), ShouldEqual, "User created") + require.NoError(t, err) + assert.Equal(t, testUserID, respJSON.Get("id").MustInt64()) + assert.Equal(t, "User created", respJSON.Get("message").MustString()) - So(userLogin, ShouldEqual, TestLogin) - So(orgId, ShouldEqual, TestOrgID) + assert.Equal(t, testLogin, userLogin) + assert.Equal(t, testOrgID, orgID) }) }) - Convey("With a nonexistent organization", func() { + t.Run("With a nonexistent organization", func(t *testing.T) { createCmd := dtos.AdminCreateUserForm{ - Login: TestLogin, - Password: TestPassword, + Login: testLogin, + Password: testPassword, OrgId: nonExistingOrgID, } - adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + bus.ClearBusHandlers() + + var userLogin string + var orgID int64 + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + userLogin = cmd.Login + orgID = cmd.OrgId + + if orgID == nonExistingOrgID { + return models.ErrOrgNotFound + } + + cmd.Result = models.User{Id: testUserID} + return nil + }) + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 400) + assert.Equal(t, 400, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("message").MustString(), ShouldEqual, "organization not found") + require.NoError(t, err) + assert.Equal(t, "organization not found", respJSON.Get("message").MustString()) - So(userLogin, ShouldEqual, TestLogin) - So(orgId, ShouldEqual, 1000) + assert.Equal(t, testLogin, userLogin) + assert.Equal(t, int64(1000), orgID) }) }) }) - Convey("When a server admin attempts to create a user with an already existing email/login", t, func() { - bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { - return models.ErrUserAlreadyExists - }) - + t.Run("When a server admin attempts to create a user with an already existing email/login", func(t *testing.T) { createCmd := dtos.AdminCreateUserForm{ - Login: TestLogin, - Password: TestPassword, + Login: testLogin, + Password: testPassword, } - adminCreateUserScenario("Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + bus.ClearBusHandlers() + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + return models.ErrUserAlreadyExists + }) + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 412) + assert.Equal(t, 412, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("error").MustString(), ShouldEqual, "user already exists") + require.NoError(t, err) + assert.Equal(t, "user already exists", respJSON.Get("error").MustString()) }) }) } -func putAdminScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, + cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return AdminUpdateUserPermissions(c, cmd) @@ -301,20 +370,22 @@ func putAdminScenario(desc string, url string, routePattern string, role models. }) } -func adminLogoutUserScenario(desc string, url string, routePattern string, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) hs := HTTPServer{ Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { + t.Log("Route handler invoked", "url", c.Req.URL) + sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.AdminLogoutUser(c) @@ -326,9 +397,9 @@ func adminLogoutUserScenario(desc string, url string, routePattern string, fn sc }) } -func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -337,12 +408,12 @@ func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern stri AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.AdminRevokeUserAuthToken(c, cmd) @@ -354,9 +425,9 @@ func adminRevokeUserAuthTokenScenario(desc string, url string, routePattern stri }) } -func adminGetUserAuthTokensScenario(desc string, url string, routePattern string, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -365,12 +436,12 @@ func adminGetUserAuthTokensScenario(desc string, url string, routePattern string AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.AdminGetUserAuthTokens(c) @@ -382,9 +453,9 @@ func adminGetUserAuthTokensScenario(desc string, url string, routePattern string }) } -func adminDisableUserScenario(desc string, action string, url string, routePattern string, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -393,10 +464,10 @@ func adminDisableUserScenario(desc string, action string, url string, routePatte AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID + sc.context.UserId = testUserID if action == "enable" { return AdminEnableUser(c) @@ -411,14 +482,14 @@ func adminDisableUserScenario(desc string, action string, url string, routePatte }) } -func adminDeleteUserScenario(desc string, url string, routePattern string, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID + sc.context.UserId = testUserID return AdminDeleteUser(c) }) @@ -429,14 +500,14 @@ func adminDeleteUserScenario(desc string, url string, routePattern string, fn sc }) } -func adminCreateUserScenario(desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID + sc.context.UserId = testUserID return AdminCreateUser(c, cmd) }) diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index a64ab697937..3baf9f9ba87 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -1,29 +1,38 @@ package api import ( + "fmt" "testing" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestAlertingApiEndpoint(t *testing.T) { - Convey("Given an alert in a dashboard with an acl", t, func() { - singleAlert := &models.Alert{Id: 1, DashboardId: 1, Name: "singlealert"} +type setUpConf struct { + aclMockResp []*models.DashboardAclInfoDTO +} +func TestAlertingAPIEndpoint(t *testing.T) { + singleAlert := &models.Alert{Id: 1, DashboardId: 1, Name: "singlealert"} + viewerRole := models.ROLE_VIEWER + editorRole := models.ROLE_EDITOR + + setUp := func(confs ...setUpConf) { bus.AddHandler("test", func(query *models.GetAlertByIdQuery) error { query.Result = singleAlert return nil }) - viewerRole := models.ROLE_VIEWER - editorRole := models.ROLE_EDITOR - aclMockResp := []*models.DashboardAclInfoDTO{} + for _, c := range confs { + if c.aclMockResp != nil { + aclMockResp = c.aclMockResp + } + } bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { query.Result = aclMockResp return nil @@ -33,39 +42,45 @@ func TestAlertingApiEndpoint(t *testing.T) { query.Result = []*models.TeamDTO{} return nil }) + } - Convey("When user is editor and not in the ACL", func() { - Convey("Should not be able to pause the alert", func() { - cmd := dtos.PauseAlertCommand{ - AlertId: 1, - Paused: true, - } - postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", models.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - CallPauseAlert(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + t.Run("When user is editor and not in the ACL", func(t *testing.T) { + cmd := dtos.PauseAlertCommand{ + AlertId: 1, + Paused: true, + } + postAlertScenario(t, "When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", + models.ROLE_EDITOR, cmd, func(sc *scenarioContext) { + setUp() + + callPauseAlert(sc) + assert.Equal(t, 403, sc.resp.Code) }) - }) + }) - Convey("When user is editor and dashboard has default ACL", func() { - aclMockResp = []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - - Convey("Should be able to pause the alert", func() { - cmd := dtos.PauseAlertCommand{ - AlertId: 1, - Paused: true, - } - postAlertScenario("When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", models.ROLE_EDITOR, cmd, func(sc *scenarioContext) { - CallPauseAlert(sc) - So(sc.resp.Code, ShouldEqual, 200) + t.Run("When user is editor and dashboard has default ACL", func(t *testing.T) { + cmd := dtos.PauseAlertCommand{ + AlertId: 1, + Paused: true, + } + postAlertScenario(t, "When calling POST on", "/api/alerts/1/pause", "/api/alerts/:alertId/pause", + models.ROLE_EDITOR, cmd, func(sc *scenarioContext) { + setUp(setUpConf{ + aclMockResp: []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + }, }) - }) - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", models.ROLE_EDITOR, func(sc *scenarioContext) { + callPauseAlert(sc) + assert.Equal(t, 200, sc.resp.Code) + }) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/alerts?dashboardId=1", "/api/alerts", + models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + var searchQuery *search.Query bus.AddHandler("test", func(query *search.Query) error { searchQuery = query @@ -81,11 +96,15 @@ func TestAlertingApiEndpoint(t *testing.T) { sc.handlerFunc = GetAlerts sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(searchQuery, ShouldBeNil) - So(getAlertsQuery, ShouldNotBeNil) + require.Nil(t, searchQuery) + assert.NotNil(t, getAlertsQuery) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", "/api/alerts", models.ROLE_EDITOR, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", + "/api/alerts?dashboardId=1&dashboardId=2&folderId=3&dashboardTag=abc&dashboardQuery=dbQuery&limit=5&query=alertQuery", + "/api/alerts", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + var searchQuery *search.Query bus.AddHandler("test", func(query *search.Query) error { searchQuery = query @@ -105,29 +124,31 @@ func TestAlertingApiEndpoint(t *testing.T) { sc.handlerFunc = GetAlerts sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(searchQuery, ShouldNotBeNil) - So(searchQuery.DashboardIds[0], ShouldEqual, 1) - So(searchQuery.DashboardIds[1], ShouldEqual, 2) - So(searchQuery.FolderIds[0], ShouldEqual, 3) - So(searchQuery.Tags[0], ShouldEqual, "abc") - So(searchQuery.Title, ShouldEqual, "dbQuery") + require.NotNil(t, searchQuery) + assert.Equal(t, int64(1), searchQuery.DashboardIds[0]) + assert.Equal(t, int64(2), searchQuery.DashboardIds[1]) + assert.Equal(t, int64(3), searchQuery.FolderIds[0]) + assert.Equal(t, "abc", searchQuery.Tags[0]) + assert.Equal(t, "dbQuery", searchQuery.Title) - So(getAlertsQuery, ShouldNotBeNil) - So(getAlertsQuery.DashboardIDs[0], ShouldEqual, 1) - So(getAlertsQuery.DashboardIDs[1], ShouldEqual, 2) - So(getAlertsQuery.Limit, ShouldEqual, 5) - So(getAlertsQuery.Query, ShouldEqual, "alertQuery") + require.NotNil(t, getAlertsQuery) + assert.Equal(t, int64(1), getAlertsQuery.DashboardIDs[0]) + assert.Equal(t, int64(2), getAlertsQuery.DashboardIDs[1]) + assert.Equal(t, int64(5), getAlertsQuery.Limit) + assert.Equal(t, "alertQuery", getAlertsQuery.Query) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/alert-notifications/1", "/alert-notifications/:notificationId", models.ROLE_ADMIN, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/alert-notifications/1", + "/alert-notifications/:notificationId", models.ROLE_ADMIN, func(sc *scenarioContext) { + setUp() + sc.handlerFunc = GetAlertNotificationByID sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) + assert.Equal(t, 404, sc.resp.Code) }) - }) } -func CallPauseAlert(sc *scenarioContext) { +func callPauseAlert(sc *scenarioContext) { bus.AddHandler("test", func(cmd *models.PauseAlertCommand) error { return nil }) @@ -135,15 +156,16 @@ func CallPauseAlert(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func postAlertScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.PauseAlertCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func postAlertScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, + cmd dtos.PauseAlertCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return PauseAlert(c, cmd) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 40f6c48720d..52754f0993f 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -247,7 +247,6 @@ func DeleteAnnotationByID(c *models.ReqContext) Response { OrgId: c.OrgId, Id: annotationID, }) - if err != nil { return Error(500, "Failed to delete annotation", err) } @@ -272,7 +271,6 @@ func canSaveByDashboardID(c *models.ReqContext, dashboardID int64) (bool, error) func canSave(c *models.ReqContext, repo annotations.Repository, annotationID int64) Response { items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId}) - if err != nil || len(items) == 0 { return Error(500, "Could not find annotation to update", err) } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 1ec021557dc..218bdcaf855 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -1,18 +1,18 @@ package api import ( + "fmt" "testing" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" ) -func TestAnnotationsApiEndpoint(t *testing.T) { - Convey("Given an annotation without a dashboard id", t, func() { +func TestAnnotationsAPIEndpoint(t *testing.T) { + t.Run("Given an annotation without a dashboard ID", func(t *testing.T) { cmd := dtos.PostAnnotationsCmd{ Time: 1000, Text: "annotation text", @@ -31,60 +31,70 @@ func TestAnnotationsApiEndpoint(t *testing.T) { Tags: []string{"tag1", "tag2"}, } - Convey("When user is an Org Viewer", func() { + t.Run("When user is an Org Viewer", func(t *testing.T) { role := models.ROLE_VIEWER - Convey("Should not be allowed to save an annotation", func() { - postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + t.Run("Should not be allowed to save an annotation", func(t *testing.T) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, + cmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 403, sc.resp.Code) + }) - putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", + role, updateCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() + assert.Equal(t, 403, sc.resp.Code) + }) - patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", + "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + assert.Equal(t, 403, sc.resp.Code) + }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - sc.handlerFunc = DeleteAnnotationByID - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", + "/api/annotations/:annotationId", role, func(sc *scenarioContext) { + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + sc.handlerFunc = DeleteAnnotationByID + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + assert.Equal(t, 403, sc.resp.Code) + }) }) }) - Convey("When user is an Org Editor", func() { + t.Run("When user is an Org Editor", func(t *testing.T) { role := models.ROLE_EDITOR - Convey("Should be able to save an annotation", func() { - postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + t.Run("Should be able to save an annotation", func(t *testing.T) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, + cmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) - putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - sc.handlerFunc = DeleteAnnotationByID - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", + "/api/annotations/:annotationId", role, func(sc *scenarioContext) { + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + sc.handlerFunc = DeleteAnnotationByID + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) }) }) }) - Convey("Given an annotation with a dashboard id and the dashboard does not have an acl", t, func() { + t.Run("Given an annotation with a dashboard ID and the dashboard does not have an ACL", func(t *testing.T) { cmd := dtos.PostAnnotationsCmd{ Time: 1000, Text: "annotation text", @@ -120,90 +130,111 @@ func TestAnnotationsApiEndpoint(t *testing.T) { {Role: &editorRole, Permission: models.PERMISSION_EDIT}, } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = aclMockResp - return nil - }) + setUp := func() { + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) - bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { - query.Result = []*models.TeamDTO{} - return nil - }) + bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { + query.Result = []*models.TeamDTO{} + return nil + }) + } - Convey("When user is an Org Viewer", func() { + t.Run("When user is an Org Viewer", func(t *testing.T) { role := models.ROLE_VIEWER - Convey("Should not be allowed to save an annotation", func() { - postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + t.Run("Should not be allowed to save an annotation", func(t *testing.T) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) + assert.Equal(t, 403, sc.resp.Code) }) - putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) + assert.Equal(t, 403, sc.resp.Code) }) - patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) + assert.Equal(t, 403, sc.resp.Code) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - sc.handlerFunc = DeleteAnnotationByID - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", + "/api/annotations/:annotationId", role, func(sc *scenarioContext) { + setUp() + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + sc.handlerFunc = DeleteAnnotationByID + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + assert.Equal(t, 403, sc.resp.Code) + }) }) }) - Convey("When user is an Org Editor", func() { + t.Run("When user is an Org Editor", func(t *testing.T) { role := models.ROLE_EDITOR - Convey("Should be able to save an annotation", func() { - postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + t.Run("Should be able to save an annotation", func(t *testing.T) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { - sc.handlerFunc = DeleteAnnotationByID - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/annotations/1", + "/api/annotations/:annotationId", role, func(sc *scenarioContext) { + setUp() + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + sc.handlerFunc = DeleteAnnotationByID + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) }) }) - Convey("When user is an Admin", func() { + t.Run("When user is an Admin", func(t *testing.T) { role := models.ROLE_ADMIN - Convey("Should be able to do anything", func() { - postAnnotationScenario("When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + t.Run("Should be able to do anything", func(t *testing.T) { + postAnnotationScenario(t, "When calling POST on", "/api/annotations", "/api/annotations", role, cmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - putAnnotationScenario("When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + putAnnotationScenario(t, "When calling PUT on", "/api/annotations/1", "/api/annotations/:annotationId", role, updateCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + patchAnnotationScenario(t, "When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + setUp() sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) - deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) - }) + deleteAnnotationsScenario(t, "When calling POST on", "/api/annotations/mass-delete", + "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { + setUp() + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 200, sc.resp.Code) + }) }) }) }) @@ -229,15 +260,16 @@ func (repo *fakeAnnotationsRepo) Find(query *annotations.ItemQuery) ([]*annotati var fakeAnnoRepo *fakeAnnotationsRepo -func postAnnotationScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.PostAnnotationsCmd, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, + cmd dtos.PostAnnotationsCmd, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return PostAnnotation(c, cmd) @@ -252,15 +284,16 @@ func postAnnotationScenario(desc string, url string, routePattern string, role m }) } -func putAnnotationScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, + cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return UpdateAnnotation(c, cmd) @@ -275,15 +308,15 @@ func putAnnotationScenario(desc string, url string, routePattern string, role mo }) } -func patchAnnotationScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return PatchAnnotation(c, cmd) @@ -298,15 +331,16 @@ func patchAnnotationScenario(desc string, url string, routePattern string, role }) } -func deleteAnnotationsScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, + cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role return DeleteAnnotations(c, cmd) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 4b0855da675..dd951fab78f 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -11,24 +11,23 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" - . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/require" "gopkg.in/macaron.v1" ) -func loggedInUserScenario(desc string, url string, fn scenarioFunc) { - loggedInUserScenarioWithRole(desc, "GET", url, url, models.ROLE_EDITOR, fn) +func loggedInUserScenario(t *testing.T, desc string, url string, fn scenarioFunc) { + loggedInUserScenarioWithRole(t, desc, "GET", url, url, models.ROLE_EDITOR, fn) } -func loggedInUserScenarioWithRole(desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = role if sc.handlerFunc != nil { return sc.handlerFunc(sc.context) @@ -48,11 +47,11 @@ func loggedInUserScenarioWithRole(desc string, method string, url string, routeP }) } -func anonymousUserScenario(desc string, method string, url string, routePattern string, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c if sc.handlerFunc != nil { @@ -76,7 +75,7 @@ func anonymousUserScenario(desc string, method string, url string, routePattern func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext { sc.resp = httptest.NewRecorder() req, err := http.NewRequest(method, url, nil) - So(err, ShouldBeNil) + require.NoError(sc.t, err) sc.req = req return sc @@ -141,9 +140,10 @@ func (sc *scenarioContext) exec() { type scenarioFunc func(c *scenarioContext) type handlerFunc func(c *models.ReqContext) Response -func setupScenarioContext(url string) *scenarioContext { +func setupScenarioContext(t *testing.T, url string) *scenarioContext { sc := &scenarioContext{ url: url, + t: t, } viewsPath, _ := filepath.Abs("../../public/views") diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 5015c9ce3e7..ab4e6629016 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -48,7 +48,9 @@ func dashboardGuardianResponse(err error) Response { } func (hs *HTTPServer) GetDashboard(c *models.ReqContext) Response { - dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) + slug := c.Params(":slug") + uid := c.Params(":uid") + dash, rsp := getDashboardHelper(c.OrgId, slug, 0, uid) if rsp != nil { return rsp } diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 7c61b094acb..5b5cc1a1df7 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "testing" "github.com/grafana/grafana/pkg/api/dtos" @@ -8,21 +9,25 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestDashboardPermissionApiEndpoint(t *testing.T) { - Convey("Dashboard permissions test", t, func() { - Convey("Given dashboard not exists", func() { - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - return models.ErrDashboardNotFound - }) +func TestDashboardPermissionAPIEndpoint(t *testing.T) { + t.Run("Dashboard permissions test", func(t *testing.T) { + t.Run("Given dashboard not exists", func(t *testing.T) { + setUp := func() { + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + return models.ErrDashboardNotFound + }) + } - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { - callGetDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 404) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + callGetDashboardPermissions(sc) + assert.Equal(t, 404, sc.resp.Code) + }) cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -30,26 +35,37 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 404) - }) + updateDashboardPermissionScenario(t, "When calling POST on", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + setUp() + callUpdateDashboardPermissions(sc) + assert.Equal(t, 404, sc.resp.Code) + }) }) - Convey("Given user has no admin permissions", func() { + t.Run("Given user has no admin permissions", func(t *testing.T) { origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) getDashboardQueryResult := models.NewDashboard("Dash") - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = getDashboardQueryResult - return nil - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { - callGetDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + setUp := func() { + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + } + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + callGetDashboardPermissions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -57,18 +73,20 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - Reset(func() { - guardian.New = origNewGuardian - }) + updateDashboardPermissionScenario(t, "When calling POST on", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + setUp() + callUpdateDashboardPermissions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) }) - Convey("Given user has admin permissions and permissions to update", func() { + t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) { origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: true, @@ -81,21 +99,25 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, }) - getDashboardQueryResult := models.NewDashboard("Dash") - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = getDashboardQueryResult - return nil - }) + setUp := func() { + getDashboardQueryResult := models.NewDashboard("Dash") + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + } - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) { - callGetDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(len(respJSON.MustArray()), ShouldEqual, 5) - So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) - So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, models.PERMISSION_VIEW) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) { + setUp() + callGetDashboardPermissions(sc) + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, 5, len(respJSON.MustArray())) + assert.Equal(t, 2, respJSON.GetIndex(0).Get("userId").MustInt()) + assert.Equal(t, int(models.PERMISSION_VIEW), respJSON.GetIndex(0).Get("permission").MustInt()) + }) cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -103,29 +125,32 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) - - Reset(func() { - guardian.New = origNewGuardian - }) + updateDashboardPermissionScenario(t, "When calling POST on", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + setUp() + callUpdateDashboardPermissions(sc) + assert.Equal(t, 200, sc.resp.Code) + }) }) - Convey("When trying to update permissions with duplicate permissions", func() { + t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) { origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, }) - getDashboardQueryResult := models.NewDashboard("Dash") - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = getDashboardQueryResult - return nil - }) + setUp := func() { + getDashboardQueryResult := models.NewDashboard("Dash") + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + } cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -133,29 +158,33 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 400) - }) - - Reset(func() { - guardian.New = origNewGuardian - }) + updateDashboardPermissionScenario(t, "When calling POST on", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + setUp() + callUpdateDashboardPermissions(sc) + assert.Equal(t, 400, sc.resp.Code) + }) }) - Convey("When trying to override inherited permissions with lower precedence", func() { + t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) { origNewGuardian := guardian.New + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ CanAdminValue: true, CheckPermissionBeforeUpdateValue: false, CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, ) - getDashboardQueryResult := models.NewDashboard("Dash") - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = getDashboardQueryResult - return nil - }) + setUp := func() { + getDashboardQueryResult := models.NewDashboard("Dash") + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = getDashboardQueryResult + return nil + }) + } cmd := dtos.UpdateDashboardAclCommand{ Items: []dtos.DashboardAclUpdateItem{ @@ -163,14 +192,12 @@ func TestDashboardPermissionApiEndpoint(t *testing.T) { }, } - updateDashboardPermissionScenario("When calling POST on", "/api/dashboards/id/1/permissions", "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { - callUpdateDashboardPermissions(sc) - So(sc.resp.Code, ShouldEqual, 400) - }) - - Reset(func() { - guardian.New = origNewGuardian - }) + updateDashboardPermissionScenario(t, "When calling POST on", "/api/dashboards/id/1/permissions", + "/api/dashboards/id/:id/permissions", cmd, func(sc *scenarioContext) { + setUp() + callUpdateDashboardPermissions(sc) + assert.Equal(t, 400, sc.resp.Code) + }) }) }) } @@ -188,16 +215,16 @@ func callUpdateDashboardPermissions(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func updateDashboardPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func updateDashboardPermissionScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.OrgId = TestOrgID - sc.context.UserId = TestUserID + sc.context.OrgId = testOrgID + sc.context.UserId = testUserID return UpdateDashboardPermissions(c, cmd) }) diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index d5e0a47dc2b..2449522531b 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -236,10 +236,10 @@ func DeleteDashboardSnapshot(c *models.ReqContext) Response { if err != nil { return Error(500, "Failed to get dashboard snapshot", err) } - if query.Result == nil { return Error(404, "Failed to get dashboard snapshot", nil) } + dashboard, err := query.Result.DashboardJSON() if err != nil { return Error(500, "Failed to get dashboard data for dashboard snapshot", err) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index f1b733721b3..9689e02a215 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -4,23 +4,38 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" - "github.com/grafana/grafana/pkg/components/securedata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/securedata" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - - . "github.com/smartystreets/goconvey/convey" ) -func TestDashboardSnapshotApiEndpoint(t *testing.T) { - Convey("Given a single snapshot", t, func() { - var externalRequest *http.Request - jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`)) +func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { + setupRemoteServer := func(fn func(http.ResponseWriter, *http.Request)) *httptest.Server { + s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fn(rw, r) + })) + t.Cleanup(s.Close) + return s + } + + jsonModel, err := simplejson.NewJson([]byte(`{"id":100}`)) + require.NoError(t, err) + + viewerRole := models.ROLE_VIEWER + editorRole := models.ROLE_EDITOR + aclMockResp := []*models.DashboardAclInfoDTO{} + + setUpSnapshotTest := func(t *testing.T) *models.DashboardSnapshot { + t.Helper() mockSnapshotResult := &models.DashboardSnapshot{ Id: 1, @@ -41,9 +56,6 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { return nil }) - viewerRole := models.ROLE_VIEWER - editorRole := models.ROLE_EDITOR - aclMockResp := []*models.DashboardAclInfoDTO{} bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { query.Result = aclMockResp return nil @@ -55,185 +67,199 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { return nil }) - setupRemoteServer := func(fn func(http.ResponseWriter, *http.Request)) *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - fn(rw, r) - })) + return mockSnapshotResult + } + + t.Run("When user has editor role and is not in the ACL", func(t *testing.T) { + loggedInUserScenarioWithRole(t, "Should not be able to delete snapshot when calling DELETE on", + "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) + + var externalRequest *http.Request + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + externalRequest = req + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, 403, sc.resp.Code) + require.Nil(t, externalRequest) + }) + }) + + t.Run("When user is anonymous", func(t *testing.T) { + anonymousUserScenario(t, "Should be able to delete a snapshot when calling GET on", "GET", + "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) + + var externalRequest *http.Request + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + externalRequest = req + }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey + sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec() + + require.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted")) + + assert.Equal(t, http.MethodGet, externalRequest.Method) + assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host)) + assert.Equal(t, "/", externalRequest.URL.EscapedPath()) + }) + }) + + t.Run("When user is editor and dashboard has default ACL", func(t *testing.T) { + aclMockResp = []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, } - Convey("When user has editor role and is not in the ACL", func() { - Convey("Should not be able to delete snapshot", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - externalRequest = req - }) + loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - - So(sc.resp.Code, ShouldEqual, 403) - So(externalRequest, ShouldBeNil) + var externalRequest *http.Request + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(200) + externalRequest = req }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted")) + assert.Equal(t, ts.URL, fmt.Sprintf("http://%s", externalRequest.Host)) + assert.Equal(t, "/", externalRequest.URL.EscapedPath()) }) - }) + }) - Convey("When user is anonymous", func() { - Convey("Should be able to delete snapshot by deleteKey", func() { - anonymousUserScenario("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) { - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(200) - externalRequest = req - }) + t.Run("When user is editor and creator of the snapshot", func(t *testing.T) { + aclMockResp = []*models.DashboardAclInfoDTO{} + loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", + "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey - sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec() + mockSnapshotResult.UserId = testUserID + mockSnapshotResult.External = false - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) - So(externalRequest.Method, ShouldEqual, http.MethodGet) - So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL) - So(externalRequest.URL.EscapedPath(), ShouldEqual, "/") + assert.True(t, strings.HasPrefix(respJSON.Get("message").MustString(), "Snapshot deleted")) + }) + }) + + t.Run("When deleting an external snapshot", func(t *testing.T) { + aclMockResp = []*models.DashboardAclInfoDTO{} + var writeErr error + loggedInUserScenarioWithRole(t, + "Should gracefully delete local snapshot when remote snapshot has already been removed when calling DELETE on", + "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) + mockSnapshotResult.UserId = testUserID + + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + _, writeErr = rw.Write([]byte(`{"message":"Failed to get dashboard snapshot"}`)) + rw.WriteHeader(500) }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + require.NoError(t, writeErr) + assert.Equal(t, 200, sc.resp.Code) }) - }) - Convey("When user is editor and dashboard has default ACL", func() { - aclMockResp = []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } + loggedInUserScenarioWithRole(t, + "Should fail to delete local snapshot when an unexpected 500 error occurs when calling DELETE on", "DELETE", + "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) + mockSnapshotResult.UserId = testUserID - Convey("Should be able to delete a snapshot", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(200) - externalRequest = req - }) - - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") - So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL) - So(externalRequest.URL.EscapedPath(), ShouldEqual, "/") - }) - }) - }) - - Convey("When user is editor and is the creator of the snapshot", func() { - aclMockResp = []*models.DashboardAclInfoDTO{} - mockSnapshotResult.UserId = TestUserID - mockSnapshotResult.External = false - - Convey("Should be able to delete a snapshot", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted") - }) - }) - }) - - Convey("When deleting an external snapshot", func() { - aclMockResp = []*models.DashboardAclInfoDTO{} - mockSnapshotResult.UserId = TestUserID - - Convey("Should gracefully delete local snapshot when remote snapshot has already been removed", func() { var writeErr error - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - _, writeErr = rw.Write([]byte(`{"message":"Failed to get dashboard snapshot"}`)) - rw.WriteHeader(500) - }) - - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - - So(writeErr, ShouldBeNil) - So(sc.resp.Code, ShouldEqual, 200) + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(500) + _, writeErr = rw.Write([]byte(`{"message":"Unexpected"}`)) }) + + t.Log("Setting external delete URL", "url", ts.URL) + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + require.NoError(t, writeErr) + assert.Equal(t, 500, sc.resp.Code) }) - Convey("Should fail to delete local snapshot when an unexpected 500 error occurs", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - var writeErr error - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(500) - _, writeErr = rw.Write([]byte(`{"message":"Unexpected"}`)) - }) + loggedInUserScenarioWithRole(t, + "Should fail to delete local snapshot when an unexpected remote error occurs when calling DELETE on", + "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + mockSnapshotResult := setUpSnapshotTest(t) + mockSnapshotResult.UserId = testUserID - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() - - So(writeErr, ShouldBeNil) - So(sc.resp.Code, ShouldEqual, 500) + ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(404) }) + + mockSnapshotResult.ExternalDeleteUrl = ts.URL + sc.handlerFunc = DeleteDashboardSnapshot + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + + assert.Equal(t, 500, sc.resp.Code) }) - Convey("Should fail to delete local snapshot when an unexpected remote error occurs", func() { - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { - rw.WriteHeader(404) - }) + loggedInUserScenarioWithRole(t, "Should be able to read a snapshot's unencrypted data when calling GET on", + "GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUpSnapshotTest(t) - mockSnapshotResult.ExternalDeleteUrl = ts.URL - sc.handlerFunc = DeleteDashboardSnapshot - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() + sc.handlerFunc = GetDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() - So(sc.resp.Code, ShouldEqual, 500) - }) + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + dashboard := respJSON.Get("dashboard") + id := dashboard.Get("id") + + assert.Equal(t, int64(100), id.MustInt64()) }) - Convey("Should be able to read a snapshot's un-encrypted data", func() { - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboardSnapshot - sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() - - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - dashboard := respJSON.Get("dashboard") - id := dashboard.Get("id") - - So(id.MustInt64(), ShouldEqual, 100) - }) - }) - - Convey("Should be able to read a snapshot's encrypted data", func() { + loggedInUserScenarioWithRole(t, "Should be able to read a snapshot's encrypted data When calling GET on", + "GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { origSecret := setting.SecretKey setting.SecretKey = "dashboard_snapshot_api_test" t.Cleanup(func() { setting.SecretKey = origSecret }) - dashboardId := 123 - jsonModel, err := simplejson.NewJson([]byte(fmt.Sprintf(`{"id":%d}`, dashboardId))) - So(err, ShouldBeNil) + const dashboardID int64 = 123 + jsonModel, err := simplejson.NewJson([]byte(fmt.Sprintf(`{"id":%d}`, dashboardID))) + require.NoError(t, err) jsonModelEncoded, err := jsonModel.Encode() - So(err, ShouldBeNil) + require.NoError(t, err) encrypted, err := securedata.Encrypt(jsonModelEncoded) - So(err, ShouldBeNil) + require.NoError(t, err) // mock snapshot with encrypted dashboard info mockSnapshotResult := &models.DashboardSnapshot{ @@ -242,21 +268,20 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { Expires: time.Now().Add(time.Duration(1000) * time.Second), } + setUpSnapshotTest(t) + bus.AddHandler("test", func(query *models.GetDashboardSnapshotQuery) error { query.Result = mockSnapshotResult return nil }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboardSnapshot - sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() + sc.handlerFunc = GetDashboardSnapshot + sc.fakeReqWithParams("GET", sc.url, map[string]string{"key": "12345"}).exec() - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("dashboard").Get("id").MustInt64(), ShouldEqual, dashboardId) - }) + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, dashboardID, respJSON.Get("dashboard").Get("id").MustInt64()) }) - }) }) } diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 8870c1601a0..edc1b47c5ea 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -16,9 +16,8 @@ import ( "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - . "github.com/smartystreets/goconvey/convey" ) func TestGetHomeDashboard(t *testing.T) { @@ -68,229 +67,255 @@ func TestGetHomeDashboard(t *testing.T) { } } +type testState struct { + dashQueries []*models.GetDashboardQuery +} + // This tests three main scenarios. // If a user has access to execute an action on a dashboard: // 1. and the dashboard is in a folder which does not have an acl // 2. and the dashboard is in a folder which does have an acl // 3. Post dashboard response tests -func TestDashboardApiEndpoint(t *testing.T) { - Convey("Given a dashboard with a parent folder which does not have an acl", t, func() { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 - fakeDash.HasAcl = false +func TestDashboardAPIEndpoint(t *testing.T) { + t.Run("Given a dashboard with a parent folder which does not have an ACL", func(t *testing.T) { + setUp := func() *testState { + fakeDash := models.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = false - bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { - dashboards := []*models.Dashboard{fakeDash} - query.Result = dashboards - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { + dashboards := []*models.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) - var getDashboardQueries []*models.GetDashboardQuery + state := &testState{} - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - getDashboardQueries = append(getDashboardQueries, query) - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + state.dashQueries = append(state.dashQueries, query) + return nil + }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil + return nil + }) - viewerRole := models.ROLE_VIEWER - editorRole := models.ROLE_EDITOR + viewerRole := models.ROLE_VIEWER + editorRole := models.ROLE_EDITOR - aclMockResp := []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + aclMockResp := []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) + + bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { + query.Result = []*models.TeamDTO{} + return nil + }) + + return state } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = aclMockResp - return nil - }) - - bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { - query.Result = []*models.TeamDTO{} - return nil - }) - // This tests two scenarios: // 1. user is an org viewer // 2. user is an org editor - Convey("When user is an Org Viewer", func() { + t.Run("When user is an Org Viewer", func(t *testing.T) { role := models.ROLE_VIEWER - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + dash := getDashboardShouldReturn200(sc) + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + + assert.False(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should not be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeFalse) - So(dash.Meta.CanSave, ShouldBeFalse) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(sc) - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + + assert.False(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should not be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeFalse) - So(dash.Meta.CanSave, ShouldBeFalse) - So(dash.Meta.CanAdmin, ShouldBeFalse) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() + + CallDeleteDashboardBySlug(sc) + assert.Equal(t, 403, sc.resp.Code) + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + CallDeleteDashboardByUID(sc) + assert.Equal(t, 403, sc.resp.Code) + + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", + "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUp() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + callGetDashboardVersion(sc) + assert.Equal(t, 403, sc.resp.Code) }) - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", + "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + callGetDashboardVersions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) }) - Convey("When user is an Org Editor", func() { + t.Run("When user is an Org Editor", func(t *testing.T) { role := models.ROLE_EDITOR - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + dash := getDashboardShouldReturn200(sc) + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(sc) - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeFalse) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() + + CallDeleteDashboardBySlug(sc) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 200) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + CallDeleteDashboardByUID(sc) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 200) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", + "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUp() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + callGetDashboardVersion(sc) + assert.Equal(t, 200, sc.resp.Code) }) - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", + "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) + callGetDashboardVersions(sc) + assert.Equal(t, 200, sc.resp.Code) + }) }) }) - Convey("Given a dashboard with a parent folder which has an acl", t, func() { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 1 - fakeDash.FolderId = 1 - fakeDash.HasAcl = true - setting.ViewersCanEdit = false - - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) - - bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { - dashboards := []*models.Dashboard{fakeDash} - query.Result = dashboards - return nil - }) - - aclMockResp := []*models.DashboardAclInfoDTO{ - { - DashboardId: 1, - Permission: models.PERMISSION_EDIT, - UserId: 200, - }, - } - - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = aclMockResp - return nil - }) - - var getDashboardQueries []*models.GetDashboardQuery - - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - getDashboardQueries = append(getDashboardQueries, query) - return nil - }) - - bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { - query.Result = []*models.TeamDTO{} - return nil - }) - + t.Run("Given a dashboard with a parent folder which has an ACL", func(t *testing.T) { hs := &HTTPServer{ Cfg: setting.NewCfg(), } + setUp := func() *testState { + state := &testState{} + + fakeDash := models.NewDashboard("Child dash") + fakeDash.Id = 1 + fakeDash.FolderId = 1 + fakeDash.HasAcl = true + + origCanEdit := setting.ViewersCanEdit + t.Cleanup(func() { + setting.ViewersCanEdit = origCanEdit + }) + setting.ViewersCanEdit = false + + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil + return nil + }) + + bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { + dashboards := []*models.Dashboard{fakeDash} + query.Result = dashboards + return nil + }) + + aclMockResp := []*models.DashboardAclInfoDTO{ + { + DashboardId: 1, + Permission: models.PERMISSION_EDIT, + UserId: 200, + }, + } + + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = aclMockResp + return nil + }) + + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + state.dashQueries = append(state.dashQueries, query) + return nil + }) + + bus.AddHandler("test", func(query *models.GetTeamsByUserQuery) error { + query.Result = []*models.TeamDTO{} + return nil + }) + + return state + } + // This tests six scenarios: // 1. user is an org viewer AND has no permissions for this dashboard // 2. user is an org editor AND has no permissions for this dashboard @@ -299,389 +324,399 @@ func TestDashboardApiEndpoint(t *testing.T) { // 5. user is an org viewer AND has been granted an admin permission // 6. user is an org editor AND has been granted a view permission - Convey("When user is an Org Viewer and has no permissions for this dashboard", func() { + t.Run("When user is an Org Viewer and has no permissions for this dashboard", func(t *testing.T) { role := models.ROLE_VIEWER - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = hs.GetDashboard - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + sc.handlerFunc = hs.GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.Equal(t, 403, sc.resp.Code) }) - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = hs.GetDashboard - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + sc.handlerFunc = hs.GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.Equal(t, 403, sc.resp.Code) }) - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() + + CallDeleteDashboardBySlug(sc) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + CallDeleteDashboardByUID(sc) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", + "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUp() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + callGetDashboardVersion(sc) + assert.Equal(t, 403, sc.resp.Code) }) - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", + "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + callGetDashboardVersions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) }) - Convey("When user is an Org Editor and has no permissions for this dashboard", func() { + t.Run("When user is an Org Editor and has no permissions for this dashboard", func(t *testing.T) { role := models.ROLE_EDITOR - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = hs.GetDashboard - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + sc.handlerFunc = hs.GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.Equal(t, 403, sc.resp.Code) }) - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) - }) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = hs.GetDashboard - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + sc.handlerFunc = hs.GetDashboard + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.Equal(t, 403, sc.resp.Code) }) - Convey("Should be denied access", func() { - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUp() + + CallDeleteDashboardBySlug(sc) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUp() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + CallDeleteDashboardByUID(sc) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) - }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", + "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUp() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + callGetDashboardVersion(sc) + assert.Equal(t, 403, sc.resp.Code) }) - }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", + "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUp() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) + callGetDashboardVersions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) }) - Convey("When user is an Org Viewer but has an edit permission", func() { + t.Run("When user is an Org Viewer but has an edit permission", func(t *testing.T) { role := models.ROLE_VIEWER mockResult := []*models.DashboardAclInfoDTO{ {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_EDIT}, } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + setUpInner := func() *testState { + state := setUp() + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil }) - - Convey("Should be able to get dashboard with edit rights", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - - Convey("Should be able to get dashboard with edit rights", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) - }) - - Convey("When user is an Org Viewer and viewers can edit", func() { - role := models.ROLE_VIEWER - setting.ViewersCanEdit = true - - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + return state } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", + "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(sc) - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should be able to get dashboard with edit rights but can save should be false", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeFalse) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", + "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + dash := getDashboardShouldReturn200(sc) - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - Convey("Should be able to get dashboard with edit rights but can save should be false", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeFalse) - So(dash.Meta.CanAdmin, ShouldBeFalse) - }) - }) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) + CallDeleteDashboardByUID(sc) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersion(sc) + assert.Equal(t, 200, sc.resp.Code) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersions(sc) + assert.Equal(t, 200, sc.resp.Code) }) }) - Convey("When user is an Org Viewer but has an admin permission", func() { + t.Run("When user is an Org Viewer and viewers can edit", func(t *testing.T) { role := models.ROLE_VIEWER - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_ADMIN}, + setUpInner := func() *testState { + state := setUp() + + mockResult := []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + } + + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + + origCanEdit := setting.ViewersCanEdit + t.Cleanup(func() { + setting.ViewersCanEdit = origCanEdit + }) + setting.ViewersCanEdit = true + + return state } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() + + dash := getDashboardShouldReturn200(sc) + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.True(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) + require.True(t, setting.ViewersCanEdit) + dash := getDashboardShouldReturn200(sc) - Convey("Should be able to get dashboard with edit rights", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeTrue) - }) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.True(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) + assert.False(t, dash.Meta.CanAdmin) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - - Convey("Should be able to get dashboard with edit rights", func() { - So(dash.Meta.CanEdit, ShouldBeTrue) - So(dash.Meta.CanSave, ShouldBeTrue) - So(dash.Meta.CanAdmin, ShouldBeTrue) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() + CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 200) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 200) - }) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) }) - Convey("When user is an Org Editor but has a view permission", func() { + t.Run("When user is an Org Viewer but has an admin permission", func(t *testing.T) { + role := models.ROLE_VIEWER + + setUpInner := func() *testState { + state := setUp() + + mockResult := []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_ADMIN}, + } + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + return state + } + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() + + dash := getDashboardShouldReturn200(sc) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.True(t, dash.Meta.CanAdmin) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() + + dash := getDashboardShouldReturn200(sc) + + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.True(t, dash.Meta.CanEdit) + assert.True(t, dash.Meta.CanSave) + assert.True(t, dash.Meta.CanAdmin) + }) + + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() + + CallDeleteDashboardBySlug(sc) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + }) + + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() + + CallDeleteDashboardByUID(sc) + assert.Equal(t, 200, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersion(sc) + assert.Equal(t, 200, sc.resp.Code) + }) + + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersions(sc) + assert.Equal(t, 200, sc.resp.Code) + }) + }) + + t.Run("When user is an Org Editor but has a view permission", func(t *testing.T) { role := models.ROLE_EDITOR - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + setUpInner := func() *testState { + state := setUp() + + mockResult := []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + } + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) + return state } - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() + + dash := getDashboardShouldReturn200(sc) + + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) + assert.False(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) - - Convey("Should not be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeFalse) - So(dash.Meta.CanSave, ShouldBeFalse) - }) + dash := getDashboardShouldReturn200(sc) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) + assert.False(t, dash.Meta.CanEdit) + assert.False(t, dash.Meta.CanSave) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - dash := GetDashboardShouldReturn200(sc) + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + state := setUpInner() - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) - - Convey("Should not be able to edit or save dashboard", func() { - So(dash.Meta.CanEdit, ShouldBeFalse) - So(dash.Meta.CanSave, ShouldBeFalse) - }) - }) - - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboardBySlug(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup dashboard by slug", func() { - So(getDashboardQueries[0].Slug, ShouldEqual, "child-dash") - }) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "child-dash", state.dashQueries[0].Slug) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { + state := setUpInner() + CallDeleteDashboardByUID(sc) - So(sc.resp.Code, ShouldEqual, 403) - - Convey("Should lookup dashboard by uid", func() { - So(getDashboardQueries[0].Uid, ShouldEqual, "abcdefghi") - }) + assert.Equal(t, 403, sc.resp.Code) + assert.Equal(t, "abcdefghi", state.dashQueries[0].Uid) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { - CallGetDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions/1", "/api/dashboards/id/:dashboardId/versions/:id", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersion(sc) + assert.Equal(t, 403, sc.resp.Code) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { - CallGetDashboardVersions(sc) - So(sc.resp.Code, ShouldEqual, 403) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", role, func(sc *scenarioContext) { + setUpInner() + + callGetDashboardVersions(sc) + assert.Equal(t, 403, sc.resp.Code) }) }) }) - Convey("Given two dashboards with the same title in different folders", t, func() { + t.Run("Given two dashboards with the same title in different folders", func(t *testing.T) { dashOne := models.NewDashboard("dash") dashOne.Id = 2 dashOne.FolderId = 1 @@ -705,22 +740,22 @@ func TestDashboardApiEndpoint(t *testing.T) { role := models.ROLE_EDITOR - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { CallDeleteDashboardBySlug(sc) - Convey("Should result in 412 Precondition failed", func() { - So(sc.resp.Code, ShouldEqual, 412) - result := sc.ToJSON() - So(result.Get("status").MustString(), ShouldEqual, "multiple-slugs-exists") - So(result.Get("message").MustString(), ShouldEqual, models.ErrDashboardsWithSameSlugExists.Error()) - }) + assert.Equal(t, 412, sc.resp.Code) + result := sc.ToJSON() + assert.Equal(t, "multiple-slugs-exists", result.Get("status").MustString()) + assert.Equal(t, models.ErrDashboardsWithSameSlugExists.Error(), result.Get("message").MustString()) }) }) - Convey("Post dashboard response tests", t, func() { + t.Run("Post dashboard response tests", func(t *testing.T) { // This tests that a valid request returns correct response + t.Run("Given a correct request for creating a dashboard", func(t *testing.T) { + const folderID int64 = 3 + const dashID int64 = 2 - Convey("Given a correct request for creating a dashboard", func() { cmd := models.SaveDashboardCommand{ OrgId: 1, UserId: 5, @@ -728,14 +763,14 @@ func TestDashboardApiEndpoint(t *testing.T) { "title": "Dash", }), Overwrite: true, - FolderId: 3, + FolderId: folderID, IsFolder: false, Message: "msg", } mock := &dashboards.FakeDashboardService{ SaveDashboardResult: &models.Dashboard{ - Id: 2, + Id: dashID, Uid: "uid", Title: "Dash", Slug: "dash", @@ -743,33 +778,28 @@ func TestDashboardApiEndpoint(t *testing.T) { }, } - postDashboardScenario("When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { - CallPostDashboardShouldReturnSuccess(sc) + postDashboardScenario(t, "When calling POST on", "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { + callPostDashboardShouldReturnSuccess(sc) - Convey("It should call dashboard service with correct data", func() { - dto := mock.SavedDashboards[0] - So(dto.OrgId, ShouldEqual, cmd.OrgId) - So(dto.User.UserId, ShouldEqual, cmd.UserId) - So(dto.Dashboard.FolderId, ShouldEqual, 3) - So(dto.Dashboard.Title, ShouldEqual, "Dash") - So(dto.Overwrite, ShouldBeTrue) - So(dto.Message, ShouldEqual, "msg") - }) + dto := mock.SavedDashboards[0] + assert.Equal(t, cmd.OrgId, dto.OrgId) + assert.Equal(t, cmd.UserId, dto.User.UserId) + assert.Equal(t, folderID, dto.Dashboard.FolderId) + assert.Equal(t, "Dash", dto.Dashboard.Title) + assert.True(t, dto.Overwrite) + assert.Equal(t, "msg", dto.Message) - Convey("It should return correct response data", func() { - result := sc.ToJSON() - So(result.Get("status").MustString(), ShouldEqual, "success") - So(result.Get("id").MustInt64(), ShouldEqual, 2) - So(result.Get("uid").MustString(), ShouldEqual, "uid") - So(result.Get("slug").MustString(), ShouldEqual, "dash") - So(result.Get("url").MustString(), ShouldEqual, "/d/uid/dash") - }) + result := sc.ToJSON() + assert.Equal(t, "success", result.Get("status").MustString()) + assert.Equal(t, dashID, result.Get("id").MustInt64()) + assert.Equal(t, "uid", result.Get("uid").MustString()) + assert.Equal(t, "dash", result.Get("slug").MustString()) + assert.Equal(t, "/d/uid/dash", result.Get("url").MustString()) }) }) // This tests that invalid requests returns expected error responses - - Convey("Given incorrect requests for creating a dashboard", func() { + t.Run("Given incorrect requests for creating a dashboard", func(t *testing.T) { testCases := []struct { SaveError error ExpectedStatusCode int @@ -806,34 +836,37 @@ func TestDashboardApiEndpoint(t *testing.T) { SaveDashboardError: tc.SaveError, } - postDashboardScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, tc.ExpectedStatusCode) - }) + postDashboardScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.SaveError.Error()), + "/api/dashboards", "/api/dashboards", mock, cmd, func(sc *scenarioContext) { + callPostDashboard(sc) + assert.Equal(t, tc.ExpectedStatusCode, sc.resp.Code) + }) } }) }) - Convey("Given two dashboards being compared", t, func() { - mockResult := []*models.DashboardAclInfoDTO{} - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = mockResult - return nil - }) + t.Run("Given two dashboards being compared", func(t *testing.T) { + setUp := func() { + mockResult := []*models.DashboardAclInfoDTO{} + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = mockResult + return nil + }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = nil - return nil - }) + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil + return nil + }) - bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { - query.Result = &models.DashboardVersion{ - Data: simplejson.NewFromAny(map[string]interface{}{ - "title": fmt.Sprintf("Dash%d", query.DashboardId), - }), - } - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { + query.Result = &models.DashboardVersion{ + Data: simplejson.NewFromAny(map[string]interface{}{ + "title": fmt.Sprintf("Dash%d", query.DashboardId), + }), + } + return nil + }) + } cmd := dtos.CalculateDiffOptions{ Base: dtos.CalculateDiffTarget{ @@ -847,44 +880,51 @@ func TestDashboardApiEndpoint(t *testing.T) { DiffType: "basic", } - Convey("when user does not have permission", func() { + t.Run("when user does not have permission", func(t *testing.T) { role := models.ROLE_VIEWER - postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 403) + postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { + setUp() + + callPostDashboard(sc) + assert.Equal(t, 403, sc.resp.Code) }) }) - Convey("when user does have permission", func() { + t.Run("when user does have permission", func(t *testing.T) { role := models.ROLE_ADMIN - postDiffScenario("When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - CallPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) + postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { + setUp() + + callPostDashboard(sc) + assert.Equal(t, 200, sc.resp.Code) }) }) }) - Convey("Given dashboard in folder being restored should restore to folder", t, func() { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 - fakeDash.FolderId = 1 - fakeDash.HasAcl = false + t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) { + const folderID int64 = 1 + setUp := func() { + fakeDash := models.NewDashboard("Child dash") + fakeDash.Id = 2 + fakeDash.FolderId = folderID + fakeDash.HasAcl = false - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) - bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { - query.Result = &models.DashboardVersion{ - DashboardId: 2, - Version: 1, - Data: fakeDash.Data, - } - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { + query.Result = &models.DashboardVersion{ + DashboardId: 2, + Version: 1, + Data: fakeDash.Data, + } + return nil + }) + } mock := &dashboards.FakeDashboardService{ SaveDashboardResult: &models.Dashboard{ @@ -900,34 +940,39 @@ func TestDashboardApiEndpoint(t *testing.T) { Version: 1, } - restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { - CallRestoreDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 200) - dto := mock.SavedDashboards[0] - So(dto.Dashboard.FolderId, ShouldEqual, 1) - So(dto.Dashboard.Title, ShouldEqual, "Child dash") - So(dto.Message, ShouldEqual, "Restored from version 1") - }) + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", + "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { + setUp() + + callRestoreDashboardVersion(sc) + assert.Equal(t, 200, sc.resp.Code) + dto := mock.SavedDashboards[0] + assert.Equal(t, folderID, dto.Dashboard.FolderId) + assert.Equal(t, "Child dash", dto.Dashboard.Title) + assert.Equal(t, "Restored from version 1", dto.Message) + }) }) - Convey("Given dashboard in general folder being restored should restore to general folder", t, func() { - fakeDash := models.NewDashboard("Child dash") - fakeDash.Id = 2 - fakeDash.HasAcl = false + t.Run("Given dashboard in general folder being restored should restore to general folder", func(t *testing.T) { + setUp := func() { + fakeDash := models.NewDashboard("Child dash") + fakeDash.Id = 2 + fakeDash.HasAcl = false - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = fakeDash - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = fakeDash + return nil + }) - bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { - query.Result = &models.DashboardVersion{ - DashboardId: 2, - Version: 1, - Data: fakeDash.Data, - } - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { + query.Result = &models.DashboardVersion{ + DashboardId: 2, + Version: 1, + Data: fakeDash.Data, + } + return nil + }) + } mock := &dashboards.FakeDashboardService{ SaveDashboardResult: &models.Dashboard{ @@ -943,72 +988,79 @@ func TestDashboardApiEndpoint(t *testing.T) { Version: 1, } - restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { - CallRestoreDashboardVersion(sc) - So(sc.resp.Code, ShouldEqual, 200) - dto := mock.SavedDashboards[0] - So(dto.Dashboard.FolderId, ShouldEqual, 0) - So(dto.Dashboard.Title, ShouldEqual, "Child dash") - So(dto.Message, ShouldEqual, "Restored from version 1") - }) + restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", + "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) { + setUp() + + callRestoreDashboardVersion(sc) + assert.Equal(t, 200, sc.resp.Code) + dto := mock.SavedDashboards[0] + assert.Equal(t, int64(0), dto.Dashboard.FolderId) + assert.Equal(t, "Child dash", dto.Dashboard.Title) + assert.Equal(t, "Restored from version 1", dto.Message) + }) }) - Convey("Given provisioned dashboard", t, func() { - bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { - query.Result = []*models.Dashboard{{}} - return nil - }) - bus.AddHandler("test", func(query *models.GetDashboardQuery) error { - query.Result = &models.Dashboard{Id: 1, Data: &simplejson.Json{}} - return nil - }) + t.Run("Given provisioned dashboard", func(t *testing.T) { + setUp := func() { + bus.AddHandler("test", func(query *models.GetDashboardsBySlugQuery) error { + query.Result = []*models.Dashboard{{}} + return nil + }) + bus.AddHandler("test", func(query *models.GetDashboardQuery) error { + query.Result = &models.Dashboard{Id: 1, Data: &simplejson.Json{}} + return nil + }) - bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { - query.Result = &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"} - return nil - }) + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { + query.Result = &models.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"} + return nil + }) - bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { - query.Result = []*models.DashboardAclInfoDTO{ - {OrgId: TestOrgID, DashboardId: 1, UserId: TestUserID, Permission: models.PERMISSION_EDIT}, - } - return nil - }) + bus.AddHandler("test", func(query *models.GetDashboardAclInfoListQuery) error { + query.Result = []*models.DashboardAclInfoDTO{ + {OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT}, + } + return nil + }) + } + + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/dash", "/api/dashboards/db/:slug", models.ROLE_EDITOR, func(sc *scenarioContext) { CallDeleteDashboardBySlug(sc) - Convey("Should result in 400", func() { - So(sc.resp.Code, ShouldEqual, 400) - result := sc.ToJSON() - So(result.Get("error").MustString(), ShouldEqual, models.ErrDashboardCannotDeleteProvisionedDashboard.Error()) - }) + assert.Equal(t, 400, sc.resp.Code) + result := sc.ToJSON() + assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Error(), result.Get("error").MustString()) }) - loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling DELETE on", "DELETE", "/api/dashboards/db/abcdefghi", "/api/dashboards/db/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + CallDeleteDashboardByUID(sc) - Convey("Should result in 400", func() { - So(sc.resp.Code, ShouldEqual, 400) - result := sc.ToJSON() - So(result.Get("error").MustString(), ShouldEqual, models.ErrDashboardCannotDeleteProvisionedDashboard.Error()) - }) + assert.Equal(t, 400, sc.resp.Code) + result := sc.ToJSON() + assert.Equal(t, models.ErrDashboardCannotDeleteProvisionedDashboard.Error(), result.Get("error").MustString()) }) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + mock := provisioning.NewProvisioningServiceMock() mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" } - dash := GetDashboardShouldReturn200WithConfig(sc, mock) + dash := getDashboardShouldReturn200WithConfig(sc, mock) - Convey("Should return relative path to provisioning file", func() { - So(dash.Meta.ProvisionedExternalId, ShouldEqual, filepath.Join("test", "dashboard1.json")) - }) + assert.Equal(t, filepath.Join("test", "dashboard1.json"), dash.Meta.ProvisionedExternalId) }) - loggedInUserScenarioWithRole("When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { + setUp() + mock := provisioning.NewProvisioningServiceMock() mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" @@ -1021,22 +1073,20 @@ func TestDashboardApiEndpoint(t *testing.T) { Cfg: setting.NewCfg(), ProvisioningService: mock, } - CallGetDashboard(sc, hs) + callGetDashboard(sc, hs) - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) dash := dtos.DashboardFullWithMeta{} err := json.NewDecoder(sc.resp.Body).Decode(&dash) - So(err, ShouldBeNil) + require.NoError(t, err) - Convey("Should have metadata that says Provisioned is false", func() { - So(dash.Meta.Provisioned, ShouldEqual, false) - }) + assert.Equal(t, false, dash.Meta.Provisioned) }) }) } -func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService provisioning.ProvisioningService) dtos. +func getDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService provisioning.ProvisioningService) dtos. DashboardFullWithMeta { if provisioningService == nil { provisioningService = provisioning.NewProvisioningServiceMock() @@ -1046,27 +1096,27 @@ func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningServ Cfg: setting.NewCfg(), ProvisioningService: provisioningService, } - CallGetDashboard(sc, hs) + callGetDashboard(sc, hs) - So(sc.resp.Code, ShouldEqual, 200) + require.Equal(sc.t, 200, sc.resp.Code) dash := dtos.DashboardFullWithMeta{} err := json.NewDecoder(sc.resp.Body).Decode(&dash) - So(err, ShouldBeNil) + require.NoError(sc.t, err) return dash } -func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { - return GetDashboardShouldReturn200WithConfig(sc, nil) +func getDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { + return getDashboardShouldReturn200WithConfig(sc, nil) } -func CallGetDashboard(sc *scenarioContext, hs *HTTPServer) { +func callGetDashboard(sc *scenarioContext, hs *HTTPServer) { sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func CallGetDashboardVersion(sc *scenarioContext) { +func callGetDashboardVersion(sc *scenarioContext) { bus.AddHandler("test", func(query *models.GetDashboardVersionQuery) error { query.Result = &models.DashboardVersion{} return nil @@ -1076,7 +1126,7 @@ func CallGetDashboardVersion(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } -func CallGetDashboardVersions(sc *scenarioContext) { +func callGetDashboardVersions(sc *scenarioContext) { bus.AddHandler("test", func(query *models.GetDashboardVersionsQuery) error { query.Result = []*models.DashboardVersionDTO{} return nil @@ -1104,27 +1154,28 @@ func CallDeleteDashboardByUID(sc *scenarioContext) { sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() } -func CallPostDashboard(sc *scenarioContext) { +func callPostDashboard(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func CallRestoreDashboardVersion(sc *scenarioContext) { +func callRestoreDashboardVersion(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) { - CallPostDashboard(sc) +func callPostDashboardShouldReturnSuccess(sc *scenarioContext) { + callPostDashboard(sc) - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(sc.t, 200, sc.resp.Code) } func (m mockDashboardProvisioningService) DeleteProvisionedDashboard(dashboardId int64, orgId int64) error { panic("implement me") } -func postDashboardScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd models.SaveDashboardCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, + mock *dashboards.FakeDashboardService, cmd models.SaveDashboardCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) hs := HTTPServer{ Bus: bus.GetBus(), @@ -1133,7 +1184,7 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d Live: &live.GrafanaLive{Cfg: setting.NewCfg()}, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c sc.context.SignedInUser = &models.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId} @@ -1160,16 +1211,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d }) } -func postDiffScenario(desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c sc.context.SignedInUser = &models.SignedInUser{ - OrgId: TestOrgID, - UserId: TestUserID, + OrgId: testOrgID, + UserId: testUserID, } sc.context.OrgRole = role @@ -1182,8 +1233,9 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal }) } -func restoreDashboardVersionScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, + mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() hs := HTTPServer{ @@ -1193,12 +1245,12 @@ func restoreDashboardVersionScenario(desc string, url string, routePattern strin Live: &live.GrafanaLive{Cfg: setting.NewCfg()}, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c sc.context.SignedInUser = &models.SignedInUser{ - OrgId: TestOrgID, - UserId: TestUserID, + OrgId: testOrgID, + UserId: testUserID, } sc.context.OrgRole = models.ROLE_ADMIN @@ -1227,7 +1279,7 @@ func restoreDashboardVersionScenario(desc string, url string, routePattern strin func (sc *scenarioContext) ToJSON() *simplejson.Json { var result *simplejson.Json err := json.NewDecoder(sc.resp.Body).Decode(&result) - So(err, ShouldBeNil) + require.NoError(sc.t, err) return result } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 1927252a559..d4f1056a0e2 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -6,64 +6,57 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/bus" - . "github.com/smartystreets/goconvey/convey" ) const ( - TestOrgID = 1 - TestUserID = 1 + testOrgID int64 = 1 + testUserID int64 = 1 ) -func TestDataSourcesProxy(t *testing.T) { - Convey("Given a user is logged in", t, func() { - loggedInUserScenario("When calling GET on", "/api/datasources/", func(sc *scenarioContext) { - // Stubs the database query - bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error { - So(query.OrgId, ShouldEqual, TestOrgID) - query.Result = []*models.DataSource{ - {Name: "mmm"}, - {Name: "ZZZ"}, - {Name: "BBB"}, - {Name: "aaa"}, - } - return nil - }) - - // handler func being tested - sc.handlerFunc = GetDataSources - sc.fakeReq("GET", "/api/datasources").exec() - - respJSON := []map[string]interface{}{} - err := json.NewDecoder(sc.resp.Body).Decode(&respJSON) - So(err, ShouldBeNil) - - Convey("should return list of datasources for org sorted alphabetically and case insensitively", func() { - So(respJSON[0]["name"], ShouldEqual, "aaa") - So(respJSON[1]["name"], ShouldEqual, "BBB") - So(respJSON[2]["name"], ShouldEqual, "mmm") - So(respJSON[3]["name"], ShouldEqual, "ZZZ") - }) +func TestDataSourcesProxy_userLoggedIn(t *testing.T) { + loggedInUserScenario(t, "When calling GET on", "/api/datasources/", func(sc *scenarioContext) { + // Stubs the database query + bus.AddHandler("test", func(query *models.GetDataSourcesQuery) error { + assert.Equal(t, testOrgID, query.OrgId) + query.Result = []*models.DataSource{ + {Name: "mmm"}, + {Name: "ZZZ"}, + {Name: "BBB"}, + {Name: "aaa"}, + } + return nil }) - Convey("Should be able to save a data source", func() { - loggedInUserScenario("When calling DELETE on non-existing", "/api/datasources/name/12345", func(sc *scenarioContext) { - sc.handlerFunc = DeleteDataSourceByName - sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - }) - }) + // handler func being tested + sc.handlerFunc = GetDataSources + sc.fakeReq("GET", "/api/datasources").exec() + + respJSON := []map[string]interface{}{} + err := json.NewDecoder(sc.resp.Body).Decode(&respJSON) + require.NoError(t, err) + + assert.Equal(t, "aaa", respJSON[0]["name"]) + assert.Equal(t, "BBB", respJSON[1]["name"]) + assert.Equal(t, "mmm", respJSON[2]["name"]) + assert.Equal(t, "ZZZ", respJSON[3]["name"]) }) + + loggedInUserScenario(t, "Should be able to save a data source when calling DELETE on non-existing", + "/api/datasources/name/12345", func(sc *scenarioContext) { + sc.handlerFunc = DeleteDataSourceByName + sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() + assert.Equal(t, 404, sc.resp.Code) + }) } // Adding data sources with invalid URLs should lead to an error. func TestAddDataSource_InvalidURL(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext("/api/datasources") - // TODO: Make this an argument to setupScenarioContext - sc.t = t + sc := setupScenarioContext(t, "/api/datasources") sc.m.Post(sc.url, Wrap(func(c *models.ReqContext) Response { return AddDataSource(c, models.AddDataSourceCommand{ @@ -93,9 +86,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { return nil }) - sc := setupScenarioContext("/api/datasources") - // TODO: Make this an argument to setupScenarioContext - sc.t = t + sc := setupScenarioContext(t, "/api/datasources") sc.m.Post(sc.url, Wrap(func(c *models.ReqContext) Response { return AddDataSource(c, models.AddDataSourceCommand{ @@ -113,9 +104,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) { func TestUpdateDataSource_InvalidURL(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext("/api/datasources/1234") - // TODO: Make this an argument to setupScenarioContext - sc.t = t + sc := setupScenarioContext(t, "/api/datasources/1234") sc.m.Put(sc.url, Wrap(func(c *models.ReqContext) Response { return AddDataSource(c, models.AddDataSourceCommand{ @@ -145,9 +134,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) { return nil }) - sc := setupScenarioContext("/api/datasources/1234") - // TODO: Make this an argument to setupScenarioContext - sc.t = t + sc := setupScenarioContext(t, "/api/datasources/1234") sc.m.Put(sc.url, Wrap(func(c *models.ReqContext) Response { return AddDataSource(c, models.AddDataSourceCommand{ diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 354df03e42e..7bad7b7920e 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -1,6 +1,7 @@ package api import ( + "fmt" "testing" "github.com/grafana/grafana/pkg/api/dtos" @@ -9,204 +10,201 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestFolderPermissionApiEndpoint(t *testing.T) { - Convey("Folder permissions test", t, func() { - Convey("Given folder not exists", func() { - mock := &fakeFolderService{ - GetFolderByUIDError: models.ErrFolderNotFound, - } +func TestFolderPermissionAPIEndpoint(t *testing.T) { + t.Run("Given folder not exists", func(t *testing.T) { + mock := &fakeFolderService{ + GetFolderByUIDError: models.ErrFolderNotFound, + } - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) + origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + dashboards.NewFolderService = origNewFolderService + }) + mockFolderService(mock) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { - callGetFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 404) - }) - - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 404) - }) - - Reset(func() { - dashboards.NewFolderService = origNewFolderService - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { + callGetFolderPermissions(sc) + assert.Equal(t, 404, sc.resp.Code) }) - Convey("Given user has no admin permissions", func() { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: models.PERMISSION_ADMIN}, + }, + } - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } + updateFolderPermissionScenario(t, "When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + assert.Equal(t, 404, sc.resp.Code) + }) + }) - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) - - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { - callGetFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 403) - }) - - Reset(func() { - guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService - }) + t.Run("Given user has no admin permissions", func(t *testing.T) { + origNewGuardian := guardian.New + origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService }) - Convey("Given user has admin permissions and permissions to update", func() { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - GetAclValue: []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_VIEW}, - {OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_EDIT}, - {OrgId: 1, DashboardId: 1, UserId: 4, Permission: models.PERMISSION_ADMIN}, - {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: models.PERMISSION_VIEW}, - {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: models.PERMISSION_ADMIN}, - }, - }) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } + mock := &fakeFolderService{ + GetFolderByUIDResult: &models.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) + mockFolderService(mock) - loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) { - callGetFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(len(respJSON.MustArray()), ShouldEqual, 5) - So(respJSON.GetIndex(0).Get("userId").MustInt(), ShouldEqual, 2) - So(respJSON.GetIndex(0).Get("permission").MustInt(), ShouldEqual, models.PERMISSION_VIEW) - }) - - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 200) - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(respJSON.Get("id").MustInt(), ShouldEqual, 1) - So(respJSON.Get("title").MustString(), ShouldEqual, "Folder") - }) - - Reset(func() { - guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService - }) + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_EDITOR, func(sc *scenarioContext) { + callGetFolderPermissions(sc) + assert.Equal(t, 403, sc.resp.Code) }) - Convey("When trying to update permissions with duplicate permissions", func() { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, - }) + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: models.PERMISSION_ADMIN}, + }, + } - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } + updateFolderPermissionScenario(t, "When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + assert.Equal(t, 403, sc.resp.Code) + }) + }) - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) - - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 400) - }) - - Reset(func() { - guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService - }) + t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) { + origNewGuardian := guardian.New + origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService }) - Convey("When trying to override inherited permissions with lower precedence", func() { - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, - ) + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: true, + GetAclValue: []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 1, UserId: 2, Permission: models.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, UserId: 3, Permission: models.PERMISSION_EDIT}, + {OrgId: 1, DashboardId: 1, UserId: 4, Permission: models.PERMISSION_ADMIN}, + {OrgId: 1, DashboardId: 1, TeamId: 1, Permission: models.PERMISSION_VIEW}, + {OrgId: 1, DashboardId: 1, TeamId: 2, Permission: models.PERMISSION_ADMIN}, + }, + }) - mock := &fakeFolderService{ - GetFolderByUIDResult: &models.Folder{ - Id: 1, - Uid: "uid", - Title: "Folder", - }, - } + mock := &fakeFolderService{ + GetFolderByUIDResult: &models.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } - origNewFolderService := dashboards.NewFolderService - mockFolderService(mock) + mockFolderService(mock) - cmd := dtos.UpdateDashboardAclCommand{ - Items: []dtos.DashboardAclUpdateItem{ - {UserId: 1000, Permission: models.PERMISSION_ADMIN}, - }, - } + loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", models.ROLE_ADMIN, func(sc *scenarioContext) { + callGetFolderPermissions(sc) + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, 5, len(respJSON.MustArray())) + assert.Equal(t, 2, respJSON.GetIndex(0).Get("userId").MustInt()) + assert.Equal(t, int(models.PERMISSION_VIEW), respJSON.GetIndex(0).Get("permission").MustInt()) + }) - updateFolderPermissionScenario("When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { - callUpdateFolderPermissions(sc) - So(sc.resp.Code, ShouldEqual, 400) - }) + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: models.PERMISSION_ADMIN}, + }, + } - Reset(func() { - guardian.New = origNewGuardian - dashboards.NewFolderService = origNewFolderService - }) + updateFolderPermissionScenario(t, "When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + assert.Equal(t, 200, sc.resp.Code) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, 1, respJSON.Get("id").MustInt()) + assert.Equal(t, "Folder", respJSON.Get("title").MustString()) + }) + }) + + t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) { + origNewGuardian := guardian.New + origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, + }) + + mock := &fakeFolderService{ + GetFolderByUIDResult: &models.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + mockFolderService(mock) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: models.PERMISSION_ADMIN}, + }, + } + + updateFolderPermissionScenario(t, "When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + assert.Equal(t, 400, sc.resp.Code) + }) + }) + + t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) { + origNewGuardian := guardian.New + origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + guardian.New = origNewGuardian + dashboards.NewFolderService = origNewFolderService + }) + + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ + CanAdminValue: true, + CheckPermissionBeforeUpdateValue: false, + CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, + ) + + mock := &fakeFolderService{ + GetFolderByUIDResult: &models.Folder{ + Id: 1, + Uid: "uid", + Title: "Folder", + }, + } + + mockFolderService(mock) + + cmd := dtos.UpdateDashboardAclCommand{ + Items: []dtos.DashboardAclUpdateItem{ + {UserId: 1000, Permission: models.PERMISSION_ADMIN}, + }, + } + + updateFolderPermissionScenario(t, "When calling POST on", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", cmd, func(sc *scenarioContext) { + callUpdateFolderPermissions(sc) + assert.Equal(t, 400, sc.resp.Code) }) }) } @@ -224,16 +222,16 @@ func callUpdateFolderPermissions(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func updateFolderPermissionScenario(desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func updateFolderPermissionScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.UpdateDashboardAclCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.OrgId = TestOrgID - sc.context.UserId = TestUserID + sc.context.OrgId = testOrgID + sc.context.UserId = testUserID return UpdateFolderPermissions(c, cmd) }) diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 74144d5d15d..e0f96ae0cbf 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -10,127 +10,121 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestFoldersApiEndpoint(t *testing.T) { - Convey("Create/update folder response tests", t, func() { - Convey("Given a correct request for creating a folder", func() { - cmd := models.CreateFolderCommand{ - Uid: "uid", - Title: "Folder", - } +func TestFoldersAPIEndpoint(t *testing.T) { + t.Run("Given a correct request for creating a folder", func(t *testing.T) { + cmd := models.CreateFolderCommand{ + Uid: "uid", + Title: "Folder", + } - mock := &fakeFolderService{ - CreateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}, - } + mock := &fakeFolderService{ + CreateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder"}, + } - createFolderScenario("When calling POST on", "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { + createFolderScenario(t, "When calling POST on", "/api/folders", "/api/folders", mock, cmd, + func(sc *scenarioContext) { callCreateFolder(sc) - Convey("It should return correct response data", func() { - folder := dtos.Folder{} - err := json.NewDecoder(sc.resp.Body).Decode(&folder) - So(err, ShouldBeNil) - So(folder.Id, ShouldEqual, 1) - So(folder.Uid, ShouldEqual, "uid") - So(folder.Title, ShouldEqual, "Folder") - }) + folder := dtos.Folder{} + err := json.NewDecoder(sc.resp.Body).Decode(&folder) + require.NoError(t, err) + assert.Equal(t, int64(1), folder.Id) + assert.Equal(t, "uid", folder.Uid) + assert.Equal(t, "Folder", folder.Title) }) - }) + }) - Convey("Given incorrect requests for creating a folder", func() { - testCases := []struct { - Error error - ExpectedStatusCode int - }{ - {Error: models.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, - {Error: models.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, - {Error: models.ErrFolderSameNameExists, ExpectedStatusCode: 400}, - {Error: models.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, - {Error: models.ErrDashboardUidTooLong, ExpectedStatusCode: 400}, - {Error: models.ErrFolderAccessDenied, ExpectedStatusCode: 403}, - {Error: models.ErrFolderNotFound, ExpectedStatusCode: 404}, - {Error: models.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, - {Error: models.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, - } + t.Run("Given incorrect requests for creating a folder", func(t *testing.T) { + testCases := []struct { + Error error + ExpectedStatusCode int + }{ + {Error: models.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, + {Error: models.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, + {Error: models.ErrFolderSameNameExists, ExpectedStatusCode: 400}, + {Error: models.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, + {Error: models.ErrDashboardUidTooLong, ExpectedStatusCode: 400}, + {Error: models.ErrFolderAccessDenied, ExpectedStatusCode: 403}, + {Error: models.ErrFolderNotFound, ExpectedStatusCode: 404}, + {Error: models.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, + {Error: models.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, + } - cmd := models.CreateFolderCommand{ - Uid: "uid", - Title: "Folder", - } - - for _, tc := range testCases { - mock := &fakeFolderService{ - CreateFolderError: tc.Error, - } - - createFolderScenario(fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { - callCreateFolder(sc) - if sc.resp.Code != tc.ExpectedStatusCode { - t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code) - } - }) - } - }) - - Convey("Given a correct request for updating a folder", func() { - cmd := models.UpdateFolderCommand{ - Title: "Folder upd", - } + cmd := models.CreateFolderCommand{ + Uid: "uid", + Title: "Folder", + } + for _, tc := range testCases { mock := &fakeFolderService{ - UpdateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}, + CreateFolderError: tc.Error, } - updateFolderScenario("When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { + createFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling POST on", tc.Error.Error()), + "/api/folders", "/api/folders", mock, cmd, func(sc *scenarioContext) { + callCreateFolder(sc) + assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for error %s", tc.Error) + }) + } + }) + + t.Run("Given a correct request for updating a folder", func(t *testing.T) { + cmd := models.UpdateFolderCommand{ + Title: "Folder upd", + } + + mock := &fakeFolderService{ + UpdateFolderResult: &models.Folder{Id: 1, Uid: "uid", Title: "Folder upd"}, + } + + updateFolderScenario(t, "When calling PUT on", "/api/folders/uid", "/api/folders/:uid", mock, cmd, + func(sc *scenarioContext) { callUpdateFolder(sc) - Convey("It should return correct response data", func() { - folder := dtos.Folder{} - err := json.NewDecoder(sc.resp.Body).Decode(&folder) - So(err, ShouldBeNil) - So(folder.Id, ShouldEqual, 1) - So(folder.Uid, ShouldEqual, "uid") - So(folder.Title, ShouldEqual, "Folder upd") - }) + folder := dtos.Folder{} + err := json.NewDecoder(sc.resp.Body).Decode(&folder) + require.NoError(t, err) + assert.Equal(t, int64(1), folder.Id) + assert.Equal(t, "uid", folder.Uid) + assert.Equal(t, "Folder upd", folder.Title) }) - }) + }) - Convey("Given incorrect requests for updating a folder", func() { - testCases := []struct { - Error error - ExpectedStatusCode int - }{ - {Error: models.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, - {Error: models.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, - {Error: models.ErrFolderSameNameExists, ExpectedStatusCode: 400}, - {Error: models.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, - {Error: models.ErrDashboardUidTooLong, ExpectedStatusCode: 400}, - {Error: models.ErrFolderAccessDenied, ExpectedStatusCode: 403}, - {Error: models.ErrFolderNotFound, ExpectedStatusCode: 404}, - {Error: models.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, - {Error: models.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, + t.Run("Given incorrect requests for updating a folder", func(t *testing.T) { + testCases := []struct { + Error error + ExpectedStatusCode int + }{ + {Error: models.ErrFolderWithSameUIDExists, ExpectedStatusCode: 400}, + {Error: models.ErrFolderTitleEmpty, ExpectedStatusCode: 400}, + {Error: models.ErrFolderSameNameExists, ExpectedStatusCode: 400}, + {Error: models.ErrDashboardInvalidUid, ExpectedStatusCode: 400}, + {Error: models.ErrDashboardUidTooLong, ExpectedStatusCode: 400}, + {Error: models.ErrFolderAccessDenied, ExpectedStatusCode: 403}, + {Error: models.ErrFolderNotFound, ExpectedStatusCode: 404}, + {Error: models.ErrFolderVersionMismatch, ExpectedStatusCode: 412}, + {Error: models.ErrFolderFailedGenerateUniqueUid, ExpectedStatusCode: 500}, + } + + cmd := models.UpdateFolderCommand{ + Title: "Folder upd", + } + + for _, tc := range testCases { + mock := &fakeFolderService{ + UpdateFolderError: tc.Error, } - cmd := models.UpdateFolderCommand{ - Title: "Folder upd", - } - - for _, tc := range testCases { - mock := &fakeFolderService{ - UpdateFolderError: tc.Error, - } - - updateFolderScenario(fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { + updateFolderScenario(t, fmt.Sprintf("Expect '%s' error when calling PUT on", tc.Error.Error()), + "/api/folders/uid", "/api/folders/:uid", mock, cmd, func(sc *scenarioContext) { callUpdateFolder(sc) - if sc.resp.Code != tc.ExpectedStatusCode { - t.Errorf("For error '%s' expected status code %d, actual %d", tc.Error, tc.ExpectedStatusCode, sc.resp.Code) - } + assert.Equalf(t, tc.ExpectedStatusCode, sc.resp.Code, "Wrong status code for %s", tc.Error) }) - } - }) + } }) } @@ -138,19 +132,20 @@ func callCreateFolder(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() } -func createFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd models.CreateFolderCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func createFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService, + cmd models.CreateFolderCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) hs := HTTPServer{ Bus: bus.GetBus(), Cfg: setting.NewCfg(), } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.SignedInUser = &models.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} + sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID} return hs.CreateFolder(c, cmd) }) @@ -172,27 +167,27 @@ func callUpdateFolder(sc *scenarioContext) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() } -func updateFolderScenario(desc string, url string, routePattern string, mock *fakeFolderService, cmd models.UpdateFolderCommand, fn scenarioFunc) { - Convey(desc+" "+url, func() { +func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, mock *fakeFolderService, + cmd models.UpdateFolderCommand, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { defer bus.ClearBusHandlers() - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.SignedInUser = &models.SignedInUser{OrgId: TestOrgID, UserId: TestUserID} + sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID} return UpdateFolder(c, cmd) }) origNewFolderService := dashboards.NewFolderService + t.Cleanup(func() { + dashboards.NewFolderService = origNewFolderService + }) mockFolderService(mock) sc.m.Put(routePattern, sc.defaultHandler) - defer func() { - dashboards.NewFolderService = origNewFolderService - }() - fn(sc) }) } diff --git a/pkg/api/frontend_logging_test.go b/pkg/api/frontend_logging_test.go index 381436fe0b5..6b6f32b96d9 100644 --- a/pkg/api/frontend_logging_test.go +++ b/pkg/api/frontend_logging_test.go @@ -27,7 +27,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendSentryEvent frontendLogger.SetHandler(origHandler) }) - sc := setupScenarioContext("/log") + sc := setupScenarioContext(t, "/log") hs := HTTPServer{} handler := Wrap(func(w http.ResponseWriter, c *models.ReqContext) Response { diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 95eb82bd0c9..496e7107faa 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -158,7 +158,6 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) Response { } ldapConfig, err := getLDAPConfig() - if err != nil { return Error(http.StatusBadRequest, "Failed to obtain the LDAP configuration. Please verify the configuration and try again", err) } diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index b3c912dafec..f21c4289606 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -50,11 +50,11 @@ func (m *LDAPMock) User(login string) (*models.ExternalUserInfo, ldap.ServerConf func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext { t.Helper() - sc := setupScenarioContext(requestURL) + sc := setupScenarioContext(t, requestURL) - ldap := setting.LDAPEnabled + origLDAP := setting.LDAPEnabled setting.LDAPEnabled = true - defer func() { setting.LDAPEnabled = ldap }() + t.Cleanup(func() { setting.LDAPEnabled = origLDAP }) hs := &HTTPServer{Cfg: setting.NewCfg()} @@ -73,7 +73,7 @@ func getUserFromLDAPContext(t *testing.T, requestURL string) *scenarioContext { return sc } -func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) { +func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) { getLDAPConfig = func() (*ldap.Config, error) { return &ldap.Config{}, nil } @@ -90,7 +90,7 @@ func TestGetUserFromLDAPApiEndpoint_UserNotFound(t *testing.T) { assert.JSONEq(t, "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", sc.resp.Body.String()) } -func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { +func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) { isAdmin := true userSearchResult = &models.ExternalUserInfo{ Name: "John Doe", @@ -152,7 +152,7 @@ func TestGetUserFromLDAPApiEndpoint_OrgNotfound(t *testing.T) { assert.JSONEq(t, expected, sc.resp.Body.String()) } -func TestGetUserFromLDAPApiEndpoint(t *testing.T) { +func TestGetUserFromLDAPAPIEndpoint(t *testing.T) { isAdmin := true userSearchResult = &models.ExternalUserInfo{ Name: "John Doe", @@ -232,7 +232,7 @@ func TestGetUserFromLDAPApiEndpoint(t *testing.T) { assert.JSONEq(t, expected, sc.resp.Body.String()) } -func TestGetUserFromLDAPApiEndpoint_WithTeamHandler(t *testing.T) { +func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) { isAdmin := true userSearchResult = &models.ExternalUserInfo{ Name: "John Doe", @@ -319,11 +319,11 @@ func getLDAPStatusContext(t *testing.T) *scenarioContext { t.Helper() requestURL := "/api/admin/ldap/status" - sc := setupScenarioContext(requestURL) + sc := setupScenarioContext(t, requestURL) ldap := setting.LDAPEnabled setting.LDAPEnabled = true - defer func() { setting.LDAPEnabled = ldap }() + t.Cleanup(func() { setting.LDAPEnabled = ldap }) hs := &HTTPServer{Cfg: setting.NewCfg()} @@ -342,7 +342,7 @@ func getLDAPStatusContext(t *testing.T) *scenarioContext { return sc } -func TestGetLDAPStatusApiEndpoint(t *testing.T) { +func TestGetLDAPStatusAPIEndpoint(t *testing.T) { pingResult = []*multildap.ServerStatus{ {Host: "10.0.0.3", Port: 361, Available: true, Error: nil}, {Host: "10.0.0.3", Port: 362, Available: true, Error: nil}, @@ -375,16 +375,21 @@ func TestGetLDAPStatusApiEndpoint(t *testing.T) { // PostSyncUserWithLDAP tests // *** -func postSyncUserWithLDAPContext(t *testing.T, requestURL string) *scenarioContext { +func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(t *testing.T)) *scenarioContext { t.Helper() - sc := setupScenarioContext(requestURL) + sc := setupScenarioContext(t, requestURL) ldap := setting.LDAPEnabled + t.Cleanup(func() { + setting.LDAPEnabled = ldap + }) setting.LDAPEnabled = true - defer func() { setting.LDAPEnabled = ldap }() - hs := &HTTPServer{Cfg: setting.NewCfg(), AuthTokenService: auth.NewFakeUserAuthTokenService()} + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + AuthTokenService: auth.NewFakeUserAuthTokenService(), + } sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c @@ -394,7 +399,11 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string) *scenarioConte sc.m.Post("/api/admin/ldap/sync/:id", sc.defaultHandler) sc.resp = httptest.NewRecorder() - req, _ := http.NewRequest(http.MethodPost, requestURL, nil) + req, err := http.NewRequest(http.MethodPost, requestURL, nil) + require.NoError(t, err) + + preHook(t) + sc.req = req sc.exec() @@ -402,39 +411,39 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string) *scenarioConte } func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { - getLDAPConfig = func() (*ldap.Config, error) { - return &ldap.Config{}, nil - } + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) { + getLDAPConfig = func() (*ldap.Config, error) { + return &ldap.Config{}, nil + } - newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { - return &LDAPMock{} - } + newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { + return &LDAPMock{} + } - userSearchResult = &models.ExternalUserInfo{ - Login: "ldap-daniel", - } + userSearchResult = &models.ExternalUserInfo{ + Login: "ldap-daniel", + } - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login) - return nil + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login) + return nil + }) + + bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { + require.Equal(t, q.Id, int64(34)) + + q.Result = &models.User{Login: "ldap-daniel", Id: 34} + return nil + }) + + bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error { + require.Equal(t, q.UserId, int64(34)) + require.Equal(t, q.AuthModule, models.AuthModuleLDAP) + + return nil + }) }) - bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { - require.Equal(t, q.Id, int64(34)) - - q.Result = &models.User{Login: "ldap-daniel", Id: 34} - return nil - }) - - bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error { - require.Equal(t, q.UserId, int64(34)) - require.Equal(t, q.AuthModule, models.AuthModuleLDAP) - - return nil - }) - - sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34") - assert.Equal(t, http.StatusOK, sc.resp.Code) expected := ` @@ -447,22 +456,22 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { - getLDAPConfig = func() (*ldap.Config, error) { - return &ldap.Config{}, nil - } + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) { + getLDAPConfig = func() (*ldap.Config, error) { + return &ldap.Config{}, nil + } - newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { - return &LDAPMock{} - } + newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { + return &LDAPMock{} + } - bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { - require.Equal(t, q.Id, int64(34)) + bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { + require.Equal(t, q.Id, int64(34)) - return models.ErrUserNotFound + return models.ErrUserNotFound + }) }) - sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34") - assert.Equal(t, http.StatusNotFound, sc.resp.Code) expected := ` @@ -475,36 +484,36 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { - getLDAPConfig = func() (*ldap.Config, error) { - return &ldap.Config{}, nil - } + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) { + getLDAPConfig = func() (*ldap.Config, error) { + return &ldap.Config{}, nil + } - newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { - return &LDAPMock{} - } + newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { + return &LDAPMock{} + } - userSearchError = multildap.ErrDidNotFindUser + userSearchError = multildap.ErrDidNotFindUser - admin := setting.AdminUser - setting.AdminUser = "ldap-daniel" - defer func() { setting.AdminUser = admin }() + admin := setting.AdminUser + t.Cleanup(func() { setting.AdminUser = admin }) + setting.AdminUser = "ldap-daniel" - bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { - require.Equal(t, q.Id, int64(34)) + bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { + require.Equal(t, q.Id, int64(34)) - q.Result = &models.User{Login: "ldap-daniel", Id: 34} - return nil + q.Result = &models.User{Login: "ldap-daniel", Id: 34} + return nil + }) + + bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error { + require.Equal(t, q.UserId, int64(34)) + require.Equal(t, q.AuthModule, models.AuthModuleLDAP) + + return nil + }) }) - bus.AddHandler("test", func(q *models.GetAuthInfoQuery) error { - require.Equal(t, q.UserId, int64(34)) - require.Equal(t, q.AuthModule, models.AuthModuleLDAP) - - return nil - }) - - sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34") - assert.Equal(t, http.StatusBadRequest, sc.resp.Code) expected := ` @@ -518,42 +527,42 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { - getLDAPConfig = func() (*ldap.Config, error) { - return &ldap.Config{}, nil - } + sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T) { + getLDAPConfig = func() (*ldap.Config, error) { + return &ldap.Config{}, nil + } - newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { - return &LDAPMock{} - } + newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { + return &LDAPMock{} + } - userSearchResult = nil + userSearchResult = nil - bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { - require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login) - return nil + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + require.Equal(t, "ldap-daniel", cmd.ExternalUser.Login) + return nil + }) + + bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { + require.Equal(t, q.Id, int64(34)) + + q.Result = &models.User{Login: "ldap-daniel", Id: 34} + return nil + }) + + bus.AddHandler("test", func(q *models.GetExternalUserInfoByLoginQuery) error { + assert.Equal(t, "ldap-daniel", q.LoginOrEmail) + q.Result = &models.ExternalUserInfo{IsDisabled: true, UserId: 34} + + return nil + }) + + bus.AddHandler("test", func(cmd *models.DisableUserCommand) error { + assert.Equal(t, 34, cmd.UserId) + return nil + }) }) - bus.AddHandler("test", func(q *models.GetUserByIdQuery) error { - require.Equal(t, q.Id, int64(34)) - - q.Result = &models.User{Login: "ldap-daniel", Id: 34} - return nil - }) - - bus.AddHandler("test", func(q *models.GetExternalUserInfoByLoginQuery) error { - assert.Equal(t, "ldap-daniel", q.LoginOrEmail) - q.Result = &models.ExternalUserInfo{IsDisabled: true, UserId: 34} - - return nil - }) - - bus.AddHandler("test", func(cmd *models.DisableUserCommand) error { - assert.Equal(t, 34, cmd.UserId) - return nil - }) - - sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34") - assert.Equal(t, http.StatusBadRequest, sc.resp.Code) expected := ` diff --git a/pkg/api/login.go b/pkg/api/login.go index 3fb73e2cdeb..5beaa56ba2e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -53,7 +53,7 @@ func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error { // when using a subUrl, the redirect_to should start with the subUrl (which contains the leading slash), otherwise the redirect // will send the user to the wrong location - if hs.Cfg.AppSubUrl != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubUrl+"/") { + if hs.Cfg.AppSubURL != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubURL+"/") { return login.ErrInvalidRedirectTo } @@ -62,8 +62,8 @@ func (hs *HTTPServer) ValidateRedirectTo(redirectTo string) error { func (hs *HTTPServer) CookieOptionsFromCfg() middleware.CookieOptions { path := "/" - if len(hs.Cfg.AppSubUrl) > 0 { - path = hs.Cfg.AppSubUrl + if len(hs.Cfg.AppSubURL) > 0 { + path = hs.Cfg.AppSubURL } return middleware.CookieOptions{ Path: path, @@ -126,7 +126,7 @@ func (hs *HTTPServer) LoginView(c *models.ReqContext) { // the user is already logged so instead of rendering the login page with error // it should be redirected to the home page. log.Debugf("Ignored invalid redirect_to cookie value: %v", redirectTo) - redirectTo = hs.Cfg.AppSubUrl + "/" + redirectTo = hs.Cfg.AppSubURL + "/" } middleware.DeleteCookie(c.Resp, "redirect_to", hs.CookieOptionsFromCfg) c.Redirect(redirectTo) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index f15daa5652f..0c584622a59 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -84,7 +84,7 @@ func TestLoginErrorCookieApiEndpoint(t *testing.T) { mockViewIndex() defer resetViewIndex() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, @@ -138,7 +138,7 @@ func TestLoginViewRedirect(t *testing.T) { mockViewIndex() defer resetViewIndex() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, @@ -254,12 +254,12 @@ func TestLoginViewRedirect(t *testing.T) { } for _, c := range redirectCases { - hs.Cfg.AppUrl = c.appURL - hs.Cfg.AppSubUrl = c.appSubURL + hs.Cfg.AppURL = c.appURL + hs.Cfg.AppSubURL = c.appSubURL t.Run(c.desc, func(t *testing.T) { expCookiePath := "/" - if len(hs.Cfg.AppSubUrl) > 0 { - expCookiePath = hs.Cfg.AppSubUrl + if len(hs.Cfg.AppSubURL) > 0 { + expCookiePath = hs.Cfg.AppSubURL } cookie := http.Cookie{ Name: "redirect_to", @@ -314,7 +314,7 @@ func TestLoginPostRedirect(t *testing.T) { mockViewIndex() defer resetViewIndex() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ log: &FakeLogger{}, Cfg: setting.NewCfg(), @@ -423,12 +423,12 @@ func TestLoginPostRedirect(t *testing.T) { } for _, c := range redirectCases { - hs.Cfg.AppUrl = c.appURL - hs.Cfg.AppSubUrl = c.appSubURL + hs.Cfg.AppURL = c.appURL + hs.Cfg.AppSubURL = c.appSubURL t.Run(c.desc, func(t *testing.T) { expCookiePath := "/" - if len(hs.Cfg.AppSubUrl) > 0 { - expCookiePath = hs.Cfg.AppSubUrl + if len(hs.Cfg.AppSubURL) > 0 { + expCookiePath = hs.Cfg.AppSubURL } cookie := http.Cookie{ Name: "redirect_to", @@ -472,7 +472,7 @@ func TestLoginOAuthRedirect(t *testing.T) { mockSetIndexViewData() defer resetSetIndexViewData() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, @@ -507,7 +507,7 @@ func TestLoginInternal(t *testing.T) { mockViewIndex() defer resetViewIndex() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, @@ -537,7 +537,7 @@ func TestLoginInternal(t *testing.T) { } func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) { - sc := setupAuthProxyLoginTest(false) + sc := setupAuthProxyLoginTest(t, false) assert.Equal(t, sc.resp.Code, 302) location, ok := sc.resp.Header()["Location"] @@ -549,7 +549,7 @@ func TestAuthProxyLoginEnableLoginTokenDisabled(t *testing.T) { } func TestAuthProxyLoginWithEnableLoginToken(t *testing.T) { - sc := setupAuthProxyLoginTest(true) + sc := setupAuthProxyLoginTest(t, true) assert.Equal(t, sc.resp.Code, 302) location, ok := sc.resp.Header()["Location"] @@ -561,11 +561,11 @@ func TestAuthProxyLoginWithEnableLoginToken(t *testing.T) { assert.Equal(t, "grafana_session=; Path=/; Max-Age=0; HttpOnly", setCookie[0]) } -func setupAuthProxyLoginTest(enableLoginToken bool) *scenarioContext { +func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioContext { mockSetIndexViewData() defer resetSetIndexViewData() - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hs := &HTTPServer{ Cfg: setting.NewCfg(), License: &licensing.OSSLicensingService{}, @@ -601,7 +601,7 @@ func (r *loginHookTest) LoginHook(loginInfo *models.LoginInfo, req *models.ReqCo } func TestLoginPostRunLokingHook(t *testing.T) { - sc := setupScenarioContext("/login") + sc := setupScenarioContext(t, "/login") hookService := &hooks.HooksService{} hs := &HTTPServer{ log: log.New("test"), diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index ed899f2ad14..c9a612625c1 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -13,7 +13,7 @@ import ( "golang.org/x/oauth2/google" ) -// ApplyRoute should use the plugin route data to set auth headers and custom headers +// ApplyRoute should use the plugin route data to set auth headers and custom headers. func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route *plugins.AppPluginRoute, ds *models.DataSource) { proxyPath = strings.TrimPrefix(proxyPath, route.Path) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index d2c77aa5e11..d03b564062b 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -26,546 +26,556 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - . "github.com/smartystreets/goconvey/convey" ) -func TestDSRouteRule(t *testing.T) { - Convey("DataSourceProxy", t, func() { - Convey("Plugin with routes", func() { - plugin := &plugins.DataSourcePlugin{ - Routes: []*plugins.AppPluginRoute{ - { - Path: "api/v4/", - URL: "https://www.google.com", - ReqRole: models.ROLE_EDITOR, - Headers: []plugins.AppPluginRouteHeader{ - {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, - }, - }, - { - Path: "api/admin", - URL: "https://www.google.com", - ReqRole: models.ROLE_ADMIN, - Headers: []plugins.AppPluginRouteHeader{ - {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, - }, - }, - { - Path: "api/anon", - URL: "https://www.google.com", - Headers: []plugins.AppPluginRouteHeader{ - {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, - }, - }, - { - Path: "api/common", - URL: "{{.JsonData.dynamicUrl}}", - URLParams: []plugins.AppPluginRouteURLParam{ - {Name: "{{.JsonData.queryParam}}", Content: "{{.SecureJsonData.key}}"}, - }, - Headers: []plugins.AppPluginRouteHeader{ - {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, - }, - }, - { - Path: "api/restricted", - ReqRole: models.ROLE_ADMIN, +func TestDataSourceProxy_routeRule(t *testing.T) { + t.Run("Plugin with routes", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{ + Routes: []*plugins.AppPluginRoute{ + { + Path: "api/v4/", + URL: "https://www.google.com", + ReqRole: models.ROLE_EDITOR, + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, }, }, - } - - setting.SecretKey = "password" //nolint:goconst - key, _ := util.Encrypt([]byte("123"), "password") - - ds := &models.DataSource{ - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "clientId": "asd", - "dynamicUrl": "https://dynamic.grafana.com", - "queryParam": "apiKey", - }), - SecureJsonData: map[string][]byte{ - "key": key, + { + Path: "api/admin", + URL: "https://www.google.com", + ReqRole: models.ROLE_ADMIN, + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, }, - } + { + Path: "api/anon", + URL: "https://www.google.com", + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, + }, + { + Path: "api/common", + URL: "{{.JsonData.dynamicUrl}}", + URLParams: []plugins.AppPluginRouteURLParam{ + {Name: "{{.JsonData.queryParam}}", Content: "{{.SecureJsonData.key}}"}, + }, + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, + }, + { + Path: "api/restricted", + ReqRole: models.ROLE_ADMIN, + }, + }, + } - req, _ := http.NewRequest("GET", "http://localhost/asd", nil) + origSecretKey := setting.SecretKey + t.Cleanup(func() { + setting.SecretKey = origSecretKey + }) + setting.SecretKey = "password" //nolint:goconst + + key, err := util.Encrypt([]byte("123"), "password") + require.NoError(t, err) + + ds := &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "clientId": "asd", + "dynamicUrl": "https://dynamic.grafana.com", + "queryParam": "apiKey", + }), + SecureJsonData: map[string][]byte{ + "key": key, + }, + } + + setUp := func() (*models.ReqContext, *http.Request) { + req, err := http.NewRequest("GET", "http://localhost/asd", nil) + require.NoError(t, err) ctx := &models.ReqContext{ Context: &macaron.Context{ Req: macaron.Request{Request: req}, }, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } + return ctx, req + } - Convey("When matching route path", func() { - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy.route = plugin.Routes[0] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) + t.Run("When matching route path", func(t *testing.T) { + ctx, req := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) + require.NoError(t, err) + proxy.route = plugin.Routes[0] + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) - Convey("should add headers and update url", func() { - So(req.URL.String(), ShouldEqual, "https://www.google.com/some/method") - So(req.Header.Get("x-header"), ShouldEqual, "my secret 123") - }) - }) - - Convey("When matching route path and has dynamic url", func() { - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy.route = plugin.Routes[3] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) - - Convey("should add headers and interpolate the url with query string parameters", func() { - So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method?apiKey=123") - So(req.Header.Get("x-header"), ShouldEqual, "my secret 123") - }) - }) - - Convey("When matching route path with no url", func() { - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy.route = plugin.Routes[4] - ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) - - Convey("Should not replace request url", func() { - So(req.URL.String(), ShouldEqual, "http://localhost/asd") - }) - }) - - Convey("Validating request", func() { - Convey("plugin route with valid role", func() { - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) - So(err, ShouldBeNil) - err = proxy.validateRequest() - So(err, ShouldBeNil) - }) - - Convey("plugin route with admin role and user is editor", func() { - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) - So(err, ShouldBeNil) - err = proxy.validateRequest() - So(err, ShouldNotBeNil) - }) - - Convey("plugin route with admin role and user is admin", func() { - ctx.SignedInUser.OrgRole = models.ROLE_ADMIN - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) - So(err, ShouldBeNil) - err = proxy.validateRequest() - So(err, ShouldBeNil) - }) - }) + assert.Equal(t, "https://www.google.com/some/method", req.URL.String()) + assert.Equal(t, "my secret 123", req.Header.Get("x-header")) }) - Convey("Plugin with multiple routes for token auth", func() { - plugin := &plugins.DataSourcePlugin{ - Routes: []*plugins.AppPluginRoute{ - { - Path: "pathwithtoken1", - URL: "https://api.nr1.io/some/path", - TokenAuth: &plugins.JwtTokenAuth{ - Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", - Params: map[string]string{ - "grant_type": "client_credentials", - "client_id": "{{.JsonData.clientId}}", - "client_secret": "{{.SecureJsonData.clientSecret}}", - "resource": "https://api.nr1.io", - }, - }, - }, - { - Path: "pathwithtoken2", - URL: "https://api.nr2.io/some/path", - TokenAuth: &plugins.JwtTokenAuth{ - Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", - Params: map[string]string{ - "grant_type": "client_credentials", - "client_id": "{{.JsonData.clientId}}", - "client_secret": "{{.SecureJsonData.clientSecret}}", - "resource": "https://api.nr2.io", - }, + t.Run("When matching route path and has dynamic url", func(t *testing.T) { + ctx, req := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method", &setting.Cfg{}) + require.NoError(t, err) + proxy.route = plugin.Routes[3] + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) + + assert.Equal(t, "https://dynamic.grafana.com/some/method?apiKey=123", req.URL.String()) + assert.Equal(t, "my secret 123", req.Header.Get("x-header")) + }) + + t.Run("When matching route path with no url", func(t *testing.T) { + ctx, req := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) + require.NoError(t, err) + proxy.route = plugin.Routes[4] + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.route, proxy.ds) + + assert.Equal(t, "http://localhost/asd", req.URL.String()) + }) + + t.Run("Validating request", func(t *testing.T) { + t.Run("plugin route with valid role", func(t *testing.T) { + ctx, _ := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method", &setting.Cfg{}) + require.NoError(t, err) + err = proxy.validateRequest() + require.NoError(t, err) + }) + + t.Run("plugin route with admin role and user is editor", func(t *testing.T) { + ctx, _ := setUp() + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) + require.NoError(t, err) + err = proxy.validateRequest() + require.Error(t, err) + }) + + t.Run("plugin route with admin role and user is admin", func(t *testing.T) { + ctx, _ := setUp() + ctx.SignedInUser.OrgRole = models.ROLE_ADMIN + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "api/admin", &setting.Cfg{}) + require.NoError(t, err) + err = proxy.validateRequest() + require.NoError(t, err) + }) + }) + }) + + t.Run("Plugin with multiple routes for token auth", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{ + Routes: []*plugins.AppPluginRoute{ + { + Path: "pathwithtoken1", + URL: "https://api.nr1.io/some/path", + TokenAuth: &plugins.JwtTokenAuth{ + Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", + Params: map[string]string{ + "grant_type": "client_credentials", + "client_id": "{{.JsonData.clientId}}", + "client_secret": "{{.SecureJsonData.clientSecret}}", + "resource": "https://api.nr1.io", }, }, }, - } - - setting.SecretKey = "password" - key, _ := util.Encrypt([]byte("123"), "password") - - ds := &models.DataSource{ - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "clientId": "asd", - "tenantId": "mytenantId", - }), - SecureJsonData: map[string][]byte{ - "clientSecret": key, + { + Path: "pathwithtoken2", + URL: "https://api.nr2.io/some/path", + TokenAuth: &plugins.JwtTokenAuth{ + Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token", + Params: map[string]string{ + "grant_type": "client_credentials", + "client_id": "{{.JsonData.clientId}}", + "client_secret": "{{.SecureJsonData.clientSecret}}", + "resource": "https://api.nr2.io", + }, + }, }, - } + }, + } - req, _ := http.NewRequest("GET", "http://localhost/asd", nil) - ctx := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, - SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, - } + origSecretKey := setting.SecretKey + t.Cleanup(func() { + setting.SecretKey = origSecretKey + }) + setting.SecretKey = "password" - Convey("When creating and caching access tokens", func() { - var authorizationHeaderCall1 string - var authorizationHeaderCall2 string + key, err := util.Encrypt([]byte("123"), "password") + require.NoError(t, err) - Convey("first call should add authorization header with access token", func() { - json, err := ioutil.ReadFile("./test-data/access-token-1.json") - So(err, ShouldBeNil) + ds := &models.DataSource{ + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "clientId": "asd", + "tenantId": "mytenantId", + }), + SecureJsonData: map[string][]byte{ + "clientSecret": key, + }, + } - client = newFakeHTTPClient(json) - proxy1, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy1.route = plugin.Routes[0] - ApplyRoute(proxy1.ctx.Req.Context(), req, proxy1.proxyPath, proxy1.route, proxy1.ds) + req, err := http.NewRequest("GET", "http://localhost/asd", nil) + require.NoError(t, err) + ctx := &models.ReqContext{ + Context: &macaron.Context{ + Req: macaron.Request{Request: req}, + }, + SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, + } - authorizationHeaderCall1 = req.Header.Get("Authorization") - So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path") - So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e") + t.Run("When creating and caching access tokens", func(t *testing.T) { + var authorizationHeaderCall1 string + var authorizationHeaderCall2 string - Convey("second call to another route should add a different access token", func() { - json2, err := ioutil.ReadFile("./test-data/access-token-2.json") - So(err, ShouldBeNil) + t.Run("first call should add authorization header with access token", func(t *testing.T) { + json, err := ioutil.ReadFile("./test-data/access-token-1.json") + require.NoError(t, err) - req, _ := http.NewRequest("GET", "http://localhost/asd", nil) - client = newFakeHTTPClient(json2) - proxy2, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy2.route = plugin.Routes[1] - ApplyRoute(proxy2.ctx.Req.Context(), req, proxy2.proxyPath, proxy2.route, proxy2.ds) + client = newFakeHTTPClient(t, json) + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) + require.NoError(t, err) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds) - authorizationHeaderCall2 = req.Header.Get("Authorization") + authorizationHeaderCall1 = req.Header.Get("Authorization") + assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) + assert.True(t, strings.HasPrefix(authorizationHeaderCall1, "Bearer eyJ0e")) - So(req.URL.String(), ShouldEqual, "https://api.nr2.io/some/path") - So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e") - So(authorizationHeaderCall2, ShouldStartWith, "Bearer eyJ0e") - So(authorizationHeaderCall2, ShouldNotEqual, authorizationHeaderCall1) + t.Run("second call to another route should add a different access token", func(t *testing.T) { + json2, err := ioutil.ReadFile("./test-data/access-token-2.json") + require.NoError(t, err) - Convey("third call to first route should add cached access token", func() { - req, _ := http.NewRequest("GET", "http://localhost/asd", nil) + req, err := http.NewRequest("GET", "http://localhost/asd", nil) + require.NoError(t, err) + client = newFakeHTTPClient(t, json2) + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2", &setting.Cfg{}) + require.NoError(t, err) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[1], proxy.ds) - client = newFakeHTTPClient([]byte{}) - proxy3, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) - So(err, ShouldBeNil) - proxy3.route = plugin.Routes[0] - ApplyRoute(proxy3.ctx.Req.Context(), req, proxy3.proxyPath, proxy3.route, proxy3.ds) + authorizationHeaderCall2 = req.Header.Get("Authorization") - authorizationHeaderCall3 := req.Header.Get("Authorization") - So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path") - So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e") - So(authorizationHeaderCall3, ShouldStartWith, "Bearer eyJ0e") - So(authorizationHeaderCall3, ShouldEqual, authorizationHeaderCall1) - }) + assert.Equal(t, "https://api.nr2.io/some/path", req.URL.String()) + assert.True(t, strings.HasPrefix(authorizationHeaderCall1, "Bearer eyJ0e")) + assert.True(t, strings.HasPrefix(authorizationHeaderCall2, "Bearer eyJ0e")) + assert.NotEqual(t, authorizationHeaderCall1, authorizationHeaderCall2) + + t.Run("third call to first route should add cached access token", func(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost/asd", nil) + require.NoError(t, err) + + client = newFakeHTTPClient(t, []byte{}) + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1", &setting.Cfg{}) + require.NoError(t, err) + ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, plugin.Routes[0], proxy.ds) + + authorizationHeaderCall3 := req.Header.Get("Authorization") + assert.Equal(t, "https://api.nr1.io/some/path", req.URL.String()) + assert.True(t, strings.HasPrefix(authorizationHeaderCall1, "Bearer eyJ0e")) + assert.True(t, strings.HasPrefix(authorizationHeaderCall3, "Bearer eyJ0e")) + assert.Equal(t, authorizationHeaderCall1, authorizationHeaderCall3) }) }) }) }) + }) - Convey("When proxying graphite", func() { - setting.BuildVersion = "5.3.0" - plugin := &plugins.DataSourcePlugin{} - ds := &models.DataSource{Url: "htttp://graphite:8080", Type: models.DS_GRAPHITE} - ctx := &models.ReqContext{} - - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) - So(err, ShouldBeNil) - req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - So(err, ShouldBeNil) - - proxy.getDirector()(req) - - Convey("Can translate request url and path", func() { - So(req.URL.Host, ShouldEqual, "graphite:8080") - So(req.URL.Path, ShouldEqual, "/render") - So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0") - }) + t.Run("When proxying graphite", func(t *testing.T) { + origBuildVer := setting.BuildVersion + t.Cleanup(func() { + setting.BuildVersion = origBuildVer }) + setting.BuildVersion = "5.3.0" - Convey("When proxying InfluxDB", func() { - plugin := &plugins.DataSourcePlugin{} + plugin := &plugins.DataSourcePlugin{} + ds := &models.DataSource{Url: "htttp://graphite:8080", Type: models.DS_GRAPHITE} + ctx := &models.ReqContext{} - ds := &models.DataSource{ - Type: models.DS_INFLUXDB_08, - Url: "http://influxdb:8083", - Database: "site", - User: "user", - Password: "password", + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + require.NoError(t, err) + + proxy.getDirector()(req) + + t.Run("Can translate request URL and path", func(t *testing.T) { + assert.Equal(t, "graphite:8080", req.URL.Host) + assert.Equal(t, "/render", req.URL.Path) + assert.Equal(t, "Grafana/5.3.0", req.Header.Get("User-Agent")) + }) + }) + + t.Run("When proxying InfluxDB", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{} + + ds := &models.DataSource{ + Type: models.DS_INFLUXDB_08, + Url: "http://influxdb:8083", + Database: "site", + User: "user", + Password: "password", + } + + ctx := &models.ReqContext{} + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + require.NoError(t, err) + + proxy.getDirector()(req) + assert.Equal(t, "/db/site/", req.URL.Path) + }) + + t.Run("When proxying a data source with no keepCookies specified", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{} + + json, err := simplejson.NewJson([]byte(`{"keepCookies": []}`)) + require.NoError(t, err) + + ds := &models.DataSource{ + Type: models.DS_GRAPHITE, + Url: "http://graphite:8086", + JsonData: json, + } + + ctx := &models.ReqContext{} + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) + require.NoError(t, err) + + requestURL, err := url.Parse("http://grafana.com/sub") + require.NoError(t, err) + req := http.Request{URL: requestURL, Header: make(http.Header)} + cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" + req.Header.Set("Cookie", cookies) + + proxy.getDirector()(&req) + + assert.Equal(t, "", req.Header.Get("Cookie")) + }) + + t.Run("When proxying a data source with keep cookies specified", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{} + + json, err := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`)) + require.NoError(t, err) + + ds := &models.DataSource{ + Type: models.DS_GRAPHITE, + Url: "http://graphite:8086", + JsonData: json, + } + + ctx := &models.ReqContext{} + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) + require.NoError(t, err) + + requestURL, err := url.Parse("http://grafana.com/sub") + require.NoError(t, err) + req := http.Request{URL: requestURL, Header: make(http.Header)} + cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" + req.Header.Set("Cookie", cookies) + + proxy.getDirector()(&req) + + assert.Equal(t, "JSESSION_ID=test", req.Header.Get("Cookie")) + }) + + t.Run("When proxying a custom datasource", func(t *testing.T) { + plugin := &plugins.DataSourcePlugin{} + ds := &models.DataSource{ + Type: "custom-datasource", + Url: "http://host/root/", + } + ctx := &models.ReqContext{} + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + req.Header.Add("Origin", "grafana.com") + req.Header.Add("Referer", "grafana.com") + req.Header.Add("X-Canary", "stillthere") + require.NoError(t, err) + + proxy.getDirector()(req) + + assert.Equal(t, "http://host/root/path/to/folder/", req.URL.String()) + + assert.Empty(t, req.Header.Get("Origin")) + assert.Empty(t, req.Header.Get("Referer")) + assert.Equal(t, "stillthere", req.Header.Get("X-Canary")) + }) + + t.Run("When proxying a datasource that has oauth token pass-through enabled", func(t *testing.T) { + social.SocialMap["generic_oauth"] = &social.SocialGenericOAuth{ + SocialBase: &social.SocialBase{ + Config: &oauth2.Config{}, + }, + } + origAuthSvc := setting.OAuthService + t.Cleanup(func() { + setting.OAuthService = origAuthSvc + }) + setting.OAuthService = &setting.OAuther{} + setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) + setting.OAuthService.OAuthInfos["generic_oauth"] = &setting.OAuthInfo{} + + bus.AddHandler("test", func(query *models.GetAuthInfoQuery) error { + query.Result = &models.UserAuth{ + Id: 1, + UserId: 1, + AuthModule: "generic_oauth", + OAuthAccessToken: "testtoken", + OAuthRefreshToken: "testrefreshtoken", + OAuthTokenType: "Bearer", + OAuthExpiry: time.Now().AddDate(0, 0, 1), } - - ctx := &models.ReqContext{} - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) - So(err, ShouldBeNil) - - req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - So(err, ShouldBeNil) - - proxy.getDirector()(req) - - Convey("Should add db to url", func() { - So(req.URL.Path, ShouldEqual, "/db/site/") - }) + return nil }) - Convey("When proxying a data source with no keepCookies specified", func() { - plugin := &plugins.DataSourcePlugin{} + plugin := &plugins.DataSourcePlugin{} + ds := &models.DataSource{ + Type: "custom-datasource", + Url: "http://host/root/", + JsonData: simplejson.NewFromAny(map[string]interface{}{ + "oauthPassThru": true, + }), + } - json, _ := simplejson.NewJson([]byte(`{"keepCookies": []}`)) + req, err := http.NewRequest("GET", "http://localhost/asd", nil) + require.NoError(t, err) + ctx := &models.ReqContext{ + SignedInUser: &models.SignedInUser{UserId: 1}, + Context: &macaron.Context{ + Req: macaron.Request{Request: req}, + }, + } + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}) + require.NoError(t, err) + req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + require.NoError(t, err) - ds := &models.DataSource{ - Type: models.DS_GRAPHITE, - Url: "http://graphite:8086", - JsonData: json, - } + proxy.getDirector()(req) - ctx := &models.ReqContext{} - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) - So(err, ShouldBeNil) + assert.Equal(t, "Bearer testtoken", req.Header.Get("Authorization")) + }) - requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL, Header: make(http.Header)} - cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" - req.Header.Set("Cookie", cookies) - - proxy.getDirector()(&req) - - Convey("Should clear all cookies", func() { - So(req.Header.Get("Cookie"), ShouldEqual, "") - }) - }) - - Convey("When proxying a data source with keep cookies specified", func() { - plugin := &plugins.DataSourcePlugin{} - - json, _ := simplejson.NewJson([]byte(`{"keepCookies": ["JSESSION_ID"]}`)) - - ds := &models.DataSource{ - Type: models.DS_GRAPHITE, - Url: "http://graphite:8086", - JsonData: json, - } - - ctx := &models.ReqContext{} - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", &setting.Cfg{}) - So(err, ShouldBeNil) - - requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL, Header: make(http.Header)} - cookies := "grafana_user=admin; grafana_remember=99; grafana_sess=11; JSESSION_ID=test" - req.Header.Set("Cookie", cookies) - - proxy.getDirector()(&req) - - Convey("Should keep named cookies", func() { - So(req.Header.Get("Cookie"), ShouldEqual, "JSESSION_ID=test") - }) - }) - - Convey("When proxying a custom datasource", func() { - plugin := &plugins.DataSourcePlugin{} - ds := &models.DataSource{ - Type: "custom-datasource", - Url: "http://host/root/", - } - ctx := &models.ReqContext{} - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}) - So(err, ShouldBeNil) - req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - req.Header.Add("Origin", "grafana.com") - req.Header.Add("Referer", "grafana.com") - req.Header.Add("X-Canary", "stillthere") - So(err, ShouldBeNil) - - proxy.getDirector()(req) - - Convey("Should keep user request (including trailing slash)", func() { - So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/") - }) - - Convey("Origin and Referer headers should be dropped", func() { - So(req.Header.Get("Origin"), ShouldEqual, "") - So(req.Header.Get("Referer"), ShouldEqual, "") - So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere") - }) - }) - - Convey("When proxying a datasource that has oauth token pass-through enabled", func() { - social.SocialMap["generic_oauth"] = &social.SocialGenericOAuth{ - SocialBase: &social.SocialBase{ - Config: &oauth2.Config{}, + t.Run("When SendUserHeader config is enabled", func(t *testing.T) { + req := getDatasourceProxiedRequest( + t, + &models.ReqContext{ + SignedInUser: &models.SignedInUser{ + Login: "test_user", }, - } - setting.OAuthService = &setting.OAuther{} - setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - setting.OAuthService.OAuthInfos["generic_oauth"] = &setting.OAuthInfo{} + }, + &setting.Cfg{SendUserHeader: true}, + ) + assert.Equal(t, "test_user", req.Header.Get("X-Grafana-User")) + }) - bus.AddHandler("test", func(query *models.GetAuthInfoQuery) error { - query.Result = &models.UserAuth{ - Id: 1, - UserId: 1, - AuthModule: "generic_oauth", - OAuthAccessToken: "testtoken", - OAuthRefreshToken: "testrefreshtoken", - OAuthTokenType: "Bearer", - OAuthExpiry: time.Now().AddDate(0, 0, 1), - } - return nil - }) + t.Run("When SendUserHeader config is disabled", func(t *testing.T) { + req := getDatasourceProxiedRequest( + t, + &models.ReqContext{ + SignedInUser: &models.SignedInUser{ + Login: "test_user", + }, + }, + &setting.Cfg{SendUserHeader: false}, + ) + // Get will return empty string even if header is not set + assert.Empty(t, req.Header.Get("X-Grafana-User")) + }) - plugin := &plugins.DataSourcePlugin{} - ds := &models.DataSource{ - Type: "custom-datasource", - Url: "http://host/root/", - JsonData: simplejson.NewFromAny(map[string]interface{}{ - "oauthPassThru": true, - }), + t.Run("When SendUserHeader config is enabled but user is anonymous", func(t *testing.T) { + req := getDatasourceProxiedRequest( + t, + &models.ReqContext{ + SignedInUser: &models.SignedInUser{IsAnonymous: true}, + }, + &setting.Cfg{SendUserHeader: true}, + ) + // Get will return empty string even if header is not set + assert.Empty(t, req.Header.Get("X-Grafana-User")) + }) + + t.Run("When proxying data source proxy should handle authentication", func(t *testing.T) { + tests := []*testCase{ + createAuthTest(t, models.DS_INFLUXDB_08, authTypePassword, authCheckQuery, false), + createAuthTest(t, models.DS_INFLUXDB_08, authTypePassword, authCheckQuery, true), + createAuthTest(t, models.DS_INFLUXDB, authTypePassword, authCheckHeader, true), + createAuthTest(t, models.DS_INFLUXDB, authTypePassword, authCheckHeader, false), + createAuthTest(t, models.DS_INFLUXDB, authTypeBasic, authCheckHeader, true), + createAuthTest(t, models.DS_INFLUXDB, authTypeBasic, authCheckHeader, false), + + // These two should be enough for any other datasource at the moment. Proxy has special handling + // only for Influx, others have the same path and only BasicAuth. Non BasicAuth datasources + // do not go through proxy but through TSDB API which is not tested here. + createAuthTest(t, models.DS_ES, authTypeBasic, authCheckHeader, false), + createAuthTest(t, models.DS_ES, authTypeBasic, authCheckHeader, true), + } + for _, test := range tests { + models.ClearDSDecryptionCache() + runDatasourceAuthTest(t, test) + } + }) + + t.Run("HandleRequest()", func(t *testing.T) { + var writeErr error + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"}) + w.WriteHeader(200) + _, writeErr = w.Write([]byte("I am the backend")) + })) + t.Cleanup(backend.Close) + + plugin := &plugins.DataSourcePlugin{} + ds := &models.DataSource{Url: backend.URL, Type: models.DS_GRAPHITE} + + responseRecorder := &CloseNotifierResponseRecorder{ + ResponseRecorder: httptest.NewRecorder(), + } + t.Cleanup(responseRecorder.Close) + + setupCtx := func(fn func(http.ResponseWriter)) *models.ReqContext { + responseWriter := macaron.NewResponseWriter("GET", responseRecorder) + if fn != nil { + fn(responseWriter) } - req, _ := http.NewRequest("GET", "http://localhost/asd", nil) - ctx := &models.ReqContext{ - SignedInUser: &models.SignedInUser{UserId: 1}, + return &models.ReqContext{ + SignedInUser: &models.SignedInUser{}, Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, - } - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/", &setting.Cfg{}) - So(err, ShouldBeNil) - req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - So(err, ShouldBeNil) - - proxy.getDirector()(req) - - Convey("Should have access token in header", func() { - So(req.Header.Get("Authorization"), ShouldEqual, fmt.Sprintf("%s %s", "Bearer", "testtoken")) - }) - }) - - Convey("When SendUserHeader config is enabled", func() { - req := getDatasourceProxiedRequest( - &models.ReqContext{ - SignedInUser: &models.SignedInUser{ - Login: "test_user", + Req: macaron.Request{ + Request: httptest.NewRequest("GET", "/render", nil), }, + Resp: responseWriter, }, - &setting.Cfg{SendUserHeader: true}, - ) - Convey("Should add header with username", func() { - So(req.Header.Get("X-Grafana-User"), ShouldEqual, "test_user") - }) + } + } + + t.Run("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func(t *testing.T) { + writeErr = nil + ctx := setupCtx(nil) + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) + require.NoError(t, err) + + proxy.HandleRequest() + + require.NoError(t, writeErr) + assert.Empty(t, proxy.ctx.Resp.Header().Get("Set-Cookie")) }) - Convey("When SendUserHeader config is disabled", func() { - req := getDatasourceProxiedRequest( - &models.ReqContext{ - SignedInUser: &models.SignedInUser{ - Login: "test_user", - }, - }, - &setting.Cfg{SendUserHeader: false}, - ) - Convey("Should not add header with username", func() { - // Get will return empty string even if header is not set - So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") + t.Run("When response header Set-Cookie is set should remove proxied Set-Cookie header and restore the original Set-Cookie header", func(t *testing.T) { + writeErr = nil + ctx := setupCtx(func(w http.ResponseWriter) { + w.Header().Set("Set-Cookie", "important_cookie=important_value") }) - }) + proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) + require.NoError(t, err) - Convey("When SendUserHeader config is enabled but user is anonymous", func() { - req := getDatasourceProxiedRequest( - &models.ReqContext{ - SignedInUser: &models.SignedInUser{IsAnonymous: true}, - }, - &setting.Cfg{SendUserHeader: true}, - ) - Convey("Should not add header with username", func() { - // Get will return empty string even if header is not set - So(req.Header.Get("X-Grafana-User"), ShouldEqual, "") - }) - }) + proxy.HandleRequest() - Convey("When proxying data source proxy should handle authentication", func() { - tests := []*Test{ - createAuthTest(models.DS_INFLUXDB_08, AUTHTYPE_PASSWORD, AUTHCHECK_QUERY, false), - createAuthTest(models.DS_INFLUXDB_08, AUTHTYPE_PASSWORD, AUTHCHECK_QUERY, true), - createAuthTest(models.DS_INFLUXDB, AUTHTYPE_PASSWORD, AUTHCHECK_HEADER, true), - createAuthTest(models.DS_INFLUXDB, AUTHTYPE_PASSWORD, AUTHCHECK_HEADER, false), - createAuthTest(models.DS_INFLUXDB, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true), - createAuthTest(models.DS_INFLUXDB, AUTHTYPE_BASIC, AUTHCHECK_HEADER, false), - - // These two should be enough for any other datasource at the moment. Proxy has special handling - // only for Influx, others have the same path and only BasicAuth. Non BasicAuth datasources - // do not go through proxy but through TSDB API which is not tested here. - createAuthTest(models.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, false), - createAuthTest(models.DS_ES, AUTHTYPE_BASIC, AUTHCHECK_HEADER, true), - } - for _, test := range tests { - models.ClearDSDecryptionCache() - runDatasourceAuthTest(test) - } - }) - - Convey("HandleRequest()", func() { - var writeErr error - backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"}) - w.WriteHeader(200) - _, writeErr = w.Write([]byte("I am the backend")) - })) - defer backend.Close() - - plugin := &plugins.DataSourcePlugin{} - ds := &models.DataSource{Url: backend.URL, Type: models.DS_GRAPHITE} - - responseRecorder := &CloseNotifierResponseRecorder{ - ResponseRecorder: httptest.NewRecorder(), - } - defer responseRecorder.Close() - - setupCtx := func(fn func(http.ResponseWriter)) *models.ReqContext { - responseWriter := macaron.NewResponseWriter("GET", responseRecorder) - if fn != nil { - fn(responseWriter) - } - - return &models.ReqContext{ - SignedInUser: &models.SignedInUser{}, - Context: &macaron.Context{ - Req: macaron.Request{ - Request: httptest.NewRequest("GET", "/render", nil), - }, - Resp: responseWriter, - }, - } - } - - Convey("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func() { - writeErr = nil - ctx := setupCtx(nil) - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) - So(err, ShouldBeNil) - - proxy.HandleRequest() - - So(writeErr, ShouldBeNil) - So(proxy.ctx.Resp.Header().Get("Set-Cookie"), ShouldBeEmpty) - }) - - Convey("When response header Set-Cookie is set should remove proxied Set-Cookie header and restore the original Set-Cookie header", func() { - writeErr = nil - ctx := setupCtx(func(w http.ResponseWriter) { - w.Header().Set("Set-Cookie", "important_cookie=important_value") - }) - proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) - So(err, ShouldBeNil) - - proxy.HandleRequest() - - So(writeErr, ShouldBeNil) - So(proxy.ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, - "important_cookie=important_value") - }) + require.NoError(t, writeErr) + assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie")) }) }) } @@ -672,7 +682,7 @@ func (r *CloseNotifierResponseRecorder) Close() { } // getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. -func getDatasourceProxiedRequest(ctx *models.ReqContext, cfg *setting.Cfg) *http.Request { +func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *setting.Cfg) *http.Request { plugin := &plugins.DataSourcePlugin{} ds := &models.DataSource{ @@ -681,25 +691,28 @@ func getDatasourceProxiedRequest(ctx *models.ReqContext, cfg *setting.Cfg) *http } proxy, err := NewDataSourceProxy(ds, plugin, ctx, "", cfg) - So(err, ShouldBeNil) + require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - So(err, ShouldBeNil) + require.NoError(t, err) proxy.getDirector()(req) return req } type httpClientStub struct { + t *testing.T fakeBody []byte } func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) { - bodyJSON, _ := simplejson.NewJson(c.fakeBody) + bodyJSON, err := simplejson.NewJson(c.fakeBody) + require.NoError(c.t, err) _, passedTokenCacheTest := bodyJSON.CheckGet("expires_on") - So(passedTokenCacheTest, ShouldBeTrue) + require.True(c.t, passedTokenCacheTest) bodyJSON.Set("expires_on", fmt.Sprint(time.Now().Add(time.Second*60).Unix())) - body, _ := bodyJSON.MarshalJSON() + body, err := bodyJSON.MarshalJSON() + require.NoError(c.t, err) resp := &http.Response{ Body: ioutil.NopCloser(bytes.NewReader(body)), } @@ -707,39 +720,40 @@ func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) { return resp, nil } -func newFakeHTTPClient(fakeBody []byte) httpClient { +func newFakeHTTPClient(t *testing.T, fakeBody []byte) httpClient { return &httpClientStub{ + t: t, fakeBody: fakeBody, } } -type Test struct { +type testCase struct { datasource *models.DataSource checkReq func(req *http.Request) } const ( - AUTHTYPE_PASSWORD = "password" - AUTHTYPE_BASIC = "basic" + authTypePassword = "password" + authTypeBasic = "basic" ) const ( - AUTHCHECK_QUERY = "query" - AUTHCHECK_HEADER = "header" + authCheckQuery = "query" + authCheckHeader = "header" ) -func createAuthTest(dsType string, authType string, authCheck string, useSecureJsonData bool) *Test { +func createAuthTest(t *testing.T, dsType string, authType string, authCheck string, useSecureJsonData bool) *testCase { // Basic user:password - base64AthHeader := "Basic dXNlcjpwYXNzd29yZA==" + base64AuthHeader := "Basic dXNlcjpwYXNzd29yZA==" - test := &Test{ + test := &testCase{ datasource: &models.DataSource{ Type: dsType, JsonData: simplejson.New(), }, } var message string - if authType == AUTHTYPE_PASSWORD { + if authType == authTypePassword { message = fmt.Sprintf("%v should add username and password", dsType) test.datasource.User = "user" if useSecureJsonData { @@ -766,35 +780,31 @@ func createAuthTest(dsType string, authType string, authCheck string, useSecureJ message += " from securejsondata" } - if authCheck == AUTHCHECK_QUERY { + if authCheck == authCheckQuery { message += " to query params" test.checkReq = func(req *http.Request) { - Convey(message, func() { - queryVals := req.URL.Query() - So(queryVals["u"][0], ShouldEqual, "user") - So(queryVals["p"][0], ShouldEqual, "password") - }) + queryVals := req.URL.Query() + assert.Equal(t, "user", queryVals["u"][0], message) + assert.Equal(t, "password", queryVals["p"][0], message) } } else { message += " to auth header" test.checkReq = func(req *http.Request) { - Convey(message, func() { - So(req.Header.Get("Authorization"), ShouldEqual, base64AthHeader) - }) + assert.Equal(t, base64AuthHeader, req.Header.Get("Authorization"), message) } } return test } -func runDatasourceAuthTest(test *Test) { +func runDatasourceAuthTest(t *testing.T, test *testCase) { plugin := &plugins.DataSourcePlugin{} ctx := &models.ReqContext{} proxy, err := NewDataSourceProxy(test.datasource, plugin, ctx, "", &setting.Cfg{}) - So(err, ShouldBeNil) + require.NoError(t, err) req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) - So(err, ShouldBeNil) + require.NoError(t, err) proxy.getDirector()(req) diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 78c452b0d28..c94aeaea3ec 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/setting" + macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -12,9 +13,8 @@ import ( "net/http" "github.com/grafana/grafana/pkg/infra/log" - . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert" - macaron "gopkg.in/macaron.v1" + "github.com/stretchr/testify/require" ) type testLogger struct { @@ -28,8 +28,8 @@ func (stub *testLogger) Warn(testMessage string, ctx ...interface{}) { stub.warnMessage = testMessage } -func TestTeamApiEndpoint(t *testing.T) { - Convey("Given two teams", t, func() { +func TestTeamAPIEndpoint(t *testing.T) { + t.Run("Given two teams", func(t *testing.T) { mockResult := models.SearchTeamQueryResult{ Teams: []*models.TeamDTO{ {Name: "team1"}, @@ -42,56 +42,52 @@ func TestTeamApiEndpoint(t *testing.T) { Cfg: setting.NewCfg(), } - Convey("When searching with no parameters", func() { - loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { - query.Result = mockResult + loggedInUserScenario(t, "When calling GET on", "/api/teams/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { + query.Result = mockResult - sentLimit = query.Limit - sendPage = query.Page + sentLimit = query.Limit + sendPage = query.Page - return nil - }) - - sc.handlerFunc = hs.SearchTeams - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - - So(sentLimit, ShouldEqual, 1000) - So(sendPage, ShouldEqual, 1) - - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - - So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) - So(len(respJSON.Get("teams").MustArray()), ShouldEqual, 2) + return nil }) + + sc.handlerFunc = hs.SearchTeams + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + + assert.Equal(t, 1000, sentLimit) + assert.Equal(t, 1, sendPage) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + + assert.Equal(t, 2, respJSON.Get("totalCount").MustInt()) + assert.Equal(t, 2, len(respJSON.Get("teams").MustArray())) }) - Convey("When searching with page and perpage parameters", func() { - loggedInUserScenario("When calling GET on", "/api/teams/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { - query.Result = mockResult + loggedInUserScenario(t, "When calling GET on", "/api/teams/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchTeamsQuery) error { + query.Result = mockResult - sentLimit = query.Limit - sendPage = query.Page + sentLimit = query.Limit + sendPage = query.Page - return nil - }) - - sc.handlerFunc = hs.SearchTeams - sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - - So(sentLimit, ShouldEqual, 10) - So(sendPage, ShouldEqual, 2) + return nil }) + + sc.handlerFunc = hs.SearchTeams + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + + assert.Equal(t, 10, sentLimit) + assert.Equal(t, 2, sendPage) }) }) - t.Run("When creating team with api key", func(t *testing.T) { + t.Run("When creating team with API key", func(t *testing.T) { defer bus.ClearBusHandlers() hs := &HTTPServer{ diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 6e95c581e02..e3e1b9b7d7d 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -10,50 +10,49 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestUserApiEndpoint(t *testing.T) { - Convey("Given a user is logged in", t, func() { - mockResult := models.SearchUserQueryResult{ - Users: []*models.UserSearchHitDTO{ - {Name: "user1"}, - {Name: "user2"}, - }, - TotalCount: 2, - } +func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { + mockResult := models.SearchUserQueryResult{ + Users: []*models.UserSearchHitDTO{ + {Name: "user1"}, + {Name: "user2"}, + }, + TotalCount: 2, + } - loggedInUserScenario("When calling GET on", "api/users/:id", func(sc *scenarioContext) { - fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) - bus.AddHandler("test", func(query *models.GetUserProfileQuery) error { - query.Result = models.UserProfileDTO{ - Id: int64(1), - Email: "daniel@grafana.com", - Name: "Daniel", - Login: "danlee", - OrgId: int64(2), - IsGrafanaAdmin: true, - IsDisabled: false, - IsExternal: false, - UpdatedAt: fakeNow, - CreatedAt: fakeNow, - } - return nil - }) + loggedInUserScenario(t, "When calling GET on", "api/users/:id", func(sc *scenarioContext) { + fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) + bus.AddHandler("test", func(query *models.GetUserProfileQuery) error { + query.Result = models.UserProfileDTO{ + Id: int64(1), + Email: "daniel@grafana.com", + Name: "Daniel", + Login: "danlee", + OrgId: int64(2), + IsGrafanaAdmin: true, + IsDisabled: false, + IsExternal: false, + UpdatedAt: fakeNow, + CreatedAt: fakeNow, + } + return nil + }) - bus.AddHandler("test", func(query *models.GetAuthInfoQuery) error { - query.Result = &models.UserAuth{ - AuthModule: models.AuthModuleLDAP, - } - return nil - }) + bus.AddHandler("test", func(query *models.GetAuthInfoQuery) error { + query.Result = &models.UserAuth{ + AuthModule: models.AuthModuleLDAP, + } + return nil + }) - sc.handlerFunc = GetUserByID - avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com") - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + sc.handlerFunc = GetUserByID + avatarUrl := dtos.GetGravatarUrl("daniel@grafana.com") + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - expected := fmt.Sprintf(` + expected := fmt.Sprintf(` { "id": 1, "email": "daniel@grafana.com", @@ -73,35 +72,35 @@ func TestUserApiEndpoint(t *testing.T) { } `, avatarUrl) - require.Equal(t, http.StatusOK, sc.resp.Code) - require.JSONEq(t, expected, sc.resp.Body.String()) + require.Equal(t, http.StatusOK, sc.resp.Code) + require.JSONEq(t, expected, sc.resp.Body.String()) + }) + + loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", func(sc *scenarioContext) { + fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) + bus.AddHandler("test", func(query *models.GetUserByLoginQuery) error { + require.Equal(t, "danlee", query.LoginOrEmail) + + query.Result = &models.User{ + Id: int64(1), + Email: "daniel@grafana.com", + Name: "Daniel", + Login: "danlee", + Theme: "light", + IsAdmin: true, + OrgId: int64(2), + IsDisabled: false, + Updated: fakeNow, + Created: fakeNow, + } + + return nil }) - loggedInUserScenario("When calling GET on", "/api/users/lookup", func(sc *scenarioContext) { - fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) - bus.AddHandler("test", func(query *models.GetUserByLoginQuery) error { - require.Equal(t, "danlee", query.LoginOrEmail) + sc.handlerFunc = GetUserByLoginOrEmail + sc.fakeReqWithParams("GET", sc.url, map[string]string{"loginOrEmail": "danlee"}).exec() - query.Result = &models.User{ - Id: int64(1), - Email: "daniel@grafana.com", - Name: "Daniel", - Login: "danlee", - Theme: "light", - IsAdmin: true, - OrgId: int64(2), - IsDisabled: false, - Updated: fakeNow, - Created: fakeNow, - } - - return nil - }) - - sc.handlerFunc = GetUserByLoginOrEmail - sc.fakeReqWithParams("GET", sc.url, map[string]string{"loginOrEmail": "danlee"}).exec() - - expected := ` + expected := ` { "id": 1, "email": "daniel@grafana.com", @@ -119,94 +118,93 @@ func TestUserApiEndpoint(t *testing.T) { } ` - require.Equal(t, http.StatusOK, sc.resp.Code) - require.JSONEq(t, expected, sc.resp.Body.String()) + require.Equal(t, http.StatusOK, sc.resp.Code) + require.JSONEq(t, expected, sc.resp.Body.String()) + }) + + loggedInUserScenario(t, "When calling GET on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult + + sentLimit = query.Limit + sendPage = query.Page + + return nil }) - loggedInUserScenario("When calling GET on", "/api/users", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchUsersQuery) error { - query.Result = mockResult + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - sentLimit = query.Limit - sendPage = query.Page + assert.Equal(t, 1000, sentLimit) + assert.Equal(t, 1, sendPage) - return nil - }) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) + assert.Equal(t, 2, len(respJSON.MustArray())) + }) - sc.handlerFunc = SearchUsers - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + loggedInUserScenario(t, "When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult - So(sentLimit, ShouldEqual, 1000) - So(sendPage, ShouldEqual, 1) + sentLimit = query.Limit + sendPage = query.Page - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) - So(len(respJSON.MustArray()), ShouldEqual, 2) + return nil }) - loggedInUserScenario("When calling GET with page and limit querystring parameters on", "/api/users", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchUsersQuery) error { - query.Result = mockResult + sc.handlerFunc = SearchUsers + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - sentLimit = query.Limit - sendPage = query.Page + assert.Equal(t, 10, sentLimit) + assert.Equal(t, 2, sendPage) + }) - return nil - }) + loggedInUserScenario(t, "When calling GET on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult - sc.handlerFunc = SearchUsers - sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() + sentLimit = query.Limit + sendPage = query.Page - So(sentLimit, ShouldEqual, 10) - So(sendPage, ShouldEqual, 2) + return nil }) - loggedInUserScenario("When calling GET on", "/api/users/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchUsersQuery) error { - query.Result = mockResult + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - sentLimit = query.Limit - sendPage = query.Page + assert.Equal(t, 1000, sentLimit) + assert.Equal(t, 1, sendPage) - return nil - }) + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + require.NoError(t, err) - sc.handlerFunc = SearchUsersWithPaging - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() + assert.Equal(t, 2, respJSON.Get("totalCount").MustInt()) + assert.Equal(t, 2, len(respJSON.Get("users").MustArray())) + }) - So(sentLimit, ShouldEqual, 1000) - So(sendPage, ShouldEqual, 1) + loggedInUserScenario(t, "When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) { + var sentLimit int + var sendPage int + bus.AddHandler("test", func(query *models.SearchUsersQuery) error { + query.Result = mockResult - respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) - So(err, ShouldBeNil) + sentLimit = query.Limit + sendPage = query.Page - So(respJSON.Get("totalCount").MustInt(), ShouldEqual, 2) - So(len(respJSON.Get("users").MustArray()), ShouldEqual, 2) + return nil }) - loggedInUserScenario("When calling GET with page and perpage querystring parameters on", "/api/users/search", func(sc *scenarioContext) { - var sentLimit int - var sendPage int - bus.AddHandler("test", func(query *models.SearchUsersQuery) error { - query.Result = mockResult + sc.handlerFunc = SearchUsersWithPaging + sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - sentLimit = query.Limit - sendPage = query.Page - - return nil - }) - - sc.handlerFunc = SearchUsersWithPaging - sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() - - So(sentLimit, ShouldEqual, 10) - So(sendPage, ShouldEqual, 2) - }) + assert.Equal(t, 10, sentLimit) + assert.Equal(t, 2, sendPage) }) } diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 77b1ee347dd..e28271b1443 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -2,115 +2,117 @@ package api import ( "context" + "fmt" "testing" "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" - - . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/assert" ) -func TestUserTokenApiEndpoint(t *testing.T) { - Convey("When current user attempts to revoke an auth token for a non-existing user", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - userId = cmd.Id - return models.ErrUserNotFound - }) - +func TestUserTokenAPIEndpoint(t *testing.T) { + t.Run("When current user attempts to revoke an auth token for a non-existing user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - revokeUserAuthTokenScenario("Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - So(userId, ShouldEqual, 200) - }) + revokeUserAuthTokenScenario(t, "Should return not found when calling POST on", "/api/user/revoke-auth-token", + "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + userID = cmd.Id + return models.ErrUserNotFound + }) + + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + assert.Equal(t, 404, sc.resp.Code) + assert.Equal(t, int64(200), userID) + }) }) - Convey("When current user gets auth tokens for a non-existing user", t, func() { - userId := int64(0) - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - userId = cmd.Id - return models.ErrUserNotFound - }) + t.Run("When current user gets auth tokens for a non-existing user", func(t *testing.T) { + getUserAuthTokensScenario(t, "Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) { + var userID int64 + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + userID = cmd.Id + return models.ErrUserNotFound + }) - getUserAuthTokensScenario("Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) - So(userId, ShouldEqual, 200) + assert.Equal(t, 404, sc.resp.Code) + assert.Equal(t, int64(200), userID) }) }) - Convey("When logout an existing user from all devices", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: 200} - return nil - }) + t.Run("When logging out an existing user from all devices", func(t *testing.T) { + logoutUserFromAllDevicesInternalScenario(t, "Should be successful", 1, func(sc *scenarioContext) { + const userID int64 = 200 + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + cmd.Result = &models.User{Id: userID} + return nil + }) - logoutUserFromAllDevicesInternalScenario("Should be successful", 1, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) }) - Convey("When logout a non-existing user from all devices", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - return models.ErrUserNotFound - }) + t.Run("When logout a non-existing user from all devices", func(t *testing.T) { + logoutUserFromAllDevicesInternalScenario(t, "Should return not found", testUserID, func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + return models.ErrUserNotFound + }) - logoutUserFromAllDevicesInternalScenario("Should return not found", TestUserID, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 404) + assert.Equal(t, 404, sc.resp.Code) }) }) - Convey("When revoke an auth token for a user", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: 200} - return nil - }) - + t.Run("When revoke an auth token for a user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} token := &models.UserToken{Id: 1} - revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) { + revokeUserAuthTokenInternalScenario(t, "Should be successful", cmd, 200, token, func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + cmd.Result = &models.User{Id: 200} + return nil + }) + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { return &models.UserToken{Id: 2}, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) }) }) - Convey("When revoke the active auth token used by himself", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: TestUserID} - return nil - }) - + t.Run("When revoke the active auth token used by himself", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} token := &models.UserToken{Id: 2} - revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) { + revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + cmd.Result = &models.User{Id: testUserID} + return nil + }) + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { return token, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 400) + assert.Equal(t, 400, sc.resp.Code) }) }) - Convey("When gets auth tokens for a user", t, func() { - bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { - cmd.Result = &models.User{Id: TestUserID} - return nil - }) - + t.Run("When gets auth tokens for a user", func(t *testing.T) { currentToken := &models.UserToken{Id: 1} - getUserAuthTokensInternalScenario("Should be successful", currentToken, func(sc *scenarioContext) { + getUserAuthTokensInternalScenario(t, "Should be successful", currentToken, func(sc *scenarioContext) { + bus.AddHandler("test", func(cmd *models.GetUserByIdQuery) error { + cmd.Result = &models.User{Id: testUserID} + return nil + }) + tokens := []*models.UserToken{ { Id: 1, @@ -132,42 +134,43 @@ func TestUserTokenApiEndpoint(t *testing.T) { } sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() - So(sc.resp.Code, ShouldEqual, 200) + assert.Equal(t, 200, sc.resp.Code) result := sc.ToJSON() - So(result.MustArray(), ShouldHaveLength, 2) + assert.Len(t, result.MustArray(), 2) resultOne := result.GetIndex(0) - So(resultOne.Get("id").MustInt64(), ShouldEqual, tokens[0].Id) - So(resultOne.Get("isActive").MustBool(), ShouldBeTrue) - So(resultOne.Get("clientIp").MustString(), ShouldEqual, "127.0.0.1") - So(resultOne.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339)) - So(resultOne.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339)) + assert.Equal(t, tokens[0].Id, resultOne.Get("id").MustInt64()) + assert.True(t, resultOne.Get("isActive").MustBool()) + assert.Equal(t, "127.0.0.1", resultOne.Get("clientIp").MustString()) + assert.Equal(t, time.Unix(tokens[0].CreatedAt, 0).Format(time.RFC3339), resultOne.Get("createdAt").MustString()) + assert.Equal(t, time.Unix(tokens[0].SeenAt, 0).Format(time.RFC3339), resultOne.Get("seenAt").MustString()) - So(resultOne.Get("device").MustString(), ShouldEqual, "Other") - So(resultOne.Get("browser").MustString(), ShouldEqual, "Chrome") - So(resultOne.Get("browserVersion").MustString(), ShouldEqual, "72.0") - So(resultOne.Get("os").MustString(), ShouldEqual, "Linux") - So(resultOne.Get("osVersion").MustString(), ShouldEqual, "") + assert.Equal(t, "Other", resultOne.Get("device").MustString()) + assert.Equal(t, "Chrome", resultOne.Get("browser").MustString()) + assert.Equal(t, "72.0", resultOne.Get("browserVersion").MustString()) + assert.Equal(t, "Linux", resultOne.Get("os").MustString()) + assert.Empty(t, resultOne.Get("osVersion").MustString()) resultTwo := result.GetIndex(1) - So(resultTwo.Get("id").MustInt64(), ShouldEqual, tokens[1].Id) - So(resultTwo.Get("isActive").MustBool(), ShouldBeFalse) - So(resultTwo.Get("clientIp").MustString(), ShouldEqual, "127.0.0.2") - So(resultTwo.Get("createdAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339)) - So(resultTwo.Get("seenAt").MustString(), ShouldEqual, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339)) + assert.Equal(t, tokens[1].Id, resultTwo.Get("id").MustInt64()) + assert.False(t, resultTwo.Get("isActive").MustBool()) + assert.Equal(t, "127.0.0.2", resultTwo.Get("clientIp").MustString()) + assert.Equal(t, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339), resultTwo.Get("createdAt").MustString()) + assert.Equal(t, time.Unix(tokens[1].CreatedAt, 0).Format(time.RFC3339), resultTwo.Get("seenAt").MustString()) - So(resultTwo.Get("device").MustString(), ShouldEqual, "iPhone") - So(resultTwo.Get("browser").MustString(), ShouldEqual, "Mobile Safari") - So(resultTwo.Get("browserVersion").MustString(), ShouldEqual, "11.0") - So(resultTwo.Get("os").MustString(), ShouldEqual, "iOS") - So(resultTwo.Get("osVersion").MustString(), ShouldEqual, "11.0") + assert.Equal(t, "iPhone", resultTwo.Get("device").MustString()) + assert.Equal(t, "Mobile Safari", resultTwo.Get("browser").MustString()) + assert.Equal(t, "11.0", resultTwo.Get("browserVersion").MustString()) + assert.Equal(t, "iOS", resultTwo.Get("os").MustString()) + assert.Equal(t, "11.0", resultTwo.Get("osVersion").MustString()) }) }) } -func revokeUserAuthTokenScenario(desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, userId int64, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, + userId int64, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -176,12 +179,12 @@ func revokeUserAuthTokenScenario(desc string, url string, routePattern string, c AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c sc.context.UserId = userId - sc.context.OrgId = TestOrgID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.RevokeUserAuthToken(c, cmd) @@ -193,9 +196,9 @@ func revokeUserAuthTokenScenario(desc string, url string, routePattern string, c }) } -func getUserAuthTokensScenario(desc string, url string, routePattern string, userId int64, fn scenarioFunc) { - Convey(desc+" "+url, func() { - defer bus.ClearBusHandlers() +func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc) { + t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -204,12 +207,12 @@ func getUserAuthTokensScenario(desc string, url string, routePattern string, use AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext(url) + sc := setupScenarioContext(t, url) sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c sc.context.UserId = userId - sc.context.OrgId = TestOrgID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.GetUserAuthTokens(c) @@ -221,20 +224,20 @@ func getUserAuthTokensScenario(desc string, url string, routePattern string, use }) } -func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scenarioFunc) { - Convey(desc, func() { - defer bus.ClearBusHandlers() +func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc) { + t.Run(desc, func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) hs := HTTPServer{ Bus: bus.GetBus(), AuthTokenService: auth.NewFakeUserAuthTokenService(), } - sc := setupScenarioContext("/") + sc := setupScenarioContext(t, "/") sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN return hs.logoutUserFromAllDevicesInternal(context.Background(), userId) @@ -246,9 +249,10 @@ func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scen }) } -func revokeUserAuthTokenInternalScenario(desc string, cmd models.RevokeAuthTokenCmd, userId int64, token *models.UserToken, fn scenarioFunc) { - Convey(desc, func() { - defer bus.ClearBusHandlers() +func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64, + token *models.UserToken, fn scenarioFunc) { + t.Run(desc, func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -257,12 +261,12 @@ func revokeUserAuthTokenInternalScenario(desc string, cmd models.RevokeAuthToken AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext("/") + sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN sc.context.UserToken = token @@ -275,9 +279,9 @@ func revokeUserAuthTokenInternalScenario(desc string, cmd models.RevokeAuthToken }) } -func getUserAuthTokensInternalScenario(desc string, token *models.UserToken, fn scenarioFunc) { - Convey(desc, func() { - defer bus.ClearBusHandlers() +func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc) { + t.Run(desc, func(t *testing.T) { + t.Cleanup(bus.ClearBusHandlers) fakeAuthTokenService := auth.NewFakeUserAuthTokenService() @@ -286,16 +290,16 @@ func getUserAuthTokensInternalScenario(desc string, token *models.UserToken, fn AuthTokenService: fakeAuthTokenService, } - sc := setupScenarioContext("/") + sc := setupScenarioContext(t, "/") sc.userAuthTokenService = fakeAuthTokenService sc.defaultHandler = Wrap(func(c *models.ReqContext) Response { sc.context = c - sc.context.UserId = TestUserID - sc.context.OrgId = TestOrgID + sc.context.UserId = testUserID + sc.context.OrgId = testOrgID sc.context.OrgRole = models.ROLE_ADMIN sc.context.UserToken = token - return hs.getUserAuthTokensInternal(c, TestUserID) + return hs.getUserAuthTokensInternal(c, testUserID) }) sc.m.Get("/", sc.defaultHandler) diff --git a/pkg/services/licensing/oss.go b/pkg/services/licensing/oss.go index 423903d59d2..b3a8556c9cc 100644 --- a/pkg/services/licensing/oss.go +++ b/pkg/services/licensing/oss.go @@ -34,7 +34,7 @@ func (*OSSLicensingService) StateInfo() string { func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string { if user.IsGrafanaAdmin { - return l.Cfg.AppSubUrl + "/admin/upgrading" + return l.Cfg.AppSubURL + "/admin/upgrading" } return "https://grafana.com/products/enterprise/?utm_source=grafana_footer" diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 1ae7ed53f42..a4b23337bfc 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -235,7 +235,7 @@ func (rs *RenderingService) getURL(path string) string { subPath := "" if rs.Cfg.ServeFromSubPath { - subPath = rs.Cfg.AppSubUrl + subPath = rs.Cfg.AppSubURL } // &render=1 signals to the legacy redirect layer to diff --git a/pkg/services/rendering/rendering_test.go b/pkg/services/rendering/rendering_test.go index bdbdd37f255..d8d02133c2b 100644 --- a/pkg/services/rendering/rendering_test.go +++ b/pkg/services/rendering/rendering_test.go @@ -27,14 +27,14 @@ func TestGetUrl(t *testing.T) { t.Run("And protocol HTTP configured should return expected path", func(t *testing.T) { rs.Cfg.ServeFromSubPath = false - rs.Cfg.AppSubUrl = "" + rs.Cfg.AppSubURL = "" setting.Protocol = setting.HTTPScheme url := rs.getURL(path) require.Equal(t, "http://localhost:3000/"+path+"&render=1", url) t.Run("And serve from sub path should return expected path", func(t *testing.T) { rs.Cfg.ServeFromSubPath = true - rs.Cfg.AppSubUrl = "/grafana" + rs.Cfg.AppSubURL = "/grafana" url := rs.getURL(path) require.Equal(t, "http://localhost:3000/grafana/"+path+"&render=1", url) }) @@ -42,7 +42,7 @@ func TestGetUrl(t *testing.T) { t.Run("And protocol HTTPS configured should return expected path", func(t *testing.T) { rs.Cfg.ServeFromSubPath = false - rs.Cfg.AppSubUrl = "" + rs.Cfg.AppSubURL = "" setting.Protocol = setting.HTTPSScheme url := rs.getURL(path) require.Equal(t, "https://localhost:3000/"+path+"&render=1", url) @@ -50,7 +50,7 @@ func TestGetUrl(t *testing.T) { t.Run("And protocol HTTP2 configured should return expected path", func(t *testing.T) { rs.Cfg.ServeFromSubPath = false - rs.Cfg.AppSubUrl = "" + rs.Cfg.AppSubURL = "" setting.Protocol = setting.HTTP2Scheme url := rs.getURL(path) require.Equal(t, "https://localhost:3000/"+path+"&render=1", url) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6b3cb83f1bb..111212cc61b 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -78,7 +78,7 @@ var ( // Log settings. LogConfigs []util.DynMap - // Http server options + // HTTP server options Protocol Scheme Domain string HttpAddr, HttpPort string @@ -193,7 +193,7 @@ var ( LDAPAllowSignup bool LDAPActiveSyncEnabled bool - // QUOTA + // Quota Quota QuotaSettings // Alerting @@ -228,8 +228,8 @@ type Cfg struct { Logger log.Logger // HTTP Server Settings - AppUrl string - AppSubUrl string + AppURL string + AppSubURL string ServeFromSubPath bool StaticRootPath string Protocol Scheme @@ -707,7 +707,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.Raw = iniFile - // Temporary keep global, to make refactor in steps + // Temporarily keep global, to make refactor in steps Raw = cfg.Raw cfg.BuildVersion = BuildVersion @@ -1203,8 +1203,8 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error { } ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false) - cfg.AppUrl = AppUrl - cfg.AppSubUrl = AppSubUrl + cfg.AppURL = AppUrl + cfg.AppSubURL = AppSubUrl cfg.ServeFromSubPath = ServeFromSubPath Protocol = HTTPScheme