Add OpenAPI
authorEgor Kislitsyn <egor@kislitsyn.com>
Wed, 1 Apr 2020 19:00:59 +0000 (23:00 +0400)
committerEgor Kislitsyn <egor@kislitsyn.com>
Wed, 1 Apr 2020 19:13:08 +0000 (23:13 +0400)
12 files changed:
lib/pleroma/web/api_spec.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/app_operation.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/app_create_request.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/schemas/app_create_response.ex [new file with mode: 0644]
lib/pleroma/web/mastodon_api/controllers/app_controller.ex
lib/pleroma/web/oauth/scopes.ex
lib/pleroma/web/router.ex
mix.exs
mix.lock
test/web/api_spec/app_operation_test.exs [new file with mode: 0644]
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/mastodon_api/controllers/app_controller_test.exs

diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
new file mode 100644 (file)
index 0000000..22f76d4
--- /dev/null
@@ -0,0 +1,30 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec do
+  alias OpenApiSpex.OpenApi
+  alias Pleroma.Web.Endpoint
+  alias Pleroma.Web.Router
+
+  @behaviour OpenApi
+
+  @impl OpenApi
+  def spec do
+    %OpenApi{
+      servers: [
+        # Populate the Server info from a phoenix endpoint
+        OpenApiSpex.Server.from_endpoint(Endpoint)
+      ],
+      info: %OpenApiSpex.Info{
+        title: "Pleroma",
+        description: Application.spec(:pleroma, :description) |> to_string(),
+        version: Application.spec(:pleroma, :vsn) |> to_string()
+      },
+      # populate the paths from a phoenix router
+      paths: OpenApiSpex.Paths.from_router(Router)
+    }
+    # discover request/response schemas from path specs
+    |> OpenApiSpex.resolve_schema_modules()
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/app_operation.ex b/lib/pleroma/web/api_spec/operations/app_operation.ex
new file mode 100644 (file)
index 0000000..2a4958a
--- /dev/null
@@ -0,0 +1,94 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AppOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
+
+  @spec open_api_operation(atom) :: Operation.t()
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  @spec create_operation() :: Operation.t()
+  def create_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Create an application",
+      description: "Create a new application to obtain OAuth2 credentials",
+      operationId: "AppController.create",
+      requestBody:
+        Operation.request_body("Parameters", "application/json", AppCreateRequest, required: true),
+      responses: %{
+        200 => Operation.response("App", "application/json", AppCreateResponse),
+        422 =>
+          Operation.response(
+            "Unprocessable Entity",
+            "application/json",
+            %Schema{
+              type: :object,
+              description:
+                "If a required parameter is missing or improperly formatted, the request will fail.",
+              properties: %{
+                error: %Schema{type: :string}
+              },
+              example: %{
+                "error" => "Validation failed: Redirect URI must be an absolute URI."
+              }
+            }
+          )
+      }
+    }
+  end
+
+  def verify_credentials_operation do
+    %Operation{
+      tags: ["apps"],
+      summary: "Verify your app works",
+      description: "Confirm that the app's OAuth2 credentials work.",
+      operationId: "AppController.verify_credentials",
+      parameters: [
+        Operation.parameter(:authorization, :header, :string, "Bearer <app token>", required: true)
+      ],
+      responses: %{
+        200 =>
+          Operation.response("App", "application/json", %Schema{
+            type: :object,
+            description:
+              "If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
+            properties: %{
+              name: %Schema{type: :string},
+              vapid_key: %Schema{type: :string},
+              website: %Schema{type: :string, nullable: true}
+            },
+            example: %{
+              "name" => "My App",
+              "vapid_key" =>
+                "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+              "website" => "https://myapp.com/"
+            }
+          }),
+        422 =>
+          Operation.response(
+            "Unauthorized",
+            "application/json",
+            %Schema{
+              type: :object,
+              description:
+                "If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
+              properties: %{
+                error: %Schema{type: :string}
+              },
+              example: %{
+                "error" => "The access token is invalid."
+              }
+            }
+          )
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_request.ex b/lib/pleroma/web/api_spec/schemas/app_create_request.ex
new file mode 100644 (file)
index 0000000..8a83abe
--- /dev/null
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
+  alias OpenApiSpex.Schema
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "AppCreateRequest",
+    description: "POST body for creating an app",
+    type: :object,
+    properties: %{
+      client_name: %Schema{type: :string, description: "A name for your application."},
+      redirect_uris: %Schema{
+        type: :string,
+        description:
+          "Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
+      },
+      scopes: %Schema{
+        type: :string,
+        description: "Space separated list of scopes. If none is provided, defaults to `read`."
+      },
+      website: %Schema{type: :string, description: "A URL to the homepage of your app"}
+    },
+    required: [:client_name, :redirect_uris],
+    example: %{
+      "client_name" => "My App",
+      "redirect_uris" => "https://myapp.com/auth/callback",
+      "website" => "https://myapp.com/"
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/app_create_response.ex b/lib/pleroma/web/api_spec/schemas/app_create_response.ex
new file mode 100644 (file)
index 0000000..f290fb0
--- /dev/null
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "AppCreateResponse",
+    description: "Response schema for an app",
+    type: :object,
+    properties: %{
+      id: %Schema{type: :string},
+      name: %Schema{type: :string},
+      client_id: %Schema{type: :string},
+      client_secret: %Schema{type: :string},
+      redirect_uri: %Schema{type: :string},
+      vapid_key: %Schema{type: :string},
+      website: %Schema{type: :string, nullable: true}
+    },
+    example: %{
+      "id" => "123",
+      "name" => "My App",
+      "client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
+      "client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
+      "vapid_key" =>
+        "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
+      "website" => "https://myapp.com/"
+    }
+  })
+end
index 5e2871f185ea7fb2d0248141dee4ef10e01cf811..005c604447e3999cf756ec2dd1b842930d46df5e 100644 (file)
@@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
   plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
+  plug(OpenApiSpex.Plug.CastAndValidate)
 
   @local_mastodon_name "Mastodon-Local"
 
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
+
   @doc "POST /api/v1/apps"
-  def create(conn, params) do
+  def create(%{body_params: params} = conn, _params) do
     scopes = Scopes.fetch_scopes(params, ["read"])
 
     app_attrs =
       params
-      |> Map.drop(["scope", "scopes"])
-      |> Map.put("scopes", scopes)
+      |> Map.take([:client_name, :redirect_uris, :website])
+      |> Map.put(:scopes, scopes)
 
     with cs <- App.register_changeset(%App{}, app_attrs),
          false <- cs.changes[:client_name] == @local_mastodon_name,
index 8ecf901f3085112f008a8c3142c7d61637f241e4..1023f16d4911cb0fc3c4b8334d1b6cf9cd742300 100644 (file)
@@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do
   Note: `scopes` is used by Mastodon — supporting it but sticking to
   OAuth's standard `scope` wherever we control it
   """
-  @spec fetch_scopes(map(), list()) :: list()
+  @spec fetch_scopes(map() | struct(), list()) :: list()
+
+  def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
+    parse_scopes(scopes, default)
+  end
+
   def fetch_scopes(params, default) do
     parse_scopes(params["scope"] || params["scopes"], default)
   end
index 5a09027391f6ab01b62553e192c8b1a1dbf627b8..3ecd59cd1a67d19eb3d7f588a662e4f456bffd69 100644 (file)
@@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.SetUserSessionIdPlug)
     plug(Pleroma.Plugs.EnsureUserKeyPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :authenticated_api do
@@ -44,6 +45,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.SetUserSessionIdPlug)
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :admin_api do
@@ -61,6 +63,7 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.UserIsAdminPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :mastodon_html do
@@ -94,10 +97,12 @@ defmodule Pleroma.Web.Router do
 
   pipeline :config do
     plug(:accepts, ["json", "xml"])
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :pleroma_api do
     plug(:accepts, ["html", "json"])
+    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :mailbox_preview do
@@ -500,6 +505,12 @@ defmodule Pleroma.Web.Router do
     )
   end
 
+  scope "/api" do
+    pipe_through(:api)
+
+    get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
+  end
+
   scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
     pipe_through(:authenticated_api)
 
diff --git a/mix.exs b/mix.exs
index 890979f8b9d1a8f41aab1c1679f35f294f127263..ebd4a5ea60e67bb6781cc18059f66e54288d442e 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -171,7 +171,8 @@ defmodule Pleroma.Mixfile do
        git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
        ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
       {:mox, "~> 0.5", only: :test},
-      {:restarter, path: "./restarter"}
+      {:restarter, path: "./restarter"},
+      {:open_api_spex, "~> 3.6"}
     ] ++ oauth_deps()
   end
 
index 62e14924a648604d46120a3c4e78fe5582b24c58..fd26ca01b5d12077b0f8be7b511341526a48ab7f 100644 (file)
--- a/mix.lock
+++ b/mix.lock
@@ -72,6 +72,7 @@
   "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
   "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
   "oban": {:hex, :oban, "0.12.1", "695e9490c6e0edfca616d80639528e448bd29b3bff7b7dd10a56c79b00a5d7fb", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1d58d69b8b5a86e7167abbb8cc92764a66f25f12f6172052595067fc6a30a17"},
+  "open_api_spex": {:hex, :open_api_spex, "3.6.0", "64205aba9f2607f71b08fd43e3351b9c5e9898ec5ef49fc0ae35890da502ade9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "126ba3473966277132079cb1d5bf1e3df9e36fe2acd00166e75fd125cecb59c5"},
   "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
   "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm", "595d09db74cb093b1903381c9de423276a931a2480a46a1a5dc7f932a2a6375b"},
   "phoenix": {:hex, :phoenix, "1.4.13", "67271ad69b51f3719354604f4a3f968f83aa61c19199343656c9caee057ff3b8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ab765a0feddb81fc62e2116c827b5f068df85159c162bee760745276ad7ddc1b"},
diff --git a/test/web/api_spec/app_operation_test.exs b/test/web/api_spec/app_operation_test.exs
new file mode 100644 (file)
index 0000000..5b96abb
--- /dev/null
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.AppOperationTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  alias Pleroma.Web.ApiSpec
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
+  alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
+
+  import OpenApiSpex.TestAssertions
+  import Pleroma.Factory
+
+  test "AppCreateRequest example matches schema" do
+    api_spec = ApiSpec.spec()
+    schema = AppCreateRequest.schema()
+    assert_schema(schema.example, "AppCreateRequest", api_spec)
+  end
+
+  test "AppCreateResponse example matches schema" do
+    api_spec = ApiSpec.spec()
+    schema = AppCreateResponse.schema()
+    assert_schema(schema.example, "AppCreateResponse", api_spec)
+  end
+
+  test "AppController produces a AppCreateResponse", %{conn: conn} do
+    api_spec = ApiSpec.spec()
+    app_attrs = build(:oauth_app)
+
+    json =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> post(
+        "/api/v1/apps",
+        Jason.encode!(%{
+          client_name: app_attrs.client_name,
+          redirect_uris: app_attrs.redirect_uris
+        })
+      )
+      |> json_response(200)
+
+    assert_schema(json, "AppCreateResponse", api_spec)
+  end
+end
index a9fa0ce48c40f1f6c127aa928676e35f0b862a3b..a450a732c8bcadf12116757318e0bb28196cf35d 100644 (file)
@@ -794,7 +794,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
 
     test "Account registration via Application", %{conn: conn} do
       conn =
-        post(conn, "/api/v1/apps", %{
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> post("/api/v1/apps", %{
           client_name: "client_name",
           redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
           scopes: "read, write, follow"
index 77d234d67eea6675b3e7e9b90c501f175ccc734f..e7b11d14e1461fa910f72ddd73c8b5359a4f5b21 100644 (file)
@@ -16,8 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
 
     conn =
       conn
-      |> assign(:user, token.user)
-      |> assign(:token, token)
+      |> put_req_header("authorization", "Bearer #{token.token}")
       |> get("/api/v1/apps/verify_credentials")
 
     app = Repo.preload(token, :app).app
@@ -37,6 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
 
     conn =
       conn
+      |> put_req_header("content-type", "application/json")
       |> assign(:user, user)
       |> post("/api/v1/apps", %{
         client_name: app_attrs.client_name,