From 3aae5231b2c8f669eadba9228cece254349dd2aa Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 2 Mar 2021 20:49:17 +0400 Subject: [PATCH 1/2] Add OpenAPI spec for AdminAPI.UserController --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 7 - .../admin_api/controllers/user_controller.ex | 128 +++--- .../web/admin_api/views/account_view.ex | 19 +- .../operations/admin/user_operation.ex | 389 ++++++++++++++++++ lib/pleroma/web/router.ex | 2 +- .../controllers/user_controller_test.exs | 118 +++--- 7 files changed, 539 insertions(+), 125 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/admin/user_operation.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 812816f48..78f21e69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
API Changes - Admin API: (`GET /api/pleroma/admin/users`) filter users by `unconfirmed` status and `actor_type`. +- Admin API: OpenAPI spec for the user-related operations - Pleroma API: `GET /api/v2/pleroma/chats` added. It is exactly like `GET /api/v1/pleroma/chats` except supports pagination. - Pleroma API: Add `idempotency_key` to the chat message entity that can be used for optimistic message sending. - Pleroma API: (`GET /api/v1/pleroma/federation_status`) Add a way to get a list of unreachable instances. diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 9942617d8..c1aa0f716 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -2255,13 +2255,6 @@ def update_background(user, background) do |> update_and_set_cache() end - def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do - %{ - admin: is_admin, - moderator: is_moderator - } - end - def validate_fields(changeset, remote? \\ false) do limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit = Config.get([:instance, limit_name], 0) diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex index 65bc63cb9..d3e4c18a3 100644 --- a/lib/pleroma/web/admin_api/controllers/user_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/user_controller.ex @@ -13,16 +13,17 @@ defmodule Pleroma.Web.AdminAPI.UserController do alias Pleroma.Web.ActivityPub.Builder alias Pleroma.Web.ActivityPub.Pipeline alias Pleroma.Web.AdminAPI - alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.Plugs.OAuthScopesPlug @users_page_size 50 + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( OAuthScopesPlug, %{scopes: ["admin:read:accounts"]} - when action in [:list, :show] + when action in [:index, :show] ) plug( @@ -44,13 +45,19 @@ defmodule Pleroma.Web.AdminAPI.UserController do when action in [:follow, :unfollow] ) + plug(:put_view, Pleroma.Web.AdminAPI.AccountView) + action_fallback(AdminAPI.FallbackController) - def delete(conn, %{"nickname" => nickname}) do - delete(conn, %{"nicknames" => [nickname]}) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation + + def delete(conn, %{nickname: nickname}) do + conn + |> Map.put(:body_params, %{nicknames: [nickname]}) + |> delete(%{}) end - def delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) Enum.each(users, fn user -> @@ -67,10 +74,16 @@ def delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do json(conn, nicknames) end - def follow(%{assigns: %{user: admin}} = conn, %{ - "follower" => follower_nick, - "followed" => followed_nick - }) do + def follow( + %{ + assigns: %{user: admin}, + body_params: %{ + follower: follower_nick, + followed: followed_nick + } + } = conn, + _ + ) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.follow(follower, followed) @@ -86,10 +99,16 @@ def follow(%{assigns: %{user: admin}} = conn, %{ json(conn, "ok") end - def unfollow(%{assigns: %{user: admin}} = conn, %{ - "follower" => follower_nick, - "followed" => followed_nick - }) do + def unfollow( + %{ + assigns: %{user: admin}, + body_params: %{ + follower: follower_nick, + followed: followed_nick + } + } = conn, + _ + ) do with %User{} = follower <- User.get_cached_by_nickname(follower_nick), %User{} = followed <- User.get_cached_by_nickname(followed_nick) do User.unfollow(follower, followed) @@ -105,9 +124,10 @@ def unfollow(%{assigns: %{user: admin}} = conn, %{ json(conn, "ok") end - def create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do + def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do changesets = - Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} -> + users + |> Enum.map(fn %{nickname: nickname, email: email, password: password} -> user_data = %{ nickname: nickname, name: nickname, @@ -124,52 +144,49 @@ def create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do end) case Pleroma.Repo.transaction(changesets) do - {:ok, users} -> - res = - users + {:ok, users_map} -> + users = + users_map |> Map.values() |> Enum.map(fn user -> {:ok, user} = User.post_register_action(user) user end) - |> Enum.map(&AccountView.render("created.json", %{user: &1})) ModerationLog.insert_log(%{ actor: admin, - subjects: Map.values(users), + subjects: users, action: "create" }) - json(conn, res) + render(conn, "created_many.json", users: users) {:error, id, changeset, _} -> - res = + changesets = Enum.map(changesets.operations, fn - {current_id, {:changeset, _current_changeset, _}} when current_id == id -> - AccountView.render("create-error.json", %{changeset: changeset}) + {^id, {:changeset, _current_changeset, _}} -> + changeset {_, {:changeset, current_changeset, _}} -> - AccountView.render("create-error.json", %{changeset: current_changeset}) + current_changeset end) conn |> put_status(:conflict) - |> json(res) + |> render("create_errors.json", changesets: changesets) end end - def show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do - conn - |> put_view(AccountView) - |> render("show.json", %{user: user}) + render(conn, "show.json", %{user: user}) else _ -> {:error, :not_found} end end - def toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do + def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do user = User.get_cached_by_nickname(nickname) {:ok, updated_user} = User.set_activation(user, !user.is_active) @@ -182,12 +199,10 @@ def toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nicknam action: action }) - conn - |> put_view(AccountView) - |> render("show.json", %{user: updated_user}) + render(conn, "show.json", user: updated_user) end - def activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) {:ok, updated_users} = User.set_activation(users, true) @@ -197,12 +212,10 @@ def activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do action: "activate" }) - conn - |> put_view(AccountView) - |> render("index.json", %{users: Keyword.values(updated_users)}) + render(conn, "index.json", users: Keyword.values(updated_users)) end - def deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) {:ok, updated_users} = User.set_activation(users, false) @@ -212,12 +225,10 @@ def deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) d action: "deactivate" }) - conn - |> put_view(AccountView) - |> render("index.json", %{users: Keyword.values(updated_users)}) + render(conn, "index.json", users: Keyword.values(updated_users)) end - def approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do + def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do users = Enum.map(nicknames, &User.get_cached_by_nickname/1) {:ok, updated_users} = User.approve(users) @@ -227,36 +238,27 @@ def approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do action: "approve" }) - conn - |> put_view(AccountView) - |> render("index.json", %{users: updated_users}) + render(conn, "index.json", users: updated_users) end - def list(conn, params) do + def index(conn, params) do {page, page_size} = page_params(params) - filters = maybe_parse_filters(params["filters"]) + filters = maybe_parse_filters(params[:filters]) search_params = %{ - query: params["query"], + query: params[:query], page: page, page_size: page_size, - tags: params["tags"], - name: params["name"], - email: params["email"], - actor_types: params["actor_types"] + tags: params[:tags], + name: params[:name], + email: params[:email], + actor_types: params[:actor_types] } |> Map.merge(filters) with {:ok, users, count} <- Search.user(search_params) do - json( - conn, - AccountView.render("index.json", - users: users, - count: count, - page_size: page_size - ) - ) + render(conn, "index.json", users: users, count: count, page_size: page_size) end end @@ -274,8 +276,8 @@ defp maybe_parse_filters(filters) do defp page_params(params) do { - fetch_integer_param(params, "page", 1), - fetch_integer_param(params, "page_size", @users_page_size) + fetch_integer_param(params, :page, 1), + fetch_integer_param(params, :page_size, @users_page_size) } end end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index d7c63d385..e053a9b67 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -75,7 +75,7 @@ def render("show.json", %{user: user}) do "display_name" => display_name, "is_active" => user.is_active, "local" => user.local, - "roles" => User.roles(user), + "roles" => roles(user), "tags" => user.tags || [], "is_confirmed" => user.is_confirmed, "is_approved" => user.is_approved, @@ -85,6 +85,10 @@ def render("show.json", %{user: user}) do } end + def render("created_many.json", %{users: users}) do + render_many(users, AccountView, "created.json", as: :user) + end + def render("created.json", %{user: user}) do %{ type: "success", @@ -96,7 +100,11 @@ def render("created.json", %{user: user}) do } end - def render("create-error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do + def render("create_errors.json", %{changesets: changesets}) do + render_many(changesets, AccountView, "create_error.json", as: :changeset) + end + + def render("create_error.json", %{changeset: %Ecto.Changeset{changes: changes, errors: errors}}) do %{ type: "error", code: 409, @@ -140,4 +148,11 @@ defp parse_error(errors) do defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(_), do: nil + + defp roles(%{is_moderator: is_moderator, is_admin: is_admin}) do + %{ + admin: is_admin, + moderator: is_moderator + } + end end diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex new file mode 100644 index 000000000..183c61236 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex @@ -0,0 +1,389 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Admin.UserOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ActorType + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Users"], + summary: "List users", + operationId: "AdminAPI.UserController.index", + security: [%{"oAuth" => ["admin:read:accounts"]}], + parameters: [ + Operation.parameter(:filters, :query, :string, "Comma separated list of filters"), + Operation.parameter(:query, :query, :string, "Search users query"), + Operation.parameter(:name, :query, :string, "Search by display name"), + Operation.parameter(:email, :query, :string, "Search by email"), + Operation.parameter(:page, :query, :integer, "Page Number"), + Operation.parameter(:page_size, :query, :integer, "Number of users to return per page"), + Operation.parameter( + :actor_types, + :query, + %Schema{type: :array, items: ActorType}, + "Filter by actor type" + ), + Operation.parameter( + :tags, + :query, + %Schema{type: :array, items: %Schema{type: :string}}, + "Filter by tags" + ) + | admin_api_params() + ], + responses: %{ + 200 => + Operation.response( + "Response", + "application/json", + %Schema{ + type: :object, + properties: %{ + users: %Schema{type: :array, items: user()}, + count: %Schema{type: :integer}, + page_size: %Schema{type: :integer} + } + } + ), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def create_operation do + %Operation{ + tags: ["Users"], + summary: "Create a single or multiple users", + operationId: "AdminAPI.UserController.create", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for creating users", + type: :object, + properties: %{ + users: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + nickname: %Schema{type: :string}, + email: %Schema{type: :string}, + password: %Schema{type: :string} + } + } + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + code: %Schema{type: :integer}, + type: %Schema{type: :string}, + data: %Schema{ + type: :object, + properties: %{ + email: %Schema{type: :string, format: :email}, + nickname: %Schema{type: :string} + } + } + } + } + }), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 409 => + Operation.response("Conflict", "application/json", %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + code: %Schema{type: :integer}, + error: %Schema{type: :string}, + type: %Schema{type: :string}, + data: %Schema{ + type: :object, + properties: %{ + email: %Schema{type: :string, format: :email}, + nickname: %Schema{type: :string} + } + } + } + } + }) + } + } + end + + def show_operation do + %Operation{ + tags: ["Users"], + summary: "Show user", + operationId: "AdminAPI.UserController.show", + security: [%{"oAuth" => ["admin:read:accounts"]}], + parameters: [ + Operation.parameter( + :nickname, + :path, + :string, + "User nickname or ID" + ) + | admin_api_params() + ], + responses: %{ + 200 => Operation.response("Response", "application/json", user()), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def follow_operation do + %Operation{ + tags: ["Users"], + summary: "Follow", + operationId: "AdminAPI.UserController.follow", + security: [%{"oAuth" => ["admin:write:follows"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + follower: %Schema{type: :string, description: "Follower nickname"}, + followed: %Schema{type: :string, description: "Followed nickname"} + } + } + ), + responses: %{ + 200 => Operation.response("Response", "application/json", %Schema{type: :string}), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def unfollow_operation do + %Operation{ + tags: ["Users"], + summary: "Unfollow", + operationId: "AdminAPI.UserController.unfollow", + security: [%{"oAuth" => ["admin:write:follows"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + follower: %Schema{type: :string, description: "Follower nickname"}, + followed: %Schema{type: :string, description: "Followed nickname"} + } + } + ), + responses: %{ + 200 => Operation.response("Response", "application/json", %Schema{type: :string}), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def approve_operation do + %Operation{ + tags: ["Users"], + summary: "Approve multiple users", + operationId: "AdminAPI.UserController.approve", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for deleting multiple users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{user: %Schema{type: :array, items: user()}} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def toggle_activation_operation do + %Operation{ + tags: ["Users"], + summary: "Toggle user activation", + operationId: "AdminAPI.UserController.toggle_activation", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: [ + Operation.parameter(:nickname, :path, :string, "User nickname") + | admin_api_params() + ], + responses: %{ + 200 => Operation.response("Response", "application/json", user()), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def activate_operation do + %Operation{ + tags: ["Users"], + summary: "Activate multiple users", + operationId: "AdminAPI.UserController.activate", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for deleting multiple users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{user: %Schema{type: :array, items: user()}} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def deactivate_operation do + %Operation{ + tags: ["Users"], + summary: "Deactivates multiple users", + operationId: "AdminAPI.UserController.deactivate", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: admin_api_params(), + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for deleting multiple users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + type: :object, + properties: %{user: %Schema{type: :array, items: user()}} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Users"], + summary: "Removes a single or multiple users", + operationId: "AdminAPI.UserController.delete", + security: [%{"oAuth" => ["admin:write:accounts"]}], + parameters: [ + Operation.parameter( + :nickname, + :query, + :string, + "User nickname" + ) + | admin_api_params() + ], + requestBody: + request_body( + "Parameters", + %Schema{ + description: "POST body for deleting multiple users", + type: :object, + properties: %{ + nicknames: %Schema{ + type: :array, + items: %Schema{type: :string} + } + } + } + ), + responses: %{ + 200 => + Operation.response("Response", "application/json", %Schema{ + description: "Array of nicknames", + type: :array, + items: %Schema{type: :string} + }), + 403 => Operation.response("Forbidden", "application/json", ApiError) + } + } + end + + defp user do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + email: %Schema{type: :string, format: :email}, + avatar: %Schema{type: :string, format: :uri}, + nickname: %Schema{type: :string}, + display_name: %Schema{type: :string}, + is_active: %Schema{type: :boolean}, + local: %Schema{type: :boolean}, + roles: %Schema{ + type: :object, + properties: %{ + admin: %Schema{type: :boolean}, + moderator: %Schema{type: :boolean} + } + }, + tags: %Schema{type: :array, items: %Schema{type: :string}}, + is_confirmed: %Schema{type: :boolean}, + is_approved: %Schema{type: :boolean}, + url: %Schema{type: :string, format: :uri}, + registration_reason: %Schema{type: :string, nullable: true}, + actor_type: %Schema{type: :string} + } + } + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 72ad14f05..de0bd27d7 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -204,7 +204,7 @@ defmodule Pleroma.Web.Router do get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials) patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) - get("/users", UserController, :list) + get("/users", UserController, :index) get("/users/:nickname", UserController, :show) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/chats", AdminAPIController, :list_user_chats) diff --git a/test/pleroma/web/admin_api/controllers/user_controller_test.exs b/test/pleroma/web/admin_api/controllers/user_controller_test.exs index beb8a5d58..31319b5e5 100644 --- a/test/pleroma/web/admin_api/controllers/user_controller_test.exs +++ b/test/pleroma/web/admin_api/controllers/user_controller_test.exs @@ -44,7 +44,7 @@ test "with valid `admin_token` query parameter, skips OAuth scopes check" do conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123") - assert json_response(conn, 200) + assert json_response_and_validate_schema(conn, 200) end test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", @@ -67,7 +67,7 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro |> assign(:token, good_token) |> get(url) - assert json_response(conn, 200) + assert json_response_and_validate_schema(conn, 200) end for good_token <- [good_token1, good_token2, good_token3] do @@ -87,7 +87,7 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro |> assign(:token, bad_token) |> get(url) - assert json_response(conn, :forbidden) + assert json_response_and_validate_schema(conn, :forbidden) end end @@ -131,7 +131,7 @@ test "single user", %{admin: admin, conn: conn} do assert ModerationLog.get_log_entry_message(log_entry) == "@#{admin.nickname} deleted users: @#{user.nickname}" - assert json_response(conn, 200) == [user.nickname] + assert json_response_and_validate_schema(conn, 200) == [user.nickname] user = Repo.get(User, user.id) refute user.is_active @@ -152,28 +152,30 @@ test "multiple users", %{admin: admin, conn: conn} do user_one = insert(:user) user_two = insert(:user) - conn = + response = conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> delete("/api/pleroma/admin/users", %{ nicknames: [user_one.nickname, user_two.nickname] }) + |> json_response_and_validate_schema(200) log_entry = Repo.one(ModerationLog) assert ModerationLog.get_log_entry_message(log_entry) == "@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}" - response = json_response(conn, 200) assert response -- [user_one.nickname, user_two.nickname] == [] end end describe "/api/pleroma/admin/users" do test "Create", %{conn: conn} do - conn = + response = conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ %{ @@ -188,8 +190,9 @@ test "Create", %{conn: conn} do } ] }) + |> json_response_and_validate_schema(200) + |> Enum.map(&Map.get(&1, "type")) - response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type")) assert response == ["success", "success"] log_entry = Repo.one(ModerationLog) @@ -203,6 +206,7 @@ test "Cannot create user with existing email", %{conn: conn} do conn = conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ %{ @@ -213,7 +217,7 @@ test "Cannot create user with existing email", %{conn: conn} do ] }) - assert json_response(conn, 409) == [ + assert json_response_and_validate_schema(conn, 409) == [ %{ "code" => 409, "data" => %{ @@ -232,6 +236,7 @@ test "Cannot create user with existing nickname", %{conn: conn} do conn = conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ %{ @@ -242,7 +247,7 @@ test "Cannot create user with existing nickname", %{conn: conn} do ] }) - assert json_response(conn, 409) == [ + assert json_response_and_validate_schema(conn, 409) == [ %{ "code" => 409, "data" => %{ @@ -261,6 +266,7 @@ test "Multiple user creation works in transaction", %{conn: conn} do conn = conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ %{ @@ -276,7 +282,7 @@ test "Multiple user creation works in transaction", %{conn: conn} do ] }) - assert json_response(conn, 409) == [ + assert json_response_and_validate_schema(conn, 409) == [ %{ "code" => 409, "data" => %{ @@ -307,7 +313,7 @@ test "Show", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - assert user_response(user) == json_response(conn, 200) + assert user_response(user) == json_response_and_validate_schema(conn, 200) end test "when the user doesn't exist", %{conn: conn} do @@ -315,7 +321,7 @@ test "when the user doesn't exist", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") - assert %{"error" => "Not found"} == json_response(conn, 404) + assert %{"error" => "Not found"} == json_response_and_validate_schema(conn, 404) end end @@ -326,6 +332,7 @@ test "allows to force-follow another user", %{admin: admin, conn: conn} do conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users/follow", %{ "follower" => follower.nickname, "followed" => user.nickname @@ -352,6 +359,7 @@ test "allows to force-unfollow another user", %{admin: admin, conn: conn} do conn |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") |> post("/api/pleroma/admin/users/unfollow", %{ "follower" => follower.nickname, "followed" => user.nickname @@ -395,7 +403,7 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do ] |> Enum.sort_by(& &1["nickname"]) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 3, "page_size" => 50, "users" => users @@ -410,7 +418,7 @@ test "pagination works correctly with service users", %{conn: conn} do assert %{"count" => 26, "page_size" => 10, "users" => users1} = conn |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert Enum.count(users1) == 10 assert service1 not in users1 @@ -418,7 +426,7 @@ test "pagination works correctly with service users", %{conn: conn} do assert %{"count" => 26, "page_size" => 10, "users" => users2} = conn |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert Enum.count(users2) == 10 assert service1 not in users2 @@ -426,7 +434,7 @@ test "pagination works correctly with service users", %{conn: conn} do assert %{"count" => 26, "page_size" => 10, "users" => users3} = conn |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert Enum.count(users3) == 6 assert service1 not in users3 @@ -437,7 +445,7 @@ test "renders empty array for the second page", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?page=2") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 2, "page_size" => 50, "users" => [] @@ -449,7 +457,7 @@ test "regular search", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?query=bo") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user, %{"local" => true})] @@ -462,7 +470,7 @@ test "search by domain", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?query=domain.com") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -475,7 +483,7 @@ test "search by full nickname", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -488,7 +496,7 @@ test "search by display name", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?name=display") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -501,7 +509,7 @@ test "search by email", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -514,7 +522,7 @@ test "regular search with page size", %{conn: conn} do conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - assert json_response(conn1, 200) == %{ + assert json_response_and_validate_schema(conn1, 200) == %{ "count" => 2, "page_size" => 1, "users" => [user_response(user)] @@ -522,7 +530,7 @@ test "regular search with page size", %{conn: conn} do conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - assert json_response(conn2, 200) == %{ + assert json_response_and_validate_schema(conn2, 200) == %{ "count" => 2, "page_size" => 1, "users" => [user_response(user2)] @@ -542,7 +550,7 @@ test "only local users" do |> assign(:token, token) |> get("/api/pleroma/admin/users?query=bo&filters=local") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -570,7 +578,7 @@ test "only local users with no query", %{conn: conn, admin: old_admin} do ] |> Enum.sort_by(& &1["nickname"]) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 3, "page_size" => 50, "users" => users @@ -587,7 +595,7 @@ test "only unconfirmed users", %{conn: conn} do result = conn |> get("/api/pleroma/admin/users?filters=unconfirmed") - |> json_response(200) + |> json_response_and_validate_schema(200) users = Enum.map([old_user, sad_user], fn user -> @@ -620,7 +628,7 @@ test "only unapproved users", %{conn: conn} do ) ] - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => users @@ -647,7 +655,7 @@ test "load only admins", %{conn: conn, admin: admin} do ] |> Enum.sort_by(& &1["nickname"]) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 2, "page_size" => 50, "users" => users @@ -661,7 +669,7 @@ test "load only moderators", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [ @@ -682,8 +690,8 @@ test "load users with actor_type is Person", %{admin: admin, conn: conn} do response = conn - |> get(user_path(conn, :list), %{actor_types: ["Person"]}) - |> json_response(200) + |> get(user_path(conn, :index), %{actor_types: ["Person"]}) + |> json_response_and_validate_schema(200) users = [ @@ -705,8 +713,8 @@ test "load users with actor_type is Person and Service", %{admin: admin, conn: c response = conn - |> get(user_path(conn, :list), %{actor_types: ["Person", "Service"]}) - |> json_response(200) + |> get(user_path(conn, :index), %{actor_types: ["Person", "Service"]}) + |> json_response_and_validate_schema(200) users = [ @@ -728,8 +736,8 @@ test "load users with actor_type is Service", %{conn: conn} do response = conn - |> get(user_path(conn, :list), %{actor_types: ["Service"]}) - |> json_response(200) + |> get(user_path(conn, :index), %{actor_types: ["Service"]}) + |> json_response_and_validate_schema(200) users = [user_response(user_service, %{"actor_type" => "Service"})] @@ -751,7 +759,7 @@ test "load users with tags list", %{conn: conn} do ] |> Enum.sort_by(& &1["nickname"]) - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 2, "page_size" => 50, "users" => users @@ -776,7 +784,7 @@ test "`active` filters out users pending approval", %{token: token} do %{"id" => ^admin_id}, %{"id" => ^user_id} ] - } = json_response(conn, 200) + } = json_response_and_validate_schema(conn, 200) end test "it works with multiple filters" do @@ -793,7 +801,7 @@ test "it works with multiple filters" do |> assign(:token, token) |> get("/api/pleroma/admin/users?filters=deactivated,external") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [user_response(user)] @@ -805,7 +813,7 @@ test "it omits relay user", %{admin: admin, conn: conn} do conn = get(conn, "/api/pleroma/admin/users") - assert json_response(conn, 200) == %{ + assert json_response_and_validate_schema(conn, 200) == %{ "count" => 1, "page_size" => 50, "users" => [ @@ -820,13 +828,14 @@ test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do user_two = insert(:user, is_active: false) conn = - patch( - conn, + conn + |> put_req_header("content-type", "application/json") + |> patch( "/api/pleroma/admin/users/activate", %{nicknames: [user_one.nickname, user_two.nickname]} ) - response = json_response(conn, 200) + response = json_response_and_validate_schema(conn, 200) assert Enum.map(response["users"], & &1["is_active"]) == [true, true] log_entry = Repo.one(ModerationLog) @@ -840,13 +849,14 @@ test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do user_two = insert(:user, is_active: true) conn = - patch( - conn, + conn + |> put_req_header("content-type", "application/json") + |> patch( "/api/pleroma/admin/users/deactivate", %{nicknames: [user_one.nickname, user_two.nickname]} ) - response = json_response(conn, 200) + response = json_response_and_validate_schema(conn, 200) assert Enum.map(response["users"], & &1["is_active"]) == [false, false] log_entry = Repo.one(ModerationLog) @@ -860,13 +870,14 @@ test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do user_two = insert(:user, is_approved: false) conn = - patch( - conn, + conn + |> put_req_header("content-type", "application/json") + |> patch( "/api/pleroma/admin/users/approve", %{nicknames: [user_one.nickname, user_two.nickname]} ) - response = json_response(conn, 200) + response = json_response_and_validate_schema(conn, 200) assert Enum.map(response["users"], & &1["is_approved"]) == [true, true] log_entry = Repo.one(ModerationLog) @@ -878,9 +889,12 @@ test "PATCH /api/pleroma/admin/users/approve", %{admin: admin, conn: conn} do test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do user = insert(:user) - conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + conn = + conn + |> put_req_header("content-type", "application/json") + |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation") - assert json_response(conn, 200) == + assert json_response_and_validate_schema(conn, 200) == user_response( user, %{"is_active" => !user.is_active} From 9876fa8e902e66a77193ebeef674a9f0e9f37657 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 4 Mar 2021 21:13:53 +0400 Subject: [PATCH 2/2] Add UserOperation to Redoc --- lib/pleroma/web/api_spec.ex | 5 +++-- .../operations/admin/user_operation.ex | 20 +++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index adc8762dc..528cd9cf4 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -92,9 +92,10 @@ def spec(opts \\ []) do "Invites", "MediaProxy cache", "OAuth application managment", - "Report managment", "Relays", - "Status administration" + "Report managment", + "Status administration", + "User administration" ] }, %{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]}, diff --git a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex index 183c61236..c9d0bfd7c 100644 --- a/lib/pleroma/web/api_spec/operations/admin/user_operation.ex +++ b/lib/pleroma/web/api_spec/operations/admin/user_operation.ex @@ -17,7 +17,7 @@ def open_api_operation(action) do def index_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "List users", operationId: "AdminAPI.UserController.index", security: [%{"oAuth" => ["admin:read:accounts"]}], @@ -63,7 +63,7 @@ def index_operation do def create_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Create a single or multiple users", operationId: "AdminAPI.UserController.create", security: [%{"oAuth" => ["admin:write:accounts"]}], @@ -134,7 +134,7 @@ def create_operation do def show_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Show user", operationId: "AdminAPI.UserController.show", security: [%{"oAuth" => ["admin:read:accounts"]}], @@ -157,7 +157,7 @@ def show_operation do def follow_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Follow", operationId: "AdminAPI.UserController.follow", security: [%{"oAuth" => ["admin:write:follows"]}], @@ -182,7 +182,7 @@ def follow_operation do def unfollow_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Unfollow", operationId: "AdminAPI.UserController.unfollow", security: [%{"oAuth" => ["admin:write:follows"]}], @@ -207,7 +207,7 @@ def unfollow_operation do def approve_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Approve multiple users", operationId: "AdminAPI.UserController.approve", security: [%{"oAuth" => ["admin:write:accounts"]}], @@ -239,7 +239,7 @@ def approve_operation do def toggle_activation_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Toggle user activation", operationId: "AdminAPI.UserController.toggle_activation", security: [%{"oAuth" => ["admin:write:accounts"]}], @@ -256,7 +256,7 @@ def toggle_activation_operation do def activate_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Activate multiple users", operationId: "AdminAPI.UserController.activate", security: [%{"oAuth" => ["admin:write:accounts"]}], @@ -288,7 +288,7 @@ def activate_operation do def deactivate_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Deactivates multiple users", operationId: "AdminAPI.UserController.deactivate", security: [%{"oAuth" => ["admin:write:accounts"]}], @@ -320,7 +320,7 @@ def deactivate_operation do def delete_operation do %Operation{ - tags: ["Users"], + tags: ["User administration"], summary: "Removes a single or multiple users", operationId: "AdminAPI.UserController.delete", security: [%{"oAuth" => ["admin:write:accounts"]}],