Add OpenAPI spec for SubscriptionController

This commit is contained in:
Egor Kislitsyn 2020-05-05 16:43:00 +04:00
parent 33f2976020
commit d861b0790a
No known key found for this signature in database
GPG key ID: 1B49CB15B71E7805
7 changed files with 288 additions and 25 deletions

View file

@ -39,7 +39,12 @@ def spec do
password: %OpenApiSpex.OAuthFlow{ password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize", authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token", tokenUrl: "/oauth/token",
scopes: %{"read" => "read", "write" => "write", "follow" => "follow"} scopes: %{
"read" => "read",
"write" => "write",
"follow" => "follow",
"push" => "push"
}
} }
} }
} }

View file

@ -0,0 +1,188 @@
# 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.SubscriptionOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Subscribe to push notifications",
description:
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
operationId: "SubscriptionController.create",
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Get current subscription",
description: "View the PushSubscription currently associated with this access token.",
operationId: "SubscriptionController.show",
security: [%{"oAuth" => ["push"]}],
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Change types of notifications",
description:
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
operationId: "SubscriptionController.update",
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Remove current subscription",
description: "Removes the current Web Push API subscription.",
operationId: "SubscriptionController.delete",
security: [%{"oAuth" => ["push"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
title: "SubscriptionCreateRequest",
description: "POST body for creating a push subscription",
type: :object,
properties: %{
subscription: %Schema{
type: :object,
properties: %{
endpoint: %Schema{
type: :string,
description: "Endpoint URL that is called when a notification event occurs."
},
keys: %Schema{
type: :object,
properties: %{
p256dh: %Schema{
type: :string,
description:
"User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
},
auth: %Schema{
type: :string,
description: "Auth secret. Base64 encoded string of 16 bytes of random data."
}
},
required: [:p256dh, :auth]
}
},
required: [:endpoint, :keys]
},
data: %Schema{
type: :object,
properties: %{
alerts: %Schema{
type: :object,
properties: %{
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
favourite: %Schema{
type: :boolean,
description: "Receive favourite notifications?"
},
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
}
}
}
}
},
required: [:subscription],
example: %{
"subscription" => %{
"endpoint" => "https://example.com/example/1234",
"keys" => %{
"auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
"p256dh" =>
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
}
},
"data" => %{
"alerts" => %{
"follow" => true,
"mention" => true,
"poll" => false
}
}
}
}
end
defp update_request do
%Schema{
title: "SubscriptionUpdateRequest",
type: :object,
properties: %{
data: %Schema{
type: :object,
properties: %{
alerts: %Schema{
type: :object,
properties: %{
follow: %Schema{type: :boolean, description: "Receive follow notifications?"},
favourite: %Schema{
type: :boolean,
description: "Receive favourite notifications?"
},
reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"},
mention: %Schema{type: :boolean, description: "Receive mention notifications?"},
poll: %Schema{type: :boolean, description: "Receive poll notifications?"}
}
}
}
}
},
example: %{
"data" => %{
"alerts" => %{
"follow" => true,
"favourite" => true,
"reblog" => true,
"mention" => true,
"poll" => true
}
}
}
}
end
end

View file

@ -0,0 +1,66 @@
# 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.PushSubscription do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "PushSubscription",
description: "Response schema for a push subscription",
type: :object,
properties: %{
id: %Schema{
anyOf: [%Schema{type: :string}, %Schema{type: :integer}],
description: "The id of the push subscription in the database."
},
endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."},
server_key: %Schema{type: :string, description: "The streaming server's VAPID key."},
alerts: %Schema{
type: :object,
description: "Which alerts should be delivered to the endpoint.",
properties: %{
follow: %Schema{
type: :boolean,
description: "Receive a push notification when someone has followed you?"
},
favourite: %Schema{
type: :boolean,
description:
"Receive a push notification when a status you created has been favourited by someone else?"
},
reblog: %Schema{
type: :boolean,
description:
"Receive a push notification when a status you created has been boosted by someone else?"
},
mention: %Schema{
type: :boolean,
description:
"Receive a push notification when someone else has mentioned you in a status?"
},
poll: %Schema{
type: :boolean,
description:
"Receive a push notification when a poll you voted in or created has ended? "
}
}
}
},
example: %{
"id" => "328_183",
"endpoint" => "https://yourdomain.example/listener",
"alerts" => %{
"follow" => true,
"favourite" => true,
"reblog" => true,
"mention" => true,
"poll" => true
},
"server_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
}
})
end

