From d0fc8bc9d3b6bb189a2ab634a5329253af9b4629 Mon Sep 17 00:00:00 2001
From: Gusted <postmaster@gusted.xyz>
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)
---
 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 37361a8b96..6fa20f9f8a 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 314116962b..7b796a35b0 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 6adf40420b..3a236e1a77 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": [
@@ -13787,6 +13897,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": [
@@ -14436,6 +14575,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": [
@@ -14837,6 +15007,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": [
@@ -15767,6 +15966,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",
@@ -21950,6 +22166,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})
+	})
+}