From 73776d61958cbc4929c485f18da2c0a91237291d Mon Sep 17 00:00:00 2001 From: Gusted Date: Fri, 9 Jun 2023 22:18:15 +0200 Subject: [PATCH] [MODERATION] add user blocking API - Follow up for: #540, #802 - Add API routes for user blocking from user and organization perspective. - The new routes have integration testing. - The new model functions have unit tests. - Actually quite boring to write and to read this pull request. (cherry picked from commit f3afaf15c7e34038363c9ce8e1ef957ec1e22b06) (cherry picked from commit 6d754db3e5faff93a58fab2867737f81f40f6599) (cherry picked from commit d0fc8bc9d3b6bb189a2ab634a5329253af9b4629) (cherry picked from commit 9a53b0d1a07455596622cb02716b476b6aaa95e4) (cherry picked from commit 44a2a4fd48678058777d6db46c13a2c7298497d4) (cherry picked from commit 182025db9cc76073bdb0221dfd1fb3b2b66f7fd4) (cherry picked from commit 558a35963eddd672f1911393a649ab08a9283e5b) --- models/user/block.go | 23 ++- models/user/block_test.go | 14 +- modules/structs/moderation.go | 13 ++ routers/api/v1/api.go | 12 ++ routers/api/v1/org/org.go | 92 +++++++++ routers/api/v1/swagger/repo.go | 7 + routers/api/v1/user/user.go | 77 ++++++++ routers/api/v1/utils/block.go | 65 +++++++ routers/web/org/setting/blocked_users.go | 3 +- routers/web/user/setting/blocked_users.go | 3 +- templates/swagger/v1_json.tmpl | 225 ++++++++++++++++++++++ tests/integration/api_block_test.go | 101 ++++++++++ 12 files changed, 626 insertions(+), 9 deletions(-) create mode 100644 modules/structs/moderation.go create mode 100644 routers/api/v1/utils/block.go create mode 100644 tests/integration/api_block_test.go diff --git a/models/user/block.go b/models/user/block.go index 838bc7431e..189cacc2a2 100644 --- a/models/user/block.go +++ b/models/user/block.go @@ -52,18 +52,29 @@ func UnblockUser(ctx context.Context, userID, blockID int64) error { return err } +// CountBlockedUsers returns the number of users the user has blocked. +func CountBlockedUsers(ctx context.Context, userID int64) (int64, error) { + return db.GetEngine(ctx).Where("user_id=?", userID).Count(&BlockedUser{}) +} + // ListBlockedUsers returns the users that the user has blocked. // The created_unix field of the user struct is overridden by the creation_unix // field of blockeduser. -func ListBlockedUsers(ctx context.Context, userID int64) ([]*User, error) { - users := make([]*User, 0, 8) - err := db.GetEngine(ctx). +func ListBlockedUsers(ctx context.Context, userID int64, opts db.ListOptions) ([]*User, error) { + sess := db.GetEngine(ctx). Select("`forgejo_blocked_user`.created_unix, `user`.*"). Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id"). - Where("`forgejo_blocked_user`.user_id=?", userID). - Find(&users) + Where("`forgejo_blocked_user`.user_id=?", userID) - return users, err + if opts.Page > 0 { + sess = db.SetSessionPagination(sess, &opts) + users := make([]*User, 0, opts.PageSize) + + return users, sess.Find(&users) + } + + users := make([]*User, 0, 8) + return users, sess.Find(&users) } // ListBlockedByUsersID returns the ids of the users that blocked the user. diff --git a/models/user/block_test.go b/models/user/block_test.go index d800eeaade..a368bb35d7 100644 --- a/models/user/block_test.go +++ b/models/user/block_test.go @@ -45,7 +45,7 @@ func TestUnblockUser(t *testing.T) { func TestListBlockedUsers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - blockedUsers, err := user_model.ListBlockedUsers(db.DefaultContext, 4) + blockedUsers, err := user_model.ListBlockedUsers(db.DefaultContext, 4, db.ListOptions{}) assert.NoError(t, err) if assert.Len(t, blockedUsers, 1) { assert.EqualValues(t, 1, blockedUsers[0].ID) @@ -61,3 +61,15 @@ func TestListBlockedByUsersID(t *testing.T) { assert.EqualValues(t, 4, blockedByUserIDs[0]) } } + +func TestCountBlockedUsers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + count, err := user_model.CountBlockedUsers(db.DefaultContext, 4) + assert.NoError(t, err) + assert.EqualValues(t, 1, count) + + count, err = user_model.CountBlockedUsers(db.DefaultContext, 1) + assert.NoError(t, err) + assert.EqualValues(t, 0, count) +} diff --git a/modules/structs/moderation.go b/modules/structs/moderation.go new file mode 100644 index 0000000000..c1e55085a7 --- /dev/null +++ b/modules/structs/moderation.go @@ -0,0 +1,13 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import "time" + +// BlockedUser represents a blocked user. +type BlockedUser struct { + BlockID int64 `json:"block_id"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 3a1203628a..2b31b46e70 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -905,6 +905,12 @@ func Routes(ctx gocontext.Context) *web.Route { Patch(bind(api.EditHookOption{}), user.EditHook). Delete(user.DeleteHook) }, reqWebhooksEnabled()) + + m.Group("", func() { + m.Get("/list_blocked", user.ListBlockedUsers) + m.Put("/block/{username}", user.BlockUser) + m.Put("/unblock/{username}", user.UnblockUser) + }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) @@ -1321,6 +1327,12 @@ func Routes(ctx gocontext.Context) *web.Route { Delete(org.DeleteHook) }, reqToken(), reqOrgOwnership(), reqWebhooksEnabled()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) + + m.Group("", func() { + m.Get("/list_blocked", reqToken(), reqOrgOwnership(), org.ListBlockedUsers) + m.Put("/block/{username}", reqToken(), reqOrgOwnership(), org.BlockUser) + m.Put("/unblock/{username}", reqToken(), reqOrgOwnership(), org.UnblockUser) + }, reqToken(), reqOrgOwnership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 4e30ad1762..3a297b1b9d 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -437,3 +437,95 @@ func ListOrgActivityFeeds(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) } + +// ListBlockedUsers list the organization's blocked users. +func ListBlockedUsers(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/list_blocked organization orgListBlockedUsers + // --- + // summary: List the organization's blocked users + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the org + // type: string + // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/BlockedUserList" + + utils.ListUserBlockedUsers(ctx, ctx.ContextUser) +} + +// BlockUser blocks a user from the organization. +func BlockUser(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/block/{username} organization orgBlockUser + // --- + // summary: Blocks a user from the organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the org + // type: string + // required: true + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + user := user.GetUserByParams(ctx) + if ctx.Written() { + return + } + + utils.BlockUser(ctx, ctx.Org.Organization.AsUser(), user) +} + +// UnblockUser unblocks a user from the organization. +func UnblockUser(ctx *context.APIContext) { + // swagger:operation PUT /orgs/{org}/unblock/{username} organization orgUnblockUser + // --- + // summary: Unblock a user from the organization + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the org + // type: string + // required: true + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + user := user.GetUserByParams(ctx) + if ctx.Written() { + return + } + + utils.UnblockUser(ctx, ctx.Org.Organization.AsUser(), user) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index 3e23aa4d5a..263e335873 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -414,3 +414,10 @@ type swaggerRepoNewIssuePinsAllowed struct { // in:body Body api.NewIssuePinsAllowed `json:"body"` } + +// BlockedUserList +// swagger:response BlockedUserList +type swaggerBlockedUserList struct { + // in:body + Body []api.BlockedUser `json:"body"` +} diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 2a2361be67..595ca04d62 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -202,3 +202,80 @@ func ListUserActivityFeeds(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer)) } + +// ListBlockedUsers list the authenticated user's blocked users. +func ListBlockedUsers(ctx *context.APIContext) { + // swagger:operation GET /user/list_blocked user userListBlockedUsers + // --- + // summary: List the authenticated user's blocked users + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/BlockedUserList" + + utils.ListUserBlockedUsers(ctx, ctx.Doer) +} + +// BlockUser blocks a user from the doer. +func BlockUser(ctx *context.APIContext) { + // swagger:operation PUT /user/block/{username} user userBlockUser + // --- + // summary: Blocks a user from the doer. + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + user := GetUserByParams(ctx) + if ctx.Written() { + return + } + + utils.BlockUser(ctx, ctx.Doer, user) +} + +// UnblockUser unblocks a user from the doer. +func UnblockUser(ctx *context.APIContext) { + // swagger:operation PUT /user/unblock/{username} user userUnblockUser + // --- + // summary: Unblocks a user from the doer. + // produces: + // - application/json + // parameters: + // - name: username + // in: path + // description: username of the user + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + user := GetUserByParams(ctx) + if ctx.Written() { + return + } + + utils.UnblockUser(ctx, ctx.Doer, user) +} diff --git a/routers/api/v1/utils/block.go b/routers/api/v1/utils/block.go new file mode 100644 index 0000000000..187d69044e --- /dev/null +++ b/routers/api/v1/utils/block.go @@ -0,0 +1,65 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "net/http" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + user_service "code.gitea.io/gitea/services/user" +) + +// ListUserBlockedUsers lists the blocked users of the provided doer. +func ListUserBlockedUsers(ctx *context.APIContext, doer *user_model.User) { + count, err := user_model.CountBlockedUsers(ctx, doer.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + blockedUsers, err := user_model.ListBlockedUsers(ctx, doer.ID, GetListOptions(ctx)) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiBlockedUsers := make([]*api.BlockedUser, len(blockedUsers)) + for i, blockedUser := range blockedUsers { + apiBlockedUsers[i] = &api.BlockedUser{ + BlockID: blockedUser.ID, + Created: blockedUser.CreatedUnix.AsTime(), + } + if err != nil { + ctx.InternalServerError(err) + return + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiBlockedUsers) +} + +// BlockUser blocks the blockUser from the doer. +func BlockUser(ctx *context.APIContext, doer, blockUser *user_model.User) { + err := user_service.BlockUser(ctx, doer.ID, blockUser.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} + +// UnblockUser unblocks the blockUser from the doer. +func UnblockUser(ctx *context.APIContext, doer, blockUser *user_model.User) { + err := user_model.UnblockUser(ctx, doer.ID, blockUser.ID) + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go index eae6f81fa0..ebec1f1df5 100644 --- a/routers/web/org/setting/blocked_users.go +++ b/routers/web/org/setting/blocked_users.go @@ -7,6 +7,7 @@ import ( "net/http" "strings" + "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/routers/utils" @@ -20,7 +21,7 @@ func BlockedUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.blocked_users") ctx.Data["PageIsSettingsBlockedUsers"] = true - blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID) + blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID, db.ListOptions{}) if err != nil { ctx.ServerError("ListBlockedUsers", err) return diff --git a/routers/web/user/setting/blocked_users.go b/routers/web/user/setting/blocked_users.go index 134becf969..ed1c340fb9 100644 --- a/routers/web/user/setting/blocked_users.go +++ b/routers/web/user/setting/blocked_users.go @@ -6,6 +6,7 @@ package setting import ( "net/http" + "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" @@ -23,7 +24,7 @@ func BlockedUsers(ctx *context.Context) { ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/blocked_users" ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/blocked_users" - blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Doer.ID) + blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Doer.ID, db.ListOptions{}) if err != nil { ctx.ServerError("ListBlockedUsers", err) return diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index d1f21b3cf9..fef00324aa 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1595,6 +1595,42 @@ } } }, + "/orgs/{org}/block/{username}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Blocks a user from the organization", + "operationId": "orgBlockUser", + "parameters": [ + { + "type": "string", + "description": "name of the org", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/hooks": { "get": { "produces": [ @@ -1959,6 +1995,44 @@ } } }, + "/orgs/{org}/list_blocked": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "List the organization's blocked users", + "operationId": "orgListBlockedUsers", + "parameters": [ + { + "type": "string", + "description": "name of the org", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BlockedUserList" + } + } + } + }, "/orgs/{org}/members": { "get": { "produces": [ @@ -2423,6 +2497,42 @@ } } }, + "/orgs/{org}/unblock/{username}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Unblock a user from the organization", + "operationId": "orgUnblockUser", + "parameters": [ + { + "type": "string", + "description": "name of the org", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/packages/{owner}": { "get": { "produces": [ @@ -13790,6 +13900,35 @@ } } }, + "/user/block/{username}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Blocks a user from the doer.", + "operationId": "userBlockUser", + "parameters": [ + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/emails": { "get": { "produces": [ @@ -14439,6 +14578,37 @@ } } }, + "/user/list_blocked": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "List the authenticated user's blocked users", + "operationId": "userListBlockedUsers", + "parameters": [ + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BlockedUserList" + } + } + } + }, "/user/orgs": { "get": { "produces": [ @@ -14840,6 +15010,35 @@ } } }, + "/user/unblock/{username}": { + "put": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Unblocks a user from the doer.", + "operationId": "userUnblockUser", + "parameters": [ + { + "type": "string", + "description": "username of the user", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/users/search": { "get": { "produces": [ @@ -15770,6 +15969,23 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "BlockedUser": { + "type": "object", + "title": "BlockedUser represents a blocked user.", + "properties": { + "block_id": { + "type": "integer", + "format": "int64", + "x-go-name": "BlockID" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Branch": { "description": "Branch represents a repository branch", "type": "object", @@ -21953,6 +22169,15 @@ } } }, + "BlockedUserList": { + "description": "BlockedUserList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/BlockedUser" + } + } + }, "Branch": { "description": "Branch", "schema": { diff --git a/tests/integration/api_block_test.go b/tests/integration/api_block_test.go new file mode 100644 index 0000000000..2118080c7a --- /dev/null +++ b/tests/integration/api_block_test.go @@ -0,0 +1,101 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIUserBlock(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := "user4" + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser) + + t.Run("BlockUser", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/block/user2?token=%s", token)) + MakeRequest(t, req, http.StatusNoContent) + + unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2}) + }) + + t.Run("ListBlocked", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/list_blocked?token=%s", token)) + resp := MakeRequest(t, req, http.StatusOK) + + // One user just got blocked and the other one is defined in the fixtures. + assert.Equal(t, "2", resp.Header().Get("X-Total-Count")) + + var blockedUsers []api.BlockedUser + DecodeJSON(t, resp, &blockedUsers) + assert.Len(t, blockedUsers, 2) + assert.EqualValues(t, 1, blockedUsers[0].BlockID) + assert.EqualValues(t, 2, blockedUsers[1].BlockID) + }) + + t.Run("UnblockUser", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/unblock/user2?token=%s", token)) + MakeRequest(t, req, http.StatusNoContent) + + unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 4, BlockID: 2}) + }) +} + +func TestAPIOrgBlock(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user := "user5" + org := "user6" + session := loginUser(t, user) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) + + t.Run("BlockUser", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/block/user2?token=%s", org, token)) + MakeRequest(t, req, http.StatusNoContent) + + unittest.AssertExistsAndLoadBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2}) + }) + + t.Run("ListBlocked", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/list_blocked?token=%s", org, token)) + resp := MakeRequest(t, req, http.StatusOK) + + assert.Equal(t, "1", resp.Header().Get("X-Total-Count")) + + var blockedUsers []api.BlockedUser + DecodeJSON(t, resp, &blockedUsers) + assert.Len(t, blockedUsers, 1) + assert.EqualValues(t, 2, blockedUsers[0].BlockID) + }) + + t.Run("UnblockUser", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/orgs/%s/unblock/user2?token=%s", org, token)) + MakeRequest(t, req, http.StatusNoContent) + + unittest.AssertNotExistsBean(t, &user_model.BlockedUser{UserID: 6, BlockID: 2}) + }) +}