View file

@ -11,14 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
action_fallback(:errors) action_fallback(:errors)
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:restrict_push_enabled)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
plug(:restrict_push_enabled) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation
# Creates PushSubscription # Creates PushSubscription
# POST /api/v1/push/subscription # POST /api/v1/push/subscription
# #
def create(%{assigns: %{user: user, token: token}} = conn, params) do def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, _} <- Subscription.delete_if_exists(user, token), with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do {:ok, subscription} <- Subscription.create(user, token, params) do
render(conn, "show.json", subscription: subscription) render(conn, "show.json", subscription: subscription)
@ -28,7 +30,7 @@ def create(%{assigns: %{user: user, token: token}} = conn, params) do
# Gets PushSubscription # Gets PushSubscription
# GET /api/v1/push/subscription # GET /api/v1/push/subscription
# #
def get(%{assigns: %{user: user, token: token}} = conn, _params) do def show(%{assigns: %{user: user, token: token}} = conn, _params) do
with {:ok, subscription} <- Subscription.get(user, token) do with {:ok, subscription} <- Subscription.get(user, token) do
render(conn, "show.json", subscription: subscription) render(conn, "show.json", subscription: subscription)
end end
@ -37,7 +39,7 @@ def get(%{assigns: %{user: user, token: token}} = conn, _params) do
# Updates PushSubscription # Updates PushSubscription
# PUT /api/v1/push/subscription # PUT /api/v1/push/subscription
# #
def update(%{assigns: %{user: user, token: token}} = conn, params) do def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do
with {:ok, subscription} <- Subscription.update(user, token, params) do with {:ok, subscription} <- Subscription.update(user, token, params) do
render(conn, "show.json", subscription: subscription) render(conn, "show.json", subscription: subscription)
end end
@ -66,7 +68,7 @@ defp restrict_push_enabled(conn, _) do
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)
|> json(dgettext("errors", "Not found")) |> json(%{error: dgettext("errors", "Record not found")})
end end
def errors(conn, _) do def errors(conn, _) do

View file

@ -25,9 +25,9 @@ defmodule Pleroma.Web.Push.Subscription do
timestamps() timestamps()
end end
@supported_alert_types ~w[follow favourite mention reblog] @supported_alert_types ~w[follow favourite mention reblog]a
defp alerts(%{"data" => %{"alerts" => alerts}}) do defp alerts(%{data: %{alerts: alerts}}) do
alerts = Map.take(alerts, @supported_alert_types) alerts = Map.take(alerts, @supported_alert_types)
%{"alerts" => alerts} %{"alerts" => alerts}
end end
@ -44,9 +44,9 @@ def create(
%User{} = user, %User{} = user,
%Token{} = token, %Token{} = token,
%{ %{
"subscription" => %{ subscription: %{
"endpoint" => endpoint, endpoint: endpoint,
"keys" => %{"auth" => key_auth, "p256dh" => key_p256dh} keys: %{auth: key_auth, p256dh: key_p256dh}
} }
} = params } = params
) do ) do

View file

@ -426,7 +426,7 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/unmute", StatusController, :unmute_conversation) post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create) post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get) get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update) put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete) delete("/push/subscription", SubscriptionController, :delete)

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.Push alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription alias Pleroma.Web.Push.Subscription
@ -27,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
build_conn() build_conn()
|> assign(:user, user) |> assign(:user, user)
|> assign(:token, token) |> assign(:token, token)
|> put_req_header("content-type", "application/json")
%{conn: conn, user: user, token: token} %{conn: conn, user: user, token: token}
end end
@ -47,8 +49,8 @@ defmacro assert_error_when_disable_push(do: yield) do
test "returns error when push disabled ", %{conn: conn} do test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do assert_error_when_disable_push do
conn conn
|> post("/api/v1/push/subscription", %{}) |> post("/api/v1/push/subscription", %{subscription: @sub})
|> json_response(403) |> json_response_and_validate_schema(403)
end end
end end
@ -59,7 +61,7 @@ test "successful creation", %{conn: conn} do
"data" => %{"alerts" => %{"mention" => true, "test" => true}}, "data" => %{"alerts" => %{"mention" => true, "test" => true}},
"subscription" => @sub "subscription" => @sub
}) })
|> json_response(200) |> json_response_and_validate_schema(200)
[subscription] = Pleroma.Repo.all(Subscription) [subscription] = Pleroma.Repo.all(Subscription)
@ -77,7 +79,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do assert_error_when_disable_push do
conn conn
|> get("/api/v1/push/subscription", %{}) |> get("/api/v1/push/subscription", %{})
|> json_response(403) |> json_response_and_validate_schema(403)
end end
end end
@ -85,9 +87,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do
res = res =
conn conn
|> get("/api/v1/push/subscription", %{}) |> get("/api/v1/push/subscription", %{})
|> json_response(404) |> json_response_and_validate_schema(404)
assert "Not found" == res assert %{"error" => "Record not found"} == res
end end
test "returns a user subsciption", %{conn: conn, user: user, token: token} do test "returns a user subsciption", %{conn: conn, user: user, token: token} do
@ -101,7 +103,7 @@ test "returns a user subsciption", %{conn: conn, user: user, token: token} do
res = res =
conn conn
|> get("/api/v1/push/subscription", %{}) |> get("/api/v1/push/subscription", %{})
|> json_response(200) |> json_response_and_validate_schema(200)
expect = %{ expect = %{
"alerts" => %{"mention" => true}, "alerts" => %{"mention" => true},
@ -130,7 +132,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do assert_error_when_disable_push do
conn conn
|> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}})
|> json_response(403) |> json_response_and_validate_schema(403)
end end
end end
@ -140,7 +142,7 @@ test "returns updated subsciption", %{conn: conn, subscription: subscription} do
|> put("/api/v1/push/subscription", %{ |> put("/api/v1/push/subscription", %{
data: %{"alerts" => %{"mention" => false, "follow" => true}} data: %{"alerts" => %{"mention" => false, "follow" => true}}
}) })
|> json_response(200) |> json_response_and_validate_schema(200)
expect = %{ expect = %{
"alerts" => %{"follow" => true, "mention" => false}, "alerts" => %{"follow" => true, "mention" => false},
@ -158,7 +160,7 @@ test "returns error when push disabled ", %{conn: conn} do
assert_error_when_disable_push do assert_error_when_disable_push do
conn conn
|> delete("/api/v1/push/subscription", %{}) |> delete("/api/v1/push/subscription", %{})
|> json_response(403) |> json_response_and_validate_schema(403)
end end
end end
@ -166,9 +168,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do
res = res =
conn conn
|> delete("/api/v1/push/subscription", %{}) |> delete("/api/v1/push/subscription", %{})
|> json_response(404) |> json_response_and_validate_schema(404)
assert "Not found" == res assert %{"error" => "Record not found"} == res
end end
test "returns empty result and delete user subsciption", %{ test "returns empty result and delete user subsciption", %{
@ -186,7 +188,7 @@ test "returns empty result and delete user subsciption", %{
res = res =
conn conn
|> delete("/api/v1/push/subscription", %{}) |> delete("/api/v1/push/subscription", %{})
|> json_response(200) |> json_response_and_validate_schema(200)
assert %{} == res assert %{} == res
refute Pleroma.Repo.get(Subscription, subscription.id) refute Pleroma.Repo.get(Subscription, subscription.id)